diff --git a/tests/fixtures/api-helper-auth.spec.ts b/tests/fixtures/api-helper-auth.spec.ts new file mode 100644 index 00000000..6c29603f --- /dev/null +++ b/tests/fixtures/api-helper-auth.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from './test'; +import { request as playwrightRequest } from '@playwright/test'; +import { TestDataManager } from '../utils/TestDataManager'; + +const TEST_EMAIL = process.env.E2E_TEST_EMAIL || 'e2e-test@example.com'; +const TEST_PASSWORD = process.env.E2E_TEST_PASSWORD || 'TestPassword123!'; + +test.describe('API helper authorization', () => { + test('TestDataManager createUser succeeds with explicit bearer token only', async ({ request, baseURL }) => { + await test.step('Acquire admin bearer token via login API', async () => { + const loginResponse = await request.post('/api/v1/auth/login', { + data: { + email: TEST_EMAIL, + password: TEST_PASSWORD, + }, + }); + + expect(loginResponse.ok()).toBe(true); + const loginBody = (await loginResponse.json()) as { token?: string }; + expect(loginBody.token).toBeTruthy(); + + const token = loginBody.token as string; + const bareContext = await playwrightRequest.newContext({ + baseURL, + extraHTTPHeaders: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + + const manager = new TestDataManager(bareContext, 'api-helper-auth', token); + + try { + await test.step('Create user through helper using bearer-authenticated API calls', async () => { + const createdUser = await manager.createUser({ + name: `Helper Auth User ${Date.now()}`, + email: `helper-auth-${Date.now()}@test.local`, + password: 'TestPass123!', + role: 'user', + }); + + expect(createdUser.id).toBeTruthy(); + expect(createdUser.email).toContain('@'); + }); + } finally { + await manager.cleanup(); + await bareContext.dispose(); + } + }); + }); +}); diff --git a/tests/fixtures/auth-fixtures.ts b/tests/fixtures/auth-fixtures.ts index 50a3da9a..6fd7d700 100644 --- a/tests/fixtures/auth-fixtures.ts +++ b/tests/fixtures/auth-fixtures.ts @@ -80,6 +80,29 @@ let tokenCache: TokenCache | null = null; let tokenCacheQueue: Promise = Promise.resolve(); const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // Refresh 5 min before expiry +function readAuthTokenFromStorageState(storageStatePath: string): string | null { + try { + const savedState = JSON.parse(readFileSync(storageStatePath, 'utf-8')); + const origins = Array.isArray(savedState.origins) ? savedState.origins : []; + + for (const originEntry of origins) { + const localStorageEntries = Array.isArray(originEntry?.localStorage) + ? originEntry.localStorage + : []; + + const tokenEntry = localStorageEntries.find( + (entry: { name?: string; value?: string }) => entry?.name === 'charon_auth_token' + ); + if (tokenEntry?.value) { + return tokenEntry.value; + } + } + } catch { + } + + return null; +} + /** * Test-only helper to reset token refresh state between tests */ @@ -249,9 +272,11 @@ export const test = base.extend({ ); } + const savedState = JSON.parse(readFileSync(STORAGE_STATE, 'utf-8')); + const authToken = readAuthTokenFromStorageState(STORAGE_STATE); + // Validate cookie domain matches baseURL to catch configuration issues early try { - const savedState = JSON.parse(readFileSync(STORAGE_STATE, 'utf-8')); const cookies = savedState.cookies || []; const authCookie = cookies.find((c: { name: string }) => c.name === 'auth_token'); @@ -281,10 +306,11 @@ export const test = base.extend({ extraHTTPHeaders: { Accept: 'application/json', 'Content-Type': 'application/json', + ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}), }, }); - const manager = new TestDataManager(authenticatedContext, testInfo.title); + const manager = new TestDataManager(authenticatedContext, testInfo.title, authToken ?? undefined); try { await use(manager); diff --git a/tests/utils/TestDataManager.ts b/tests/utils/TestDataManager.ts index babd588e..c4c2fbb2 100644 --- a/tests/utils/TestDataManager.ts +++ b/tests/utils/TestDataManager.ts @@ -163,20 +163,36 @@ export class TestDataManager { private namespace: string; private request: APIRequestContext; private baseURLPromise: Promise | null = null; + private authBearerToken: string | null; /** * Creates a new TestDataManager instance * @param request - Playwright API request context * @param testName - Optional test name for namespace generation */ - constructor(request: APIRequestContext, testName?: string) { + constructor(request: APIRequestContext, testName?: string, authBearerToken?: string) { this.request = request; + this.authBearerToken = authBearerToken ?? null; // Create unique namespace per test to avoid conflicts this.namespace = testName ? `test-${this.sanitize(testName)}-${Date.now()}` : `test-${crypto.randomUUID()}`; } + private buildRequestHeaders( + extra: Record = {} + ): Record | undefined { + const headers = { + ...extra, + }; + + if (this.authBearerToken) { + headers.Authorization = `Bearer ${this.authBearerToken}`; + } + + return Object.keys(headers).length > 0 ? headers : undefined; + } + private async getBaseURL(): Promise { if (this.baseURLPromise) { return await this.baseURLPromise; @@ -230,7 +246,10 @@ export class TestDataManager { const retryStatuses = options.retryStatuses ?? [429]; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - const response = await this.request.post(url, { data }); + const response = await this.request.post(url, { + data, + headers: this.buildRequestHeaders(), + }); if (!retryStatuses.includes(response.status()) || attempt === maxAttempts) { return response; } @@ -244,7 +263,10 @@ export class TestDataManager { await new Promise((resolve) => setTimeout(resolve, backoffMs)); } - return this.request.post(url, { data }); + return this.request.post(url, { + data, + headers: this.buildRequestHeaders(), + }); } private async deleteWithRetry( @@ -260,7 +282,9 @@ export class TestDataManager { const retryStatuses = options.retryStatuses ?? [429]; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - const response = await this.request.delete(url); + const response = await this.request.delete(url, { + headers: this.buildRequestHeaders(), + }); if (!retryStatuses.includes(response.status()) || attempt === maxAttempts) { return response; } @@ -274,7 +298,9 @@ export class TestDataManager { await new Promise((resolve) => setTimeout(resolve, backoffMs)); } - return this.request.delete(url); + return this.request.delete(url, { + headers: this.buildRequestHeaders(), + }); } /** @@ -307,6 +333,7 @@ export class TestDataManager { const response = await this.request.post('/api/v1/proxy-hosts', { data: payload, timeout: 30000, // 30s timeout + headers: this.buildRequestHeaders(), }); if (!response.ok()) { @@ -396,6 +423,7 @@ export class TestDataManager { const response = await this.request.post('/api/v1/certificates', { data: namespaced, + headers: this.buildRequestHeaders(), }); if (!response.ok()) { @@ -441,6 +469,7 @@ export class TestDataManager { const response = await this.request.post('/api/v1/dns-providers', { data: payload, + headers: this.buildRequestHeaders(), }); if (!response.ok()) {