b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
134 lines
3.8 KiB
TypeScript
134 lines
3.8 KiB
TypeScript
/**
|
|
* Unit tests for _rev token generation and validation.
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import { encodeRev, decodeRev, validateRev } from "../../../src/api/rev.js";
|
|
import type { ContentItem } from "../../../src/database/repositories/types.js";
|
|
|
|
function makeItem(overrides: Partial<ContentItem> = {}): ContentItem {
|
|
return {
|
|
id: "item_1",
|
|
type: "posts",
|
|
slug: "test",
|
|
status: "draft",
|
|
data: {},
|
|
authorId: null,
|
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
updatedAt: "2026-01-15T12:30:00.000Z",
|
|
publishedAt: null,
|
|
scheduledAt: null,
|
|
liveRevisionId: null,
|
|
draftRevisionId: null,
|
|
version: 3,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("encodeRev", () => {
|
|
it("produces a base64-encoded string", () => {
|
|
const item = makeItem();
|
|
const rev = encodeRev(item);
|
|
|
|
expect(rev).toBeTruthy();
|
|
// Should be valid base64
|
|
expect(() => atob(rev)).not.toThrow();
|
|
});
|
|
|
|
it("encodes version and updatedAt", () => {
|
|
const item = makeItem({ version: 5, updatedAt: "2026-02-14T10:00:00.000Z" });
|
|
const rev = encodeRev(item);
|
|
const decoded = atob(rev);
|
|
|
|
expect(decoded).toBe("5:2026-02-14T10:00:00.000Z");
|
|
});
|
|
|
|
it("produces different revs for different versions", () => {
|
|
const rev1 = encodeRev(makeItem({ version: 1 }));
|
|
const rev2 = encodeRev(makeItem({ version: 2 }));
|
|
expect(rev1).not.toBe(rev2);
|
|
});
|
|
|
|
it("produces different revs for different updatedAt", () => {
|
|
const rev1 = encodeRev(makeItem({ updatedAt: "2026-01-01T00:00:00.000Z" }));
|
|
const rev2 = encodeRev(makeItem({ updatedAt: "2026-01-02T00:00:00.000Z" }));
|
|
expect(rev1).not.toBe(rev2);
|
|
});
|
|
});
|
|
|
|
describe("decodeRev", () => {
|
|
it("decodes a valid rev", () => {
|
|
const rev = btoa("5:2026-02-14T10:00:00.000Z");
|
|
const result = decodeRev(rev);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result!.version).toBe(5);
|
|
expect(result!.updatedAt).toBe("2026-02-14T10:00:00.000Z");
|
|
});
|
|
|
|
it("returns null for invalid base64", () => {
|
|
expect(decodeRev("not-valid-base64!!!")).toBeNull();
|
|
});
|
|
|
|
it("returns null for missing colon", () => {
|
|
expect(decodeRev(btoa("nocolon"))).toBeNull();
|
|
});
|
|
|
|
it("returns null for non-numeric version", () => {
|
|
expect(decodeRev(btoa("abc:2026-01-01"))).toBeNull();
|
|
});
|
|
|
|
it("round-trips with encodeRev", () => {
|
|
const item = makeItem({ version: 7, updatedAt: "2026-03-01T08:15:30.000Z" });
|
|
const rev = encodeRev(item);
|
|
const decoded = decodeRev(rev);
|
|
|
|
expect(decoded).not.toBeNull();
|
|
expect(decoded!.version).toBe(7);
|
|
expect(decoded!.updatedAt).toBe("2026-03-01T08:15:30.000Z");
|
|
});
|
|
});
|
|
|
|
describe("validateRev", () => {
|
|
it("returns valid when no rev is provided", () => {
|
|
const result = validateRev(undefined, makeItem());
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it("returns valid when rev matches", () => {
|
|
const item = makeItem({ version: 3, updatedAt: "2026-01-15T12:30:00.000Z" });
|
|
const rev = encodeRev(item);
|
|
|
|
const result = validateRev(rev, item);
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it("returns invalid when version mismatches", () => {
|
|
const item = makeItem({ version: 3, updatedAt: "2026-01-15T12:30:00.000Z" });
|
|
const staleRev = btoa("2:2026-01-15T12:30:00.000Z"); // Version 2, but item is at 3
|
|
|
|
const result = validateRev(staleRev, item);
|
|
expect(result.valid).toBe(false);
|
|
if (!result.valid) {
|
|
expect(result.message).toContain("modified");
|
|
}
|
|
});
|
|
|
|
it("returns invalid when updatedAt mismatches", () => {
|
|
const item = makeItem({ version: 3, updatedAt: "2026-01-15T12:30:00.000Z" });
|
|
const staleRev = btoa("3:2026-01-14T00:00:00.000Z"); // Right version, wrong timestamp
|
|
|
|
const result = validateRev(staleRev, item);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it("returns invalid for malformed rev", () => {
|
|
const result = validateRev("garbage", makeItem());
|
|
expect(result.valid).toBe(false);
|
|
if (!result.valid) {
|
|
expect(result.message).toContain("Malformed");
|
|
}
|
|
});
|
|
});
|