Reuse authenticated state in multi-tenant release E2E

This commit is contained in:
rcourtman 2026-07-09 13:32:34 +01:00
parent 5fe6bfde57
commit 1a05c715ac
5 changed files with 109 additions and 1 deletions

View file

@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import {
buildManagedLocalBackendEnv,
@ -11,6 +12,9 @@ import {
shouldBuildManagedLocalBackendBinary,
} from './managed-local-backend.mjs';
const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
const integrationRoot = path.resolve(scriptsDir, '..');
test('buildManagedLocalBackendState uses deterministic defaults', () => {
const state = buildManagedLocalBackendState({});
assert.equal(state.repoRoot.endsWith('/repos/pulse'), true);
@ -143,6 +147,26 @@ test('buildManagedLocalBackendEnv seeds deterministic auth in demo mode', () =>
assert.equal(env.PULSE_AUTH_PASS, 'adminadminadmin');
});
test('multi-tenant release auth reuses storage state and classifies login rate limits', async () => {
const helpers = await fs.readFile(path.join(integrationRoot, 'tests', 'helpers.ts'), 'utf8');
const multiTenantSpec = await fs.readFile(
path.join(integrationRoot, 'tests', '03-multi-tenant.spec.ts'),
'utf8',
);
assert.match(multiTenantSpec, /createAuthenticatedStorageState/);
assert.match(multiTenantSpec, /storageState:\s*async\s*\(\{\s*authStorageStatePath\s*\},\s*use\)/);
assert.match(multiTenantSpec, /authStorageStatePath:\s*\[/);
assert.match(multiTenantSpec, /multi-tenant-\$\{workerInfo\.project\.name\}\.json/);
assert.match(multiTenantSpec, /\{\s*scope:\s*'worker'\s*\}/);
assert.match(helpers, /new URL\(response\.url\(\)\)\.pathname === "\/api\/login"/);
assert.match(helpers, /response\.request\(\)\.method\(\)\.toUpperCase\(\) === "POST"/);
assert.match(helpers, /loginResponse\?\.status\(\) === 429/);
assert.match(helpers, /return "error:Too many requests"/);
assert.match(helpers, /\/too many\/i\.test\(lastOutcome\) \? 15_000/);
});
test('shouldBuildManagedLocalBackendBinary returns true when binary is missing', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'pulse-managed-backend-'));
const state = {

View file

@ -1,6 +1,10 @@
import { expect, test } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { test as base, expect } from '@playwright/test';
import {
apiRequest,
createAuthenticatedStorageState,
createOrg,
deleteOrg,
ensureAuthenticated,
@ -10,6 +14,38 @@ import {
waitForAppShell,
} from './helpers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
type WorkerFixtures = {
authStorageStatePath: string;
};
const test = base.extend<{}, WorkerFixtures>({
storageState: async ({ authStorageStatePath }, use) => {
await use(authStorageStatePath);
},
authStorageStatePath: [
async ({ browser }, use, workerInfo) => {
const storageStatePath = path.resolve(
__dirname,
'..',
'..',
'tmp',
'playwright-auth',
`multi-tenant-${workerInfo.project.name}.json`,
);
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
await createAuthenticatedStorageState(browser, storageStatePath);
try {
await use(storageStatePath);
} finally {
fs.rmSync(storageStatePath, { force: true });
}
},
{ scope: 'worker' },
],
});
type Organization = {
id: string;
displayName?: string;

View file

@ -925,6 +925,15 @@ export async function login(page: Page, credentials = E2E_CREDENTIALS) {
.first();
const submitAndAwaitOutcome = async (): Promise<string> => {
const loginResponsePromise = page
.waitForResponse(
(response) =>
new URL(response.url()).pathname === "/api/login" &&
response.request().method().toUpperCase() === "POST",
{ timeout: 30_000 },
)
.catch(() => null);
await page.fill('input[name="username"]', credentials.username);
await page.fill('input[name="password"]', credentials.password);
await page.click('button[type="submit"]');
@ -941,6 +950,13 @@ export async function login(page: Page, credentials = E2E_CREDENTIALS) {
const message = (
(await loginErrorText.textContent()) || "login_error"
).trim();
const loginResponse = await Promise.race([
loginResponsePromise,
page.waitForTimeout(100).then(() => null),
]);
if (loginResponse?.status() === 429) {
return "error:Too many requests";
}
return `error:${message}`;
}
await page.waitForTimeout(250);