fix: implement bearer token handling in TestDataManager and add API helper authorization tests
This commit is contained in:
51
tests/fixtures/api-helper-auth.spec.ts
vendored
Normal file
51
tests/fixtures/api-helper-auth.spec.ts
vendored
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
30
tests/fixtures/auth-fixtures.ts
vendored
30
tests/fixtures/auth-fixtures.ts
vendored
@@ -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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user