fix: resolve TODO.md security/perf/test-coverage backlog

Fixes the 13-item codebase health scan backlog: wraps meal-plan
generation in a transaction, adds missing userId/GIN indexes, fixes
an IPv6-parsing gap in the webhook SSRF guard (and an identical
duplicated bug in the AI URL-import path, now consolidated onto one
implementation), paginates the collections list, dedupes the AI
recipe Zod schemas, wires up Stripe tier sync, rate-limits AI key
rotation, gets `pnpm typecheck` actually working, and adds test
coverage for the previously-untested admin/webhooks routes.

Two flagged issues (collection removeRecipeId IDOR, tier-limit race)
turned out to already be fixed/non-issues on inspection — noted in
TODO.md rather than silently dropped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-02 12:12:42 +02:00
parent 2154512e54
commit d2faf98ac1
38 changed files with 7598 additions and 315 deletions
+17 -34
View File
@@ -4,6 +4,7 @@ import { TierLimitError } from "../tiers";
const mockDb = vi.hoisted(() => ({
select: vi.fn(),
insert: vi.fn(),
execute: vi.fn(),
}));
vi.mock("@epicure/db", () => ({
@@ -22,7 +23,7 @@ vi.mock("@epicure/db", () => ({
}));
// Import after mock
const { checkTierLimit, incrementUsage } = await import("../tiers");
const { checkAndIncrementTierLimit, incrementUsage } = await import("../tiers");
function makeChain(finalValue: unknown) {
const chain = {
@@ -54,7 +55,7 @@ describe("TierLimitError", () => {
});
});
describe("checkTierLimit", () => {
describe("checkAndIncrementTierLimit", () => {
const tierDef = {
tier: "free",
maxRecipes: 10,
@@ -63,50 +64,32 @@ describe("checkTierLimit", () => {
maxPublicRecipes: 3,
};
it("does not throw when usage is under limit", async () => {
mockDb.select
.mockReturnValueOnce(makeChain([tierDef]))
.mockReturnValueOnce(makeChain([{ aiCallsUsed: 3, recipeCount: 2, storageUsedMb: 0 }]));
it("does not throw when the atomic upsert returns a row (under limit)", async () => {
mockDb.select.mockReturnValueOnce(makeChain([tierDef]));
mockDb.execute.mockResolvedValueOnce([{ aiCallsUsed: 4 }]);
await expect(checkTierLimit("user1", "free", "aiCall")).resolves.toBeUndefined();
await expect(checkAndIncrementTierLimit("user1", "free", "aiCall")).resolves.toBeUndefined();
});
it("throws TierLimitError when aiCall limit reached", async () => {
mockDb.select
.mockReturnValueOnce(makeChain([tierDef]))
.mockReturnValueOnce(makeChain([{ aiCallsUsed: 5, recipeCount: 0, storageUsedMb: 0 }]));
it("throws TierLimitError when the upsert's WHERE clause excludes the row (limit reached)", async () => {
mockDb.select.mockReturnValueOnce(makeChain([tierDef]));
mockDb.execute.mockResolvedValueOnce([]);
await expect(checkTierLimit("user1", "free", "aiCall")).rejects.toThrow(TierLimitError);
await expect(checkAndIncrementTierLimit("user1", "free", "aiCall")).rejects.toThrow(TierLimitError);
});
it("throws TierLimitError when recipe limit reached", async () => {
mockDb.select
.mockReturnValueOnce(makeChain([tierDef]))
.mockReturnValueOnce(makeChain([{ aiCallsUsed: 0, recipeCount: 10, storageUsedMb: 0 }]));
it("throws TierLimitError for recipe key when limit reached", async () => {
mockDb.select.mockReturnValueOnce(makeChain([tierDef]));
mockDb.execute.mockResolvedValueOnce([]);
await expect(checkTierLimit("user1", "free", "recipe")).rejects.toThrow(TierLimitError);
});
it("does not throw when no usage row exists (treats as zero)", async () => {
mockDb.select
.mockReturnValueOnce(makeChain([tierDef]))
.mockReturnValueOnce(makeChain([]));
await expect(checkTierLimit("user1", "free", "aiCall")).resolves.toBeUndefined();
await expect(checkAndIncrementTierLimit("user1", "free", "recipe")).rejects.toThrow(TierLimitError);
});
it("does not throw when tier definition does not exist", async () => {
mockDb.select.mockReturnValueOnce(makeChain([]));
await expect(checkTierLimit("user1", "free", "aiCall")).resolves.toBeUndefined();
});
it("does not throw for storage key (no limit enforced)", async () => {
mockDb.select
.mockReturnValueOnce(makeChain([tierDef]))
.mockReturnValueOnce(makeChain([{ aiCallsUsed: 0, recipeCount: 0, storageUsedMb: 9999 }]));
await expect(checkTierLimit("user1", "free", "storage")).resolves.toBeUndefined();
await expect(checkAndIncrementTierLimit("user1", "free", "aiCall")).resolves.toBeUndefined();
expect(mockDb.execute).not.toHaveBeenCalled();
});
});