Files
Rayyan 7ce0de4e91 feat(identity): redesign identity module and introduce RBAC foundation
- Redesign the Identity module with a richer domain model.
- Extend the User entity to support username, Authentik integration, activity tracking, and storage information.
- Add Role and Permission domain models with many-to-many relationships.
- Implement RBAC foundation using UserRole, RolePermission, and UserPermission mappings.
- Add user storage quota and usage fields with default values.
- Introduce Authentik identifiers and synchronization metadata.
- Refactor user domain logic for role and permission management.
- Update Prisma schema to support the new identity architecture.
- Improve JWT authentication and permission guard integration.
- Update repositories, handlers, controllers, mappers, DTOs, and Swagger configuration.
- Refresh environment configuration and project dependencies.
2026-08-02 00:25:36 +07:00

9.3 KiB

Instructions

  • Following Playwright test failed.
  • Explain why, be concise, respect Playwright best practices.
  • Provide a snippet of code with the fix, if possible.

Test info

  • Name: user.integration.spec.ts >> Integration - User lifecycle >> login via Authentik creates local user and issues internal JWT
  • Location: tests\integration\user.integration.spec.ts:82:7

Error details

Error: No jwt cookie set after login. Saved screenshot/cookies/page to tests/test-artifacts

Test source

  19  | // increase default timeout for slow integration flows
  20  | test.setTimeout(60000);
  21  | 
  22  | async function adminLogin() {
  23  |   const adminUser = process.env.ADMIN_USERNAME;
  24  |   const adminPass = process.env.ADMIN_PASSWORD;
  25  |   if (!adminUser || !adminPass) throw new Error('ADMIN_USERNAME/ADMIN_PASSWORD must be set in env for integration tests');
  26  | 
  27  |   const browser = await chromium.launch({ headless: true });
  28  |   const context = await browser.newContext();
  29  |   const page = await context.newPage();
  30  |   await page.goto(BASE_URL, { waitUntil: 'networkidle' });
  31  | 
  32  |   try {
  33  |     await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 });
  34  |     const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]');
  35  |     if (usernameInput) await usernameInput.fill(adminUser);
  36  |     const passwordInput = await page.$('input[type="password"], input[name="password"]');
  37  |     if (passwordInput) await passwordInput.fill(adminPass);
  38  |     const submitButton = await page.$('button[type="submit"], input[type="submit"]');
  39  |     if (submitButton) {
  40  |       await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]);
  41  |     }
  42  |   } catch (e) {
  43  |     // ignore if login form not present
  44  |   }
  45  | 
  46  |   await page.waitForTimeout(1000);
  47  |   const cookies = await context.cookies();
  48  |   const jwtCookie = cookies.find(c => c.name === 'raylab_jwt');
  49  |   await context.close();
  50  |   await browser.close();
  51  | 
  52  |   if (!jwtCookie) throw new Error('Admin login failed: no session cookie');
  53  |   return jwtCookie.value;
  54  | }
  55  | 
  56  | async function apiRequest(path, token, opts = {}) {
  57  |   const headers = Object.assign({ 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, opts.headers || {});
  58  |   const method = opts.method || 'GET';
  59  |   const body = opts.body ? JSON.stringify(opts.body) : undefined;
  60  |   let url;
  61  |   try {
  62  |     url = new URL(path, BASE_URL).toString();
  63  |   } catch (e) {
  64  |     throw new Error(`Invalid BASE_URL for integration tests: ${BASE_URL}`);
  65  |   }
  66  |   const res = await fetch(url, { method, headers, body });
  67  |   let json = null;
  68  |   try { json = await res.json(); } catch (e) { json = null; }
  69  |   return { status: res.status, body: json };
  70  | }
  71  | 
  72  | 
  73  | test.describe('Integration - User lifecycle', () => {
  74  |   let adminToken;
  75  |   let createdUser = null;
  76  | 
  77  |   test.beforeAll(async () => {
  78  |     // Integration tests assume test users are created in Authentik prior to running.
  79  |     // RayLab will create local user record upon first successful login via Authentik.
  80  |   });
  81  | 
  82  |   test('login via Authentik creates local user and issues internal JWT', async () => {
  83  |     // use browser flow to login as the test user
  84  |     const browser = await chromium.launch({ headless: true });
  85  |     const context = await browser.newContext();
  86  |     const page = await context.newPage();
  87  |     await page.goto(BASE_URL, { waitUntil: 'networkidle' });
  88  | 
  89  |     try {
  90  |       await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 });
  91  |       const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]');
  92  |       if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME);
  93  |       const passwordInput = await page.$('input[type="password"], input[name="password"]');
  94  |       if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD);
  95  |       const submitButton = await page.$('button[type="submit"], input[type="submit"]');
  96  |       if (submitButton) {
  97  |         await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]);
  98  |       }
  99  |     } catch (e) {
  100 |       // ignore if login form not present
  101 |     }
  102 | 
  103 |     await page.waitForTimeout(1000);
  104 |     const cookies = await context.cookies();
  105 |     const jwtCookie = cookies.find(c => c.name === 'raylab_jwt');
  106 |     const refreshCookie = cookies.find(c => c.name === 'raylab_refresh');
  107 | 
  108 |     if (!jwtCookie) {
  109 |       const fs = require('fs');
  110 |       const dir = 'tests/test-artifacts';
  111 |       if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  112 |       const ts = Date.now();
  113 |       await page.screenshot({ path: `${dir}/failed-login-${ts}.png`, fullPage: true });
  114 |       fs.writeFileSync(`${dir}/failed-login-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8');
  115 |       const content = await page.content();
  116 |       fs.writeFileSync(`${dir}/failed-login-${ts}-page.html`, content, 'utf-8');
  117 |       await context.close();
  118 |       await browser.close();
> 119 |       throw new Error(`No jwt cookie set after login. Saved screenshot/cookies/page to ${dir}`);
      |             ^ Error: No jwt cookie set after login. Saved screenshot/cookies/page to tests/test-artifacts
  120 |     }
  121 | 
  122 |     // ensure we have an internal jwt cookie
  123 |     expect(jwtCookie).toBeDefined();
  124 |     expect(jwtCookie.value).toBeTruthy();
  125 | 
  126 |     // validate /auth/me using the internal jwt
  127 |     const token = jwtCookie.value;
  128 |     const meUrl = new URL('/auth/me', BASE_URL).toString();
  129 |     const meResp = await fetch(meUrl, { headers: { Authorization: `Bearer ${token}` } });
  130 |     const meJson = await meResp.json();
  131 |     expect(meResp.status).toBeLessThan(300);
  132 |     expect(meJson.success).toBe(true);
  133 |     expect(meJson.data).toBeDefined();
  134 |     expect(meJson.data.email).toBe(TEST_USER_EMAIL);
  135 | 
  136 |     await context.close();
  137 |     await browser.close();
  138 |   });
  139 | 
  140 |   test('login via Authentik (created user) issues internal JWT', async () => {
  141 |     // use browser flow to login as the created user
  142 |     const browser = await chromium.launch({ headless: true });
  143 |     const context = await browser.newContext();
  144 |     const page = await context.newPage();
  145 |     await page.goto(BASE_URL, { waitUntil: 'networkidle' });
  146 | 
  147 |     try {
  148 |       await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 });
  149 |       const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]');
  150 |       if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME);
  151 |       const passwordInput = await page.$('input[type="password"], input[name="password"]');
  152 |       if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD);
  153 |       const submitButton = await page.$('button[type="submit"], input[type="submit"]');
  154 |       if (submitButton) {
  155 |         await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]);
  156 |       }
  157 |     } catch (e) {
  158 |       // ignore if login form not present
  159 |     }
  160 | 
  161 |     await page.waitForTimeout(1000);
  162 |     const cookies = await context.cookies();
  163 |     const jwtCookie = cookies.find(c => c.name === 'raylab_jwt');
  164 | 
  165 |     if (!jwtCookie) {
  166 |       const fs = require('fs');
  167 |       const dir = 'tests/test-artifacts';
  168 |       if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  169 |       const ts = Date.now();
  170 |       await page.screenshot({ path: `${dir}/failed-login-2-${ts}.png`, fullPage: true });
  171 |       fs.writeFileSync(`${dir}/failed-login-2-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8');
  172 |       const content = await page.content();
  173 |       fs.writeFileSync(`${dir}/failed-login-2-${ts}-page.html`, content, 'utf-8');
  174 |       await context.close();
  175 |       await browser.close();
  176 |       throw new Error(`No jwt cookie set after login (second test). Saved screenshot/cookies/page to ${dir}`);
  177 |     }
  178 | 
  179 |     await context.close();
  180 |     await browser.close();
  181 | 
  182 |     expect(jwtCookie).toBeDefined();
  183 |     expect(jwtCookie.value).toBeTruthy();
  184 |   });
  185 | 
  186 |   // Cleanup via API provisioning has been removed. If test environment requires cleanup, perform manually in Authentik.
  187 | 
  188 | });
  189 |