feat(auth): add remembered local login to the frontend
This commit is contained in:
@@ -1,16 +1,23 @@
|
||||
// frontend/src/shell/SteerInput.test.tsx
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { server } from "../test/msw";
|
||||
import { canonicalWorkspaceFixture, workspaceSummaryFixture } from "../test/workspace-fixtures";
|
||||
import { canonicalWorkspaceFixture, workspaceRevisionFixture, workspaceSummaryFixture } from "../test/workspace-fixtures";
|
||||
import { useSessionStore } from "../store/sessionStore";
|
||||
import { workspacePreferences } from "../workspaces/preferences";
|
||||
import { ComposerFooter, ContextGauge, SteerInput } from "./SteerInput";
|
||||
import { setAuthState } from "../auth/authState";
|
||||
|
||||
const userA = {
|
||||
issuer: "local", subject: "user-a", roles: ["user"] as const,
|
||||
permissions: ["session.use"], isAdmin: false, csrfToken: "a".repeat(43), session: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
setAuthState(userA);
|
||||
server.use(
|
||||
http.post("/api/sessions/:id/steer", () =>
|
||||
new HttpResponse(null, { status: 204 }),
|
||||
@@ -73,6 +80,131 @@ test("submitting typed text POSTs to /steer and clears the input", async () => {
|
||||
expect(input).toHaveValue("");
|
||||
});
|
||||
|
||||
test("a delayed steer from user A cannot mutate user B's store or composer", async () => {
|
||||
let release!: () => void;
|
||||
let started!: () => void;
|
||||
let settled!: () => void;
|
||||
const held = new Promise<void>((resolve) => { release = resolve; });
|
||||
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
|
||||
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
|
||||
server.use(http.post("/api/sessions/:id/steer", async () => {
|
||||
started();
|
||||
try {
|
||||
await held;
|
||||
return new HttpResponse(null, { status: 204 });
|
||||
} finally {
|
||||
settled();
|
||||
}
|
||||
}));
|
||||
setAuthState(userA);
|
||||
const view = render(<SteerInput sessionId="s1" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
await userEvent.type(input, "A-only steer");
|
||||
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
||||
await requestStarted;
|
||||
|
||||
act(() => setAuthState({ ...userA, subject: "user-b", csrfToken: "b".repeat(43) }));
|
||||
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "B-entry" }));
|
||||
release();
|
||||
await act(async () => { await requestSettled; });
|
||||
|
||||
expect(useSessionStore.getState().lastUserEntry).toEqual({ kind: "input", text: "B-entry" });
|
||||
expect(input).toHaveValue("A-only steer");
|
||||
view.unmount();
|
||||
});
|
||||
|
||||
test("a settings preflight from user A prevents session POST after user B logs in", async () => {
|
||||
let releaseSettings!: () => void;
|
||||
let settingsStarted!: () => void;
|
||||
let settingsSettled!: () => void;
|
||||
let sessionPosts = 0;
|
||||
const settingsGate = new Promise<void>((resolve) => { releaseSettings = resolve; });
|
||||
const started = new Promise<void>((resolve) => { settingsStarted = resolve; });
|
||||
const settled = new Promise<void>((resolve) => { settingsSettled = resolve; });
|
||||
server.use(
|
||||
http.get("/api/settings", async () => {
|
||||
settingsStarted();
|
||||
try {
|
||||
await settingsGate;
|
||||
return HttpResponse.json({ workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low" });
|
||||
} finally {
|
||||
settingsSettled();
|
||||
}
|
||||
}),
|
||||
http.post("/api/sessions", () => {
|
||||
sessionPosts += 1;
|
||||
return HttpResponse.json({ id: "a-session" });
|
||||
}),
|
||||
);
|
||||
const userB = { ...userA, subject: "user-b", csrfToken: "b".repeat(43) };
|
||||
const view = render(<SteerInput sessionId={null} />);
|
||||
const input = screen.getByRole("textbox", { name: /new question/i });
|
||||
await userEvent.type(input, "A-only question");
|
||||
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
||||
await started;
|
||||
|
||||
act(() => setAuthState(userB));
|
||||
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "B-entry" }));
|
||||
releaseSettings();
|
||||
await act(async () => { await settled; });
|
||||
|
||||
expect(sessionPosts).toBe(0);
|
||||
expect(useSessionStore.getState().lastUserEntry).toEqual({ kind: "input", text: "B-entry" });
|
||||
view.unmount();
|
||||
});
|
||||
|
||||
test("a workspace-policy preflight from user A prevents session POST after user B logs in", async () => {
|
||||
workspacePreferences.save({
|
||||
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
|
||||
});
|
||||
let releaseWorkspaces!: () => void;
|
||||
let workspacesStarted!: () => void;
|
||||
let workspacesSettled!: () => void;
|
||||
let sessionPosts = 0;
|
||||
const workspacesGate = new Promise<void>((resolve) => { releaseWorkspaces = resolve; });
|
||||
const started = new Promise<void>((resolve) => { workspacesStarted = resolve; });
|
||||
const settled = new Promise<void>((resolve) => { workspacesSettled = resolve; });
|
||||
server.use(
|
||||
http.get("/api/workspaces", () => HttpResponse.json([{
|
||||
...workspaceSummaryFixture("psd-clinical", {
|
||||
displayName: "PSD Clinical",
|
||||
revision: workspaceRevisionFixture("psd-clinical"),
|
||||
}),
|
||||
}])),
|
||||
http.get("/api/workspaces/psd-clinical", async () => {
|
||||
workspacesStarted();
|
||||
try {
|
||||
await workspacesGate;
|
||||
return HttpResponse.json({
|
||||
workspace: canonicalWorkspaceFixture("psd-clinical"),
|
||||
revision: workspaceRevisionFixture("psd-clinical"),
|
||||
});
|
||||
} finally {
|
||||
workspacesSettled();
|
||||
}
|
||||
}),
|
||||
http.post("/api/sessions", () => {
|
||||
sessionPosts += 1;
|
||||
return HttpResponse.json({ id: "a-session" });
|
||||
}),
|
||||
);
|
||||
const userB = { ...userA, subject: "user-b", csrfToken: "b".repeat(43) };
|
||||
const view = render(<SteerInput sessionId={null} />);
|
||||
const input = screen.getByRole("textbox", { name: /new question/i });
|
||||
await userEvent.type(input, "A-policy-question");
|
||||
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
||||
await started;
|
||||
|
||||
act(() => setAuthState(userB));
|
||||
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "B-entry" }));
|
||||
releaseWorkspaces();
|
||||
await act(async () => { await settled; });
|
||||
|
||||
expect(sessionPosts).toBe(0);
|
||||
expect(useSessionStore.getState().lastUserEntry).toEqual({ kind: "input", text: "B-entry" });
|
||||
view.unmount();
|
||||
});
|
||||
|
||||
test("pressing Enter in the input submits the steer", async () => {
|
||||
let captured: unknown = null;
|
||||
server.use(
|
||||
|
||||
Reference in New Issue
Block a user