238 lines
6.8 KiB
TypeScript
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;
|
|
}
|