Skip to content

Commit 5dfc734

Browse files
committed
test(FR-2639): add E2E regression for sToken login boundary routes
Resolves FR-2639 and FR-2643 (bundled — same test surface with matching structure for LoginView and EduAppLauncher routes). Adds two Playwright spec files covering the regression surface that does not depend on a customer-specific auth plugin for minting valid sTokens: 1. e2e/auth/stoken-login.spec.ts - Visiting / without a sToken still renders the LoginView form (STokenGuard passthrough, regression guard). - Invalid sToken on / surfaces the boundary error card with a Retry button and a Copy error details button. - The token stays in the URL on error (boundary only strips on successful onSuccess — spec "많읽 같르", Pitfall #7). - /interactive-login handles the same flow (shared STokenGuard). 2. e2e/app-launcher/edu-applauncher-stoken.spec.ts - Invalid sToken on /edu-applauncher surfaces the error card. - Invalid sToken on /applauncher (legacy alias) surfaces the same error card (same EduAppLauncherPage powers both routes). - Error state keeps app / session_id / sToken URL params intact so the launcher's subsequent steps can still observe them. Valid-token happy-path is not covered here — standard Backend.AI installs do not ship an sToken-minting auth plugin, so the happy path is exercised by customer-specific integration tests instead. The unit tests in `STokenLoginBoundary.test.tsx` already cover the success contract (children render, backend-ai-connected dispatched once, onSuccess called once, retry idempotency). Refs FR-2616, FR-2626, FR-2627
1 parent 541721c commit 5dfc734

3 files changed

Lines changed: 178 additions & 9 deletions

File tree

.specs/draft-stoken-login-boundary/metadata.json

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,31 @@
7979
"subtasks": [
8080
{
8181
"key": "FR-2636",
82-
"title": "[2.1] Wrap / and /interactive-login routes with STokenLoginBoundary conditionally"
82+
"title": "[2.1] Wrap / and /interactive-login routes with STokenLoginBoundary conditionally",
83+
"prNumber": 6861,
84+
"prUrl": "https://github.com/lablup/backend.ai-webui/pull/6861",
85+
"note": "bundled into Story 2 PR #6861"
8386
},
8487
{
8588
"key": "FR-2637",
86-
"title": "[2.2] Move postConnectSetup side effects into route-level onSuccess"
89+
"title": "[2.2] Move postConnectSetup side effects into route-level onSuccess",
90+
"prNumber": 6861,
91+
"prUrl": "https://github.com/lablup/backend.ai-webui/pull/6861",
92+
"note": "bundled into Story 2 PR #6861"
8793
},
8894
{
8995
"key": "FR-2638",
90-
"title": "[2.3] Remove connectUsingSession sToken branch and URL parsing in LoginView"
96+
"title": "[2.3] Remove connectUsingSession sToken branch and URL parsing in LoginView",
97+
"prNumber": 6861,
98+
"prUrl": "https://github.com/lablup/backend.ai-webui/pull/6861",
99+
"note": "bundled into Story 2 PR #6861"
91100
},
92101
{
93102
"key": "FR-2639",
94-
"title": "[2.4] E2E regression for LoginView sToken entry"
103+
"title": "[2.4] E2E regression for LoginView sToken entry",
104+
"prNumber": 6865,
105+
"prUrl": "https://github.com/lablup/backend.ai-webui/pull/6865",
106+
"note": "bundled into E2E PR #6865"
95107
}
96108
]
97109
},
@@ -106,19 +118,31 @@
106118
"subtasks": [
107119
{
108120
"key": "FR-2640",
109-
"title": "[3.1] Investigate get_user_credential(sToken) liveness (Q8)"
121+
"title": "[3.1] Investigate get_user_credential(sToken) liveness (Q8)",
122+
"prNumber": null,
123+
"prUrl": null,
124+
"note": "investigation only (Jira comment); resolved to option (a) prop-threading"
110125
},
111126
{
112127
"key": "FR-2641",
113-
"title": "[3.2] Wrap /edu-applauncher and /applauncher routes with STokenLoginBoundary"
128+
"title": "[3.2] Wrap /edu-applauncher and /applauncher routes with STokenLoginBoundary",
129+
"prNumber": 6864,
130+
"prUrl": "https://github.com/lablup/backend.ai-webui/pull/6864",
131+
"note": "bundled into Story 3 PR #6864"
114132
},
115133
{
116134
"key": "FR-2642",
117-
"title": "[3.3] Remove _token_login and manual backend-ai-connected dispatch in EduAppLauncher"
135+
"title": "[3.3] Remove _token_login and manual backend-ai-connected dispatch in EduAppLauncher",
136+
"prNumber": 6864,
137+
"prUrl": "https://github.com/lablup/backend.ai-webui/pull/6864",
138+
"note": "bundled into Story 3 PR #6864"
118139
},
119140
{
120141
"key": "FR-2643",
121-
"title": "[3.4] E2E regression for EduApp sToken entry with and without session_id"
142+
"title": "[3.4] E2E regression for EduApp sToken entry with and without session_id",
143+
"prNumber": 6865,
144+
"prUrl": "https://github.com/lablup/backend.ai-webui/pull/6865",
145+
"note": "bundled into E2E PR #6865"
122146
}
123147
]
124148
}
@@ -149,5 +173,5 @@
149173
},
150174
"sourceIssues": [],
151175
"createdAt": "2026-04-21T00:00:00Z",
152-
"notes": "Epic FR-2616 filed. Spec Task FR-2618 created. Story 1 implementation: PRs #6850-#6853, #6855-#6857 submitted (FR-2628, FR-2629, FR-2630, FR-2631, FR-2632, FR-2633, refactor). FR-2634 closed as Not Planned (duplicate of FR-2633 test assertion). FR-2635 closed as investigation-only (Jira comment). Directory still uses draft- prefix; rename to FR-2616-stoken-login-boundary as a follow-up."
176+
"notes": "Epic FR-2616 filed. Spec Task FR-2618 + Story 1-3 (FR-2625, FR-2626, FR-2627). Story 1: PRs #6850-#6853, #6855-#6857 (7 PRs). Story 2: PR #6861 bundles FR-2636/2637/2638 (LoginView migration). Story 3: PR #6864 bundles FR-2641/2642 (EduAppLauncher migration). Q8 (FR-2640) resolved via prop-threading, no PR. E2E (FR-2639, FR-2643): PR #6865 bundles both. FR-2634 (CI grep rule) closed as redundant with FR-2633 test. FR-2635 (cookie encoding investigation) closed. Directory still uses draft- prefix; rename to FR-2616-stoken-login-boundary as a follow-up after spec PR merges."
153177
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* E2E regression for FR-2616 Story 3 — `STokenLoginBoundary` applied to
3+
* the EduAppLauncher routes (`/edu-applauncher` and `/applauncher`).
4+
*
5+
* Parity checks with the LoginView sToken flow:
6+
* - the boundary mounts on both route aliases,
7+
* - an invalid token surfaces the error card with a Retry button.
8+
*
9+
* URL handling differs from the LoginView path:
10+
* - LoginView (`/`, `/interactive-login`) strips `sToken` from the URL
11+
* on successful auth (security, via `clearSToken(null)` in
12+
* `onSuccess`).
13+
* - EduAppLauncher (`/edu-applauncher`, `/applauncher`) intentionally
14+
* does NOT strip: `_createEduSession` re-reads `sToken` for the
15+
* customer-specific `eduApp.get_user_credential(sToken)` call, and
16+
* the `app` / `session_id` / resource-hint params drive downstream
17+
* Relay loaders. The edu token URL is LMS-issued so leaving it in
18+
* browser history is an accepted trade-off.
19+
*
20+
* Valid-token happy-path is out of scope here for the same reason as
21+
* `stoken-login.spec.ts`: a customer-specific auth plugin is required
22+
* to mint real sTokens.
23+
*/
24+
import { webuiEndpoint } from '../utils/test-util';
25+
import { expect, test } from '@playwright/test';
26+
27+
test.describe(
28+
'EduAppLauncher sToken boundary',
29+
{ tag: ['@regression', '@app-launcher', '@functional'] },
30+
() => {
31+
test('invalid sToken on `/edu-applauncher` surfaces the boundary error card', async ({
32+
page,
33+
}) => {
34+
await page.goto(
35+
`${webuiEndpoint}/edu-applauncher?sToken=invalid-token-for-e2e&app=jupyter&session_id=test-session`,
36+
);
37+
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible({
38+
timeout: 15_000,
39+
});
40+
});
41+
42+
test('invalid sToken on `/applauncher` (legacy alias) surfaces the same error card', async ({
43+
page,
44+
}) => {
45+
await page.goto(
46+
`${webuiEndpoint}/applauncher?sToken=invalid-token-for-e2e`,
47+
);
48+
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible({
49+
timeout: 15_000,
50+
});
51+
});
52+
53+
test('error state keeps non-sToken URL params intact (app, session_id)', async ({
54+
page,
55+
}) => {
56+
await page.goto(
57+
`${webuiEndpoint}/edu-applauncher?sToken=invalid-token-for-e2e&app=jupyter&session_id=test-session`,
58+
);
59+
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible({
60+
timeout: 15_000,
61+
});
62+
// On failure the URL is untouched.
63+
const url = page.url();
64+
expect(url).toContain('app=jupyter');
65+
expect(url).toContain('session_id=test-session');
66+
expect(url).toContain('sToken=invalid-token-for-e2e');
67+
});
68+
},
69+
);

