docfast/src/__tests__/keys-branch-coverage.test.ts
OpenClaw Subagent f5ec837e20
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Failing after 17m26s
test: improve branch coverage for billing.ts and keys.ts
billing.ts branches: 78.66% → 82.66%
- isDocFastSubscription expanded product object (not just string)
- isDocFastSubscription error handling path
- getOrCreateProPrice: new product + price creation
- getOrCreateProPrice: existing product, no active prices

keys.ts:
- createProKey UPSERT ON CONFLICT path (DB-only key)
- downgradeByCustomer customer not found in cache or DB
- updateKeyEmail DB fallback not-found path
- updateEmailByCustomer DB fallback not-found path

14 new tests, all 743 passing.
2026-03-14 11:09:01 +01:00

222 lines
7.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
// Unmock keys service — test the real implementation
vi.unmock("../services/keys.js");
vi.mock("../services/db.js", () => ({
default: { query: vi.fn(), connect: vi.fn(), on: vi.fn(), end: vi.fn() },
pool: { query: vi.fn(), connect: vi.fn(), on: vi.fn(), end: vi.fn() },
queryWithRetry: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
connectWithRetry: vi.fn().mockResolvedValue(undefined),
initDatabase: vi.fn().mockResolvedValue(undefined),
cleanupStaleData: vi.fn(),
isTransientError: vi.fn(),
}));
vi.mock("../services/logger.js", () => ({
default: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
}));
import { queryWithRetry } from "../services/db.js";
import { createProKey, downgradeByCustomer, updateKeyEmail, updateEmailByCustomer } from "../services/keys.js";
const mockQuery = vi.mocked(queryWithRetry);
describe("Keys Branch Coverage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("createProKey - UPSERT conflict path (line 142)", () => {
it("should return existing key when stripe_customer_id already exists in DB but NOT in cache", async () => {
// Scenario: Another pod created a key for this customer, so it's in DB but not in our cache
// The UPSERT will hit ON CONFLICT and return the existing key via RETURNING clause
const existingKey = {
key: "df_pro_existing_abc",
tier: "pro",
email: "existing@test.com",
created_at: "2026-01-01T00:00:00.000Z",
stripe_customer_id: "cus_existing",
};
// Mock: UPSERT returns the existing key (ON CONFLICT triggered)
mockQuery.mockResolvedValueOnce({ rows: [existingKey], rowCount: 1 } as any);
const result = await createProKey("new@test.com", "cus_existing");
// Should return the existing key
expect(result.key).toBe("df_pro_existing_abc");
expect(result.email).toBe("existing@test.com"); // Original email, not the new one
expect(result.stripeCustomerId).toBe("cus_existing");
expect(result.tier).toBe("pro");
// Verify UPSERT was called
const upsertCall = mockQuery.mock.calls.find(
(c) => typeof c[0] === "string" && c[0].includes("ON CONFLICT")
);
expect(upsertCall).toBeTruthy();
});
it("should handle conflict when inserting new key with existing customer ID", async () => {
// First call: load empty cache
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
const { loadKeys } = await import("../services/keys.js");
await loadKeys();
vi.clearAllMocks();
const conflictingKey = {
key: "df_pro_conflict_xyz",
tier: "pro",
email: "conflict@test.com",
created_at: "2025-12-31T00:00:00.000Z",
stripe_customer_id: "cus_conflict",
};
// UPSERT returns existing key on conflict
mockQuery.mockResolvedValueOnce({ rows: [conflictingKey], rowCount: 1 } as any);
const result = await createProKey("different-email@test.com", "cus_conflict");
expect(result.key).toBe("df_pro_conflict_xyz");
expect(result.email).toBe("conflict@test.com"); // Original, not the new email
});
});
describe("downgradeByCustomer - customer not found (lines 153-155)", () => {
it("should return false when customer is NOT in cache AND NOT in DB", async () => {
// Mock: SELECT query returns empty (customer not in DB)
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
const result = await downgradeByCustomer("cus_nonexistent");
expect(result).toBe(false);
// Verify SELECT was called
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("SELECT"),
expect.arrayContaining(["cus_nonexistent"])
);
// Verify UPDATE was NOT called (no point updating a non-existent key)
const updateCalls = mockQuery.mock.calls.filter((c) =>
(c[0] as string).includes("UPDATE")
);
expect(updateCalls).toHaveLength(0);
});
it("should return false for completely unknown stripe customer ID", async () => {
// Load empty cache first
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
const { loadKeys } = await import("../services/keys.js");
await loadKeys();
vi.clearAllMocks();
// Mock: DB also doesn't have this customer
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
const result = await downgradeByCustomer("cus_unknown_12345");
expect(result).toBe(false);
});
});
describe("updateKeyEmail - DB fallback path (line 175)", () => {
it("should return false when key is NOT in cache AND NOT in DB", async () => {
// Mock: SELECT query returns empty (key not in DB)
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
const result = await updateKeyEmail("df_pro_nonexistent", "new@test.com");
expect(result).toBe(false);
// Verify SELECT was called
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("SELECT"),
expect.arrayContaining(["df_pro_nonexistent"])
);
// Verify UPDATE was NOT called
const updateCalls = mockQuery.mock.calls.filter((c) =>
(c[0] as string).includes("UPDATE")
);
expect(updateCalls).toHaveLength(0);
});
it("should update and cache when key exists in DB but not in cache", async () => {
const dbKey = {
key: "df_pro_db_only",
tier: "pro",
email: "old@test.com",
created_at: "2026-01-15T00:00:00.000Z",
stripe_customer_id: "cus_db_only",
};
// Mock: SELECT returns the key from DB
mockQuery
.mockResolvedValueOnce({ rows: [dbKey], rowCount: 1 } as any)
.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); // UPDATE success
const result = await updateKeyEmail("df_pro_db_only", "updated@test.com");
expect(result).toBe(true);
// Verify UPDATE was called
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("UPDATE"),
expect.arrayContaining(["updated@test.com", "df_pro_db_only"])
);
});
});
describe("updateEmailByCustomer - DB fallback path (line 175)", () => {
it("should return false when customer is NOT in cache AND NOT in DB", async () => {
// Mock: SELECT query returns empty
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
const result = await updateEmailByCustomer("cus_nonexistent", "new@test.com");
expect(result).toBe(false);
// Verify SELECT was called
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("SELECT"),
expect.arrayContaining(["cus_nonexistent"])
);
// Verify UPDATE was NOT called
const updateCalls = mockQuery.mock.calls.filter((c) =>
(c[0] as string).includes("UPDATE")
);
expect(updateCalls).toHaveLength(0);
});
it("should update and cache when customer exists in DB but not in cache", async () => {
const dbKey = {
key: "df_pro_customer_db",
tier: "pro",
email: "oldcustomer@test.com",
created_at: "2026-02-01T00:00:00.000Z",
stripe_customer_id: "cus_db_customer",
};
// Mock: SELECT returns the key
mockQuery
.mockResolvedValueOnce({ rows: [dbKey], rowCount: 1 } as any)
.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); // UPDATE success
const result = await updateEmailByCustomer("cus_db_customer", "newcustomer@test.com");
expect(result).toBe(true);
// Verify UPDATE was called with correct params
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("UPDATE"),
expect.arrayContaining(["newcustomer@test.com", "cus_db_customer"])
);
});
});
});