137 lines
6.0 KiB
TypeScript
137 lines
6.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../../db.server", () => ({ db: {} }));
|
|
vi.mock("../../db/entities/entities.db.server", () => ({ findSimpleEntityByName: vi.fn() }));
|
|
vi.mock("../../db/stripe/checkoutSessions.db.server", () => ({ createCheckoutSessionStatus: vi.fn(), getCheckoutSessionStatus: vi.fn() }));
|
|
vi.mock("../../db/subscriptionProducts.db.server", () => ({ getAllSubscriptionFeatures: vi.fn() }));
|
|
vi.mock("@toolctrl/db/subscriptions.server", () => ({
|
|
createTenantSubscriptionUsageRecord: vi.fn(),
|
|
listTenantSubscriptionIds: vi.fn(),
|
|
updateTenantSubscriptionProduct: vi.fn(),
|
|
}));
|
|
vi.mock("../../db/tenantSubscriptions.db.server", () => ({ getTenantSubscription: vi.fn(), getTenantSubscriptions: vi.fn() }));
|
|
vi.mock("~/modules/emails/services/EmailService", () => ({ sendEmail: vi.fn() }));
|
|
vi.mock("../../helpers/ApiKeyHelper", () => ({ apiKeyCreditableStatusCodes: [], apiKeyIgnoreEndpoints: [] }));
|
|
vi.mock("../../stripe.server", () => ({
|
|
cancelStripeSubscription: vi.fn(),
|
|
createUsageRecord: vi.fn(),
|
|
getStripeSession: vi.fn(),
|
|
getStripeSubscription: vi.fn(),
|
|
}));
|
|
vi.mock("../../url.server", () => ({ getBaseURL: vi.fn() }));
|
|
vi.mock("./pricingService", () => ({ getAcquiredItemsFromCheckoutSession: vi.fn() }));
|
|
vi.mock("../../cache.server", () => ({ cachified: vi.fn(), clearCacheKey: vi.fn(), clearCacheKeyPrefix: vi.fn() }));
|
|
// Stable invalidate spy so tests can assert the cross-replica publish path.
|
|
const subscriptionCacheInvalidateMock = vi.fn().mockResolvedValue(undefined);
|
|
vi.mock("./subscriptionCache.server", () => ({
|
|
getSubscriptionCache: () => ({
|
|
invalidate: subscriptionCacheInvalidateMock,
|
|
get: vi.fn(),
|
|
set: vi.fn(),
|
|
subscribe: vi.fn(),
|
|
flushLocal: vi.fn(),
|
|
deleteLocal: vi.fn(),
|
|
}),
|
|
}));
|
|
vi.mock("~/utils/db/subscriptions/tenantSubscriptionProducts.db.server", () => ({
|
|
cancelTenantSubscriptionProduct: vi.fn(),
|
|
getTenantSubscriptionProductById: vi.fn(),
|
|
}));
|
|
vi.mock("~/modules/events/services/.server/EventsService", () => ({ default: { create: vi.fn() } }));
|
|
vi.mock("~/utils/db/tenants.db.server", () => ({ getTenant: vi.fn() }));
|
|
vi.mock("~/utils/db/users.db.server", () => ({ getUser: vi.fn() }));
|
|
vi.mock("~/modules/emails/utils/EmailTemplates", () => ({ default: {} }));
|
|
vi.mock("~/utils/db/appConfiguration.db.server", () => ({ getAppConfiguration: vi.fn() }));
|
|
|
|
import {
|
|
cancelTenantSubscription,
|
|
clearAllSubscriptionsCache,
|
|
clearTenantSubscriptionCache,
|
|
} from "./subscriptionService";
|
|
import { cancelStripeSubscription } from "../../stripe.server";
|
|
import { getTenantSubscriptionProductById } from "~/utils/db/subscriptions/tenantSubscriptionProducts.db.server";
|
|
import { clearCacheKey } from "../../cache.server";
|
|
import { listTenantSubscriptionIds } from "@toolctrl/db/subscriptions.server";
|
|
|
|
describe("cancelTenantSubscription", () => {
|
|
it("returns without logging or calling Stripe when the product is not subscribed", async () => {
|
|
vi.mocked(getTenantSubscriptionProductById).mockResolvedValueOnce({
|
|
id: "tenant-sub-product-id",
|
|
stripeSubscriptionId: null,
|
|
} as Awaited<ReturnType<typeof getTenantSubscriptionProductById>>);
|
|
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
|
|
await cancelTenantSubscription("tenant-sub-product-id", { tenantId: "tenant-id", userId: "user-id" });
|
|
|
|
expect(consoleLog).not.toHaveBeenCalled();
|
|
expect(cancelStripeSubscription).not.toHaveBeenCalled();
|
|
|
|
consoleLog.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("subscription cache invalidation", () => {
|
|
it("clearTenantSubscriptionCache only invalidates the affected tenant key", () => {
|
|
const clearCacheKeyMock = vi.mocked(clearCacheKey);
|
|
clearCacheKeyMock.mockClear();
|
|
|
|
clearTenantSubscriptionCache("tenant-a");
|
|
|
|
expect(clearCacheKeyMock).toHaveBeenCalledTimes(1);
|
|
expect(clearCacheKeyMock).toHaveBeenCalledWith("tenantSubscription:tenant-a");
|
|
});
|
|
|
|
it("clearTenantSubscriptionCache does not enumerate other tenants (no global thrash)", () => {
|
|
const clearCacheKeyMock = vi.mocked(clearCacheKey);
|
|
const listMock = vi.mocked(listTenantSubscriptionIds);
|
|
clearCacheKeyMock.mockClear();
|
|
listMock.mockClear();
|
|
|
|
clearTenantSubscriptionCache("tenant-x");
|
|
|
|
expect(listMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("clearTenantSubscriptionCache publishes a cross-replica invalidation for the tenant", () => {
|
|
subscriptionCacheInvalidateMock.mockClear();
|
|
|
|
clearTenantSubscriptionCache("tenant-b");
|
|
|
|
// Peer web replicas must drop their L1 too — not just this process.
|
|
expect(subscriptionCacheInvalidateMock).toHaveBeenCalledTimes(1);
|
|
expect(subscriptionCacheInvalidateMock).toHaveBeenCalledWith("tenant-b");
|
|
});
|
|
|
|
it("clearAllSubscriptionsCache broadcasts a wildcard cross-replica invalidation", async () => {
|
|
subscriptionCacheInvalidateMock.mockClear();
|
|
vi.mocked(listTenantSubscriptionIds).mockResolvedValueOnce(
|
|
[] as Awaited<ReturnType<typeof listTenantSubscriptionIds>>,
|
|
);
|
|
|
|
await clearAllSubscriptionsCache();
|
|
|
|
// No key argument == wildcard flush across every replica's L1.
|
|
expect(subscriptionCacheInvalidateMock).toHaveBeenCalledTimes(1);
|
|
expect(subscriptionCacheInvalidateMock).toHaveBeenCalledWith();
|
|
});
|
|
|
|
it("clearAllSubscriptionsCache invalidates every cached tenant subscription", async () => {
|
|
const clearCacheKeyMock = vi.mocked(clearCacheKey);
|
|
const listMock = vi.mocked(listTenantSubscriptionIds);
|
|
clearCacheKeyMock.mockClear();
|
|
listMock.mockReset();
|
|
listMock.mockResolvedValueOnce([
|
|
{ id: "sub-1", tenantId: "tenant-1" },
|
|
{ id: "sub-2", tenantId: "tenant-2" },
|
|
{ id: "sub-3", tenantId: "tenant-3" },
|
|
] as Awaited<ReturnType<typeof listTenantSubscriptionIds>>);
|
|
|
|
await clearAllSubscriptionsCache();
|
|
|
|
expect(clearCacheKeyMock).toHaveBeenCalledTimes(3);
|
|
expect(clearCacheKeyMock).toHaveBeenCalledWith("tenantSubscription:tenant-1");
|
|
expect(clearCacheKeyMock).toHaveBeenCalledWith("tenantSubscription:tenant-2");
|
|
expect(clearCacheKeyMock).toHaveBeenCalledWith("tenantSubscription:tenant-3");
|
|
});
|
|
});
|