Files
fotospiel-app/tests/ui/helpers/test-fixtures.ts

238 lines
6.8 KiB
TypeScript

/* eslint-disable react-hooks/rules-of-hooks */
import 'dotenv/config';
import { test as base, expect, Page, APIRequestContext, APIResponse } from '@playwright/test';
export type TenantCredentials = {
email: string;
password: string;
};
export type TenantAdminFixtures = {
tenantAdminCredentials: TenantCredentials | null;
signInTenantAdmin: () => Promise<void>;
};
export type MailboxEntry = {
id: string;
subject: string | null;
to: Array<{ email: string; name: string | null }>;
from: Array<{ email: string; name: string | null }>;
html: string | null;
text: string | null;
sent_at: string;
};
export type CouponSeedDefinition = {
code: string;
type: 'percentage' | 'flat' | 'flat_per_seat';
amount: number;
currency?: string | null;
description?: string;
enabled_for_checkout?: boolean;
usage_limit?: number | null;
per_customer_limit?: number | null;
starts_at?: string | null;
ends_at?: string | null;
packages?: number[];
};
export type TestingApiFixtures = {
clearTestMailbox: () => Promise<void>;
getTestMailbox: () => Promise<MailboxEntry[]>;
seedTestCoupons: (definitions?: CouponSeedDefinition[]) => Promise<Array<{ id: number; code: string }>>;
getLatestCheckoutSession: (filters?: { email?: string; tenantId?: number; status?: string }) => Promise<CheckoutSessionSummary | null>;
simulatePaddleCompletion: (sessionId: string, overrides?: Partial<PaddleSimulationOverrides>) => Promise<void>;
fetchJoinToken: (params: { eventId?: number; slug?: string; ensureActive?: boolean }) => Promise<JoinTokenPayload>;
};
export type CheckoutSessionSummary = {
id: string;
status: string;
provider: string | null;
tenant_id: number | null;
package_id: number | null;
user_email: string | null;
coupon_id: number | null;
amount_subtotal: string | null;
amount_total: string | null;
created_at: string | null;
};
export type PaddleSimulationOverrides = {
event_type: string;
transaction_id?: string;
status?: string;
checkout_id?: string;
metadata?: Record<string, unknown>;
};
export type JoinTokenPayload = {
event_id: number;
token_id: number;
token: string;
join_url: string;
qr_svg: string;
expires_at: string | null;
usage_count: number;
usage_limit: number | null;
};
const tenantAdminEmail = process.env.E2E_TENANT_EMAIL ?? 'hello@lumen-moments.demo';
const tenantAdminPassword = process.env.E2E_TENANT_PASSWORD ?? 'Demo1234!';
export const test = base.extend<TenantAdminFixtures & TestingApiFixtures>({
tenantAdminCredentials: async (_context, use) => {
if (!tenantAdminEmail || !tenantAdminPassword) {
await use(null);
return;
}
await use({
email: tenantAdminEmail,
password: tenantAdminPassword,
});
},
signInTenantAdmin: async ({ page, tenantAdminCredentials }, use) => {
if (!tenantAdminCredentials) {
await use(async () => {
throw new Error('Tenant admin credentials missing. Provide E2E_TENANT_EMAIL and E2E_TENANT_PASSWORD.');
});
return;
}
await use(async () => {
await performTenantSignIn(page, tenantAdminCredentials);
});
},
clearTestMailbox: async ({ request }, use) => {
await use(async () => {
await expectApiSuccess(request.delete('/api/_testing/mailbox'));
});
},
getTestMailbox: async ({ request }, use) => {
await use(async () => {
const response = await expectApiSuccess(request.get('/api/_testing/mailbox'));
const json = await response.json();
return Array.isArray(json.data) ? (json.data as MailboxEntry[]) : [];
});
},
seedTestCoupons: async ({ request }, use) => {
await use(async (definitions?: CouponSeedDefinition[]) => {
const response = await expectApiSuccess(
request.post('/api/_testing/coupons/seed', {
data: definitions && definitions.length > 0 ? { coupons: definitions } : undefined,
})
);
const json = await response.json();
return Array.isArray(json.data) ? json.data : [];
});
},
getLatestCheckoutSession: async ({ request }, use) => {
await use(async (filters?: { email?: string; tenantId?: number; status?: string }) => {
const response = await request.get('/api/_testing/checkout/sessions/latest', {
params: {
email: filters?.email,
tenant_id: filters?.tenantId,
status: filters?.status,
},
});
if (response.status() === 404) {
return null;
}
await expectApiSuccess(Promise.resolve(response));
const json = await response.json();
return json.data as CheckoutSessionSummary;
});
},
simulatePaddleCompletion: async ({ request }, use) => {
await use(async (sessionId: string, overrides?: Partial<PaddleSimulationOverrides>) => {
await expectApiSuccess(
request.post(`/api/_testing/checkout/sessions/${sessionId}/simulate-paddle`, {
data: overrides,
})
);
});
},
fetchJoinToken: async ({ request }, use) => {
await use(async ({ eventId, slug, ensureActive = true }: { eventId?: number; slug?: string; ensureActive?: boolean }) => {
const response = await expectApiSuccess(
request.get('/api/_testing/events/join-token', {
params: {
event_id: eventId,
slug,
ensure_active: ensureActive,
},
})
);
const json = await response.json();
return json.data as JoinTokenPayload;
});
},
});
export const expectFixture = expect;
async function performTenantSignIn(page: Page, credentials: TenantCredentials) {
const token = await exchangeToken(page.request, credentials);
await page.addInitScript(({ stored }) => {
localStorage.setItem('tenant_admin.token.v1', JSON.stringify(stored));
sessionStorage.setItem('tenant_admin.token.session.v1', JSON.stringify(stored));
}, { stored: token });
await page.goto('/event-admin');
await page.waitForLoadState('domcontentloaded');
}
type StoredTokenPayload = {
accessToken: string;
abilities: string[];
issuedAt: number;
};
async function exchangeToken(request: APIRequestContext, credentials: TenantCredentials): Promise<StoredTokenPayload> {
const response = await request.post('/api/v1/tenant-auth/login', {
data: {
login: credentials.email,
password: credentials.password,
},
});
if (!response.ok()) {
throw new Error(`Tenant PAT login failed: ${response.status()} ${await response.text()}`);
}
const body = await response.json();
return {
accessToken: body.token,
abilities: Array.isArray(body.abilities) ? body.abilities : [],
issuedAt: Date.now(),
};
}
async function expectApiSuccess(responsePromise: Promise<APIResponse>): Promise<APIResponse> {
const response = await responsePromise;
if (!response.ok()) {
throw new Error(`Test API request failed: ${response.status()} ${await response.text()}`);
}
return response;
}