Update features and dependencies
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { TierLimitError } from "../tiers";
|
||||
|
||||
const mockDb = vi.hoisted(() => ({
|
||||
select: vi.fn(),
|
||||
insert: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@epicure/db", () => ({
|
||||
db: mockDb,
|
||||
tierDefinitions: { tier: "tier" },
|
||||
userUsage: {
|
||||
userId: "user_id",
|
||||
month: "month",
|
||||
aiCallsUsed: "ai_calls_used",
|
||||
recipeCount: "recipe_count",
|
||||
storageUsedMb: "storage_used_mb",
|
||||
},
|
||||
eq: vi.fn((col, val) => ({ col, val, op: "eq" })),
|
||||
and: vi.fn((...args) => ({ args, op: "and" })),
|
||||
sql: vi.fn((strings, ...values) => ({ strings, values, op: "sql" })),
|
||||
}));
|
||||
|
||||
// Import after mock
|
||||
const { checkTierLimit, incrementUsage } = await import("../tiers");
|
||||
|
||||
function makeChain(finalValue: unknown) {
|
||||
const chain = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockResolvedValue(finalValue),
|
||||
};
|
||||
return chain;
|
||||
}
|
||||
|
||||
function makeInsertChain() {
|
||||
const chain = {
|
||||
values: vi.fn().mockReturnThis(),
|
||||
onConflictDoUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
return chain;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("TierLimitError", () => {
|
||||
it("has correct name and message", () => {
|
||||
const err = new TierLimitError("aiCall", "free");
|
||||
expect(err.name).toBe("TierLimitError");
|
||||
expect(err.message).toContain("aiCall");
|
||||
expect(err.message).toContain("free");
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkTierLimit", () => {
|
||||
const tierDef = {
|
||||
tier: "free",
|
||||
maxRecipes: 10,
|
||||
aiCallsPerMonth: 5,
|
||||
storageMb: 100,
|
||||
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 }]));
|
||||
|
||||
await expect(checkTierLimit("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 }]));
|
||||
|
||||
await expect(checkTierLimit("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 }]));
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe("incrementUsage", () => {
|
||||
it("calls insert with correct aiCall initial values", async () => {
|
||||
const chain = makeInsertChain();
|
||||
mockDb.insert.mockReturnValue(chain);
|
||||
|
||||
await incrementUsage("user1", "aiCall");
|
||||
|
||||
expect(chain.values).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: "user1",
|
||||
aiCallsUsed: 1,
|
||||
recipeCount: 0,
|
||||
storageUsedMb: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("calls insert with correct recipe initial values", async () => {
|
||||
const chain = makeInsertChain();
|
||||
mockDb.insert.mockReturnValue(chain);
|
||||
|
||||
await incrementUsage("user1", "recipe");
|
||||
|
||||
expect(chain.values).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
recipeCount: 1,
|
||||
aiCallsUsed: 0,
|
||||
storageUsedMb: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("respects custom amount", async () => {
|
||||
const chain = makeInsertChain();
|
||||
mockDb.insert.mockReturnValue(chain);
|
||||
|
||||
await incrementUsage("user1", "storage", 50);
|
||||
|
||||
expect(chain.values).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ storageUsedMb: 50 })
|
||||
);
|
||||
});
|
||||
|
||||
it("uses onConflictDoUpdate to increment (not overwrite)", async () => {
|
||||
const chain = makeInsertChain();
|
||||
mockDb.insert.mockReturnValue(chain);
|
||||
|
||||
await incrementUsage("user1", "aiCall");
|
||||
|
||||
expect(chain.onConflictDoUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
set: expect.objectContaining({ aiCallsUsed: expect.anything() }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user