-
Notifications
You must be signed in to change notification settings - Fork 413
fix: mark personal-preference reads private and uncacheable #1731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { DEFAULT_KEYBOARD_SHORTCUTS } from "@open-inspect/shared/types/keyboard-shortcuts"; | ||
| import type * as AuthenticateModule from "../auth/authenticate"; | ||
| import { | ||
| createTestRequestHandler, | ||
| ownerAuthorizationDatabase, | ||
| TEST_BACKGROUND_TASK_CONTEXT, | ||
| TEST_SERVICE_SECRETS, | ||
| } from "../router.test-support"; | ||
| import type { Env } from "../types"; | ||
| import { keyboardShortcutRoutes } from "./keyboard-shortcuts"; | ||
|
|
||
| const mocks = vi.hoisted(() => ({ authenticate: vi.fn() })); | ||
|
|
||
| vi.mock("../auth/authenticate", async (importOriginal) => ({ | ||
| ...(await importOriginal<typeof AuthenticateModule>()), | ||
| authenticate: mocks.authenticate, | ||
| })); | ||
|
|
||
| const mockStore = { get: vi.fn(), set: vi.fn() }; | ||
| vi.mock("../db/keyboard-shortcut-preferences", () => ({ | ||
| KeyboardShortcutPreferencesStore: vi.fn().mockImplementation(function () { | ||
| return mockStore; | ||
| }), | ||
| })); | ||
|
|
||
| const handleRequest = createTestRequestHandler([keyboardShortcutRoutes]); | ||
| const env = { ...TEST_SERVICE_SECRETS, DB: ownerAuthorizationDatabase() } as unknown as Env; | ||
|
|
||
| describe("keyboard shortcut routes", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mocks.authenticate.mockImplementation(async (request: Request) => ({ | ||
| principal: { kind: "user", userId: "user-1" }, | ||
| request, | ||
| })); | ||
| }); | ||
|
|
||
| it("answers a personal read privately and uncacheably", async () => { | ||
| mockStore.get.mockResolvedValue({ "session.new": "mod+k" }); | ||
|
|
||
| const response = await handleRequest( | ||
| new Request("https://test.local/keyboard-shortcuts"), | ||
| env, | ||
| TEST_BACKGROUND_TASK_CONTEXT | ||
| ); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(response.headers.get("Cache-Control")).toBe("private, no-store"); | ||
| await expect(response.json()).resolves.toEqual({ shortcuts: { "session.new": "mod+k" } }); | ||
| expect(mockStore.get).toHaveBeenCalledWith("user-1"); | ||
| }); | ||
|
|
||
| it("declares no cache policy on the write", async () => { | ||
| mockStore.set.mockResolvedValue(DEFAULT_KEYBOARD_SHORTCUTS); | ||
|
|
||
| const response = await handleRequest( | ||
| new Request("https://test.local/keyboard-shortcuts", { | ||
| method: "PUT", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ shortcuts: DEFAULT_KEYBOARD_SHORTCUTS }), | ||
| }), | ||
| env, | ||
| TEST_BACKGROUND_TASK_CONTEXT | ||
| ); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(response.headers.get("Cache-Control")).toBeNull(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { DEFAULT_ENABLED_MODELS } from "@open-inspect/shared/models"; | ||
| import type * as AuthenticateModule from "../auth/authenticate"; | ||
| import { | ||
| createTestRequestHandler, | ||
| ownerAuthorizationDatabase, | ||
| TEST_BACKGROUND_TASK_CONTEXT, | ||
| TEST_SERVICE_SECRETS, | ||
| } from "../router.test-support"; | ||
| import type { Env } from "../types"; | ||
| import { modelPreferencesRoutes } from "./model-preferences"; | ||
|
|
||
| const mocks = vi.hoisted(() => ({ authenticate: vi.fn() })); | ||
|
|
||
| vi.mock("../auth/authenticate", async (importOriginal) => ({ | ||
| ...(await importOriginal<typeof AuthenticateModule>()), | ||
| authenticate: mocks.authenticate, | ||
| })); | ||
|
|
||
| const mockStore = { getEnabledModels: vi.fn(), setEnabledModels: vi.fn() }; | ||
| vi.mock("../db/model-preferences", async (importOriginal) => ({ | ||
| ...(await importOriginal<Record<string, unknown>>()), | ||
| ModelPreferencesStore: vi.fn().mockImplementation(function () { | ||
| return mockStore; | ||
| }), | ||
| })); | ||
|
|
||
| const handleRequest = createTestRequestHandler([modelPreferencesRoutes]); | ||
| const env = { | ||
| ...TEST_SERVICE_SECRETS, | ||
| SCM_PROVIDER: "github", | ||
| DB: ownerAuthorizationDatabase(), | ||
| } as unknown as Env; | ||
|
|
||
| describe("model preference routes", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mocks.authenticate.mockImplementation(async (request: Request) => ({ | ||
| principal: { kind: "user", userId: "user-1" }, | ||
| request, | ||
| })); | ||
| }); | ||
|
|
||
| it("answers the preference read privately and uncacheably", async () => { | ||
| mockStore.getEnabledModels.mockResolvedValue(null); | ||
|
|
||
| const response = await handleRequest( | ||
| new Request("https://test.local/model-preferences"), | ||
| env, | ||
| TEST_BACKGROUND_TASK_CONTEXT | ||
| ); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(response.headers.get("Cache-Control")).toBe("private, no-store"); | ||
| await expect(response.json()).resolves.toEqual({ enabledModels: DEFAULT_ENABLED_MODELS }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,6 +103,7 @@ modelPreferencesRoutes.get( | |
| admit({ | ||
| ...GITHUB_USER_OR_SERVICE_ROUTE, | ||
| authorization: activeGlobal({ actorlessGrants: [{ service: "slack-bot" }] }), | ||
| cacheControl: "private, no-store", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [deep review] This policy does not reach the browser-facing
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed and fixed in a74563a: |
||
| }), | ||
| (c) => getModelPreferences(c.var.admitted.ctx) | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import type * as AuthenticateModule from "../auth/authenticate"; | ||
| import { | ||
| createTestRequestHandler, | ||
| ownerAuthorizationDatabase, | ||
| TEST_BACKGROUND_TASK_CONTEXT, | ||
| TEST_SERVICE_SECRETS, | ||
| } from "../router.test-support"; | ||
| import type { Env } from "../types"; | ||
| import { skillRoutes } from "./skills"; | ||
|
|
||
| const mocks = vi.hoisted(() => ({ authenticate: vi.fn() })); | ||
|
|
||
| vi.mock("../auth/authenticate", async (importOriginal) => ({ | ||
| ...(await importOriginal<typeof AuthenticateModule>()), | ||
| authenticate: mocks.authenticate, | ||
| })); | ||
|
|
||
| const mockProfileStore = { list: vi.fn() }; | ||
| vi.mock("../db/skill-profiles", async (importOriginal) => ({ | ||
| ...(await importOriginal<Record<string, unknown>>()), | ||
| SkillProfileStore: vi.fn().mockImplementation(function () { | ||
| return mockProfileStore; | ||
| }), | ||
| })); | ||
|
|
||
| const handleRequest = createTestRequestHandler([skillRoutes]); | ||
| const env = { ...TEST_SERVICE_SECRETS, DB: ownerAuthorizationDatabase() } as unknown as Env; | ||
|
|
||
| describe("skill profile routes", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mocks.authenticate.mockImplementation(async (request: Request) => ({ | ||
| principal: { kind: "user", userId: "user-1" }, | ||
| request, | ||
| })); | ||
| }); | ||
|
|
||
| it("answers the caller's own profiles privately and uncacheably", async () => { | ||
| mockProfileStore.list.mockResolvedValue([]); | ||
|
|
||
| const response = await handleRequest( | ||
| new Request("https://test.local/skill-profiles"), | ||
| env, | ||
| TEST_BACKGROUND_TASK_CONTEXT | ||
| ); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(response.headers.get("Cache-Control")).toBe("private, no-store"); | ||
| await expect(response.json()).resolves.toEqual({ profiles: [] }); | ||
| expect(mockProfileStore.list).toHaveBeenCalledWith("user-1"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[deep review] These three new route suites add roughly 180 lines of repeated authentication/store/env scaffolding to assert a three-line policy change, but they do not add a distinct contract boundary: the catalog snapshot records each exact declaration, the admission-matrix integration test exercises each declared policy on production routes, and
request-lifecycle.test.tsverifies that the policy is stamped onto responses. The incidental payload/store assertions cover unchanged behavior, and this testing shape still missed the model-preferences BFF dropping the header. Please keep the policy coverage in those canonical tests and spend the targeted request-level coverage at the web boundary where behavior can actually be lost. The PUT assertion below should also go: absence ofCache-Controlis not a behavioral invariant and would make a future no-store hardening fail for no reason.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. The three route-local suites are removed in a74563a; the conformance snapshot and the lifecycle test carry the policy coverage, and the new test sits at the web boundary where the header was actually lost. The PUT no-header assertion went with them.