e2e/auth/stoken-login.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* E2E regression for FR-2616 Story 2 — `STokenLoginBoundary` applied to
3+
* the LoginView route tree (`/` and `/interactive-login`).
4+
*
5+
* Valid-token happy-path coverage requires a Backend.AI install with a
6+
* customer-specific auth plugin (auth-keypair / OpenID) to mint real
7+
* sTokens; standard installs do not provide one. These tests therefore
8+
* focus on:
9+
* - invalid-token error UI renders and exposes a Retry button;
10+
* - the non-sToken entry points still render the regular login form
11+
* (regression guard — the boundary must not leak into non-sToken
12+
* flows);
13+
* - URL preservation on error: the boundary only strips `sToken`
14+
* after a successful login, so an invalid token remains in the URL
15+
* and the user's Retry can still pick it up.
16+
*/
17+
import { webuiEndpoint } from '../utils/test-util';
18+
import { expect, test } from '@playwright/test';
19+
20+
test.describe(
21+
'sToken login boundary (LoginView routes)',
22+
{ tag: ['@regression', '@auth', '@functional'] },
23+
() => {
24+
test('visiting `/` without a sToken still renders the login form', async ({
25+
page,
26+
}) => {
27+
await page.goto(webuiEndpoint);
28+
// Regression guard: the route-level STokenGuard passes through when
29+
// no sToken is present, so the ordinary LoginView panel still mounts.
30+
await expect(page.getByLabel('Email or Username')).toBeVisible();
31+
await expect(page.getByLabel('Password')).toBeVisible();
32+
});
33+
34+
test('invalid sToken on `/` surfaces the boundary error card with a Retry button', async ({
35+
page,
36+
}) => {
37+
await page.goto(`${webuiEndpoint}/?sToken=invalid-token-for-e2e`);
38+
39+
// The boundary renders one of the `STokenLoginError.kind`-specific
40+
// cards. Depending on reachability the kind will be either
41+
// `token-invalid` (server rejected the token) or `server-unreachable`
42+
// (no cluster), and in edge cases `endpoint-unresolved`. Match the
43+
// Retry button that is present in every kind's default card.
44+
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible({
45+
timeout: 15_000,
46+
});
47+
await expect(
48+
page.getByRole('button', { name: /copy error details/i }),
49+
).toBeVisible();
50+
});
51+
52+
test('invalid sToken on `/` does not strip the token from the URL', async ({
53+
page,
54+
}) => {
55+
await page.goto(`${webuiEndpoint}/?sToken=invalid-token-for-e2e`);
56+
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible({
57+
timeout: 15_000,
58+
});
59+
// The boundary only calls `clearSToken(null)` from `onSuccess`.
60+
// On failure the token stays in the URL so the user can read it,
61+
// report it, or Retry which re-consumes the same value.
62+
expect(page.url()).toContain('sToken=invalid-token-for-e2e');
63+
});
64+
65+
test('invalid sToken on `/interactive-login` surfaces the same error card', async ({
66+
page,
67+
}) => {
68+
await page.goto(
69+
`${webuiEndpoint}/interactive-login?sToken=invalid-token-for-e2e`,
70+
);
71+
await expect(page.getByRole('button', { name: /retry/i })).toBeVisible({
72+
timeout: 15_000,
73+
});
74+
});
75+
},
76+
);

0 commit comments

Comments
 (0)