Files
PiCloud/dashboard/tests/e2e/integration/integration.spec.ts
MechaCat02 beb3bcb97c fix(e2e): use uniqueUsername helper in integration.spec
Date.now() can collide across workers running on the same millisecond
boundary. The worker-aware helper that the rest of the suite uses
side-steps that without changing the test's intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:41:47 +02:00

159 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, request, type Page } from '@playwright/test';
import { test } from '../fixtures/ids';
import { CleanupRegistry } from '../fixtures/cleanup';
import { adminApi } from '../fixtures/api';
// Full-stack integration scenarios. Unlike the per-page B1B8 specs,
// these drive a complete user journey across multiple pages and then
// verify the data plane / API surface behaves the way the dashboard
// promised it would.
const API_BASE = process.env.E2E_API_BASE ?? 'http://127.0.0.1:18080';
const cleanup = new CleanupRegistry();
test.afterEach(async () => {
await cleanup.run();
});
async function fillCodeMirror(page: Page, locator: string, text: string): Promise<void> {
const cm = page.locator(locator).first();
await cm.click();
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('Delete');
await page.keyboard.type(text);
}
test('end-to-end: app + domain + script + route via dashboard → invoke via public URL', async ({
page,
uniqueSlug
}) => {
const slug = uniqueSlug('public');
const domain = `${slug}.local`;
const routePath = `/${slug}/hello`;
const scriptName = `${slug}-hello`;
const scriptSource = `return #{ statusCode: 200, body: #{ source: "public", slug: "${slug}" } };`;
// 1. Create the app from the apps list.
await page.goto('/admin/apps');
await page.getByRole('button', { name: 'New app' }).click();
await page.getByLabel('Name').fill(slug);
const slugInput = page.getByLabel('Slug');
await slugInput.fill('');
await slugInput.fill(slug);
await page.getByRole('button', { name: 'Create app' }).click();
cleanup.app(slug);
await expect(page.getByRole('link', { name: new RegExp(slug) })).toBeVisible();
// 2. Open the app and claim the domain on the Domains tab.
await page.getByRole('link', { name: new RegExp(slug) }).click();
await expect(page).toHaveURL(new RegExp(`/admin/apps/${slug}$`));
await page.getByRole('button', { name: /^Domains \(\d+\)$/ }).click();
const domainForm = page.locator('form.create-form.inline');
await domainForm.getByPlaceholder(/app\.example\.com/).fill(domain);
await domainForm.getByRole('button', { name: /^Add domain$/ }).click();
await expect(page.locator('.domain-row')).toContainText(domain);
// 3. Create the script on the Scripts tab.
await page.getByRole('button', { name: /^Scripts \(\d+\)$/ }).click();
await page.getByRole('button', { name: /^New script$/ }).click();
await page.getByLabel('Name').fill(scriptName);
await fillCodeMirror(page, '.cm-content', scriptSource);
await page.getByRole('button', { name: /^Create script$/ }).click();
// 4. Open the script and bind a route on the Routing tab.
await page.getByRole('link', { name: new RegExp(scriptName) }).click();
await page.getByRole('button', { name: 'Routing' }).click();
await page.getByRole('button', { name: '+ Add route' }).click();
const routeForm = page.locator('form.route-form');
await routeForm.getByLabel('Path', { exact: true }).fill(routePath);
await routeForm.getByLabel('Method').selectOption('GET');
await routeForm.getByLabel(/^Host/).fill(domain);
await page.getByRole('button', { name: /^Create route$/ }).click();
await expect(page.locator('.route-list')).toContainText(routePath);
// 5. Invoke via the public URL, with the Host header pointing at
// the claimed domain. The dev backend listens on 127.0.0.1; the
// orchestrator resolves the app from Host, then the route.
const publicCtx = await request.newContext({ baseURL: API_BASE });
try {
const res = await publicCtx.get(routePath, { headers: { host: domain } });
expect(res.status()).toBe(200);
const body = (await res.json()) as { source: string; slug: string };
expect(body.source).toBe('public');
expect(body.slug).toBe(slug);
} finally {
await publicCtx.dispose();
}
});
test('api key minted via dashboard works as a CLI bearer, then revoke disables it', async ({
page,
uniqueUsername
}) => {
// Worker-aware unique helper instead of Date.now() — keeps two
// workers from minting the same name on the same millisecond.
const name = uniqueUsername('cli');
// 1. Mint the key from /profile and capture the revealed token.
await page.goto('/admin/profile');
await page.getByRole('button', { name: /\+ Mint API key/ }).click();
const mintForm = page.locator('form.mint');
await mintForm.getByPlaceholder('e.g. ci-deploy').fill(name);
// script:read is enough to read the scripts list — that's our
// "CLI verb" below.
await page.locator('label.scope-chip', { hasText: 'script:read' }).click();
await page.getByRole('button', { name: /^Mint key$/ }).click();
const reveal = page.locator('.reveal');
await expect(reveal).toBeVisible();
const rawToken = (await reveal.locator('code.token').textContent())?.trim();
expect(rawToken).toBeTruthy();
await reveal.getByRole('checkbox', { name: /saved this token/i }).check();
await reveal.getByRole('button', { name: /^Done$/ }).click();
// 2. Act like a CLI: call the API directly with Bearer <token>.
const cli = await request.newContext({
baseURL: API_BASE,
extraHTTPHeaders: { authorization: `Bearer ${rawToken}` }
});
try {
const ok = await cli.get('/api/v1/admin/scripts');
expect(ok.status()).toBe(200);
const body = (await ok.json()) as unknown;
expect(Array.isArray(body)).toBe(true);
// Sanity: a route the scope doesn't cover must reject.
// `script:read` cannot list instance admins (that's
// instance:admin territory).
const denied = await cli.get('/api/v1/admin/admins');
expect(denied.status()).toBe(403);
// 3. Revoke via the dashboard.
await page.reload();
const revokeBtn = page.getByRole('button', { name: `Revoke ${name}` });
await expect(revokeBtn).toBeVisible();
await revokeBtn.click();
await page.getByRole('dialog').getByRole('button', { name: /^Revoke$/ }).click();
await expect(revokeBtn).toHaveCount(0);
// 4. Same CLI call must now fail auth.
const afterRevoke = await cli.get('/api/v1/admin/scripts');
expect(afterRevoke.status()).toBe(401);
} finally {
await cli.dispose();
}
// Belt-and-braces cleanup: if the UI revoke missed, drop via API.
const api = await adminApi();
try {
const list = await api.get('/api/v1/admin/api-keys');
if (list.ok()) {
const all = (await list.json()) as Array<{ id: string; name: string }>;
const k = all.find((x) => x.name === name);
if (k) cleanup.apiKey(k.id);
}
} finally {
await api.dispose();
}
});