Answer stability round 1: 0 🔴, 2 🟡 (0 misread); the admin recovery journey gets an E2E, the Kratos comments no longer claim a mailed link
This commit is contained in:
@@ -138,6 +138,26 @@ test.describe.serial("authenticated admin journey", () => {
|
|||||||
await expect(page.locator("tr", { hasText: email })).toHaveCount(0);
|
await expect(page.locator("tr", { hasText: email })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Kratos accepts an admin-minted code only in the flow it was minted on, which the notice's link opens.
|
||||||
|
test("recovery code: the notice's link opens the flow that accepts the code, and the code signs the user in", async () => {
|
||||||
|
test.setTimeout(90_000);
|
||||||
|
const email = `admin-recovery-${suffix}@plainpages.local`;
|
||||||
|
await createIdentity(email);
|
||||||
|
await page.goto("/admin/users");
|
||||||
|
const editHref = await page.locator("tr", { hasText: email }).locator('a[href^="/admin/users/"]').first().getAttribute("href");
|
||||||
|
await page.goto(editHref!);
|
||||||
|
await page.getByRole("button", { name: "Generate recovery code" }).click();
|
||||||
|
const code = await page.locator(".recovery-code code").innerText();
|
||||||
|
const link = await page.locator(".alert-body a").getAttribute("href");
|
||||||
|
|
||||||
|
const user = await watchedPage(await browser.newContext());
|
||||||
|
await user.goto(link!);
|
||||||
|
await user.fill('input[name="code"]', code);
|
||||||
|
await user.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await expect(user).toHaveURL(/\/settings/);
|
||||||
|
await user.context().close();
|
||||||
|
});
|
||||||
|
|
||||||
test("groups CRUD: create a group (writes go to Keto), see it listed, then grant it a permission", async ({ browser }) => {
|
test("groups CRUD: create a group (writes go to Keto), see it listed, then grant it a permission", async ({ browser }) => {
|
||||||
// A Keto set exists only while it has ≥1 member, so create needs a first member (the form
|
// A Keto set exists only while it has ≥1 member, so create needs a first member (the form
|
||||||
// enforces it); pick the first option (a user) from the required picker.
|
// enforces it); pick the first option (a user) from the required picker.
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ test("updateMetadataPublic PATCHes a JSON-Patch `add /metadata_public` so it nev
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("createRecoveryCode POSTs the identity id to /admin/recovery/code → { code, link }", async () => {
|
test("createRecoveryCode POSTs the identity id to /admin/recovery/code → { code, link }", async () => {
|
||||||
const { calls, fetchImpl } = recorder(() => res(200, { recovery_code: "123456", recovery_link: "http://kratos/self-service/recovery?flow=f&code=123456" }));
|
const { calls, fetchImpl } = recorder(() => res(200, { recovery_code: "123456", recovery_link: "http://kratos/self-service/recovery?flow=f" }));
|
||||||
const out = await createKratosAdmin({ baseUrl: BASE, fetchImpl }).createRecoveryCode(ID);
|
const out = await createKratosAdmin({ baseUrl: BASE, fetchImpl }).createRecoveryCode(ID);
|
||||||
assert.deepEqual(out, { code: "123456", link: "http://kratos/self-service/recovery?flow=f&code=123456" });
|
assert.deepEqual(out, { code: "123456", link: "http://kratos/self-service/recovery?flow=f" });
|
||||||
assert.equal(calls[0]!.method, "POST");
|
assert.equal(calls[0]!.method, "POST");
|
||||||
assert.match(calls[0]!.url, /\/admin\/recovery\/code$/);
|
assert.match(calls[0]!.url, /\/admin\/recovery\/code$/);
|
||||||
assert.deepEqual(JSON.parse(calls[0]!.body!), { identity_id: ID });
|
assert.deepEqual(JSON.parse(calls[0]!.body!), { identity_id: ID });
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export interface ListOptions {
|
|||||||
pageToken?: string;
|
pageToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A one-time recovery code + the self-service link wrapping it (admin "trigger recovery").
|
// A one-time recovery code + the URL of the recovery flow it was minted on, the only flow that accepts it.
|
||||||
export interface RecoveryCode {
|
export interface RecoveryCode {
|
||||||
code: string;
|
code: string;
|
||||||
link: string;
|
link: string;
|
||||||
@@ -65,8 +65,8 @@ export function createKratosAdmin(config: { baseUrl: string; fetchImpl?: typeof
|
|||||||
return (await res.json()) as Identity;
|
return (await res.json()) as Identity;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mint a recovery code for an identity (admin "trigger recovery") — the link is mailed to the
|
// Mint a recovery code for an identity (admin "trigger recovery"). Kratos mails nothing, so the
|
||||||
// user by Kratos; the code/link are also returned so an operator can hand them over directly.
|
// operator hands the code and link to the user.
|
||||||
async createRecoveryCode(userId, opts = {}) {
|
async createRecoveryCode(userId, opts = {}) {
|
||||||
const body: Record<string, unknown> = { identity_id: userId };
|
const body: Record<string, unknown> = { identity_id: userId };
|
||||||
if (opts.expiresIn) body.expires_in = opts.expiresIn;
|
if (opts.expiresIn) body.expires_in = opts.expiresIn;
|
||||||
|
|||||||
Reference in New Issue
Block a user