From d2e367bd145e7d0471d3127a5a61be65b181ad39 Mon Sep 17 00:00:00 2001 From: Jayadeep Bejoy Date: Wed, 22 Apr 2026 19:35:26 +0530 Subject: [PATCH 1/2] refactor(HUGE REWORK) --- src/__tests__/setup.ts | 11 - .../unit/context/requestContext.test.ts | 268 -------- src/__tests__/unit/errors/logger.test.ts | 170 ----- .../unit/http/createdCheckout.test.ts | 435 ------------- src/__tests__/unit/interceptors/auth.test.ts | 100 --- .../storage/postgres/addAiTokenUsage.test.ts | 584 ------------------ .../unit/storage/postgres/addKey.test.ts | 259 -------- .../unit/storage/postgres/addPayment.test.ts | 202 ------ .../unit/storage/postgres/addSdkCall.test.ts | 170 ----- .../postgres/priceRequestAiTokenUsage.test.ts | 308 --------- .../postgres/priceRequestPayment.test.ts | 390 ------------ .../postgres/priceRequestSdkCall.test.ts | 180 ------ src/__tests__/unit/utils/apiKeyCache.test.ts | 119 ---- .../unit/utils/generateAPIKey.test.ts | 30 - src/__tests__/unit/utils/hashAPIKey.test.ts | 74 --- src/__tests__/unit/utils/parseExpr.test.ts | 259 -------- src/__tests__/unit/zod/apikey.test.ts | 105 ---- src/__tests__/unit/zod/event.test.ts | 228 ------- src/__tests__/unit/zod/payment.test.ts | 53 -- .../RequestEvents/RequestAITokenUsage.ts | 52 +- src/events/RequestEvents/RequestPayment.ts | 52 +- src/events/RequestEvents/RequestSDKCall.ts | 52 +- ...ctory.ts => EventStorageAdapterFactory.ts} | 25 +- src/factory/index.ts | 2 +- src/interface/event/Event.ts | 36 -- src/interface/storage/Storage.ts | 9 +- src/routes/gRPC/auth/createAPIKey.ts | 6 +- src/routes/gRPC/payment/createCheckoutLink.ts | 10 +- src/routes/http/createdCheckout.ts | 14 +- .../handlers/priceRequestAiTokenUsage.ts | 26 +- .../postgres/handlers/priceRequestPayment.ts | 21 +- .../postgres/handlers/priceRequestSdkCall.ts | 27 +- src/storage/adapter/postgres/postgres.ts | 82 +-- src/utils/eventHelpers.ts | 7 +- 34 files changed, 168 insertions(+), 4198 deletions(-) delete mode 100644 src/__tests__/setup.ts delete mode 100644 src/__tests__/unit/context/requestContext.test.ts delete mode 100644 src/__tests__/unit/errors/logger.test.ts delete mode 100644 src/__tests__/unit/http/createdCheckout.test.ts delete mode 100644 src/__tests__/unit/interceptors/auth.test.ts delete mode 100644 src/__tests__/unit/storage/postgres/addAiTokenUsage.test.ts delete mode 100644 src/__tests__/unit/storage/postgres/addKey.test.ts delete mode 100644 src/__tests__/unit/storage/postgres/addPayment.test.ts delete mode 100644 src/__tests__/unit/storage/postgres/addSdkCall.test.ts delete mode 100644 src/__tests__/unit/storage/postgres/priceRequestAiTokenUsage.test.ts delete mode 100644 src/__tests__/unit/storage/postgres/priceRequestPayment.test.ts delete mode 100644 src/__tests__/unit/storage/postgres/priceRequestSdkCall.test.ts delete mode 100644 src/__tests__/unit/utils/apiKeyCache.test.ts delete mode 100644 src/__tests__/unit/utils/generateAPIKey.test.ts delete mode 100644 src/__tests__/unit/utils/hashAPIKey.test.ts delete mode 100644 src/__tests__/unit/utils/parseExpr.test.ts delete mode 100644 src/__tests__/unit/zod/apikey.test.ts delete mode 100644 src/__tests__/unit/zod/event.test.ts delete mode 100644 src/__tests__/unit/zod/payment.test.ts rename src/factory/{StorageAdapterFactory.ts => EventStorageAdapterFactory.ts} (52%) diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts deleted file mode 100644 index d5c7e2c..0000000 --- a/src/__tests__/setup.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Vitest setup file - runs before all tests -// Set required environment variables before any modules are imported -process.env.HMAC_SECRET = "test-secret-key-for-testing"; -process.env.LEMON_SQUEEZY_API_KEY = "test-api-key"; -process.env.LEMON_SQUEEZY_WEBHOOK_SECRET = "test-webhook-secret"; - -// Mock vi.mock for hoisted mocks -import { vi } from "vitest"; - -// Ensure vi is available globally -(globalThis as any).vi = vi; diff --git a/src/__tests__/unit/context/requestContext.test.ts b/src/__tests__/unit/context/requestContext.test.ts deleted file mode 100644 index b16b021..0000000 --- a/src/__tests__/unit/context/requestContext.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { - WideEventBuilder, - createWideEventBuilder, - generateRequestId, -} from "../../../context/requestContext"; - -describe("requestContext", () => { - describe("generateRequestId", () => { - it("should generate a valid UUID v4", () => { - const requestId = generateRequestId(); - - // UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - const uuidRegex = - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - expect(requestId).toMatch(uuidRegex); - }); - - it("should generate unique IDs", () => { - const ids = new Set(); - for (let i = 0; i < 100; i++) { - ids.add(generateRequestId()); - } - expect(ids.size).toBe(100); - }); - }); - - describe("WideEventBuilder", () => { - let builder: WideEventBuilder; - const requestId = "test-request-id-123"; - const method = "unary"; - const url = "https://api.example.com/event.v1.EventService/RegisterEvent"; - - beforeEach(() => { - builder = new WideEventBuilder(requestId, method, url); - }); - - it("should initialize with request metadata", () => { - const event = builder.build(); - - expect(event.requestId).toBe(requestId); - expect(event.method).toBe(method); - expect(event.path).toBe("/event.v1.EventService/RegisterEvent"); - expect(event.timestamp).toBeDefined(); - expect(event.env).toBeDefined(); - }); - - it("should extract path from full URL", () => { - const event = builder.build(); - expect(event.path).toBe("/event.v1.EventService/RegisterEvent"); - }); - - it("should handle relative URLs", () => { - const relativeBuilder = new WideEventBuilder( - requestId, - method, - "/event.v1.EventService/RegisterEvent" - ); - const event = relativeBuilder.build(); - expect(event.path).toBe("/event.v1.EventService/RegisterEvent"); - }); - - describe("setAuth", () => { - it("should set auth context with cache hit", () => { - builder.setAuth("api-key-123", true); - const event = builder.build(); - - expect(event.apiKeyId).toBe("api-key-123"); - expect(event.cacheHit).toBe(true); - }); - - it("should set auth context with cache miss", () => { - builder.setAuth("api-key-456", false); - const event = builder.build(); - - expect(event.apiKeyId).toBe("api-key-456"); - expect(event.cacheHit).toBe(false); - }); - }); - - describe("setUser", () => { - it("should set user ID", () => { - builder.setUser("user-789"); - const event = builder.build(); - - expect(event.userId).toBe("user-789"); - }); - - it("should support numeric user IDs", () => { - builder.setUser(12345); - const event = builder.build(); - - expect(event.userId).toBe(12345); - }); - }); - - describe("setEventContext", () => { - it("should set event type", () => { - builder.setEventContext({ eventType: "SDK_CALL" }); - const event = builder.build(); - - expect(event.eventType).toBe("SDK_CALL"); - }); - - it("should set event count", () => { - builder.setEventContext({ eventCount: 10 }); - const event = builder.build(); - - expect(event.eventCount).toBe(10); - }); - - it("should set both", () => { - builder.setEventContext({ eventType: "AI_TOKEN_USAGE", eventCount: 5 }); - const event = builder.build(); - - expect(event.eventType).toBe("AI_TOKEN_USAGE"); - expect(event.eventCount).toBe(5); - }); - }); - - describe("setPaymentContext", () => { - it("should set credit amount", () => { - builder.setPaymentContext({ creditAmount: 5000 }); - const event = builder.build(); - - expect(event.creditAmount).toBe(5000); - }); - - it("should set debit amount", () => { - builder.setPaymentContext({ debitAmount: 100 }); - const event = builder.build(); - - expect(event.debitAmount).toBe(100); - }); - - it("should set price amount", () => { - builder.setPaymentContext({ priceAmount: 2500 }); - const event = builder.build(); - - expect(event.priceAmount).toBe(2500); - }); - }); - - describe("setApiKeyContext", () => { - it("should set API key name", () => { - builder.setApiKeyContext({ name: "production-key" }); - const event = builder.build(); - - expect(event.apiKeyName).toBe("production-key"); - }); - - it("should set API key expiration", () => { - builder.setApiKeyContext({ expiration: "2027-01-31T00:00:00.000Z" }); - const event = builder.build(); - - expect(event.apiKeyExpiration).toBe("2027-01-31T00:00:00.000Z"); - }); - }); - - describe("setWebhookContext", () => { - it("should set webhook event", () => { - builder.setWebhookContext({ webhookEvent: "order_created" }); - const event = builder.build(); - - expect(event.webhookEvent).toBe("order_created"); - }); - - it("should set order ID", () => { - builder.setWebhookContext({ orderId: "order-123" }); - const event = builder.build(); - - expect(event.orderId).toBe("order-123"); - }); - }); - - describe("addContext", () => { - it("should add arbitrary context", () => { - builder.addContext({ customField: "custom-value", count: 42 }); - const event = builder.build(); - - expect(event.customField).toBe("custom-value"); - expect(event.count).toBe(42); - }); - }); - - describe("setSuccess", () => { - it("should set success outcome with default status code", () => { - builder.setSuccess(); - const event = builder.build(); - - expect(event.outcome).toBe("success"); - expect(event.statusCode).toBe(200); - }); - - it("should set success outcome with custom status code", () => { - builder.setSuccess(201); - const event = builder.build(); - - expect(event.outcome).toBe("success"); - expect(event.statusCode).toBe(201); - }); - }); - - describe("setError", () => { - it("should set error outcome with details", () => { - builder.setError(400, { - type: "VALIDATION_FAILED", - message: "userId is required", - }); - const event = builder.build(); - - expect(event.outcome).toBe("error"); - expect(event.statusCode).toBe(400); - expect(event.error?.type).toBe("VALIDATION_FAILED"); - expect(event.error?.message).toBe("userId is required"); - }); - - it("should set error with cause", () => { - builder.setError(500, { - type: "DATABASE_ERROR", - message: "Query failed", - cause: "Connection timeout", - }); - const event = builder.build(); - - expect(event.error?.cause).toBe("Connection timeout"); - }); - }); - - describe("build", () => { - it("should calculate duration", async () => { - // Wait a small amount to ensure some time passes - await new Promise((resolve) => setTimeout(resolve, 10)); - const event = builder.build(); - - expect(event.durationMs).toBeGreaterThanOrEqual(0); - }); - - it("should default outcome to success", () => { - const event = builder.build(); - expect(event.outcome).toBe("success"); - }); - }); - - describe("chaining", () => { - it("should support method chaining", () => { - const event = builder - .setAuth("api-123", true) - .setUser("user-456") - .setEventContext({ eventType: "SDK_CALL" }) - .setSuccess(200) - .build(); - - expect(event.apiKeyId).toBe("api-123"); - expect(event.userId).toBe("user-456"); - expect(event.eventType).toBe("SDK_CALL"); - expect(event.outcome).toBe("success"); - }); - }); - }); - - describe("createWideEventBuilder", () => { - it("should create a new WideEventBuilder", () => { - const builder = createWideEventBuilder("req-123", "POST", "/api/events"); - expect(builder).toBeInstanceOf(WideEventBuilder); - }); - }); -}); diff --git a/src/__tests__/unit/errors/logger.test.ts b/src/__tests__/unit/errors/logger.test.ts deleted file mode 100644 index 7feba22..0000000 --- a/src/__tests__/unit/errors/logger.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { WideEvent } from "../../../errors/logger"; - -describe("WideEvent interface", () => { - describe("WideEvent structure", () => { - it("should have required fields", () => { - const event: WideEvent = { - requestId: "test-request-id", - method: "unary", - path: "/event.v1.EventService/RegisterEvent", - timestamp: "2026-01-31T10:00:00.000Z", - env: "test", - outcome: "success", - durationMs: 50, - }; - - expect(event.requestId).toBe("test-request-id"); - expect(event.method).toBe("unary"); - expect(event.path).toBe("/event.v1.EventService/RegisterEvent"); - expect(event.timestamp).toBe("2026-01-31T10:00:00.000Z"); - expect(event.env).toBe("test"); - expect(event.outcome).toBe("success"); - expect(event.durationMs).toBe(50); - }); - - it("should support success outcome", () => { - const event: WideEvent = { - requestId: "test-id", - method: "POST", - path: "/test", - timestamp: new Date().toISOString(), - env: "test", - outcome: "success", - durationMs: 100, - statusCode: 200, - }; - - expect(event.outcome).toBe("success"); - expect(event.statusCode).toBe(200); - }); - - it("should support error outcome with error details", () => { - const event: WideEvent = { - requestId: "test-id", - method: "POST", - path: "/test", - timestamp: new Date().toISOString(), - env: "test", - outcome: "error", - durationMs: 25, - statusCode: 400, - error: { - type: "VALIDATION_FAILED", - message: "userId: Required", - }, - }; - - expect(event.outcome).toBe("error"); - expect(event.statusCode).toBe(400); - expect(event.error?.type).toBe("VALIDATION_FAILED"); - expect(event.error?.message).toBe("userId: Required"); - }); - - it("should support auth context", () => { - const event: WideEvent = { - requestId: "test-id", - method: "POST", - path: "/test", - timestamp: new Date().toISOString(), - env: "test", - outcome: "success", - durationMs: 50, - apiKeyId: "api-key-123", - cacheHit: true, - }; - - expect(event.apiKeyId).toBe("api-key-123"); - expect(event.cacheHit).toBe(true); - }); - - it("should support user and event context", () => { - const event: WideEvent = { - requestId: "test-id", - method: "POST", - path: "/test", - timestamp: new Date().toISOString(), - env: "test", - outcome: "success", - durationMs: 50, - userId: "user-456", - eventType: "SDK_CALL", - eventCount: 5, - }; - - expect(event.userId).toBe("user-456"); - expect(event.eventType).toBe("SDK_CALL"); - expect(event.eventCount).toBe(5); - }); - - it("should support payment context", () => { - const event: WideEvent = { - requestId: "test-id", - method: "POST", - path: "/test", - timestamp: new Date().toISOString(), - env: "test", - outcome: "success", - durationMs: 50, - creditAmount: 5000, - debitAmount: 100, - priceAmount: 2500, - }; - - expect(event.creditAmount).toBe(5000); - expect(event.debitAmount).toBe(100); - expect(event.priceAmount).toBe(2500); - }); - - it("should support API key context", () => { - const event: WideEvent = { - requestId: "test-id", - method: "POST", - path: "/test", - timestamp: new Date().toISOString(), - env: "test", - outcome: "success", - durationMs: 50, - apiKeyName: "production-key", - apiKeyExpiration: "2027-01-31T00:00:00.000Z", - }; - - expect(event.apiKeyName).toBe("production-key"); - expect(event.apiKeyExpiration).toBe("2027-01-31T00:00:00.000Z"); - }); - - it("should support webhook context", () => { - const event: WideEvent = { - requestId: "test-id", - method: "POST", - path: "/test", - timestamp: new Date().toISOString(), - env: "test", - outcome: "success", - durationMs: 50, - webhookEvent: "order_created", - orderId: "order-123", - }; - - expect(event.webhookEvent).toBe("order_created"); - expect(event.orderId).toBe("order-123"); - }); - - it("should support extensible fields", () => { - const event: WideEvent = { - requestId: "test-id", - method: "POST", - path: "/test", - timestamp: new Date().toISOString(), - env: "test", - outcome: "success", - durationMs: 50, - customField: "custom-value", - anotherField: 42, - }; - - expect(event.customField).toBe("custom-value"); - expect(event.anotherField).toBe(42); - }); - }); -}); diff --git a/src/__tests__/unit/http/createdCheckout.test.ts b/src/__tests__/unit/http/createdCheckout.test.ts deleted file mode 100644 index 6f08a97..0000000 --- a/src/__tests__/unit/http/createdCheckout.test.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { EventEmitter } from "node:events"; -import type { IncomingMessage, ServerResponse } from "node:http"; -import { createHmac } from "node:crypto"; -import { WideEventBuilder } from "../../../context/requestContext"; - -// Track Payment constructor calls -const paymentConstructorCalls: Array<{ userId: string; data: unknown }> = []; - -class PaymentMock { - public userId: string; - public data: unknown; - public readonly type = "PAYMENT" as const; - public reported_timestamp = { toISO: () => "2024-01-01T00:00:00.000Z" }; - - constructor(userId: string, data: unknown) { - this.userId = userId; - this.data = data; - paymentConstructorCalls.push({ userId, data }); - } - - serialize() { - return { - SQL: { - type: this.type, - userId: this.userId, - reported_timestamp: this.reported_timestamp, - data: this.data, - }, - }; - } -} - -vi.mock("../../../factory/StorageAdapterFactory.ts", () => ({ - StorageAdapterFactory: { - getStorageAdapter: vi.fn(), - }, -})); - -vi.mock("../../../events/RawEvents/Payment.ts", () => ({ - Payment: class Payment { - public userId: string; - public data: unknown; - public readonly type = "PAYMENT" as const; - public reported_timestamp = { toISO: () => "2024-01-01T00:00:00.000Z" }; - - constructor(userId: string, data: unknown) { - this.userId = userId; - this.data = data; - paymentConstructorCalls.push({ userId, data }); - } - - serialize() { - return { - SQL: { - type: this.type, - userId: this.userId, - reported_timestamp: this.reported_timestamp, - data: this.data, - }, - }; - } - }, -})); - -vi.mock("@lemonsqueezy/lemonsqueezy.js", () => ({ - lemonSqueezySetup: vi.fn(), -})); - -class MockRequest extends EventEmitter { - public headers: Record = {}; -} - -class TestResponse { - public statusCode: number | undefined; - public headers: Record = {}; - public body = ""; - - writeHead(statusCode: number, headers: Record): void { - this.statusCode = statusCode; - this.headers = headers; - } - - end(chunk?: string): void { - if (chunk) { - this.body += chunk; - } - } -} - -async function importHandler() { - const module = await import("../../../routes/http/createdCheckout.ts"); - return module.handleLemonSqueezyWebhook; -} - -function emitBody(req: MockRequest, body: string): void { - setImmediate(() => { - req.emit("data", Buffer.from(body)); - req.emit("end"); - }); -} - -/** - * Create a mock WideEventBuilder for testing. - */ -function createMockBuilder(): WideEventBuilder { - return new WideEventBuilder( - "test-request-id", - "POST", - "/webhooks/lemonsqueezy/createdCheckout" - ); -} - -describe("handleLemonSqueezyWebhook", () => { - let storageModule: any; - let lsModule: any; - - beforeEach(async () => { - vi.clearAllMocks(); - paymentConstructorCalls.length = 0; - - // Import mocked modules - storageModule = await import("../../../factory/StorageAdapterFactory.ts"); - lsModule = await import("@lemonsqueezy/lemonsqueezy.js"); - - // Default env; individual tests can override - process.env.LEMON_SQUEEZY_API_KEY = "test-api-key"; - process.env.LEMON_SQUEEZY_WEBHOOK_SECRET = "test-webhook-secret"; - }); - - it("returns 500 when webhook secret is missing", async () => { - delete process.env.LEMON_SQUEEZY_WEBHOOK_SECRET; - - const handleWebhook = await importHandler(); - - const req = new MockRequest() as unknown as IncomingMessage; - const res = new TestResponse() as unknown as ServerResponse; - const builder = createMockBuilder(); - - const payload = JSON.stringify({ - meta: { event_name: "order_created" }, - data: { attributes: { total: 100 } }, - }); - - (req as any).headers["x-signature"] = "any"; - emitBody(req as MockRequest, payload); - - await handleWebhook(req, res, builder); - - expect((res as any).statusCode).toBe(500); - expect((res as any).body).toContain("Webhook secret not configured"); - }); - - it("returns 401 for invalid signature", async () => { - const handleWebhook = await importHandler(); - - const req = new MockRequest() as unknown as IncomingMessage; - const res = new TestResponse() as unknown as ServerResponse; - const builder = createMockBuilder(); - - const payload = JSON.stringify({ - meta: { event_name: "order_created" }, - data: { attributes: { total: 100 } }, - }); - - (req as any).headers["x-signature"] = "invalid-signature"; - emitBody(req as MockRequest, payload); - - await handleWebhook(req, res, builder); - - expect((res as any).statusCode).toBe(401); - expect((res as any).body).toContain("Invalid signature"); - }); - - it("returns 400 for invalid JSON payload", async () => { - const secret = "test-webhook-secret"; - process.env.LEMON_SQUEEZY_WEBHOOK_SECRET = secret; - - const handleWebhook = await importHandler(); - - const req = new MockRequest() as unknown as IncomingMessage; - const res = new TestResponse() as unknown as ServerResponse; - const builder = createMockBuilder(); - - const rawBody = "{"; // invalid JSON - const signature = createHmac("sha256", secret) - .update(rawBody) - .digest("hex"); - - (req as any).headers["x-signature"] = signature; - emitBody(req as MockRequest, rawBody); - - await handleWebhook(req, res, builder); - - expect((res as any).statusCode).toBe(400); - expect((res as any).body).toContain("Invalid JSON payload"); - }); - - it("ignores non-order_created events", async () => { - const secret = "test-webhook-secret"; - process.env.LEMON_SQUEEZY_WEBHOOK_SECRET = secret; - - const handleWebhook = await importHandler(); - - const req = new MockRequest() as unknown as IncomingMessage; - const res = new TestResponse() as unknown as ServerResponse; - const builder = createMockBuilder(); - - const payload = JSON.stringify({ - meta: { event_name: "subscription_created" }, - data: { attributes: { total: 100 } }, - }); - - const signature = createHmac("sha256", secret) - .update(payload) - .digest("hex"); - - (req as any).headers["x-signature"] = signature; - emitBody(req as MockRequest, payload); - - await handleWebhook(req, res, builder); - - expect((res as any).statusCode).toBe(200); - expect((res as any).body).toContain("Event ignored"); - expect( - storageModule.StorageAdapterFactory.getStorageAdapter - ).not.toHaveBeenCalled(); - expect(paymentConstructorCalls.length).toBe(0); - }); - - it("returns 400 when user_id is missing", async () => { - const secret = "test-webhook-secret"; - process.env.LEMON_SQUEEZY_WEBHOOK_SECRET = secret; - - const handleWebhook = await importHandler(); - - const req = new MockRequest() as unknown as IncomingMessage; - const res = new TestResponse() as unknown as ServerResponse; - const builder = createMockBuilder(); - - const payload = JSON.stringify({ - meta: { - event_name: "order_created", - // custom_data missing user_id - custom_data: {}, - }, - data: { attributes: { total: 100 } }, - }); - - const signature = createHmac("sha256", secret) - .update(payload) - .digest("hex"); - - (req as any).headers["x-signature"] = signature; - emitBody(req as MockRequest, payload); - - await handleWebhook(req, res, builder); - - expect((res as any).statusCode).toBe(400); - expect((res as any).body).toContain("Missing user_id in webhook payload"); - }); - - it("returns 400 when apiKeyId is missing", async () => { - const secret = "test-webhook-secret"; - process.env.LEMON_SQUEEZY_WEBHOOK_SECRET = secret; - - const handleWebhook = await importHandler(); - - const req = new MockRequest() as unknown as IncomingMessage; - const res = new TestResponse() as unknown as ServerResponse; - const builder = createMockBuilder(); - - const payload = JSON.stringify({ - meta: { - event_name: "order_created", - custom_data: { - user_id: "user-123", - // api_key_id missing - }, - }, - data: { attributes: { total: 100 } }, - }); - - const signature = createHmac("sha256", secret) - .update(payload) - .digest("hex"); - - (req as any).headers["x-signature"] = signature; - emitBody(req as MockRequest, payload); - - await handleWebhook(req, res, builder); - - expect((res as any).statusCode).toBe(400); - expect((res as any).body).toContain("Missing apiKeyId in webhook payload"); - }); - - it("stores payment and returns 200 on success", async () => { - const secret = "test-webhook-secret"; - process.env.LEMON_SQUEEZY_WEBHOOK_SECRET = secret; - - const adapterAddMock = vi.fn().mockResolvedValue(undefined); - const getStorageAdapterMock = storageModule.StorageAdapterFactory - .getStorageAdapter as ReturnType; - getStorageAdapterMock.mockResolvedValue({ - add: adapterAddMock, - } as any); - - const handleWebhook = await importHandler(); - - const req = new MockRequest() as unknown as IncomingMessage; - const res = new TestResponse() as unknown as ServerResponse; - const builder = createMockBuilder(); - - const payload = JSON.stringify({ - meta: { - event_name: "order_created", - custom_data: { - user_id: "user-123", - api_key_id: "api-key-456", - }, - }, - data: { - attributes: { - total: 123.4, - total_usd: 123.4, - store_id: 1, - customer_id: 2, - order_number: 3, - status: "paid", - created_at: "2024-01-01T00:00:00.000Z", - updated_at: "2024-01-01T00:00:00.000Z", - }, - }, - }); - - const signature = createHmac("sha256", secret) - .update(payload) - .digest("hex"); - - (req as any).headers["x-signature"] = signature; - emitBody(req as MockRequest, payload); - - await handleWebhook(req, res, builder); - - expect(paymentConstructorCalls.length).toBe(1); - expect(paymentConstructorCalls[0]).toEqual({ - userId: "user-123", - data: { creditAmount: 123 }, - }); - - expect( - storageModule.StorageAdapterFactory.getStorageAdapter - ).toHaveBeenCalledTimes(1); - expect(getStorageAdapterMock.mock.calls[0]?.[1]).toBe("api-key-456"); - - expect(adapterAddMock).toHaveBeenCalledTimes(1); - - expect((res as any).statusCode).toBe(200); - expect((res as any).body).toContain("Webhook processed successfully"); - }); - - it("returns 500 when database error occurs", async () => { - const secret = "test-webhook-secret"; - process.env.LEMON_SQUEEZY_WEBHOOK_SECRET = secret; - - const dbError = new Error("DB error"); - const adapterAddMock = vi.fn().mockRejectedValue(dbError); - const getStorageAdapterMock = storageModule.StorageAdapterFactory - .getStorageAdapter as ReturnType; - getStorageAdapterMock.mockResolvedValue({ - add: adapterAddMock, - } as any); - - const handleWebhook = await importHandler(); - - const req = new MockRequest() as unknown as IncomingMessage; - const res = new TestResponse() as unknown as ServerResponse; - const builder = createMockBuilder(); - - const payload = JSON.stringify({ - meta: { - event_name: "order_created", - custom_data: { - user_id: "user-123", - api_key_id: "api-key-456", - }, - }, - data: { - attributes: { - total: 50, - total_usd: 50, - store_id: 1, - customer_id: 2, - order_number: 3, - status: "paid", - created_at: "2024-01-01T00:00:00.000Z", - updated_at: "2024-01-01T00:00:00.000Z", - }, - }, - }); - - const signature = createHmac("sha256", secret) - .update(payload) - .digest("hex"); - - (req as any).headers["x-signature"] = signature; - emitBody(req as MockRequest, payload); - - await handleWebhook(req, res, builder); - - expect((res as any).statusCode).toBe(500); - expect((res as any).body).toContain("Database error"); - }); - - it("returns 500 on unexpected errors (e.g. readBody error)", async () => { - const secret = "test-webhook-secret"; - process.env.LEMON_SQUEEZY_WEBHOOK_SECRET = secret; - - const handleWebhook = await importHandler(); - - const req = new MockRequest() as unknown as IncomingMessage; - const res = new TestResponse() as unknown as ServerResponse; - const builder = createMockBuilder(); - - // Emit an error instead of data/end so readBody rejects - setImmediate(() => { - (req as MockRequest).emit("error", new Error("read error")); - }); - - await handleWebhook(req, res, builder); - - expect((res as any).statusCode).toBe(500); - expect((res as any).body).toContain("Internal server error"); - }); -}); diff --git a/src/__tests__/unit/interceptors/auth.test.ts b/src/__tests__/unit/interceptors/auth.test.ts deleted file mode 100644 index 43189e3..0000000 --- a/src/__tests__/unit/interceptors/auth.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { authInterceptor, no_auth } from "../../../interceptors/auth"; -import * as dbModule from "../../../storage/db/postgres/db"; -import * as hashModule from "../../../utils/hashAPIKey"; - -describe("authInterceptor", () => { - const makeReq = (auth?: string) => ({ - url: "https://api.example.com/protected_endpoint", - header: auth - ? new Map([["Authorization", auth]]) - : new Map(), - contextValues: new Map(), - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - beforeEach(() => { - vi.clearAllMocks(); - - // Mock DB to return valid API key record - const mockDb = { - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - limit: vi.fn().mockResolvedValue([ - { - id: "test-api-key-id", - expiresAt: new Date(Date.now() + 86400000).toISOString(), // expires tomorrow - revoked: false, - }, - ]), - }; - - vi.spyOn(dbModule, "getPostgresDB").mockReturnValue({ - ...mockDb, - } as any); - vi.spyOn(hashModule, "hashAPIKey").mockReturnValue("mocked-hash"); - }); - - it("Ignores no_auth endpoints", async () => { - const next = vi.fn().mockResolvedValue("next called"); - const req = { - url: "https://api.example.com/no_auth_endpoint", - header: new Map(), - }; - - // Mock no_auth to include the test endpoint - no_auth.length = 0; - no_auth.push("no_auth_endpoint"); - - const interceptor = authInterceptor(); - const result = await interceptor(next)(req as any); - - expect(result).toBe("next called"); - expect(next).toHaveBeenCalledWith(req); - }); - - it("Validates Authorization header cases", async () => { - const next = vi.fn().mockResolvedValue("next called"); - const interceptor = authInterceptor(); - - // Empty Authorization should be rejected - await expect(interceptor(next)(makeReq() as any)).rejects.toThrow( - "Missing Authorization header" - ); - - // Authorization that does not start with Bearer should be rejected - await expect( - interceptor(next)(makeReq("Token abcdef") as any) - ).rejects.toThrow( - 'Authorization header must be in format "Bearer "' - ); - - // Authorization with invalid API key format (not starting with scrn_) should be rejected - await expect( - interceptor(next)(makeReq("Bearer " + "a".repeat(37)) as any) - ).rejects.toThrow("Invalid API key: Invalid API key format"); - - // Authorization with invalid API key format (wrong length) should be rejected - await expect( - interceptor(next)(makeReq("Bearer scrn_short") as any) - ).rejects.toThrow("Invalid API key: Invalid API key format"); - }); - - it("Sets the context values", async () => { - const next = vi.fn().mockResolvedValue("next called"); - const interceptor = authInterceptor(); - - const validApiKey = "Bearer scrn_" + "a".repeat(32); - const req = makeReq(validApiKey); - const result = await interceptor(next)(req as any); - - expect(result).toBe("next called"); - expect(next).toHaveBeenCalledWith(req); - const apiKeyId = Array.from(req.contextValues.values())[0]; - expect(apiKeyId).toBe("test-api-key-id"); - }); -}); diff --git a/src/__tests__/unit/storage/postgres/addAiTokenUsage.test.ts b/src/__tests__/unit/storage/postgres/addAiTokenUsage.test.ts deleted file mode 100644 index 9d8f20e..0000000 --- a/src/__tests__/unit/storage/postgres/addAiTokenUsage.test.ts +++ /dev/null @@ -1,584 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { handleAddAiTokenUsage } from "../../../../storage/adapter/postgres/handlers/addAiTokenUsage"; -import * as dbModule from "../../../../storage/db/postgres/db"; -import { DateTime } from "luxon"; - -describe("handleAddAiTokenUsage - Aggregation and Batch Insert", () => { - let mockTransaction: any; - let mockDb: any; - - beforeEach(() => { - mockTransaction = { - insert: vi.fn().mockReturnThis(), - values: vi.fn().mockReturnThis(), - returning: vi.fn(), - onConflictDoNothing: vi.fn().mockReturnThis(), - }; - - mockDb = { - transaction: vi.fn(async (callback) => { - return await callback(mockTransaction); - }), - }; - - vi.spyOn(dbModule, "getPostgresDB").mockReturnValue(mockDb as any); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("aggregation logic", () => { - it("aggregates multiple events for same user and model into one row", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 200, - outputTokens: 100, - inputDebitAmount: 20, - outputDebitAmount: 10, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 50, - outputTokens: 25, - inputDebitAmount: 5, - outputDebitAmount: 2, - }, - }, - ]; - - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-1" }]); - - await handleAddAiTokenUsage(events, "api-key-123"); - - // Should insert only 1 event (aggregated) - const eventInsertCall = mockTransaction.values.mock.calls[1]; - expect(eventInsertCall[0]).toHaveLength(1); - expect(eventInsertCall[0][0].userId).toBe("user-1"); - - // Should insert only 1 AI token usage record with aggregated values - const aiTokenUsageInsertCall = mockTransaction.values.mock.calls[2]; - expect(aiTokenUsageInsertCall[0]).toHaveLength(1); - expect(aiTokenUsageInsertCall[0][0]).toEqual({ - id: "event-1", - model: "gpt-4", - inputTokens: 350, // 100 + 200 + 50 - outputTokens: 175, // 50 + 100 + 25 - inputDebitAmount: 35, // 10 + 20 + 5 - outputDebitAmount: 17, // 5 + 10 + 2 - }); - }); - - it("creates separate rows for different models of same user", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "claude-3-opus", - inputTokens: 150, - outputTokens: 75, - inputDebitAmount: 15, - outputDebitAmount: 7, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 200, - outputTokens: 100, - inputDebitAmount: 20, - outputDebitAmount: 10, - }, - }, - ]; - - mockTransaction.returning.mockResolvedValueOnce([ - { id: "event-1" }, - { id: "event-2" }, - ]); - - await handleAddAiTokenUsage(events, "api-key-123"); - - // Should insert 2 events (one per model) - const eventInsertCall = mockTransaction.values.mock.calls[1]; - expect(eventInsertCall[0]).toHaveLength(2); - - // Should insert 2 AI token usage records - const aiTokenUsageInsertCall = mockTransaction.values.mock.calls[2]; - expect(aiTokenUsageInsertCall[0]).toHaveLength(2); - - // Find GPT-4 aggregated record - const gpt4Record = aiTokenUsageInsertCall[0].find( - (r: any) => r.model === "gpt-4" - ); - expect(gpt4Record).toEqual({ - id: expect.any(String), - model: "gpt-4", - inputTokens: 300, // 100 + 200 - outputTokens: 150, // 50 + 100 - inputDebitAmount: 30, // 10 + 20 - outputDebitAmount: 15, // 5 + 10 - }); - - // Find Claude aggregated record - const claudeRecord = aiTokenUsageInsertCall[0].find( - (r: any) => r.model === "claude-3-opus" - ); - expect(claudeRecord).toEqual({ - id: expect.any(String), - model: "claude-3-opus", - inputTokens: 150, - outputTokens: 75, - inputDebitAmount: 15, - outputDebitAmount: 7, - }); - }); - - it("creates separate rows for different users with same model", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-2", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 200, - outputTokens: 100, - inputDebitAmount: 20, - outputDebitAmount: 10, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 50, - outputTokens: 25, - inputDebitAmount: 5, - outputDebitAmount: 2, - }, - }, - ]; - - mockTransaction.returning.mockResolvedValueOnce([ - { id: "event-1" }, - { id: "event-2" }, - ]); - - await handleAddAiTokenUsage(events, "api-key-123"); - - // Should insert 2 events (one per user) - const eventInsertCall = mockTransaction.values.mock.calls[1]; - expect(eventInsertCall[0]).toHaveLength(2); - - // Should insert 2 AI token usage records - const aiTokenUsageInsertCall = mockTransaction.values.mock.calls[2]; - expect(aiTokenUsageInsertCall[0]).toHaveLength(2); - }); - - it("handles complex scenario with multiple users and models", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 200, - outputTokens: 100, - inputDebitAmount: 20, - outputDebitAmount: 10, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "claude-3-sonnet", - inputTokens: 150, - outputTokens: 75, - inputDebitAmount: 15, - outputDebitAmount: 7, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-2", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 300, - outputTokens: 150, - inputDebitAmount: 30, - outputDebitAmount: 15, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-2", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - ]; - - mockTransaction.returning.mockResolvedValueOnce([ - { id: "event-1" }, - { id: "event-2" }, - { id: "event-3" }, - ]); - - await handleAddAiTokenUsage(events, "api-key-123"); - - // Should insert 3 aggregated events: - // 1. user-1 + gpt-4 - // 2. user-1 + claude-3-sonnet - // 3. user-2 + gpt-4 - const eventInsertCall = mockTransaction.values.mock.calls[1]; - expect(eventInsertCall[0]).toHaveLength(3); - - const aiTokenUsageInsertCall = mockTransaction.values.mock.calls[2]; - expect(aiTokenUsageInsertCall[0]).toHaveLength(3); - - // Verify aggregation: should have 2 gpt-4 records and 1 claude record - const gpt4Records = aiTokenUsageInsertCall[0].filter( - (r: any) => r.model === "gpt-4" - ); - expect(gpt4Records).toHaveLength(2); - - const claudeRecords = aiTokenUsageInsertCall[0].filter( - (r: any) => r.model === "claude-3-sonnet" - ); - expect(claudeRecords).toHaveLength(1); - - // Verify the gpt-4 records have correct aggregated values - const gpt4Tokens = gpt4Records.map((r: any) => r.inputTokens).sort(); - expect(gpt4Tokens).toEqual([300, 400]); // user-1: 100+200=300, user-2: 300+100=400 - }); - }); - - describe("single event handling", () => { - it("handles single event without aggregation", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - ]; - - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-1" }]); - - await handleAddAiTokenUsage(events, "api-key-123"); - - const aiTokenUsageInsertCall = mockTransaction.values.mock.calls[2]; - expect(aiTokenUsageInsertCall[0]).toHaveLength(1); - expect(aiTokenUsageInsertCall[0][0]).toEqual({ - id: "event-1", - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }); - }); - }); - - describe("user insertion", () => { - it("batch inserts all unique users", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-2", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 200, - outputTokens: 100, - inputDebitAmount: 20, - outputDebitAmount: 10, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-3", - reported_timestamp: DateTime.now(), - data: { - model: "claude-3-opus", - inputTokens: 150, - outputTokens: 75, - inputDebitAmount: 15, - outputDebitAmount: 7, - }, - }, - ]; - - mockTransaction.returning.mockResolvedValueOnce([ - { id: "event-1" }, - { id: "event-2" }, - { id: "event-3" }, - ]); - - await handleAddAiTokenUsage(events, "api-key-123"); - - // Check users insert - const usersInsertCall = mockTransaction.values.mock.calls[0]; - expect(usersInsertCall[0]).toHaveLength(3); - expect(usersInsertCall[0]).toEqual( - expect.arrayContaining([ - { id: "user-1" }, - { id: "user-2" }, - { id: "user-3" }, - ]) - ); - }); - - it("inserts duplicate users only once", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "claude-3-opus", - inputTokens: 200, - outputTokens: 100, - inputDebitAmount: 20, - outputDebitAmount: 10, - }, - }, - ]; - - mockTransaction.returning.mockResolvedValueOnce([ - { id: "event-1" }, - { id: "event-2" }, - ]); - - await handleAddAiTokenUsage(events, "api-key-123"); - - // Should only insert user-1 once - const usersInsertCall = mockTransaction.values.mock.calls[0]; - expect(usersInsertCall[0]).toHaveLength(1); - expect(usersInsertCall[0][0]).toEqual({ id: "user-1" }); - }); - }); - - describe("edge cases", () => { - it("handles empty array", async () => { - const result = await handleAddAiTokenUsage([], "api-key-123"); - expect(result).toBeUndefined(); - expect(mockTransaction.insert).not.toHaveBeenCalled(); - }); - - it("handles event insert returning no IDs", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - ]; - - mockTransaction.returning.mockResolvedValueOnce([]); - - await expect( - handleAddAiTokenUsage(events, "api-key-123") - ).rejects.toThrow("Event insert returned no IDs"); - }); - - it("handles event ID count mismatch", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-2", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 200, - outputTokens: 100, - inputDebitAmount: 20, - outputDebitAmount: 10, - }, - }, - ]; - - // Return only 1 ID when expecting 2 - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-1" }]); - - await expect( - handleAddAiTokenUsage(events, "api-key-123") - ).rejects.toThrow("Expected 2 event IDs but got 1"); - }); - }); - - describe("database errors", () => { - it("handles transaction failure", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - ]; - - mockDb.transaction.mockRejectedValueOnce(new Error("Transaction failed")); - - await expect( - handleAddAiTokenUsage(events, "api-key-123") - ).rejects.toThrow("Transaction failed"); - }); - - it("handles event insert failure", async () => { - const events = [ - { - type: "AI_TOKEN_USAGE" as const, - userId: "user-1", - reported_timestamp: DateTime.now(), - data: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebitAmount: 10, - outputDebitAmount: 5, - }, - }, - ]; - - mockTransaction.returning.mockRejectedValueOnce( - new Error("Insert failed") - ); - - await expect( - handleAddAiTokenUsage(events, "api-key-123") - ).rejects.toThrow(); - }); - }); -}); diff --git a/src/__tests__/unit/storage/postgres/addKey.test.ts b/src/__tests__/unit/storage/postgres/addKey.test.ts deleted file mode 100644 index 713a465..0000000 --- a/src/__tests__/unit/storage/postgres/addKey.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { PostgresAdapter } from "../../../../storage/adapter/postgres/postgres"; -import { AddKey } from "../../../../events/RawEvents/AddKey"; -import * as dbModule from "../../../../storage/db/postgres/db"; - -describe("PostgresAdapter - addKey handler", () => { - let mockTransaction: any; - let mockDb: any; - - beforeEach(() => { - mockTransaction = { - insert: vi.fn().mockReturnThis(), - values: vi.fn().mockReturnThis(), - returning: vi.fn(), - onConflictDoNothing: vi.fn().mockReturnThis(), - }; - - mockDb = { - transaction: vi.fn(async (callback) => { - return await callback(mockTransaction); - }), - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - }; - - vi.spyOn(dbModule, "getPostgresDB").mockReturnValue(mockDb as any); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("successful operations", () => { - it("adds API key successfully and returns ID", async () => { - const addKeyEvent = new AddKey({ - name: "Production API Key", - key: "scrn_prod_12345678901234567890123456", - expiresAt: new Date(Date.now() + 86400000).toISOString(), - }); - - mockTransaction.returning.mockResolvedValueOnce([ - { id: "api-key-id-123" }, - ]); - - const adapter = new PostgresAdapter(addKeyEvent); - const serialized = addKeyEvent.serialize(); - const result = await adapter.add(serialized); - - expect(result).toEqual({ id: "api-key-id-123" }); - }); - - it("inserts API key with correct data", async () => { - const keyData = { - name: "Test API Key", - key: "scrn_test_12345678901234567890123456", - expiresAt: new Date(Date.now() + 3600000).toISOString(), - }; - - const addKeyEvent = new AddKey(keyData); - - mockTransaction.returning.mockResolvedValueOnce([ - { id: "api-key-id-456" }, - ]); - - const adapter = new PostgresAdapter(addKeyEvent); - const serialized = addKeyEvent.serialize(); - await adapter.add(serialized); - - const insertCall = mockTransaction.values.mock.calls[0][0]; - expect(insertCall.name).toBe(keyData.name); - expect(insertCall.key).toBe(keyData.key); - expect(insertCall.expiresAt).toBe(keyData.expiresAt); - }); - }); - - describe("validation errors", () => { - it("throws error when data field is missing", async () => { - const invalidEvent = { - type: "ADD_KEY" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: undefined, - serialize: () => ({ - SQL: { - type: "ADD_KEY" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: undefined, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.add(serialized)).rejects.toThrow( - "Missing data field" - ); - }); - - it("throws error when name is missing", async () => { - const invalidEvent = { - type: "ADD_KEY" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { - key: "scrn_test_12345678901234567890123456", - expiresAt: new Date().toISOString(), - }, - serialize: () => ({ - SQL: { - type: "ADD_KEY" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { - key: "scrn_test_12345678901234567890123456", - expiresAt: new Date().toISOString(), - }, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.add(serialized)).rejects.toThrow( - "Invalid or missing 'name'" - ); - }); - - it("throws error when key is missing", async () => { - const invalidEvent = { - type: "ADD_KEY" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { - name: "Test Key", - expiresAt: new Date().toISOString(), - }, - serialize: () => ({ - SQL: { - type: "ADD_KEY" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { - name: "Test Key", - expiresAt: new Date().toISOString(), - }, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.add(serialized)).rejects.toThrow( - "Invalid or missing 'key'" - ); - }); - - it("throws error when key is empty string", async () => { - const invalidEvent = { - type: "ADD_KEY" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { - name: "Test Key", - key: " ", - expiresAt: new Date().toISOString(), - }, - serialize: () => ({ - SQL: { - type: "ADD_KEY" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { - name: "Test Key", - key: " ", - expiresAt: new Date().toISOString(), - }, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.add(serialized)).rejects.toThrow( - "API key cannot be empty" - ); - }); - - it("throws error when timestamp is empty", async () => { - const invalidEvent = { - type: "ADD_KEY" as const, - reported_timestamp: { toISO: () => "" }, - data: { - name: "Test Key", - key: "scrn_test_12345678901234567890123456", - expiresAt: new Date().toISOString(), - }, - serialize: function () { - return { - SQL: { - type: this.type, - reported_timestamp: this.reported_timestamp, - data: this.data, - }, - }; - }, - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.add(serialized)).rejects.toThrow( - "Timestamp is undefined or empty" - ); - }); - }); - - describe("database errors", () => { - it("handles database insert failure", async () => { - const addKeyEvent = new AddKey({ - name: "Test Key", - key: "scrn_test_12345678901234567890123456", - expiresAt: new Date(Date.now() + 86400000).toISOString(), - }); - - mockTransaction.returning.mockRejectedValueOnce( - new Error("Database connection error") - ); - - const adapter = new PostgresAdapter(addKeyEvent); - const serialized = addKeyEvent.serialize(); - await expect(adapter.add(serialized)).rejects.toThrow(); - }); - - it("handles empty API key ID response", async () => { - const addKeyEvent = new AddKey({ - name: "Test Key", - key: "scrn_test_12345678901234567890123456", - expiresAt: new Date(Date.now() + 86400000).toISOString(), - }); - - mockTransaction.returning.mockResolvedValueOnce([]); - - const adapter = new PostgresAdapter(addKeyEvent); - const serialized = addKeyEvent.serialize(); - await expect(adapter.add(serialized)).rejects.toThrow( - "API key insert returned no record" - ); - }); - - it("handles API key response without id field", async () => { - const addKeyEvent = new AddKey({ - name: "Test Key", - key: "scrn_test_12345678901234567890123456", - expiresAt: new Date(Date.now() + 86400000).toISOString(), - }); - - mockTransaction.returning.mockResolvedValueOnce([{}]); - - const adapter = new PostgresAdapter(addKeyEvent); - const serialized = addKeyEvent.serialize(); - await expect(adapter.add(serialized)).rejects.toThrow( - "API key insert returned object without id field" - ); - }); - }); -}); diff --git a/src/__tests__/unit/storage/postgres/addPayment.test.ts b/src/__tests__/unit/storage/postgres/addPayment.test.ts deleted file mode 100644 index a44056e..0000000 --- a/src/__tests__/unit/storage/postgres/addPayment.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { PostgresAdapter } from "../../../../storage/adapter/postgres/postgres"; -import { Payment } from "../../../../events/RawEvents/Payment"; -import * as dbModule from "../../../../storage/db/postgres/db"; - -describe("PostgresAdapter - addPayment handler", () => { - let mockTransaction: any; - let mockDb: any; - - beforeEach(() => { - mockTransaction = { - insert: vi.fn().mockReturnThis(), - values: vi.fn().mockReturnThis(), - returning: vi.fn(), - onConflictDoNothing: vi.fn().mockReturnThis(), - }; - - mockDb = { - transaction: vi.fn(async (callback) => { - return await callback(mockTransaction); - }), - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - leftJoin: vi.fn().mockReturnThis(), - groupBy: vi.fn().mockReturnThis(), - }; - - vi.spyOn(dbModule, "getPostgresDB").mockReturnValue(mockDb as any); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("successful operations", () => { - it("adds PAYMENT event successfully with API key", async () => { - const paymentEvent = new Payment("550e8400-e29b-41d4-a716-446655440000", { - creditAmount: 5000, - }); - - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-123" }]); - - const adapter = new PostgresAdapter(paymentEvent, "api-key-123"); - const serialized = paymentEvent.serialize(); - await adapter.add(serialized); - - const eventInsertCall = mockTransaction.values.mock.calls[1][0]; - expect(eventInsertCall.api_keyId).toBe("api-key-123"); - }); - - it("adds PAYMENT event without apiKeyId (webhook)", async () => { - const paymentEvent = new Payment("550e8400-e29b-41d4-a716-446655440000", { - creditAmount: 10000, - }); - - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-456" }]); - - const adapter = new PostgresAdapter(paymentEvent); - const serialized = paymentEvent.serialize(); - await adapter.add(serialized); - - const eventInsertCall = mockTransaction.values.mock.calls[1][0]; - expect(eventInsertCall.api_keyId).toBeUndefined(); - }); - - it("inserts payment event with correct credit amount", async () => { - const paymentEvent = new Payment("550e8400-e29b-41d4-a716-446655440000", { - creditAmount: 15000, - }); - - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-3" }]); - - const adapter = new PostgresAdapter(paymentEvent); - const serialized = paymentEvent.serialize(); - await adapter.add(serialized); - - const paymentInsertCall = mockTransaction.values.mock.calls[2][0]; - expect(paymentInsertCall.creditAmount).toBe(15000); - expect(paymentInsertCall.id).toBe("event-id-3"); - }); - - it("accepts payment with minimal positive credit amount", async () => { - const paymentEvent = new Payment("550e8400-e29b-41d4-a716-446655440000", { - creditAmount: 1, - }); - - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-pos" }]); - - const adapter = new PostgresAdapter(paymentEvent); - const serialized = paymentEvent.serialize(); - await adapter.add(serialized); - - const paymentInsertCall = mockTransaction.values.mock.calls[2][0]; - expect(paymentInsertCall.creditAmount).toBe(1); - expect(paymentInsertCall.id).toBe("event-id-pos"); - }); - }); - - describe("validation errors", () => { - it("throws error when creditAmount is zero", async () => { - const invalidEvent = { - type: "PAYMENT" as const, - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { creditAmount: 0 }, - serialize: () => ({ - SQL: { - type: "PAYMENT" as const, - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { creditAmount: 0 }, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.add(serialized)).rejects.toThrow(/positive/); - }); - - it("throws error when creditAmount is negative", async () => { - const invalidEvent = { - type: "PAYMENT" as const, - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { creditAmount: -1000 }, - serialize: () => ({ - SQL: { - type: "PAYMENT" as const, - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: { creditAmount: -1000 }, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.add(serialized)).rejects.toThrow(/positive/); - }); - - it("throws error when timestamp is empty", async () => { - const invalidEvent = { - type: "PAYMENT" as const, - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: { toISO: () => " " }, - data: { creditAmount: 5000 }, - serialize: function () { - return { - SQL: { - type: this.type, - userId: this.userId, - reported_timestamp: this.reported_timestamp, - data: this.data, - }, - }; - }, - }; - - mockTransaction.returning.mockResolvedValueOnce([]); - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.add(serialized)).rejects.toThrow( - "Timestamp is undefined or empty" - ); - }); - }); - - describe("database errors", () => { - it("handles event insert failure", async () => { - const paymentEvent = new Payment("550e8400-e29b-41d4-a716-446655440000", { - creditAmount: 5000, - }); - - mockTransaction.returning.mockResolvedValueOnce([]); - mockTransaction.returning.mockRejectedValueOnce( - new Error("Event insert failed") - ); - - const adapter = new PostgresAdapter(paymentEvent); - const serialized = paymentEvent.serialize(); - await expect(adapter.add(serialized)).rejects.toThrow(); - }); - - it("handles empty event ID response", async () => { - const paymentEvent = new Payment("550e8400-e29b-41d4-a716-446655440000", { - creditAmount: 5000, - }); - - mockTransaction.returning.mockResolvedValueOnce([]); - mockTransaction.returning.mockResolvedValueOnce([]); - - const adapter = new PostgresAdapter(paymentEvent); - const serialized = paymentEvent.serialize(); - await expect(adapter.add(serialized)).rejects.toThrow( - "Event insert returned no ID" - ); - }); - }); -}); diff --git a/src/__tests__/unit/storage/postgres/addSdkCall.test.ts b/src/__tests__/unit/storage/postgres/addSdkCall.test.ts deleted file mode 100644 index 705ce02..0000000 --- a/src/__tests__/unit/storage/postgres/addSdkCall.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { PostgresAdapter } from "../../../../storage/adapter/postgres/postgres"; -import { SDKCall } from "../../../../events/RawEvents/SDKCall"; -import * as dbModule from "../../../../storage/db/postgres/db"; - -describe("PostgresAdapter - addSdkCall handler", () => { - let mockTransaction: any; - let mockDb: any; - - beforeEach(() => { - mockTransaction = { - insert: vi.fn().mockReturnThis(), - values: vi.fn().mockReturnThis(), - returning: vi.fn(), - onConflictDoNothing: vi.fn().mockReturnThis(), - }; - - mockDb = { - transaction: vi.fn(async (callback) => { - return await callback(mockTransaction); - }), - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - leftJoin: vi.fn().mockReturnThis(), - groupBy: vi.fn().mockReturnThis(), - }; - - vi.spyOn(dbModule, "getPostgresDB").mockReturnValue(mockDb as any); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("successful operations", () => { - it("adds SDK_CALL event with RAW type successfully", async () => { - const sdkCallEvent = new SDKCall("550e8400-e29b-41d4-a716-446655440000", { - sdkCallType: "RAW", - debitAmount: 1000, - }); - - mockTransaction.returning.mockResolvedValueOnce([ - { id: "550e8400-e29b-41d4-a716-446655443214" }, - ]); - - const adapter = new PostgresAdapter(sdkCallEvent, "api-key-123"); - const serialized = sdkCallEvent.serialize(); - await adapter.add(serialized); - - const eventInsertCall = mockTransaction.values.mock.calls[1][0]; - expect(eventInsertCall.api_keyId).toBe("api-key-123"); - }); - - it("adds SDK_CALL event with MIDDLEWARE_CALL type successfully", async () => { - const sdkCallEvent = new SDKCall("550e8400-e29b-41d4-a716-446655440000", { - sdkCallType: "MIDDLEWARE_CALL", - debitAmount: 2500, - }); - - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-456" }]); - - const adapter = new PostgresAdapter(sdkCallEvent, "api-key-456"); - const serialized = sdkCallEvent.serialize(); - await adapter.add(serialized); - - const sdkCallInsertCall = mockTransaction.values.mock.calls[2][0]; - expect(sdkCallInsertCall.debitAmount).toBe(2500); - }); - - it("inserts event with correct timestamp format", async () => { - const sdkCallEvent = new SDKCall("550e8400-e29b-41d4-a716-446655440000", { - sdkCallType: "RAW", - debitAmount: 1000, - }); - - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-1" }]); - - const adapter = new PostgresAdapter(sdkCallEvent, "api-key-1"); - const serialized = sdkCallEvent.serialize(); - await adapter.add(serialized); - - const eventInsertCall = mockTransaction.values.mock.calls[1][0]; - expect(eventInsertCall).toHaveProperty("reported_timestamp"); - expect(typeof eventInsertCall.reported_timestamp).toBe("string"); - }); - - it("accepts and stores a positive debit amount", async () => { - const sdkCallEvent = new SDKCall("550e8400-e29b-41d4-a716-446655440000", { - sdkCallType: "RAW", - debitAmount: 500, - }); - - mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-pos" }]); - - const adapter = new PostgresAdapter(sdkCallEvent, "api-key-pos"); - const serialized = sdkCallEvent.serialize(); - await adapter.add(serialized); - - const insertedValues = mockTransaction.values.mock.calls.map( - (c: any) => c[0] - ); - const sdkCallRecord = insertedValues.find( - (v: any) => v && v.debitAmount === 500 - ); - - expect(sdkCallRecord).toBeDefined(); - expect(sdkCallRecord.debitAmount).toBeGreaterThan(0); - }); - }); - - describe("database errors", () => { - it("handles event insert failure", async () => { - const sdkCallEvent = new SDKCall("550e8400-e29b-41d4-a716-446655440000", { - sdkCallType: "RAW", - debitAmount: 1000, - }); - - mockTransaction.returning.mockRejectedValueOnce( - new Error("Event insert failed") - ); - - const adapter = new PostgresAdapter(sdkCallEvent, "api-key"); - const serialized = sdkCallEvent.serialize(); - await expect(adapter.add(serialized)).rejects.toThrow(); - }); - - it("handles empty event ID response", async () => { - const sdkCallEvent = new SDKCall("550e8400-e29b-41d4-a716-446655440000", { - sdkCallType: "RAW", - debitAmount: 1000, - }); - - mockTransaction.returning.mockResolvedValueOnce([]); - - const adapter = new PostgresAdapter(sdkCallEvent, "api-key"); - const serialized = sdkCallEvent.serialize(); - await expect(adapter.add(serialized)).rejects.toThrow( - "Event insert returned no ID" - ); - }); - }); - - describe("timestamp handling", () => { - it("throws error when timestamp is empty", async () => { - const invalidEvent = { - type: "SDK_CALL" as const, - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: { toISO: () => "" }, - data: { sdkCallType: "RAW", debitAmount: 1000 }, - serialize: function () { - return { - SQL: { - type: this.type, - userId: this.userId, - reported_timestamp: this.reported_timestamp, - data: this.data, - }, - }; - }, - }; - - const adapter = new PostgresAdapter(invalidEvent as any, "api-key"); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.add(serialized)).rejects.toThrow( - "Timestamp is undefined or empty" - ); - }); - }); -}); diff --git a/src/__tests__/unit/storage/postgres/priceRequestAiTokenUsage.test.ts b/src/__tests__/unit/storage/postgres/priceRequestAiTokenUsage.test.ts deleted file mode 100644 index cbd0216..0000000 --- a/src/__tests__/unit/storage/postgres/priceRequestAiTokenUsage.test.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { PostgresAdapter } from "../../../../storage/adapter/postgres/postgres"; -import { RequestAITokenUsage } from "../../../../events/RequestEvents/RequestAITokenUsage"; -import * as dbModule from "../../../../storage/db/postgres/db"; - -describe("PostgresAdapter - priceRequestAiTokenUsage handler", () => { - let mockDb: any; - - beforeEach(() => { - mockDb = { - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - leftJoin: vi.fn().mockReturnThis(), - groupBy: vi.fn().mockReturnThis(), - transaction: vi.fn(), - }; - - vi.spyOn(dbModule, "getPostgresDB").mockReturnValue(mockDb as any); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("successful operations", () => { - it("calculates price for user with AI token usage events", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: "2500" }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(2500); - }); - - it("returns zero for user with no AI token usage events", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(0); - }); - - it("returns zero when price is null", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: null }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(0); - }); - - it("returns zero when price is undefined", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: undefined }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(0); - }); - - it("parses string price to integer", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: "54321" }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(typeof price).toBe("number"); - expect(price).toBe(54321); - }); - - it("handles large price values", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: "999999999" }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(999999999); - }); - }); - - describe("validation errors", () => { - it("throws error when userId is missing", async () => { - const invalidEvent = { - type: "REQUEST_AI_TOKEN_USAGE" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - serialize: () => ({ - SQL: { - type: "REQUEST_AI_TOKEN_USAGE" as const, - userId: undefined, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.price(serialized)).rejects.toThrow("Missing userId"); - }); - - it("throws error when userId is empty string", async () => { - const invalidEvent = { - type: "REQUEST_AI_TOKEN_USAGE" as const, - userId: " ", - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - serialize: () => ({ - SQL: { - type: "REQUEST_AI_TOKEN_USAGE" as const, - userId: " ", - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.price(serialized)).rejects.toThrow( - "Invalid userId format" - ); - }); - - it("throws error when userId is not a string", async () => { - const invalidEvent = { - type: "REQUEST_AI_TOKEN_USAGE" as const, - userId: 12345, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - serialize: () => ({ - SQL: { - type: "REQUEST_AI_TOKEN_USAGE" as const, - userId: 12345, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.price(serialized)).rejects.toThrow( - "Invalid userId format" - ); - }); - }); - - describe("database errors", () => { - it("handles database query failure", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockRejectedValueOnce( - new Error("Database connection error") - ); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - await expect(adapter.price(serialized)).rejects.toThrow(); - }); - - it("handles null query result", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce(null); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - await expect(adapter.price(serialized)).rejects.toThrow( - "Price query returned null" - ); - }); - - it("handles non-array query result", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce({ price: "2500" }); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - await expect(adapter.price(serialized)).rejects.toThrow( - "Query result is not an array" - ); - }); - - it("handles unparseable price value", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: "not-a-number" }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - await expect(adapter.price(serialized)).rejects.toThrow(); - }); - }); - - describe("edge cases", () => { - it("handles empty result array", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(0); - }); - - it("handles result array with undefined first element", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([undefined]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(0); - }); - - it("handles zero price", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: "0" }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(0); - }); - - it("handles negative price (logs warning but returns value)", async () => { - const requestEvent = new RequestAITokenUsage( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: "-100" }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(-100); - }); - }); -}); diff --git a/src/__tests__/unit/storage/postgres/priceRequestPayment.test.ts b/src/__tests__/unit/storage/postgres/priceRequestPayment.test.ts deleted file mode 100644 index e427034..0000000 --- a/src/__tests__/unit/storage/postgres/priceRequestPayment.test.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { handlePriceRequestPayment } from "../../../../storage/adapter/postgres/handlers/priceRequestPayment"; -import { StorageAdapterFactory } from "../../../../factory"; -import type { SqlRecord } from "../../../../interface/event/Event"; - -describe("PostgresAdapter - priceRequestPayment handler", () => { - let mockSdkAdapter: any; - let mockAiAdapter: any; - - beforeEach(() => { - mockSdkAdapter = { - price: vi.fn(), - }; - - mockAiAdapter = { - price: vi.fn(), - }; - - vi.spyOn(StorageAdapterFactory, "getStorageAdapter").mockImplementation( - (event: any) => { - if (event.type === "REQUEST_SDK_CALL") { - return Promise.resolve(mockSdkAdapter); - } - if (event.type === "REQUEST_AI_TOKEN_USAGE") { - return Promise.resolve(mockAiAdapter); - } - return Promise.resolve(null); - } - ); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("successful operations", () => { - it("calculates total price by summing SDK and AI prices", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(1500); - mockAiAdapter.price.mockResolvedValueOnce(2500); - - const totalPrice = await handlePriceRequestPayment(eventData); - - expect(totalPrice).toBe(4000); - expect(mockSdkAdapter.price).toHaveBeenCalledTimes(1); - expect(mockAiAdapter.price).toHaveBeenCalledTimes(1); - }); - - it("handles zero SDK price", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(0); - mockAiAdapter.price.mockResolvedValueOnce(3000); - - const totalPrice = await handlePriceRequestPayment(eventData); - - expect(totalPrice).toBe(3000); - }); - - it("handles zero AI price", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(2000); - mockAiAdapter.price.mockResolvedValueOnce(0); - - const totalPrice = await handlePriceRequestPayment(eventData); - - expect(totalPrice).toBe(2000); - }); - - it("handles both prices being zero", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(0); - mockAiAdapter.price.mockResolvedValueOnce(0); - - const totalPrice = await handlePriceRequestPayment(eventData); - - expect(totalPrice).toBe(0); - }); - - it("handles large price values", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(999999); - mockAiAdapter.price.mockResolvedValueOnce(888888); - - const totalPrice = await handlePriceRequestPayment(eventData); - - expect(totalPrice).toBe(1888887); - }); - }); - - describe("validation errors", () => { - it("throws error when userId is missing", async () => { - const eventData = { - type: "REQUEST_PAYMENT" as const, - reported_timestamp: {} as any, - data: null, - } as any; - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow( - "Missing userId" - ); - }); - - it("throws error when userId is undefined", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: undefined as any, - reported_timestamp: {} as any, - data: null, - }; - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow( - "Missing userId" - ); - }); - }); - - describe("storage adapter errors", () => { - it("throws error when SDK storage adapter is null", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - vi.spyOn(StorageAdapterFactory, "getStorageAdapter").mockImplementation( - (event: any) => { - if (event.type === "REQUEST_SDK_CALL") { - return Promise.resolve(null as any); - } - return Promise.resolve(mockAiAdapter); - } - ); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - - it("throws error when AI storage adapter is null", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(1500); - - vi.spyOn(StorageAdapterFactory, "getStorageAdapter").mockImplementation( - (event: any) => { - if (event.type === "REQUEST_SDK_CALL") { - return Promise.resolve(mockSdkAdapter); - } - if (event.type === "REQUEST_AI_TOKEN_USAGE") { - return Promise.resolve(null as any); - } - return Promise.resolve(null); - } - ); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - - it("throws error when SDK storage adapter is undefined", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - vi.spyOn(StorageAdapterFactory, "getStorageAdapter").mockImplementation( - (event: any) => { - if (event.type === "REQUEST_SDK_CALL") { - return Promise.resolve(undefined as any); - } - return Promise.resolve(mockAiAdapter); - } - ); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - }); - - describe("price calculation errors", () => { - it("throws error when SDK price is NaN", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(NaN); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - - it("throws error when AI price is NaN", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(1500); - mockAiAdapter.price.mockResolvedValueOnce(NaN); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - - it("throws error when SDK price is not a number", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce("not-a-number" as any); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - - it("throws error when AI price is not a number", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(1500); - mockAiAdapter.price.mockResolvedValueOnce("not-a-number" as any); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - - it("throws error when SDK price returns null", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(null); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - - it("throws error when AI price returns null", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(1500); - mockAiAdapter.price.mockResolvedValueOnce(null); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - }); - - describe("adapter method errors", () => { - it("handles SDK adapter price method throwing error", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockRejectedValueOnce( - new Error("Database connection failed") - ); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - - it("handles AI adapter price method throwing error", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(1500); - mockAiAdapter.price.mockRejectedValueOnce(new Error("Query timeout")); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - - it("wraps non-StorageError exceptions", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockRejectedValueOnce(new Error("Unexpected error")); - - await expect(handlePriceRequestPayment(eventData)).rejects.toThrow(); - }); - }); - - describe("edge cases", () => { - it("handles negative SDK price", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(-100); - mockAiAdapter.price.mockResolvedValueOnce(200); - - const totalPrice = await handlePriceRequestPayment(eventData); - - expect(totalPrice).toBe(100); - }); - - it("handles negative AI price", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(300); - mockAiAdapter.price.mockResolvedValueOnce(-50); - - const totalPrice = await handlePriceRequestPayment(eventData); - - expect(totalPrice).toBe(250); - }); - - it("handles both negative prices", async () => { - const eventData: SqlRecord<"REQUEST_PAYMENT"> = { - type: "REQUEST_PAYMENT", - userId: "550e8400-e29b-41d4-a716-446655440000", - reported_timestamp: {} as any, - data: null, - }; - - mockSdkAdapter.price.mockResolvedValueOnce(-100); - mockAiAdapter.price.mockResolvedValueOnce(-50); - - const totalPrice = await handlePriceRequestPayment(eventData); - - expect(totalPrice).toBe(-150); - }); - }); -}); diff --git a/src/__tests__/unit/storage/postgres/priceRequestSdkCall.test.ts b/src/__tests__/unit/storage/postgres/priceRequestSdkCall.test.ts deleted file mode 100644 index 380a4a4..0000000 --- a/src/__tests__/unit/storage/postgres/priceRequestSdkCall.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { PostgresAdapter } from "../../../../storage/adapter/postgres/postgres"; -import { RequestSDKCall } from "../../../../events/RequestEvents/RequestSDKCall"; -import * as dbModule from "../../../../storage/db/postgres/db"; - -describe("PostgresAdapter - priceRequestSdkCall handler", () => { - let mockDb: any; - - beforeEach(() => { - mockDb = { - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - leftJoin: vi.fn().mockReturnThis(), - groupBy: vi.fn().mockReturnThis(), - transaction: vi.fn(), - }; - - vi.spyOn(dbModule, "getPostgresDB").mockReturnValue(mockDb as any); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("successful operations", () => { - it("calculates price for user with SDK call events", async () => { - const requestEvent = new RequestSDKCall( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: "1500" }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(1500); - }); - - it("returns zero for user with no SDK call events", async () => { - const requestEvent = new RequestSDKCall( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(0); - }); - - it("returns zero when price is null", async () => { - const requestEvent = new RequestSDKCall( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: null }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(price).toBe(0); - }); - - it("parses string price to integer", async () => { - const requestEvent = new RequestSDKCall( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce([{ price: "12345" }]); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - const price = await adapter.price(serialized); - - expect(typeof price).toBe("number"); - expect(price).toBe(12345); - }); - }); - - describe("validation errors", () => { - it("throws error when userId is missing", async () => { - const invalidEvent = { - type: "REQUEST_SDK_CALL" as const, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - serialize: () => ({ - SQL: { - type: "REQUEST_SDK_CALL" as const, - userId: undefined, - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.price(serialized)).rejects.toThrow("Missing userId"); - }); - - it("throws error when userId is empty string", async () => { - const invalidEvent = { - type: "REQUEST_SDK_CALL" as const, - userId: " ", - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - serialize: () => ({ - SQL: { - type: "REQUEST_SDK_CALL" as const, - userId: " ", - reported_timestamp: { toISO: () => "2024-01-01T00:00:00.000Z" }, - data: null, - }, - }), - }; - - const adapter = new PostgresAdapter(invalidEvent as any); - const serialized = invalidEvent.serialize() as any; - await expect(adapter.price(serialized)).rejects.toThrow( - "Invalid userId format" - ); - }); - }); - - describe("database errors", () => { - it("handles database query failure", async () => { - const requestEvent = new RequestSDKCall( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockRejectedValueOnce( - new Error("Database connection error") - ); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - await expect(adapter.price(serialized)).rejects.toThrow(); - }); - - it("handles null query result", async () => { - const requestEvent = new RequestSDKCall( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce(null); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - await expect(adapter.price(serialized)).rejects.toThrow( - "Price query returned null" - ); - }); - - it("handles non-array query result", async () => { - const requestEvent = new RequestSDKCall( - "550e8400-e29b-41d4-a716-446655440000", - null - ); - - mockDb.groupBy.mockResolvedValueOnce({ price: "1500" }); - - const adapter = new PostgresAdapter(requestEvent); - const serialized = requestEvent.serialize(); - await expect(adapter.price(serialized)).rejects.toThrow( - "Query result is not an array" - ); - }); - }); -}); diff --git a/src/__tests__/unit/utils/apiKeyCache.test.ts b/src/__tests__/unit/utils/apiKeyCache.test.ts deleted file mode 100644 index 5dd5a11..0000000 --- a/src/__tests__/unit/utils/apiKeyCache.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { apiKeyCache } from "../../../utils/apiKeyCache"; - -describe("apiKeyCache", () => { - let originalNow: () => number; - - beforeEach(() => { - originalNow = Date.now; - apiKeyCache.clear(); - }); - - afterEach(() => { - // Restore Date.now if mocked - (Date as any).now = originalNow; - }); - - it("returns null when key is not cached", () => { - const result = apiKeyCache.get("missing-hash"); - expect(result).toBeNull(); - }); - - it("stores and retrieves API key data", () => { - const now = Date.now(); - (Date as any).now = () => now; - - const expiresAt = new Date(now + 10 * 60 * 1000).toISOString(); - - apiKeyCache.set("hash-1", { - id: "api-key-id-1", - expiresAt, - }); - - const cached = apiKeyCache.get("hash-1"); - expect(cached).not.toBeNull(); - expect(cached?.id).toBe("api-key-id-1"); - expect(cached?.expiresAt).toBe(expiresAt); - }); - - it("evicts entry when cache TTL expires", () => { - const base = Date.now(); - (Date as any).now = () => base; - - const expiresAt = new Date(base + 60 * 60 * 1000).toISOString(); // 1h in future - - apiKeyCache.set("hash-ttl", { - id: "ttl-id", - expiresAt, - }); - - // Advance time by > 5 minutes TTL - const later = base + 6 * 60 * 1000; - (Date as any).now = () => later; - - const cached = apiKeyCache.get("hash-ttl"); - expect(cached).toBeNull(); - - const stats = apiKeyCache.getStats(); - expect(stats.size).toBe(0); - }); - - it("evicts entry when API key itself has expired", () => { - const base = Date.now(); - (Date as any).now = () => base; - - const expiredAt = new Date(base - 1000).toISOString(); // already expired - - apiKeyCache.set("hash-expired", { - id: "expired-id", - expiresAt: expiredAt, - }); - - const cached = apiKeyCache.get("hash-expired"); - expect(cached).toBeNull(); - - const stats = apiKeyCache.getStats(); - expect(stats.size).toBe(0); - }); - - it("deletes specific key from cache", () => { - const now = Date.now(); - const expiresAt = new Date(now + 60 * 60 * 1000).toISOString(); - - apiKeyCache.set("hash-1", { id: "1", expiresAt }); - apiKeyCache.set("hash-2", { id: "2", expiresAt }); - - apiKeyCache.delete("hash-1"); - - expect(apiKeyCache.get("hash-1")).toBeNull(); - expect(apiKeyCache.get("hash-2")).not.toBeNull(); - }); - - it("clear() removes all entries", () => { - const now = Date.now(); - const expiresAt = new Date(now + 60 * 60 * 1000).toISOString(); - - apiKeyCache.set("hash-1", { id: "1", expiresAt }); - apiKeyCache.set("hash-2", { id: "2", expiresAt }); - - apiKeyCache.clear(); - - expect(apiKeyCache.get("hash-1")).toBeNull(); - expect(apiKeyCache.get("hash-2")).toBeNull(); - const stats = apiKeyCache.getStats(); - expect(stats.size).toBe(0); - }); - - it("returns correct cache statistics", () => { - const now = Date.now(); - const expiresAt = new Date(now + 60 * 60 * 1000).toISOString(); - - apiKeyCache.set("hash-1", { id: "1", expiresAt }); - apiKeyCache.set("hash-2", { id: "2", expiresAt }); - - const stats = apiKeyCache.getStats(); - expect(stats.size).toBe(2); - expect(stats.maxSize).toBe(1000); - expect(stats.ttlMinutes).toBe(5); - }); -}); diff --git a/src/__tests__/unit/utils/generateAPIKey.test.ts b/src/__tests__/unit/utils/generateAPIKey.test.ts deleted file mode 100644 index 494431b..0000000 --- a/src/__tests__/unit/utils/generateAPIKey.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { generateAPIKey } from "../../../utils/generateAPIKey"; - -describe("generateAPIKey", () => { - it("generates API key with correct format", () => { - const key = generateAPIKey(); - - expect(key.startsWith("scrn_")).toBe(true); - expect(key.length).toBe(5 + 32); // "scrn_" + 32 chars - expect(key).toMatch(/^scrn_[A-Za-z0-9]{32}$/); - }); - - it("generates different keys on each call", () => { - const key1 = generateAPIKey(); - const key2 = generateAPIKey(); - - // Extremely unlikely to collide; good enough as a sanity check - expect(key1).not.toBe(key2); - }); - - it("generates only alphanumeric characters after prefix", () => { - const key = generateAPIKey(); - const keyPart = key.substring(5); // Remove "scrn_" prefix - - // Should not contain special characters that were replaced - expect(keyPart).not.toContain("+"); - expect(keyPart).not.toContain("/"); - expect(keyPart).not.toContain("="); - }); -}); diff --git a/src/__tests__/unit/utils/hashAPIKey.test.ts b/src/__tests__/unit/utils/hashAPIKey.test.ts deleted file mode 100644 index 7f23868..0000000 --- a/src/__tests__/unit/utils/hashAPIKey.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, it, expect, beforeAll } from "vitest"; -import { hashAPIKey, verifyAPIKey } from "../../../utils/hashAPIKey"; - -beforeAll(() => { - // Set up test environment variable for HMAC hashing - process.env.HMAC_SECRET = "test-hmac-secret-for-unit-tests"; -}); - -describe("hashAPIKey", () => { - it("produces deterministic hash for same input", () => { - const key = "scrn_test_12345678901234567890123456"; - - const hash1 = hashAPIKey(key); - const hash2 = hashAPIKey(key); - - expect(hash1).toBe(hash2); - }); - - it("produces different hashes for different inputs", () => { - const key1 = "scrn_test_aaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const key2 = "scrn_test_bbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - - const hash1 = hashAPIKey(key1); - const hash2 = hashAPIKey(key2); - - expect(hash1).not.toBe(hash2); - }); - - it("produces hex string hash", () => { - const key = "scrn_test_12345678901234567890123456"; - const hash = hashAPIKey(key); - - // HMAC-SHA256 produces 64 hex characters - expect(hash).toMatch(/^[a-f0-9]{64}$/); - }); -}); - -describe("verifyAPIKey", () => { - it("returns true for matching key and hash", () => { - const key = "scrn_test_12345678901234567890123456"; - const hash = hashAPIKey(key); - - const result = verifyAPIKey(key, hash); - expect(result).toBe(true); - }); - - it("returns false for non-matching key", () => { - const key = "scrn_test_12345678901234567890123456"; - const otherKey = "scrn_test_aaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const hash = hashAPIKey(key); - - const result = verifyAPIKey(otherKey, hash); - expect(result).toBe(false); - }); - - it("returns false when hash length differs", () => { - const key = "scrn_test_12345678901234567890123456"; - const invalidHash = "short-hash"; - - const result = verifyAPIKey(key, invalidHash); - expect(result).toBe(false); - }); - - it("uses constant-time comparison for security", () => { - const key = "scrn_test_12345678901234567890123456"; - const hash = hashAPIKey(key); - - // Verify with slightly modified hash (single char different) - const modifiedHash = hash.substring(0, hash.length - 1) + "X"; - - const result = verifyAPIKey(key, modifiedHash); - expect(result).toBe(false); - }); -}); diff --git a/src/__tests__/unit/utils/parseExpr.test.ts b/src/__tests__/unit/utils/parseExpr.test.ts deleted file mode 100644 index 04cbd5e..0000000 --- a/src/__tests__/unit/utils/parseExpr.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { describe, it, expect, vi, beforeEach, type Mock } from "vitest"; -import { EventError } from "../../../errors/event"; - -// Mock fetchTagAmount before importing parseExpr -vi.mock("../../../utils/fetchTagAmount", () => ({ - fetchTagAmount: vi.fn(), -})); - -import { - parseAndEvaluateExpr, - extractTagNames, - validateExprSyntax, -} from "../../../utils/parseExpr"; -import { fetchTagAmount } from "../../../utils/fetchTagAmount"; - -const mockFetchTagAmount = fetchTagAmount as Mock; - -describe("parseExpr", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("extractTagNames", () => { - it("extracts single tag name", () => { - const tags = extractTagNames("tag(PREMIUM_CALL)"); - expect(tags).toEqual(["PREMIUM_CALL"]); - }); - - it("extracts multiple tag names", () => { - const tags = extractTagNames("add(tag(PREMIUM),tag(FEE),100)"); - expect(tags).toEqual(["PREMIUM", "FEE"]); - }); - - it("extracts unique tag names when duplicates exist", () => { - const tags = extractTagNames("add(tag(FEE),mul(tag(FEE),2))"); - expect(tags).toEqual(["FEE"]); - }); - - it("returns empty array when no tags present", () => { - const tags = extractTagNames("add(100,200,300)"); - expect(tags).toEqual([]); - }); - - it("handles complex nested expressions", () => { - const tags = extractTagNames( - "add(mul(tag(PREMIUM_CALL),3),tag(EXTRA_FEE),250)" - ); - expect(tags).toContain("PREMIUM_CALL"); - expect(tags).toContain("EXTRA_FEE"); - expect(tags).toHaveLength(2); - }); - }); - - describe("validateExprSyntax", () => { - it("accepts valid simple expressions", () => { - expect(() => validateExprSyntax("250")).not.toThrow(); - expect(() => validateExprSyntax("add(100,200)")).not.toThrow(); - expect(() => validateExprSyntax("tag(PREMIUM)")).not.toThrow(); - }); - - it("accepts valid complex expressions", () => { - expect(() => - validateExprSyntax("add(mul(tag(PREMIUM_CALL),3),tag(EXTRA_FEE),250)") - ).not.toThrow(); - }); - - it("rejects empty expressions", () => { - expect(() => validateExprSyntax("")).toThrow(/cannot be empty/); - expect(() => validateExprSyntax(" ")).toThrow(/cannot be empty/); - }); - - it("rejects unmatched opening parenthesis", () => { - expect(() => validateExprSyntax("add(100,200")).toThrow( - /unmatched opening parenthesis/ - ); - }); - - it("rejects unmatched closing parenthesis", () => { - expect(() => validateExprSyntax("add100,200)")).toThrow( - /unmatched closing parenthesis/ - ); - }); - - it("rejects unknown functions", () => { - expect(() => validateExprSyntax("unknown(100)")).toThrow( - /Unknown function in expression: unknown/ - ); - }); - - it("rejects invalid tag name format", () => { - expect(() => validateExprSyntax("tag(lowercase)")).toThrow( - /Invalid tag name format/ - ); - expect(() => validateExprSyntax("tag(123INVALID)")).toThrow( - /Invalid tag name format/ - ); - }); - - it("accepts valid tag name formats", () => { - expect(() => validateExprSyntax("tag(VALID_TAG)")).not.toThrow(); - expect(() => validateExprSyntax("tag(TAG123)")).not.toThrow(); - expect(() => validateExprSyntax("tag(_UNDERSCORE_START)")).not.toThrow(); - }); - }); - - describe("parseAndEvaluateExpr", () => { - describe("simple amounts", () => { - it("evaluates plain numbers", async () => { - const result = await parseAndEvaluateExpr("250"); - expect(result).toBe(250); - }); - - it("evaluates decimal numbers and floors result", async () => { - const result = await parseAndEvaluateExpr("250.7"); - expect(result).toBe(250); - }); - }); - - describe("add operation", () => { - it("adds two numbers", async () => { - const result = await parseAndEvaluateExpr("add(100,200)"); - expect(result).toBe(300); - }); - - it("adds multiple numbers", async () => { - const result = await parseAndEvaluateExpr("add(100,200,300,400)"); - expect(result).toBe(1000); - }); - }); - - describe("sub operation", () => { - it("subtracts two numbers", async () => { - const result = await parseAndEvaluateExpr("sub(500,200)"); - expect(result).toBe(300); - }); - - it("handles negative results", async () => { - const result = await parseAndEvaluateExpr("sub(100,250)"); - expect(result).toBe(-150); - }); - }); - - describe("mul operation", () => { - it("multiplies two numbers", async () => { - const result = await parseAndEvaluateExpr("mul(10,20)"); - expect(result).toBe(200); - }); - - it("multiplies multiple numbers", async () => { - const result = await parseAndEvaluateExpr("mul(2,3,4,5)"); - expect(result).toBe(120); - }); - }); - - describe("div operation", () => { - it("divides two numbers", async () => { - const result = await parseAndEvaluateExpr("div(100,4)"); - expect(result).toBe(25); - }); - - it("floors the result of division", async () => { - const result = await parseAndEvaluateExpr("div(100,3)"); - expect(result).toBe(33); // 100/3 = 33.333... → 33 - }); - - it("throws error on division by zero", async () => { - await expect(parseAndEvaluateExpr("div(100,0)")).rejects.toThrow( - /Division by zero/ - ); - }); - }); - - describe("tag resolution", () => { - it("resolves single tag", async () => { - mockFetchTagAmount.mockResolvedValue(500); - - const result = await parseAndEvaluateExpr("tag(PREMIUM_CALL)"); - - expect(result).toBe(500); - expect(mockFetchTagAmount).toHaveBeenCalledWith( - "PREMIUM_CALL", - "Tag not found: PREMIUM_CALL" - ); - }); - - it("resolves multiple tags", async () => { - mockFetchTagAmount - .mockResolvedValueOnce(100) // PREMIUM - .mockResolvedValueOnce(50); // FEE - - const result = await parseAndEvaluateExpr("add(tag(PREMIUM),tag(FEE))"); - - expect(result).toBe(150); - expect(mockFetchTagAmount).toHaveBeenCalledTimes(2); - }); - - it("throws error when tag not found", async () => { - mockFetchTagAmount.mockRejectedValue( - EventError.validationFailed("Tag not found: UNKNOWN_TAG") - ); - - await expect(parseAndEvaluateExpr("tag(UNKNOWN_TAG)")).rejects.toThrow( - /Tag not found/ - ); - }); - }); - - describe("complex expressions", () => { - it("evaluates nested operations", async () => { - // add(mul(10,3),sub(100,20),50) = 30 + 80 + 50 = 160 - const result = await parseAndEvaluateExpr( - "add(mul(10,3),sub(100,20),50)" - ); - expect(result).toBe(160); - }); - - it("evaluates expression with tags and operations", async () => { - mockFetchTagAmount - .mockResolvedValueOnce(100) // PREMIUM_CALL - .mockResolvedValueOnce(50); // EXTRA_FEE - - // add(mul(tag(PREMIUM_CALL),3),tag(EXTRA_FEE),250) = 300 + 50 + 250 = 600 - const result = await parseAndEvaluateExpr( - "add(mul(tag(PREMIUM_CALL),3),tag(EXTRA_FEE),250)" - ); - - expect(result).toBe(600); - }); - - it("handles deeply nested expressions", async () => { - // div(mul(add(10,20),sub(50,10)),4) = div(mul(30,40),4) = div(1200,4) = 300 - const result = await parseAndEvaluateExpr( - "div(mul(add(10,20),sub(50,10)),4)" - ); - expect(result).toBe(300); - }); - }); - - describe("error handling", () => { - it("throws EventError for invalid syntax", async () => { - await expect(parseAndEvaluateExpr("add(100,")).rejects.toThrow( - /unmatched opening parenthesis/ - ); - }); - - it("throws EventError for unknown functions", async () => { - await expect(parseAndEvaluateExpr("unknown(100)")).rejects.toThrow( - /Unknown function/ - ); - }); - - it("throws EventError for empty expression", async () => { - await expect(parseAndEvaluateExpr("")).rejects.toThrow( - /cannot be empty/ - ); - }); - }); - }); -}); diff --git a/src/__tests__/unit/zod/apikey.test.ts b/src/__tests__/unit/zod/apikey.test.ts deleted file mode 100644 index 74c9f7c..0000000 --- a/src/__tests__/unit/zod/apikey.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { createAPIKeySchema } from "../../../zod/apikey"; - -describe("createAPIKeySchema", () => { - it("validates a valid API key creation request", () => { - const validRequest = { - name: "My API Key", - expiresIn: 86400, - }; - - const result = createAPIKeySchema.safeParse(validRequest); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.name).toBe("My API Key"); - expect(result.data.expiresIn).toBe(86400); - } - }); - - it("transforms bigint expiresIn to number", () => { - const validRequest = { - name: "Bigint Expiry Key", - expiresIn: BigInt(3600), - }; - - const result = createAPIKeySchema.safeParse(validRequest); - expect(result.success).toBe(true); - if (result.success) { - expect(typeof result.data.expiresIn).toBe("number"); - expect(result.data.expiresIn).toBe(3600); - } - }); - - it("rejects empty name", () => { - const invalidRequest = { - name: "", - expiresIn: 3600, - }; - - const result = createAPIKeySchema.safeParse(invalidRequest); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues[0]?.message).toBe("API key name is required"); - } - }); - - it("rejects name longer than 255 characters", () => { - const invalidRequest = { - name: "a".repeat(256), - expiresIn: 3600, - }; - - const result = createAPIKeySchema.safeParse(invalidRequest); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues[0]?.message).toBe( - "API key name must be less than 255 characters" - ); - } - }); - - it("rejects non-integer expiresIn values", () => { - const invalidRequest = { - name: "Test Key", - expiresIn: 123.456, - }; - - const result = createAPIKeySchema.safeParse(invalidRequest); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues[0]?.message).toBe( - "Expiration time must be an integer" - ); - } - }); - - it("rejects expiresIn less than 60 seconds", () => { - const invalidRequest = { - name: "Test Key", - expiresIn: 59, - }; - - const result = createAPIKeySchema.safeParse(invalidRequest); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues[0]?.message).toBe( - "Expiration time must be at least 60 seconds" - ); - } - }); - - it("rejects expiresIn greater than 1 year", () => { - const invalidRequest = { - name: "Test Key", - expiresIn: 365 * 24 * 60 * 60 + 1, - }; - - const result = createAPIKeySchema.safeParse(invalidRequest); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues[0]?.message).toBe( - "Expiration time cannot exceed 1 year" - ); - } - }); -}); diff --git a/src/__tests__/unit/zod/event.test.ts b/src/__tests__/unit/zod/event.test.ts deleted file mode 100644 index c8248bb..0000000 --- a/src/__tests__/unit/zod/event.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -vi.mock("../../../storage/db/postgres/db", () => ({ - getPostgresDB: vi.fn(() => ({})), -})); - -import { registerEventSchema, streamEventSchema } from "../../../zod/event"; - -describe("registerEventSchema", () => { - it("validates and transforms SDK_CALL event with RAW type", async () => { - const validEvent = { - type: 1, - userId: "550e8400-e29b-41d4-a716-446655440000", - data: { - case: "sdkCall", - value: { - sdkCallType: 1, - debit: { - case: "amount", - value: 10.5, - }, - }, - }, - }; - - const result = await registerEventSchema.safeParseAsync(validEvent); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe("SDK_CALL"); - expect(result.data.userId).toBe("550e8400-e29b-41d4-a716-446655440000"); - if (result.data.type === "SDK_CALL") { - expect(result.data.data.sdkCallType).toBe("RAW"); - expect(result.data.data.debitAmount).toBe(1050); - } - } - }); - - it("validates and transforms SDK_CALL event with MIDDLEWARE_CALL type", async () => { - const validEvent = { - type: 1, - userId: "550e8400-e29b-41d4-a716-446655440000", - data: { - case: "sdkCall", - value: { - sdkCallType: 2, - debit: { - case: "amount", - value: 25.99, - }, - }, - }, - }; - - const result = await registerEventSchema.safeParseAsync(validEvent); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe("SDK_CALL"); - if (result.data.type === "SDK_CALL") { - expect(result.data.data.sdkCallType).toBe("MIDDLEWARE_CALL"); - expect(result.data.data.debitAmount).toBe(2599); - } - } - }); - - it("transforms debitAmount from dollars to cents correctly", async () => { - const validEvent = { - type: 1, - userId: "550e8400-e29b-41d4-a716-446655440000", - data: { - case: "sdkCall", - value: { - sdkCallType: 1, - debit: { - case: "amount", - value: 123.456, - }, - }, - }, - }; - - const result = await registerEventSchema.safeParseAsync(validEvent); - expect(result.success).toBe(true); - if (result.success && result.data.type === "SDK_CALL") { - expect(result.data.data.debitAmount).toBe(12345); - } - }); - - it("transforms data structure from protobuf format to internal format", async () => { - const validEvent = { - type: 1, - userId: "550e8400-e29b-41d4-a716-446655440000", - data: { - case: "sdkCall", - value: { - sdkCallType: 1, - debit: { - case: "amount", - value: 5.5, - }, - }, - }, - }; - - const result = await registerEventSchema.safeParseAsync(validEvent); - expect(result.success).toBe(true); - if (result.success && result.data.type === "SDK_CALL") { - expect(result.data.data).not.toHaveProperty("case"); - expect(result.data.data).toHaveProperty("sdkCallType"); - expect(result.data.data).toHaveProperty("debitAmount"); - } - }); - - it("rejects invalid userId", async () => { - const invalidEvent = { - type: 1, - userId: "not-a-valid-uuid", - data: { - case: "sdkCall", - value: { - sdkCallType: 1, - debit: { - case: "amount", - value: 10.0, - }, - }, - }, - }; - - const result = await registerEventSchema.safeParseAsync(invalidEvent); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues[0]?.message).toBe("Invalid UUID"); - } - }); - - it("rejects invalid event type", async () => { - const invalidEvent = { - type: 999, - userId: "550e8400-e29b-41d4-a716-446655440000", - data: { - case: "sdkCall", - value: { - sdkCallType: 1, - debit: { - case: "amount", - value: 10.0, - }, - }, - }, - }; - - const result = await registerEventSchema.safeParseAsync(invalidEvent); - expect(result.success).toBe(false); - }); - - it("rejects invalid sdkCallType", async () => { - const invalidEvent = { - type: 1, - userId: "550e8400-e29b-41d4-a716-446655440000", - data: { - case: "sdkCall", - value: { - sdkCallType: 999, - debit: { - case: "amount", - value: 10.0, - }, - }, - }, - }; - - const result = await registerEventSchema.safeParseAsync(invalidEvent); - expect(result.success).toBe(false); - }); -}); - -describe("streamEventSchema", () => { - it("accepts AI_TOKEN_USAGE events", async () => { - const validEvent = { - type: 2, - userId: "550e8400-e29b-41d4-a716-446655440000", - data: { - case: "aiTokenUsage", - value: { - model: "gpt-4", - inputTokens: 100, - outputTokens: 50, - inputDebit: { - case: "inputAmount", - value: 0.01, - }, - outputDebit: { - case: "outputAmount", - value: 0.02, - }, - }, - }, - }; - - const result = await streamEventSchema.safeParseAsync(validEvent); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe("AI_TOKEN_USAGE"); - if (result.data.type === "AI_TOKEN_USAGE") { - expect(result.data.data.model).toBe("gpt-4"); - } - } - }); - - it("rejects SDK_CALL events", async () => { - const invalidEvent = { - type: 1, - userId: "550e8400-e29b-41d4-a716-446655440000", - data: { - case: "sdkCall", - value: { - sdkCallType: 1, - debit: { - case: "amount", - value: 10.5, - }, - }, - }, - }; - - const result = await streamEventSchema.safeParseAsync(invalidEvent); - expect(result.success).toBe(false); - }); -}); diff --git a/src/__tests__/unit/zod/payment.test.ts b/src/__tests__/unit/zod/payment.test.ts deleted file mode 100644 index 5e3c019..0000000 --- a/src/__tests__/unit/zod/payment.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { createCheckoutLinkSchema } from "../../../zod/payment"; - -describe("createCheckoutLinkSchema", () => { - it("validates a valid checkout link request with UUID", () => { - const validRequest = { - userId: "550e8400-e29b-41d4-a716-446655440000", - }; - - const result = createCheckoutLinkSchema.safeParse(validRequest); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.userId).toBe("550e8400-e29b-41d4-a716-446655440000"); - } - }); - - it("rejects invalid UUID format", () => { - const invalidRequest = { - userId: "not-a-valid-uuid", - }; - - const result = createCheckoutLinkSchema.safeParse(invalidRequest); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues[0]?.message).toBe("Invalid UUID"); - } - }); - - it("rejects empty string userId", () => { - const invalidRequest = { - userId: "", - }; - - const result = createCheckoutLinkSchema.safeParse(invalidRequest); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues[0]?.message).toBe("Invalid UUID"); - } - }); - - it("strips extra fields from payload", () => { - const payloadWithExtra = { - userId: "550e8400-e29b-41d4-a716-446655440000", - extraField: "should be stripped", - }; - - const result = createCheckoutLinkSchema.safeParse(payloadWithExtra); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).not.toHaveProperty("extraField"); - } - }); -}); diff --git a/src/events/RequestEvents/RequestAITokenUsage.ts b/src/events/RequestEvents/RequestAITokenUsage.ts index bcb2c4a..9b26b9b 100644 --- a/src/events/RequestEvents/RequestAITokenUsage.ts +++ b/src/events/RequestEvents/RequestAITokenUsage.ts @@ -1,29 +1,29 @@ -import type { - RequestAITokenUsageEventData, - RequestAITokenUsageEvent, -} from "../../interface/event/Event"; -import { DateTime } from "luxon"; -import type { UserId } from "../../config/identifiers"; +// import type { +// RequestAITokenUsageEventData, +// RequestAITokenUsageEvent, +// } from "../../interface/event/Event"; +// import { DateTime } from "luxon"; +// import type { UserId } from "../../config/identifiers"; -export class RequestAITokenUsage implements RequestAITokenUsageEvent { - public reported_timestamp: DateTime; - public readonly type = "REQUEST_AI_TOKEN_USAGE" as const; +// export class RequestAITokenUsage implements RequestAITokenUsageEvent { +// public reported_timestamp: DateTime; +// public readonly type = "REQUEST_AI_TOKEN_USAGE" as const; - constructor( - public userId: UserId, - public data: RequestAITokenUsageEventData - ) { - this.reported_timestamp = DateTime.utc(); - } +// constructor( +// public userId: UserId, +// public data: RequestAITokenUsageEventData +// ) { +// this.reported_timestamp = DateTime.utc(); +// } - serialize() { - return { - SQL: { - type: this.type, - userId: this.userId, - reported_timestamp: this.reported_timestamp, - data: this.data, - }, - }; - } -} +// serialize() { +// return { +// SQL: { +// type: this.type, +// userId: this.userId, +// reported_timestamp: this.reported_timestamp, +// data: this.data, +// }, +// }; +// } +// } diff --git a/src/events/RequestEvents/RequestPayment.ts b/src/events/RequestEvents/RequestPayment.ts index 32554ae..4665bab 100644 --- a/src/events/RequestEvents/RequestPayment.ts +++ b/src/events/RequestEvents/RequestPayment.ts @@ -1,29 +1,29 @@ -import type { - RequestPaymentEvent, - RequestPaymentEventData, -} from "../../interface/event/Event"; -import { DateTime } from "luxon"; -import type { UserId } from "../../config/identifiers"; +// import type { +// RequestPaymentEvent, +// RequestPaymentEventData, +// } from "../../interface/event/Event"; +// import { DateTime } from "luxon"; +// import type { UserId } from "../../config/identifiers"; -export class RequestPayment implements RequestPaymentEvent { - public reported_timestamp: DateTime; - public readonly type = "REQUEST_PAYMENT" as const; +// export class RequestPayment implements RequestPaymentEvent { +// public reported_timestamp: DateTime; +// public readonly type = "REQUEST_PAYMENT" as const; - constructor( - public userId: UserId, - public data: RequestPaymentEventData - ) { - this.reported_timestamp = DateTime.utc(); - } +// constructor( +// public userId: UserId, +// public data: RequestPaymentEventData +// ) { +// this.reported_timestamp = DateTime.utc(); +// } - serialize() { - return { - SQL: { - type: this.type, - userId: this.userId, - reported_timestamp: this.reported_timestamp, - data: this.data, - }, - }; - } -} +// serialize() { +// return { +// SQL: { +// type: this.type, +// userId: this.userId, +// reported_timestamp: this.reported_timestamp, +// data: this.data, +// }, +// }; +// } +// } diff --git a/src/events/RequestEvents/RequestSDKCall.ts b/src/events/RequestEvents/RequestSDKCall.ts index fb58160..fcb7b2c 100644 --- a/src/events/RequestEvents/RequestSDKCall.ts +++ b/src/events/RequestEvents/RequestSDKCall.ts @@ -1,29 +1,29 @@ -import type { - RequestSDKCallEventData, - RequestSDKCallEvent, -} from "../../interface/event/Event"; -import { DateTime } from "luxon"; -import type { UserId } from "../../config/identifiers"; +// import type { +// RequestSDKCallEventData, +// RequestSDKCallEvent, +// } from "../../interface/event/Event"; +// import { DateTime } from "luxon"; +// import type { UserId } from "../../config/identifiers"; -export class RequestSDKCall implements RequestSDKCallEvent { - public reported_timestamp: DateTime; - public readonly type = "REQUEST_SDK_CALL" as const; +// export class RequestSDKCall implements RequestSDKCallEvent { +// public reported_timestamp: DateTime; +// public readonly type = "REQUEST_SDK_CALL" as const; - constructor( - public userId: UserId, - public data: RequestSDKCallEventData - ) { - this.reported_timestamp = DateTime.utc(); - } +// constructor( +// public userId: UserId, +// public data: RequestSDKCallEventData +// ) { +// this.reported_timestamp = DateTime.utc(); +// } - serialize() { - return { - SQL: { - type: this.type, - userId: this.userId, - reported_timestamp: this.reported_timestamp, - data: this.data, - }, - }; - } -} +// serialize() { +// return { +// SQL: { +// type: this.type, +// userId: this.userId, +// reported_timestamp: this.reported_timestamp, +// data: this.data, +// }, +// }; +// } +// } diff --git a/src/factory/StorageAdapterFactory.ts b/src/factory/EventStorageAdapterFactory.ts similarity index 52% rename from src/factory/StorageAdapterFactory.ts rename to src/factory/EventStorageAdapterFactory.ts index 122998b..66471c8 100644 --- a/src/factory/StorageAdapterFactory.ts +++ b/src/factory/EventStorageAdapterFactory.ts @@ -1,4 +1,4 @@ -import type { Event } from "../interface/event/Event.ts"; +import type { EventKind } from "../interface/event/Event.ts"; import { PostgresAdapter } from "../storage/adapter/postgres/postgres.ts"; /** @@ -15,31 +15,22 @@ export class StorageAdapterFactory { * @param apiKeyId - Optional API key ID to associate with the event * @returns The storage adapter instance for the event type */ - public static async getStorageAdapter(event: Event, apiKeyId?: string) { - switch (event.type) { + public static async getEventStorageAdapter(RequestType: EventKind) { + switch (RequestType) { case "SDK_CALL": { - return new PostgresAdapter(event, apiKeyId); + return new PostgresAdapter(); } case "AI_TOKEN_USAGE": { - return new PostgresAdapter(event, apiKeyId); + return new PostgresAdapter(); } case "PAYMENT": { - return new PostgresAdapter(event, apiKeyId); + return new PostgresAdapter(); } case "ADD_KEY": { - return new PostgresAdapter(event); - } - case "REQUEST_PAYMENT": { - return new PostgresAdapter(event); - } - case "REQUEST_SDK_CALL": { - return new PostgresAdapter(event); - } - case "REQUEST_AI_TOKEN_USAGE": { - return new PostgresAdapter(event); + return new PostgresAdapter(); } default: { - throw new Error(`Unknown event type: ${event}`); + throw new Error(`Unknown event type: ${RequestType}`); } } } diff --git a/src/factory/index.ts b/src/factory/index.ts index 837b2b2..ab24bb3 100644 --- a/src/factory/index.ts +++ b/src/factory/index.ts @@ -1 +1 @@ -export { StorageAdapterFactory } from "./StorageAdapterFactory.ts"; +export { StorageAdapterFactory } from "./EventStorageAdapterFactory.ts"; diff --git a/src/interface/event/Event.ts b/src/interface/event/Event.ts index 40c530f..1f3f435 100644 --- a/src/interface/event/Event.ts +++ b/src/interface/event/Event.ts @@ -27,12 +27,6 @@ export type PaymentEventData = { creditAmount: number; }; -export type RequestPaymentEventData = null; - -export type RequestSDKCallEventData = null; - -export type RequestAITokenUsageEventData = null; - /** * Event kind discriminator */ @@ -41,9 +35,6 @@ export type EventKind = | "AI_TOKEN_USAGE" | "ADD_KEY" | "PAYMENT" - | "REQUEST_PAYMENT" - | "REQUEST_SDK_CALL" - | "REQUEST_AI_TOKEN_USAGE"; /** * Mapping of event kinds to their data structures @@ -53,9 +44,6 @@ export type EventDataMap = { AI_TOKEN_USAGE: AITokenUsageEventData; ADD_KEY: AddKeyEventData; PAYMENT: PaymentEventData; - REQUEST_PAYMENT: RequestPaymentEventData; - REQUEST_SDK_CALL: RequestSDKCallEventData; - REQUEST_AI_TOKEN_USAGE: RequestAITokenUsageEventData; }; /** @@ -87,9 +75,6 @@ type SqlRecordMap = { SDK_CALL: SqlRecordWithUserId<"SDK_CALL">; AI_TOKEN_USAGE: SqlRecordWithUserId<"AI_TOKEN_USAGE">; PAYMENT: SqlRecordWithUserId<"PAYMENT">; - REQUEST_PAYMENT: SqlRecordWithUserId<"REQUEST_PAYMENT">; - REQUEST_SDK_CALL: SqlRecordWithUserId<"REQUEST_SDK_CALL">; - REQUEST_AI_TOKEN_USAGE: SqlRecordWithUserId<"REQUEST_AI_TOKEN_USAGE">; }; /** @@ -140,24 +125,3 @@ export interface AddKeyEvent extends Event<"ADD_KEY"> {} export interface PaymentEvent extends Event<"PAYMENT"> { readonly userId: UserId; } - -/** - * Payment Request Event - */ -export interface RequestPaymentEvent extends Event<"REQUEST_PAYMENT"> { - readonly userId: UserId; -} - -/** - * SDK Call Request Event - */ -export interface RequestSDKCallEvent extends Event<"REQUEST_SDK_CALL"> { - readonly userId: UserId; -} - -/** - * AI Token Usage Request Event - */ -export interface RequestAITokenUsageEvent extends Event<"REQUEST_AI_TOKEN_USAGE"> { - readonly userId: UserId; -} diff --git a/src/interface/storage/Storage.ts b/src/interface/storage/Storage.ts index 01685b0..68100a4 100644 --- a/src/interface/storage/Storage.ts +++ b/src/interface/storage/Storage.ts @@ -1,12 +1,15 @@ import type { SerializedEvent, EventKind } from "../event/Event"; +import { type UserId } from "../../config/identifiers"; /** * Storage Adapter - consumes and persists events */ export interface StorageAdapter { - name: string; connectionObject: unknown; - add(serialized: SerializedEvent): Promise<{ id: string } | void>; - price(serialized: SerializedEvent): Promise; + add( + serialized: SerializedEvent, + apiKeyId: string + ): Promise<{ id: string } | void>; + price(userID: UserId, event_type: EventKind): Promise; } diff --git a/src/routes/gRPC/auth/createAPIKey.ts b/src/routes/gRPC/auth/createAPIKey.ts index 5ae67a4..5349067 100644 --- a/src/routes/gRPC/auth/createAPIKey.ts +++ b/src/routes/gRPC/auth/createAPIKey.ts @@ -55,8 +55,10 @@ export async function createAPIKey( expiresAt: expiresAt.toISOString(), }); - const adapter = await StorageAdapterFactory.getStorageAdapter(addKeyEvent); - const keyEventData = await adapter.add(addKeyEvent.serialize()); + const adapter = await StorageAdapterFactory.getEventStorageAdapter( + addKeyEvent.type + ); + const keyEventData = await adapter.add(addKeyEvent.serialize(), ""); if (!keyEventData) { throw APIKeyError.creationFailed("Storage returned no ID"); diff --git a/src/routes/gRPC/payment/createCheckoutLink.ts b/src/routes/gRPC/payment/createCheckoutLink.ts index 9e8867b..e561042 100644 --- a/src/routes/gRPC/payment/createCheckoutLink.ts +++ b/src/routes/gRPC/payment/createCheckoutLink.ts @@ -17,9 +17,9 @@ import { createCheckout, } from "@lemonsqueezy/lemonsqueezy.js"; import { StorageAdapterFactory } from "../../../factory"; -import { RequestPayment } from "../../../events/RequestEvents/RequestPayment"; import { apiKeyContextKey } from "../../../context/auth"; import { wideEventContextKey } from "../../../context/requestContext"; +import type { UserId } from "../../../config/identifiers"; export async function createCheckoutLink( req: CreateCheckoutLinkRequest, @@ -95,15 +95,15 @@ function validateRequest( } } -async function calculatePrice(userId: string): Promise { - const event = new RequestPayment(userId, null); - const storageAdapter = await StorageAdapterFactory.getStorageAdapter(event); +async function calculatePrice(userId: UserId): Promise { + const storageAdapter = + await StorageAdapterFactory.getEventStorageAdapter("PAYMENT"); if (!storageAdapter) { throw PaymentError.storageAdapterFailed("Storage adapter not available"); } - const price = await storageAdapter.price(event.serialize()); + const price = await storageAdapter.price(userId, "PAYMENT"); if (typeof price !== "number" || isNaN(price) || price < 0) { throw PaymentError.priceCalculationFailed( diff --git a/src/routes/http/createdCheckout.ts b/src/routes/http/createdCheckout.ts index 9236b51..a2b9f6d 100644 --- a/src/routes/http/createdCheckout.ts +++ b/src/routes/http/createdCheckout.ts @@ -2,7 +2,7 @@ import crypto from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; import { lemonSqueezySetup } from "@lemonsqueezy/lemonsqueezy.js"; import { Payment } from "../../events/RawEvents/Payment.ts"; -import { StorageAdapterFactory } from "../../factory/StorageAdapterFactory.ts"; +import { StorageAdapterFactory } from "../../factory/EventStorageAdapterFactory.ts"; import type { WideEventBuilder } from "../../context/requestContext.ts"; const isDev = process.env.NODE_ENV !== "production"; @@ -191,16 +191,16 @@ export async function handleLemonSqueezyWebhook( // Create and store the payment event try { const paymentEvent = new Payment(userId, { creditAmount }); - const adapter = await StorageAdapterFactory.getStorageAdapter( - paymentEvent, - apiKeyId - ); + const adapter = + await StorageAdapterFactory.getEventStorageAdapter("PAYMENT"); - await adapter.add(paymentEvent.serialize()); + await adapter.add(paymentEvent.serialize(), apiKeyId); builder.setSuccess(200); res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ message: "Webhook processed successfully" })); + res.end( + JSON.stringify({ message: "Webhook processed successfuladdEvent " }) + ); } catch (dbError) { const errorMessage = dbError instanceof Error ? dbError.message : String(dbError); diff --git a/src/storage/adapter/postgres/handlers/priceRequestAiTokenUsage.ts b/src/storage/adapter/postgres/handlers/priceRequestAiTokenUsage.ts index a55cb76..acd96cd 100644 --- a/src/storage/adapter/postgres/handlers/priceRequestAiTokenUsage.ts +++ b/src/storage/adapter/postgres/handlers/priceRequestAiTokenUsage.ts @@ -6,26 +6,22 @@ import { import { StorageError } from "../../../../errors/storage"; import { eq, sum, sql } from "drizzle-orm"; import { type SqlRecord } from "../../../../interface/event/Event"; +import { type UserId } from "../../../../config/identifiers"; export async function handlePriceRequestAiTokenUsage( - event_data: SqlRecord<"REQUEST_AI_TOKEN_USAGE"> + userId: UserId ): Promise { const connectionObject = getPostgresDB(); try { - if (!event_data.userId) { + if (!userId) { throw StorageError.invalidData( "Missing userId in REQUEST_AI_TOKEN_USAGE event" ); } - if ( - typeof event_data.userId !== "string" || - event_data.userId.trim().length === 0 - ) { - throw StorageError.invalidData( - `Invalid userId format: ${typeof event_data.userId}` - ); + if (typeof userId !== "string" || userId.trim().length === 0) { + throw StorageError.invalidData(`Invalid userId format: ${typeof userId}`); } let result; @@ -38,24 +34,24 @@ export async function handlePriceRequestAiTokenUsage( }) .from(aiTokenUsageEventsTable) .leftJoin(eventsTable, eq(aiTokenUsageEventsTable.id, eventsTable.id)) - .where(eq(eventsTable.userId, event_data.userId)) + .where(eq(eventsTable.userId, userId)) .groupBy(eventsTable.userId); } catch (e) { throw StorageError.queryFailed( - `Failed to query REQUEST_AI_TOKEN_USAGE events for user ${event_data.userId}`, + `Failed to query REQUEST_AI_TOKEN_USAGE events for user ${userId}`, e instanceof Error ? e : new Error(String(e)) ); } if (!result) { throw StorageError.emptyResult( - `Price query returned null for user ${event_data.userId}` + `Price query returned null for user ${userId}` ); } if (!Array.isArray(result)) { throw StorageError.queryFailed( - `Query result is not an array for user ${event_data.userId}` + `Query result is not an array for user ${userId}` ); } @@ -74,14 +70,14 @@ export async function handlePriceRequestAiTokenUsage( parsedPrice = parseInt(priceValue); } catch (e) { throw StorageError.priceCalculationFailed( - event_data.userId, + userId, new Error(`Failed to parse price value: ${priceValue}`) ); } if (isNaN(parsedPrice)) { throw StorageError.priceCalculationFailed( - event_data.userId, + userId, new Error(`Price parsed to NaN from value: ${priceValue}`) ); } diff --git a/src/storage/adapter/postgres/handlers/priceRequestPayment.ts b/src/storage/adapter/postgres/handlers/priceRequestPayment.ts index eb409c0..749e040 100644 --- a/src/storage/adapter/postgres/handlers/priceRequestPayment.ts +++ b/src/storage/adapter/postgres/handlers/priceRequestPayment.ts @@ -1,21 +1,19 @@ import { StorageError } from "../../../../errors/storage"; -import { RequestSDKCall } from "../../../../events/RequestEvents/RequestSDKCall"; -import { RequestAITokenUsage } from "../../../../events/RequestEvents/RequestAITokenUsage"; import { StorageAdapterFactory } from "../../../../factory"; import { type SqlRecord } from "../../../../interface/event/Event"; +import type { UserId } from "../../../../config/identifiers"; export async function handlePriceRequestPayment( - event_data: SqlRecord<"REQUEST_PAYMENT"> + userId: UserId ): Promise { try { - if (!event_data.userId) { + if (!userId) { throw StorageError.invalidData("Missing userId in REQUEST_PAYMENT event"); } // Calculate SDK call price - const sdkEvent = new RequestSDKCall(event_data.userId, null); const sdkStorageAdapter = - await StorageAdapterFactory.getStorageAdapter(sdkEvent); + await StorageAdapterFactory.getEventStorageAdapter("SDK_CALL"); if (!sdkStorageAdapter) { throw StorageError.unknown( @@ -25,19 +23,18 @@ export async function handlePriceRequestPayment( ); } - const sdkPrice = await sdkStorageAdapter.price(sdkEvent.serialize()); + const sdkPrice = await sdkStorageAdapter.price(userId, "SDK_CALL"); if (typeof sdkPrice !== "number" || isNaN(sdkPrice)) { throw StorageError.priceCalculationFailed( - event_data.userId, + userId, new Error(`Invalid SDK price value returned: ${sdkPrice}`) ); } // Calculate AI token usage price - const aiEvent = new RequestAITokenUsage(event_data.userId, null); const aiStorageAdapter = - await StorageAdapterFactory.getStorageAdapter(aiEvent); + await StorageAdapterFactory.getEventStorageAdapter("AI_TOKEN_USAGE"); if (!aiStorageAdapter) { throw StorageError.unknown( @@ -47,11 +44,11 @@ export async function handlePriceRequestPayment( ); } - const aiPrice = await aiStorageAdapter.price(aiEvent.serialize()); + const aiPrice = await aiStorageAdapter.price(userId, "AI_TOKEN_USAGE"); if (typeof aiPrice !== "number" || isNaN(aiPrice)) { throw StorageError.priceCalculationFailed( - event_data.userId, + userId, new Error(`Invalid AI price value returned: ${aiPrice}`) ); } diff --git a/src/storage/adapter/postgres/handlers/priceRequestSdkCall.ts b/src/storage/adapter/postgres/handlers/priceRequestSdkCall.ts index 5adaa9f..69c69b3 100644 --- a/src/storage/adapter/postgres/handlers/priceRequestSdkCall.ts +++ b/src/storage/adapter/postgres/handlers/priceRequestSdkCall.ts @@ -2,27 +2,22 @@ import { getPostgresDB } from "../../../db/postgres/db"; import { sdkCallEventsTable, eventsTable } from "../../../db/postgres/schema"; import { StorageError } from "../../../../errors/storage"; import { eq, sum } from "drizzle-orm"; -import { type SqlRecord } from "../../../../interface/event/Event"; +import { type UserId } from "../../../../config/identifiers"; export async function handlePriceRequestSdkCall( - event_data: SqlRecord<"REQUEST_SDK_CALL"> + userId: UserId ): Promise { const connectionObject = getPostgresDB(); try { - if (!event_data.userId) { + if (!userId) { throw StorageError.invalidData( "Missing userId in REQUEST_SDK_CALL event" ); } - if ( - typeof event_data.userId !== "string" || - event_data.userId.trim().length === 0 - ) { - throw StorageError.invalidData( - `Invalid userId format: ${typeof event_data.userId}` - ); + if (typeof userId !== "string" || userId.trim().length === 0) { + throw StorageError.invalidData(`Invalid userId format: ${typeof userId}`); } let result; @@ -33,24 +28,24 @@ export async function handlePriceRequestSdkCall( }) .from(sdkCallEventsTable) .leftJoin(eventsTable, eq(sdkCallEventsTable.id, eventsTable.id)) - .where(eq(eventsTable.userId, event_data.userId)) + .where(eq(eventsTable.userId, userId)) .groupBy(eventsTable.userId); } catch (e) { throw StorageError.queryFailed( - `Failed to query SDK_CALL events for user ${event_data.userId}`, + `Failed to query SDK_CALL events for user ${userId}`, e instanceof Error ? e : new Error(String(e)) ); } if (!result) { throw StorageError.emptyResult( - `Price query returned null for user ${event_data.userId}` + `Price query returned null for user ${userId}` ); } if (!Array.isArray(result)) { throw StorageError.queryFailed( - `Query result is not an array for user ${event_data.userId}` + `Query result is not an array for user ${userId}` ); } @@ -69,14 +64,14 @@ export async function handlePriceRequestSdkCall( parsedPrice = parseInt(priceValue); } catch (e) { throw StorageError.priceCalculationFailed( - event_data.userId, + userId, new Error(`Failed to parse price value: ${priceValue}`) ); } if (isNaN(parsedPrice)) { throw StorageError.priceCalculationFailed( - event_data.userId, + userId, new Error(`Price parsed to NaN from value: ${priceValue}`) ); } diff --git a/src/storage/adapter/postgres/postgres.ts b/src/storage/adapter/postgres/postgres.ts index b30c8ce..94c8c15 100644 --- a/src/storage/adapter/postgres/postgres.ts +++ b/src/storage/adapter/postgres/postgres.ts @@ -14,21 +14,15 @@ import { import type { SerializedEvent, EventKind, + SqlRecord, } from "../../../interface/event/Event"; +import type { UserId } from "../../../config/identifiers"; export class PostgresAdapter implements StorageAdapter { - name: string; - connectionObject; - apiKeyId?: string; + connectionObject = getPostgresDB(); - constructor(event: Event, apiKeyId?: string) { - this.name = event.type; - this.connectionObject = getPostgresDB(); - this.apiKeyId = apiKeyId; - } - - async add(serialized: SerializedEvent) { - let event_data; + async add(serialized: SerializedEvent, apiKeyId: string) { + let event_data: SqlRecord; try { const { SQL } = serialized; @@ -57,10 +51,17 @@ export class PostgresAdapter implements StorageAdapter { switch (event_data.type) { case "SDK_CALL": { - if (!this.apiKeyId) { + if (!apiKeyId) { throw StorageError.missingApiKeyId(); } - return await handleAddSdkCall(event_data, this.apiKeyId); + return await handleAddSdkCall(event_data, apiKeyId); + } + + case "AI_TOKEN_USAGE": { + if (!apiKeyId) { + throw StorageError.missingApiKeyId(); + } + return await handleAddAiTokenUsage([event_data], apiKeyId); } case "ADD_KEY": { @@ -68,65 +69,32 @@ export class PostgresAdapter implements StorageAdapter { } case "PAYMENT": { - return await handleAddPayment(event_data, this.apiKeyId); - } - - case "AI_TOKEN_USAGE": { - if (!this.apiKeyId) { - throw StorageError.missingApiKeyId(); - } - return await handleAddAiTokenUsage([event_data], this.apiKeyId); + return await handleAddPayment(event_data, apiKeyId); } default: { + //@ts-ignore throw StorageError.unknownEventType(event_data.type); } } } - async price(serialized: SerializedEvent): Promise { - let event_data; - - try { - const { SQL } = serialized; - event_data = SQL; - - if (!event_data) { - throw StorageError.serializationFailed( - "Event serialization returned null or undefined" - ); - } - } catch (e) { - // Use duck typing instead of instanceof to work with mocked modules - if ( - e && - typeof e === "object" && - "type" in e && - (e as any).name === "StorageError" - ) { - throw e; - } - throw StorageError.serializationFailed( - "Failed to serialize event data for price calculation", - e instanceof Error ? e : new Error(String(e)) - ); - } - - switch (event_data.type) { - case "REQUEST_PAYMENT": { - return await handlePriceRequestPayment(event_data); + async price(userID: UserId, event_type: EventKind): Promise { + switch (event_type) { + case "PAYMENT": { + return await handlePriceRequestPayment(userID); } - case "REQUEST_SDK_CALL": { - return await handlePriceRequestSdkCall(event_data); + case "SDK_CALL": { + return await handlePriceRequestSdkCall(userID); } - case "REQUEST_AI_TOKEN_USAGE": { - return await handlePriceRequestAiTokenUsage(event_data); + case "AI_TOKEN_USAGE": { + return await handlePriceRequestAiTokenUsage(userID); } default: { - throw StorageError.unknownEventType(event_data.type); + throw StorageError.unknownEventType(event_type); } } } diff --git a/src/utils/eventHelpers.ts b/src/utils/eventHelpers.ts index af92432..a37794d 100644 --- a/src/utils/eventHelpers.ts +++ b/src/utils/eventHelpers.ts @@ -111,9 +111,8 @@ export async function storeEvent( event: Event, apiKeyId: string ): Promise { - const adapter = await StorageAdapterFactory.getStorageAdapter( - event, - apiKeyId + const adapter = await StorageAdapterFactory.getEventStorageAdapter( + event.type ); - await adapter.add(event.serialize()); + await adapter.add(event.serialize(), apiKeyId); } From 8ba75f2adec06661b38e2c95909f40f3b4c26256 Mon Sep 17 00:00:00 2001 From: Jayadeep Bejoy Date: Wed, 22 Apr 2026 22:33:05 +0530 Subject: [PATCH 2/2] refactor(request) --- .../RequestEvents/RequestAITokenUsage.ts | 29 ------------------- src/events/RequestEvents/RequestPayment.ts | 29 ------------------- src/events/RequestEvents/RequestSDKCall.ts | 29 ------------------- 3 files changed, 87 deletions(-) delete mode 100644 src/events/RequestEvents/RequestAITokenUsage.ts delete mode 100644 src/events/RequestEvents/RequestPayment.ts delete mode 100644 src/events/RequestEvents/RequestSDKCall.ts diff --git a/src/events/RequestEvents/RequestAITokenUsage.ts b/src/events/RequestEvents/RequestAITokenUsage.ts deleted file mode 100644 index 9b26b9b..0000000 --- a/src/events/RequestEvents/RequestAITokenUsage.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import type { -// RequestAITokenUsageEventData, -// RequestAITokenUsageEvent, -// } from "../../interface/event/Event"; -// import { DateTime } from "luxon"; -// import type { UserId } from "../../config/identifiers"; - -// export class RequestAITokenUsage implements RequestAITokenUsageEvent { -// public reported_timestamp: DateTime; -// public readonly type = "REQUEST_AI_TOKEN_USAGE" as const; - -// constructor( -// public userId: UserId, -// public data: RequestAITokenUsageEventData -// ) { -// this.reported_timestamp = DateTime.utc(); -// } - -// serialize() { -// return { -// SQL: { -// type: this.type, -// userId: this.userId, -// reported_timestamp: this.reported_timestamp, -// data: this.data, -// }, -// }; -// } -// } diff --git a/src/events/RequestEvents/RequestPayment.ts b/src/events/RequestEvents/RequestPayment.ts deleted file mode 100644 index 4665bab..0000000 --- a/src/events/RequestEvents/RequestPayment.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import type { -// RequestPaymentEvent, -// RequestPaymentEventData, -// } from "../../interface/event/Event"; -// import { DateTime } from "luxon"; -// import type { UserId } from "../../config/identifiers"; - -// export class RequestPayment implements RequestPaymentEvent { -// public reported_timestamp: DateTime; -// public readonly type = "REQUEST_PAYMENT" as const; - -// constructor( -// public userId: UserId, -// public data: RequestPaymentEventData -// ) { -// this.reported_timestamp = DateTime.utc(); -// } - -// serialize() { -// return { -// SQL: { -// type: this.type, -// userId: this.userId, -// reported_timestamp: this.reported_timestamp, -// data: this.data, -// }, -// }; -// } -// } diff --git a/src/events/RequestEvents/RequestSDKCall.ts b/src/events/RequestEvents/RequestSDKCall.ts deleted file mode 100644 index fcb7b2c..0000000 --- a/src/events/RequestEvents/RequestSDKCall.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import type { -// RequestSDKCallEventData, -// RequestSDKCallEvent, -// } from "../../interface/event/Event"; -// import { DateTime } from "luxon"; -// import type { UserId } from "../../config/identifiers"; - -// export class RequestSDKCall implements RequestSDKCallEvent { -// public reported_timestamp: DateTime; -// public readonly type = "REQUEST_SDK_CALL" as const; - -// constructor( -// public userId: UserId, -// public data: RequestSDKCallEventData -// ) { -// this.reported_timestamp = DateTime.utc(); -// } - -// serialize() { -// return { -// SQL: { -// type: this.type, -// userId: this.userId, -// reported_timestamp: this.reported_timestamp, -// data: this.data, -// }, -// }; -// } -// }