Add full sprint pricing workflow and supporting infrastructure: - New sidebar navigation entry for the sprint dashboard - PocketBase category collection schema and migration script - Server API routes for sprint item claims, attachments, counts, and file proxying - Dev identity utilities and UI switcher for multi-user testing - Reusable FilterTabs component replacing ItemStatusTabs - Updated TypeScript types for sprint-related entities Also include: - Fix PocketBase file URL method casing (getUrl → getURL) across codebase - Add development utility, smoke test, and helper scripts - Update package.json with new sprint migration command
101 lines
2.9 KiB
TypeScript
101 lines
2.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
DEFAULT_DEV_EMAIL,
|
|
DEV_IDENTITY_COOKIE,
|
|
DEV_IDENTITY_HEADER,
|
|
getServerIdentity,
|
|
} from "./dev-identity";
|
|
import type { NextRequest } from "next/server";
|
|
|
|
/**
|
|
* Minimal NextRequest stub — only the bits getServerIdentity reads.
|
|
* Avoids pulling in Next's actual class for unit tests.
|
|
*/
|
|
function makeReq(opts: {
|
|
header?: string;
|
|
cookie?: string;
|
|
envEmail?: string;
|
|
}): NextRequest {
|
|
const headers = new Headers();
|
|
if (opts.header) headers.set(DEV_IDENTITY_HEADER, opts.header);
|
|
|
|
// Build a minimal cookie jar NextRequest understands.
|
|
const cookieValue = opts.cookie
|
|
? `${DEV_IDENTITY_COOKIE}=${opts.cookie}`
|
|
: "";
|
|
|
|
const req = {
|
|
headers,
|
|
cookies: {
|
|
get(name: string) {
|
|
if (name !== DEV_IDENTITY_COOKIE) return undefined;
|
|
if (!cookieValue) return undefined;
|
|
// mimic NextRequest's behavior: returns the raw value
|
|
const eq = cookieValue.indexOf("=");
|
|
return { name, value: cookieValue.slice(eq + 1) };
|
|
},
|
|
},
|
|
} as unknown as NextRequest;
|
|
|
|
if (opts.envEmail !== undefined) {
|
|
process.env.DEV_USER_EMAIL = opts.envEmail;
|
|
} else {
|
|
delete process.env.DEV_USER_EMAIL;
|
|
}
|
|
|
|
return req;
|
|
}
|
|
|
|
describe("getServerIdentity", () => {
|
|
it("returns the X-User-Email header when present", () => {
|
|
const req = makeReq({ header: "alice@example.com" });
|
|
expect(getServerIdentity(req)).toBe("alice@example.com");
|
|
});
|
|
|
|
it("falls back to the cookie when the header is missing", () => {
|
|
const req = makeReq({
|
|
cookie: encodeURIComponent("bob@example.com"),
|
|
});
|
|
expect(getServerIdentity(req)).toBe("bob@example.com");
|
|
});
|
|
|
|
it("falls back to DEV_USER_EMAIL when header and cookie are missing", () => {
|
|
const req = makeReq({ envEmail: "ci@example.com" });
|
|
expect(getServerIdentity(req)).toBe("ci@example.com");
|
|
});
|
|
|
|
it("falls back to the default when nothing is set", () => {
|
|
const req = makeReq({});
|
|
expect(getServerIdentity(req)).toBe(DEFAULT_DEV_EMAIL);
|
|
});
|
|
|
|
it("prefers the header over the cookie", () => {
|
|
const req = makeReq({
|
|
header: "header@example.com",
|
|
cookie: encodeURIComponent("cookie@example.com"),
|
|
});
|
|
expect(getServerIdentity(req)).toBe("header@example.com");
|
|
});
|
|
|
|
it("prefers the cookie over the env default", () => {
|
|
const req = makeReq({
|
|
cookie: encodeURIComponent("cookie@example.com"),
|
|
envEmail: "env@example.com",
|
|
});
|
|
expect(getServerIdentity(req)).toBe("cookie@example.com");
|
|
});
|
|
|
|
it("trims whitespace from all sources", () => {
|
|
const req = makeReq({ header: " spaced@example.com " });
|
|
expect(getServerIdentity(req)).toBe("spaced@example.com");
|
|
});
|
|
|
|
it("ignores empty values and falls through to the next source", () => {
|
|
const req = makeReq({
|
|
header: " ",
|
|
cookie: encodeURIComponent(" "),
|
|
envEmail: "env@example.com",
|
|
});
|
|
expect(getServerIdentity(req)).toBe("env@example.com");
|
|
});
|
|
});
|