fix: implement bearer token handling in TestDataManager and add API helper authorization tests

This commit is contained in:
GitHub Actions
2026-02-24 21:07:10 +00:00
parent 2b4f60615f
commit bf53712b7c
3 changed files with 113 additions and 7 deletions

51
tests/fixtures/api-helper-auth.spec.ts vendored Normal file
View File

@@ -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();
}
});
});
});

View File

@@ -80,6 +80,29 @@ let tokenCache: TokenCache | null = null;
let tokenCacheQueue: Promise<void> = 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<AuthFixtures>({
);
}
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<AuthFixtures>({
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);

View File

@@ -163,20 +163,36 @@ export class TestDataManager {
private namespace: string;
private request: APIRequestContext;
private baseURLPromise: Promise<string> | 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<string, string> = {}
): Record<string, string> | undefined {
const headers = {
...extra,
};
if (this.authBearerToken) {
headers.Authorization = `Bearer ${this.authBearerToken}`;
}
return Object.keys(headers).length > 0 ? headers : undefined;
}
private async getBaseURL(): Promise<string> {
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()) {