-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathauth.ts
More file actions
166 lines (151 loc) · 6.76 KB
/
Copy pathauth.ts
File metadata and controls
166 lines (151 loc) · 6.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import { test as base, expect, type Page } from '@playwright/test';
import { BACKEND_URL, E2E_EMAIL, E2E_PASSWORD } from '../playwright.config';
import { attachBusLog, type BusLogCapture } from './bus-log';
import { JaegerCapture, attachJaegerEvidence } from './jaeger';
import { attachPageErrors, attachPageErrorsArtifact, type PageErrorsCapture } from './page-errors';
/**
* Polyfill crypto.randomUUID for non-secure-context test environments.
*
* The frontend calls `crypto.randomUUID()` (e.g. in busRequest for
* correlationIds). That API is only defined in secure contexts —
* HTTPS, `http://localhost`, `http://127.0.0.1`. When tests run against
* a container IP over HTTP (e.g. `http://192.168.64.60:3000`), it's
* `undefined` and every emit throws "crypto.randomUUID is not a function".
*
* This is also a real product bug — any user accessing the frontend
* over HTTP from a non-localhost hostname will hit it. TODO: file and
* fix in the frontend (swap to a uuid library that doesn't require a
* secure context, or require HTTPS).
*/
const CRYPTO_POLYFILL = `
if (typeof crypto !== 'undefined' && !crypto.randomUUID) {
Object.defineProperty(crypto, 'randomUUID', {
value: () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0;
var v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
}),
configurable: true,
});
}
`;
/**
* Sign in via the real UI form: Connect → host/port/email/password → submit.
*
* Leaves the page on `/en/know/discover` with a live authenticated session.
* Idempotent: re-invocation on an already-signed-in page is a no-op.
*/
export async function signIn(page: Page): Promise<void> {
await page.addInitScript(CRYPTO_POLYFILL);
// Start at the root; the locale redirect drops us on /en and eventually
// /en/know/discover after session resolution.
await page.goto('/');
// If the session is already authenticated (persisted context), the app
// is already on a know/* route. Detect that and bail.
if (await isAlreadySignedIn(page)) return;
const backend = new URL(BACKEND_URL);
const host = backend.hostname;
const port = backend.port || (backend.protocol === 'https:' ? '443' : '80');
const protocol = backend.protocol === 'https:' ? 'https' : 'http';
// KnowledgeBasePanel auto-opens the Connect form when there are zero
// registered KBs. When at least one KB is registered, the form is
// collapsed and we have to click "Add Knowledge Base" first. Race the
// two states rather than assuming one.
const emailField = page.getByRole('textbox', { name: /^email$/i });
const addButton = page.getByRole('button', { name: /add knowledge base/i });
await expect(async () => {
const emailVisible = await emailField.isVisible().catch(() => false);
const addVisible = await addButton.isVisible().catch(() => false);
expect(emailVisible || addVisible).toBe(true);
}).toPass({ timeout: 15_000 });
if (!(await emailField.isVisible().catch(() => false))) {
await addButton.click();
await expect(emailField).toBeVisible({ timeout: 5_000 });
}
// Fill the form. Fields have labels derived from their placeholder
// text (the LoginForm uses `placeholder="Host"` etc which Playwright's
// accessibility tree exposes as textbox names).
//
// IMPORTANT: set host BEFORE protocol. Filling the host runs
// `handleHostChange` which calls `defaultProtocol(host)` and can flip
// the protocol to HTTPS for IP-like hostnames, overwriting an earlier
// protocol selection.
await page.getByRole('textbox', { name: /^host$/i }).fill(host);
await page.getByRole('combobox').first().selectOption(protocol);
await page.getByRole('spinbutton').first().fill(port);
await emailField.fill(E2E_EMAIL);
await page.getByRole('textbox', { name: /^password$/i }).fill(E2E_PASSWORD);
// Submit — the primary button in the form is "Connect" (not "Sign in";
// that's the re-auth form's label).
await page.getByRole('button', { name: /^connect$/i }).click();
// Wait until the URL changes OR the form disappears. Either is proof
// the sign-in was accepted.
await expect(async () => {
const signedIn = await isAlreadySignedIn(page);
expect(signedIn).toBe(true);
}).toPass({ timeout: 20_000 });
}
/**
* Best-effort heuristic: we're signed in if the discover route is visible
* and the sign-in form is not.
*/
async function isAlreadySignedIn(page: Page): Promise<boolean> {
// No sign-in form visible implies either signed in or still loading.
const emailInput = page.getByRole('textbox', { name: /^email$/i });
const emailVisible = await emailInput.isVisible().catch(() => false);
if (emailVisible) return false;
// The authenticated Knowledge section uses a /know/ URL and has no
// sign-in form; that combination is proof-of-auth.
const url = page.url();
return /\/know\//.test(url);
}
/**
* Playwright test with a signed-in fixture. Use like:
*
* import { test } from '../fixtures/auth';
*
* test('something', async ({ signedInPage }) => {
* // already at /en/know/discover with a valid session
* });
*/
export const test = base.extend<{
signedInPage: Page;
bus: BusLogCapture;
jaeger: JaegerCapture;
pageErrors: PageErrorsCapture;
}>({
bus: async ({ page }, use) => {
const capture = await attachBusLog(page);
await use(capture);
},
pageErrors: async ({ page }, use, testInfo) => {
// Captures uncaught browser errors during the test. Soft by default
// (attaches a `page-errors.json` artifact when entries exist);
// set `PAGE_ERRORS_FAIL=1` to fail tests with errors. See
// `fixtures/page-errors.ts` for the wiring rationale.
const capture = await attachPageErrors(page);
await use(capture);
await attachPageErrorsArtifact(testInfo, capture);
},
jaeger: async ({ bus }, use, testInfo) => {
// Records the start time so the teardown query has a tight window.
// Depends on `bus` so prefix capture is wired before any test code
// runs. After `use`, fetches matching Jaeger traces and attaches
// them to the Playwright report (failure-only by default; configurable
// via `JAEGER_ATTACH=always|failure|off`).
const capture = new JaegerCapture();
await use(capture);
await attachJaegerEvidence(testInfo, bus, capture);
},
signedInPage: async ({ page, bus: _bus, jaeger: _jaeger, pageErrors: _pageErrors }, use) => {
// Depend on `bus` and `jaeger` so the init scripts run before signIn
// and the Jaeger teardown sees the full test window. The unused
// parameters force fixture ordering.
await signIn(page);
await use(page);
},
});
export { expect };
export type { BusLogCapture } from './bus-log';
export type { JaegerCapture } from './jaeger';
export type { PageErrorsCapture } from './page-errors';