7015fb0153
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
171 lines
5.9 KiB
TypeScript
171 lines
5.9 KiB
TypeScript
/**
|
|
* Desktop mode: one-shot desktop-login, Bearer-token shutdown, desktopMode in /api/me,
|
|
* and the desktop-session password change without oldPassword.
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import { apiClient, createTestApp, loginAdmin } from "./helpers.js";
|
|
import type { MeResponse } from "../src/api/types.js";
|
|
|
|
const TOKEN = "test-desktop-token";
|
|
|
|
function desktopApp() {
|
|
return createTestApp({ config: { desktopToken: TOKEN } });
|
|
}
|
|
|
|
describe("desktop-login", () => {
|
|
it("redeems the token once: cookie session, redirect to /, second attempt 401", async () => {
|
|
const t = await desktopApp();
|
|
try {
|
|
const res = await t.app.request(`/api/auth/desktop-login?token=${TOKEN}`);
|
|
expect(res.status).toBe(302);
|
|
expect(res.headers.get("location")).toBe("/");
|
|
const cookie = res.headers.get("set-cookie");
|
|
expect(cookie).toContain("penguin_session=");
|
|
|
|
const me = await t.app.request("/api/me", {
|
|
headers: { cookie: cookie!.split(";")[0]! },
|
|
});
|
|
expect(me.status).toBe(200);
|
|
const body = (await me.json()) as MeResponse;
|
|
expect(body.user.userId).toBe("admin");
|
|
expect(body.desktopMode).toBe(true);
|
|
|
|
const replay = await t.app.request(`/api/auth/desktop-login?token=${TOKEN}`);
|
|
expect(replay.status).toBe(401);
|
|
} finally {
|
|
await t.cleanup();
|
|
}
|
|
});
|
|
|
|
it("rejects a wrong or missing token without consuming the real one", async () => {
|
|
const t = await desktopApp();
|
|
try {
|
|
expect((await t.app.request("/api/auth/desktop-login?token=wrong")).status).toBe(401);
|
|
expect((await t.app.request("/api/auth/desktop-login")).status).toBe(401);
|
|
// The real token still works after failed attempts.
|
|
expect((await t.app.request(`/api/auth/desktop-login?token=${TOKEN}`)).status).toBe(302);
|
|
} finally {
|
|
await t.cleanup();
|
|
}
|
|
});
|
|
|
|
it("is 404 outside desktop mode, and /api/me reports desktopMode false", async () => {
|
|
const t = await createTestApp();
|
|
try {
|
|
expect((await t.app.request("/api/auth/desktop-login?token=x")).status).toBe(404);
|
|
const admin = await loginAdmin(t.app);
|
|
const me = await apiClient(t.app, admin.cookie).get("/api/me");
|
|
expect(((await me.json()) as MeResponse).desktopMode).toBe(false);
|
|
} finally {
|
|
await t.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("desktop shutdown endpoint", () => {
|
|
it("accepts the Bearer token repeatedly and triggers the registered handler", async () => {
|
|
const t = await desktopApp();
|
|
try {
|
|
let requested = 0;
|
|
t.deps.desktop!.onShutdownRequest(() => {
|
|
requested += 1;
|
|
});
|
|
const res = await t.app.request("/api/desktop/shutdown", {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${TOKEN}` },
|
|
});
|
|
expect(res.status).toBe(202);
|
|
// The route defers the trigger so the 202 can flush first.
|
|
await new Promise((r) => setTimeout(r, 80));
|
|
expect(requested).toBe(1);
|
|
|
|
// Unlike the login token, the shutdown credential is NOT one-shot.
|
|
const again = await t.app.request("/api/desktop/shutdown", {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${TOKEN}` },
|
|
});
|
|
expect(again.status).toBe(202);
|
|
} finally {
|
|
await t.cleanup();
|
|
}
|
|
});
|
|
|
|
it("rejects wrong or missing tokens, and does not exist outside desktop mode", async () => {
|
|
const t = await desktopApp();
|
|
try {
|
|
const wrong = await t.app.request("/api/desktop/shutdown", {
|
|
method: "POST",
|
|
headers: { authorization: "Bearer nope" },
|
|
});
|
|
expect(wrong.status).toBe(401);
|
|
const missing = await t.app.request("/api/desktop/shutdown", { method: "POST" });
|
|
expect(missing.status).toBe(401);
|
|
} finally {
|
|
await t.cleanup();
|
|
}
|
|
|
|
const plain = await createTestApp();
|
|
try {
|
|
// Outside desktop mode the route is not mounted; the request falls through to the
|
|
// cookie auth middleware, which rejects the cookieless caller with 401.
|
|
const res = await plain.app.request("/api/desktop/shutdown", {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${TOKEN}` },
|
|
});
|
|
expect(res.status).toBe(401);
|
|
} finally {
|
|
await plain.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("desktop-session password change", () => {
|
|
async function desktopCookie(t: Awaited<ReturnType<typeof desktopApp>>): Promise<string> {
|
|
const res = await t.app.request(`/api/auth/desktop-login?token=${TOKEN}`);
|
|
return res.headers.get("set-cookie")!.split(";")[0]!;
|
|
}
|
|
|
|
it("allows omitting oldPassword for a desktop session and clears the initial flag", async () => {
|
|
const t = await desktopApp();
|
|
try {
|
|
const cookie = await desktopCookie(t);
|
|
const res = await apiClient(t.app, cookie).put("/api/me/password", {
|
|
newPassword: "brand-new-password",
|
|
});
|
|
expect(res.status).toBe(204);
|
|
const me = (await (await apiClient(t.app, cookie).get("/api/me")).json()) as MeResponse;
|
|
expect(me.user.passwordIsInitial).toBe(false);
|
|
} finally {
|
|
await t.cleanup();
|
|
}
|
|
});
|
|
|
|
it("still validates oldPassword when it is provided by a desktop session", async () => {
|
|
const t = await desktopApp();
|
|
try {
|
|
const cookie = await desktopCookie(t);
|
|
const res = await apiClient(t.app, cookie).put("/api/me/password", {
|
|
oldPassword: "wrong-password",
|
|
newPassword: "brand-new-password",
|
|
});
|
|
expect(res.status).toBe(400);
|
|
} finally {
|
|
await t.cleanup();
|
|
}
|
|
});
|
|
|
|
it("keeps requiring oldPassword for password-established sessions in desktop mode", async () => {
|
|
const t = await desktopApp();
|
|
try {
|
|
// Sign in via the regular login form against the same desktop-mode server.
|
|
const admin = await loginAdmin(t.app);
|
|
const res = await apiClient(t.app, admin.cookie).put("/api/me/password", {
|
|
newPassword: "brand-new-password",
|
|
});
|
|
expect(res.status).toBe(400);
|
|
} finally {
|
|
await t.cleanup();
|
|
}
|
|
});
|
|
});
|