ca26af9e9d
Security: - Clear the offline IndexedDB cache on sign-in (not only logout) so a previous user's persisted query cache and Yjs page bodies cannot leak to the next user on a shared device when the prior session ended without an explicit logout. Regressions: - Remove the double Yjs title write from the AI title-generation path: the title editor is bound to the Yjs `title` fragment and the server REST update reseeds it, so the local setContent raced that reseed and doubled/garbled the title. Conventions / i18n / docs: - Remove the unused showAiMenuAtom. - Register the 3 offline-fallback strings in en-US and ru-RU. - Fix the 5 broken links to the nonexistent docs/offline-sync-plan.md. Stability / simplification: - warmInfiniteAll now reports truncation (returns false) when it hits maxPages with a cursor still pending instead of silently succeeding. - space-tree make-offline catch logs the raw error and surfaces the real cause. - Move the Offline/Mobile/CORS CHANGELOG entries from the released 0.93.0 section into [Unreleased] (CORS is a documented breaking change). - Drop the pass-through sync-flag forwarders in use-page-collab-providers; set the atoms directly. - Collapse the three isSwaggerEnabled true-cases into it.each. Tests / architecture: - Extract collabTokenNeedsRefresh (pure) and cover all four token states. - Extract shouldPropagateTitleChange and cover the collab-origin skip; add a TitleEditor render test for the static-h1 vs collaborative-editor switch. - Add a use-auth test asserting the sign-in cache purge runs before login. - Add an OFFLINE_PERSIST_ROOTS guard test asserting every persisted root maps to an exported query-key factory; route make-offline's currentUser warm through a new userKeys factory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
// jwt-decode is mocked so we can drive the four token states deterministically
|
|
// (decode success with a chosen exp, or a thrown decode error).
|
|
const decodeMock = vi.hoisted(() => vi.fn());
|
|
vi.mock("jwt-decode", () => ({
|
|
jwtDecode: decodeMock,
|
|
}));
|
|
|
|
import { collabTokenNeedsRefresh } from "./collab-token";
|
|
|
|
const NOW_MS = 1_000_000_000; // fixed "now" in ms (so NOW_MS/1000 seconds)
|
|
|
|
beforeEach(() => {
|
|
decodeMock.mockReset();
|
|
});
|
|
|
|
describe("collabTokenNeedsRefresh", () => {
|
|
it("returns true when there is no token (fetch a fresh one)", () => {
|
|
expect(collabTokenNeedsRefresh(undefined, NOW_MS)).toBe(true);
|
|
// jwtDecode must not even be called for a missing token.
|
|
expect(decodeMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns true when the token is malformed (jwtDecode throws)", () => {
|
|
decodeMock.mockImplementation(() => {
|
|
throw new Error("invalid token");
|
|
});
|
|
expect(collabTokenNeedsRefresh("garbage", NOW_MS)).toBe(true);
|
|
});
|
|
|
|
it("returns false for a valid, not-yet-expired token (no reconnect)", () => {
|
|
// exp is in the future relative to NOW.
|
|
decodeMock.mockReturnValue({ exp: NOW_MS / 1000 + 60 });
|
|
expect(collabTokenNeedsRefresh("good", NOW_MS)).toBe(false);
|
|
});
|
|
|
|
it("returns true for a valid but expired token (refresh + reconnect)", () => {
|
|
// exp is in the past relative to NOW.
|
|
decodeMock.mockReturnValue({ exp: NOW_MS / 1000 - 60 });
|
|
expect(collabTokenNeedsRefresh("expired", NOW_MS)).toBe(true);
|
|
});
|
|
|
|
it("treats exp exactly equal to now as expired (>= boundary)", () => {
|
|
decodeMock.mockReturnValue({ exp: NOW_MS / 1000 });
|
|
expect(collabTokenNeedsRefresh("boundary", NOW_MS)).toBe(true);
|
|
});
|
|
});
|