From 39fb8e01e79cce45a0a4ac1222be9b69c6e3cf8b Mon Sep 17 00:00:00 2001 From: OpenClaw Subagent Date: Thu, 12 Mar 2026 14:12:23 +0100 Subject: [PATCH] Revert "add coverage reporting + improve test coverage for undertested files" This reverts commit 0a17e27fcd74e0d74259e997112c2c2a58aecb2d. --- package-lock.json | 198 -------- package.json | 1 - src/__tests__/billing-error-paths.test.ts | 455 ------------------- src/__tests__/browser-error-handling.test.ts | 316 ------------- src/__tests__/db-retry-logic.test.ts | 335 -------------- vitest.config.ts | 6 - 6 files changed, 1311 deletions(-) delete mode 100644 src/__tests__/billing-error-paths.test.ts delete mode 100644 src/__tests__/browser-error-handling.test.ts delete mode 100644 src/__tests__/db-retry-logic.test.ts diff --git a/package-lock.json b/package-lock.json index ec69028..aa6ac4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "@types/pg": "^8.18.0", "@types/supertest": "^7.2.0", "@types/swagger-jsdoc": "^6.0.4", - "@vitest/coverage-v8": "^4.0.18", "supertest": "^7.2.2", "terser": "^5.46.0", "tsx": "^4.21.0", @@ -96,16 +95,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", @@ -115,46 +104,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -1313,37 +1262,6 @@ "@types/node": "*" } }, - "node_modules/@vitest/coverage-v8": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", - "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.18", - "ast-v8-to-istanbul": "^0.3.10", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", - "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.0.18", - "vitest": "4.0.18" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -1547,25 +1465,6 @@ "node": ">=4" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2816,16 +2715,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2875,13 +2764,6 @@ "node": ">=18.0.0" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -3062,45 +2944,6 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3170,34 +3013,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/marked": { "version": "17.0.4", "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", @@ -4579,19 +4394,6 @@ "node": ">=14.18.0" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/swagger-jsdoc": { "version": "6.2.8", "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", diff --git a/package.json b/package.json index 7295b16..d9636e6 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@types/pg": "^8.18.0", "@types/supertest": "^7.2.0", "@types/swagger-jsdoc": "^6.0.4", - "@vitest/coverage-v8": "^4.0.18", "supertest": "^7.2.2", "terser": "^5.46.0", "tsx": "^4.21.0", diff --git a/src/__tests__/billing-error-paths.test.ts b/src/__tests__/billing-error-paths.test.ts deleted file mode 100644 index 1e881e4..0000000 --- a/src/__tests__/billing-error-paths.test.ts +++ /dev/null @@ -1,455 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import request from "supertest"; - -// Mock logger -const mockLogger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), -}; - -vi.mock("../services/logger.js", () => ({ - default: mockLogger, -})); - -// Mock keys service -const mockCreateProKey = vi.fn(); -const mockDowngradeByCustomer = vi.fn(); -const mockUpdateEmailByCustomer = vi.fn(); -const mockFindKeyByCustomerId = vi.fn(); - -vi.mock("../services/keys.js", () => ({ - createProKey: mockCreateProKey, - downgradeByCustomer: mockDowngradeByCustomer, - updateEmailByCustomer: mockUpdateEmailByCustomer, - findKeyByCustomerId: mockFindKeyByCustomerId, -})); - -// Mock billing templates -const mockRenderSuccessPage = vi.fn(); -const mockRenderAlreadyProvisionedPage = vi.fn(); - -vi.mock("../utils/billing-templates.js", () => ({ - renderSuccessPage: mockRenderSuccessPage, - renderAlreadyProvisionedPage: mockRenderAlreadyProvisionedPage, -})); - -// Mock Stripe -const mockStripe = { - checkout: { - sessions: { - create: vi.fn(), - retrieve: vi.fn(), - listLineItems: vi.fn(), - }, - }, - customers: { - retrieve: vi.fn(), - update: vi.fn(), - }, - subscriptions: { - retrieve: vi.fn(), - }, - webhooks: { - constructEvent: vi.fn(), - }, - products: { - list: vi.fn(), - }, - prices: { - list: vi.fn(), - create: vi.fn(), - }, -}; - -vi.mock("stripe", () => ({ - default: vi.fn().mockImplementation(() => mockStripe), -})); - -describe("Billing Error Paths", () => { - let app: any; - let billingRouter: any; - - beforeEach(async () => { - vi.clearAllMocks(); - - // Set up environment - process.env.STRIPE_SECRET_KEY = "sk_test_123"; - process.env.STRIPE_WEBHOOK_SECRET = "whsec_test"; - - // Mock default responses - mockRenderSuccessPage.mockReturnValue("Success"); - mockRenderAlreadyProvisionedPage.mockReturnValue("Already Provisioned"); - - // Import and set up minimal Express app - const express = await import("express"); - const billingModule = await import("../routes/billing.js"); - - app = express.default(); - app.use(express.default.json()); - app.use(express.default.raw({ type: "application/json" })); - app.use("/", billingModule.default); - - billingRouter = billingModule.default; - }); - - afterEach(() => { - delete process.env.STRIPE_SECRET_KEY; - delete process.env.STRIPE_WEBHOOK_SECRET; - }); - - describe("getStripe function error handling", () => { - it("should throw error when STRIPE_SECRET_KEY is not configured", async () => { - delete process.env.STRIPE_SECRET_KEY; - - // Clear the module cache to get a fresh import - vi.resetModules(); - - // Re-import the billing module - const express = await import("express"); - - const testApp = express.default(); - testApp.use(express.default.json()); - - // Import billing routes (this should work despite missing env var) - const billingModule = await import("../routes/billing.js"); - testApp.use("/", billingModule.default); - - // Making a checkout request should trigger the getStripe() function and fail - const response = await request(testApp) - .post("/checkout") - .send({ email: "test@example.com" }); - - expect(response.status).toBe(500); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - err: expect.objectContaining({ - message: "STRIPE_SECRET_KEY not configured", - }), - }), - "Checkout error" - ); - }); - }); - - describe("checkout error handling", () => { - beforeEach(() => { - process.env.STRIPE_SECRET_KEY = "sk_test_123"; - }); - - it("should handle Stripe session creation failure", async () => { - const stripeError = new Error("Stripe down"); - stripeError.code = "api_connection_error"; - mockStripe.checkout.sessions.create.mockRejectedValueOnce(stripeError); - - const response = await request(app) - .post("/checkout") - .send({ email: "test@example.com" }); - - expect(response.status).toBe(500); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - err: expect.objectContaining({ - message: "Stripe down", - }), - }), - "Checkout error" - ); - }); - - it("should handle product/price retrieval failure during checkout", async () => { - const stripeError = new Error("Product not found"); - stripeError.code = "resource_missing"; - mockStripe.products.list.mockRejectedValueOnce(stripeError); - - const response = await request(app) - .post("/checkout") - .send({ email: "test@example.com" }); - - expect(response.status).toBe(500); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - err: expect.objectContaining({ - message: "Product not found", - }), - }), - "Checkout error" - ); - }); - }); - - describe("success page error handling", () => { - it("should handle missing session_id parameter", async () => { - const response = await request(app) - .get("/success"); - - expect(response.status).toBe(400); - expect(response.text).toBe("Missing session_id"); - }); - - it("should handle Stripe session retrieval failure", async () => { - const stripeError = new Error("Session expired"); - stripeError.code = "resource_missing"; - mockStripe.checkout.sessions.retrieve.mockRejectedValueOnce(stripeError); - - const response = await request(app) - .get("/success?session_id=cs_test_invalid"); - - expect(response.status).toBe(500); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - err: expect.objectContaining({ - message: "Session expired", - }), - }), - "Success page error" - ); - }); - - it("should handle line items retrieval failure", async () => { - mockStripe.checkout.sessions.retrieve.mockResolvedValueOnce({ - id: "cs_test_123", - status: "complete", - customer: "cus_test", - customer_email: "test@example.com", - }); - - const stripeError = new Error("Stripe retrieve failed"); - stripeError.code = "api_error"; - mockStripe.checkout.sessions.listLineItems.mockRejectedValueOnce(stripeError); - - const response = await request(app) - .get("/success?session_id=cs_test_123"); - - expect(response.status).toBe(500); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - err: expect.objectContaining({ - message: "Stripe retrieve failed", - }), - sessionId: "cs_test_123", - }), - "Failed to retrieve session line_items" - ); - }); - }); - - describe("webhook error handling", () => { - it("should reject webhooks without signature header", async () => { - const response = await request(app) - .post("/webhook") - .send({ type: "checkout.session.completed" }); - - expect(response.status).toBe(400); - expect(response.text).toBe("Missing stripe-signature header"); - }); - - it("should reject webhooks when STRIPE_WEBHOOK_SECRET is not configured", async () => { - delete process.env.STRIPE_WEBHOOK_SECRET; - - const response = await request(app) - .post("/webhook") - .set("stripe-signature", "t=123,v1=abc") - .send({ type: "checkout.session.completed" }); - - expect(response.status).toBe(400); - expect(response.text).toBe("Webhook secret not configured"); - expect(mockLogger.error).toHaveBeenCalledWith( - "STRIPE_WEBHOOK_SECRET is not configured — refusing to process unverified webhooks" - ); - }); - - it("should handle invalid webhook signature", async () => { - const signatureError = new Error("Invalid signature"); - signatureError.code = "signature_verification_failed"; - mockStripe.webhooks.constructEvent.mockImplementation(() => { - throw signatureError; - }); - - const response = await request(app) - .post("/webhook") - .set("stripe-signature", "invalid_signature") - .send(JSON.stringify({ type: "checkout.session.completed" })); - - expect(response.status).toBe(400); - expect(response.text).toBe("Invalid signature"); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - err: expect.objectContaining({ - message: "Invalid signature", - }), - }), - "Webhook signature verification failed" - ); - }); - - it("should handle checkout.session.completed with missing data gracefully", async () => { - const webhookEvent = { - type: "checkout.session.completed", - data: { - object: { - id: "cs_test_123", - customer: null, // Missing customer - customer_email: null, // Missing email - }, - }, - }; - - mockStripe.webhooks.constructEvent.mockReturnValueOnce(webhookEvent); - - const response = await request(app) - .post("/webhook") - .set("stripe-signature", "valid_signature") - .send(JSON.stringify(webhookEvent)); - - expect(response.status).toBe(200); - expect(mockLogger.warn).toHaveBeenCalledWith( - "checkout.session.completed: missing customerId or email, skipping key provisioning" - ); - }); - - it("should handle subscription retrieval failure during webhook processing", async () => { - const webhookEvent = { - type: "customer.subscription.updated", - data: { - object: { - id: "sub_test_123", - customer: "cus_test_123", - status: "active", - cancel_at_period_end: false, - items: { - data: [{ price: { product: "prod_test" } }], - }, - }, - }, - }; - - mockStripe.webhooks.constructEvent.mockReturnValueOnce(webhookEvent); - - const stripeError = new Error("Subscription not found"); - stripeError.code = "resource_missing"; - mockStripe.subscriptions.retrieve.mockRejectedValueOnce(stripeError); - - const response = await request(app) - .post("/webhook") - .set("stripe-signature", "valid_signature") - .send(JSON.stringify(webhookEvent)); - - expect(response.status).toBe(500); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - err: expect.objectContaining({ - message: "Subscription not found", - }), - }), - "Error processing webhook" - ); - }); - }); - - describe("session cleanup functionality", () => { - it("should clean up old provisioned sessions", () => { - vi.useFakeTimers(); - - // Set a fixed time - const fixedTime = new Date("2022-01-02T12:00:00Z").getTime(); - vi.setSystemTime(fixedTime); - - // Manually trigger the cleanup logic by advancing time - // The cleanup interval is 1 hour, so advance by more than that - vi.advanceTimersByTime(60 * 60 * 1000 + 1000); // 1 hour + 1 second - - // The cleanup function should log when it runs - expect(mockLogger.info).toHaveBeenCalledWith( - expect.objectContaining({ - cleanedCount: expect.any(Number), - remainingCount: expect.any(Number), - }), - "Cleaned up expired provisioned sessions" - ); - - vi.useRealTimers(); - }); - }); - - describe("edge cases in webhook event processing", () => { - it("should handle customer email update for missing customer", async () => { - const webhookEvent = { - type: "customer.updated", - data: { - object: { - id: "cus_nonexistent", - email: "newemail@test.com", - }, - }, - }; - - mockStripe.webhooks.constructEvent.mockReturnValueOnce(webhookEvent); - mockFindKeyByCustomerId.mockResolvedValueOnce(null); // Customer not found - - const response = await request(app) - .post("/webhook") - .set("stripe-signature", "valid_signature") - .send(JSON.stringify(webhookEvent)); - - expect(response.status).toBe(200); - // Should not attempt to update email if customer key is not found - expect(mockUpdateEmailByCustomer).not.toHaveBeenCalled(); - }); - - it("should handle database errors during key provisioning", async () => { - const webhookEvent = { - type: "checkout.session.completed", - data: { - object: { - id: "cs_test_123", - customer: "cus_test_123", - customer_email: "test@example.com", - }, - }, - }; - - mockStripe.webhooks.constructEvent.mockReturnValueOnce(webhookEvent); - - const dbError = new Error("Database connection failed"); - mockCreateProKey.mockRejectedValueOnce(dbError); - - const response = await request(app) - .post("/webhook") - .set("stripe-signature", "valid_signature") - .send(JSON.stringify(webhookEvent)); - - expect(response.status).toBe(500); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - err: expect.objectContaining({ - message: "Database connection failed", - }), - }), - "Error processing webhook" - ); - }); - - it("should handle unknown webhook event types gracefully", async () => { - const webhookEvent = { - type: "unknown.event.type", - data: { - object: { - id: "obj_123", - }, - }, - }; - - mockStripe.webhooks.constructEvent.mockReturnValueOnce(webhookEvent); - - const response = await request(app) - .post("/webhook") - .set("stripe-signature", "valid_signature") - .send(JSON.stringify(webhookEvent)); - - expect(response.status).toBe(200); - // Should not crash or log errors for unknown event types - expect(mockLogger.error).not.toHaveBeenCalled(); - }); - }); -}); \ No newline at end of file diff --git a/src/__tests__/browser-error-handling.test.ts b/src/__tests__/browser-error-handling.test.ts deleted file mode 100644 index 9f30c2d..0000000 --- a/src/__tests__/browser-error-handling.test.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; - -// Don't use the global mock — we test the real browser service -vi.unmock("../services/browser.js"); - -// Mock logger -vi.mock("../services/logger.js", () => ({ - default: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, -})); - -function createMockPage() { - const page: any = { - setJavaScriptEnabled: vi.fn().mockResolvedValue(undefined), - setContent: vi.fn().mockResolvedValue(undefined), - addStyleTag: vi.fn().mockResolvedValue(undefined), - pdf: vi.fn().mockResolvedValue(Buffer.from("%PDF-1.4 test")), - goto: vi.fn().mockResolvedValue(undefined), - close: vi.fn().mockResolvedValue(undefined), - setRequestInterception: vi.fn().mockResolvedValue(undefined), - removeAllListeners: vi.fn().mockReturnThis(), - createCDPSession: vi.fn().mockResolvedValue({ - send: vi.fn().mockResolvedValue(undefined), - detach: vi.fn().mockResolvedValue(undefined), - }), - cookies: vi.fn().mockResolvedValue([{ name: "test", value: "cookie" }]), - deleteCookie: vi.fn().mockResolvedValue(undefined), - on: vi.fn(), - continue: vi.fn().mockResolvedValue(undefined), - abort: vi.fn().mockResolvedValue(undefined), - }; - return page; -} - -function createMockBrowser(pagesPerBrowser = 2) { - const pages = Array.from({ length: pagesPerBrowser }, () => createMockPage()); - let pageIndex = 0; - const browser: any = { - newPage: vi.fn().mockImplementation(() => Promise.resolve(pages[pageIndex++] || createMockPage())), - close: vi.fn().mockResolvedValue(undefined), - _pages: pages, - }; - return browser; -} - -// Set env vars before importing -process.env.BROWSER_COUNT = "1"; -process.env.PAGES_PER_BROWSER = "1"; - -let mockBrowsers: any[] = []; -let launchCallCount = 0; - -// Mock puppeteer -vi.mock("puppeteer", () => { - return { - default: { - launch: vi.fn().mockImplementation(() => { - const browser = createMockBrowser(1); - mockBrowsers.push(browser); - launchCallCount++; - return Promise.resolve(browser); - }), - }, - }; -}); - -describe("Browser Service Error Handling", () => { - let browserService: any; - - beforeEach(async () => { - vi.clearAllMocks(); - mockBrowsers = []; - launchCallCount = 0; - - // Dynamic import to ensure mocks are set up - browserService = await import("../services/browser.js"); - await browserService.initBrowser(); - }); - - afterEach(async () => { - await browserService.closeBrowser(); - }); - - describe("recyclePage error handling", () => { - it("should handle CDP session creation failure gracefully", async () => { - const failingPage = createMockPage(); - failingPage.createCDPSession = vi.fn().mockRejectedValue(new Error("CDP failed")); - - // Should not throw - await expect(browserService.recyclePage(failingPage)).resolves.toBeUndefined(); - }); - - it("should handle CDP detach failure gracefully", async () => { - const failingPage = createMockPage(); - failingPage.createCDPSession = vi.fn().mockResolvedValue({ - send: vi.fn().mockResolvedValue(undefined), - detach: vi.fn().mockRejectedValue(new Error("Detach failed")), - }); - - // Should not throw - await expect(browserService.recyclePage(failingPage)).resolves.toBeUndefined(); - }); - - it("should handle setRequestInterception failure gracefully", async () => { - const failingPage = createMockPage(); - failingPage.setRequestInterception = vi.fn().mockRejectedValue(new Error("Request interception failed")); - - // Should not throw - await expect(browserService.recyclePage(failingPage)).resolves.toBeUndefined(); - }); - - it("should handle goto failure gracefully", async () => { - const failingPage = createMockPage(); - failingPage.goto = vi.fn().mockRejectedValue(new Error("Goto failed")); - - // Should not throw - await expect(browserService.recyclePage(failingPage)).resolves.toBeUndefined(); - }); - - it("should handle deleteCookie failure gracefully", async () => { - const failingPage = createMockPage(); - failingPage.deleteCookie = vi.fn().mockRejectedValue(new Error("Delete cookie failed")); - - // Should not throw - await expect(browserService.recyclePage(failingPage)).resolves.toBeUndefined(); - }); - }); - - describe("queue timeout handling", () => { - it("should timeout and reject with QUEUE_FULL when all pages are busy", async () => { - // Simulate all pages being busy by not releasing any - const result1 = browserService.renderPdf("Test 1"); - const result2 = browserService.renderPdf("Test 2"); - - // This third request should queue and eventually timeout - const timeoutPromise = browserService.renderPdf("Test 3"); - - // Fast-forward time to trigger timeout - vi.useFakeTimers(); - const startTime = Date.now(); - - await expect(async () => { - vi.advanceTimersByTime(30_000); // Advance by 30 seconds to trigger timeout - await timeoutPromise; - }).rejects.toThrow("QUEUE_FULL"); - - vi.useRealTimers(); - - // Clean up the running renders - await result1; - await result2; - }, 35000); - }); - - describe("renderUrlPdf with hostResolverRules error handling", () => { - it("should handle request interception errors gracefully", async () => { - const mockPage = createMockPage(); - mockPage.goto = vi.fn().mockResolvedValue(undefined); - mockPage.pdf = vi.fn().mockResolvedValue(Buffer.from("%PDF-1.4 test")); - mockPage.setRequestInterception = vi.fn().mockResolvedValue(undefined); - - // Mock the on method to simulate request handling - let requestHandler: any; - mockPage.on = vi.fn().mockImplementation((event: string, handler: any) => { - if (event === "request") { - requestHandler = handler; - } - }); - - const mockRequest = { - url: () => "http://example.com/test", - headers: () => ({}), - continue: vi.fn().mockRejectedValue(new Error("Continue failed")), - abort: vi.fn().mockResolvedValue(undefined), - }; - - // Replace the mock browser's newPage to return our special mock page - const mockBrowser = mockBrowsers[0]; - mockBrowser.newPage = vi.fn().mockResolvedValue(mockPage); - - const result = await browserService.renderUrlPdf("http://example.com/test", { - hostResolverRules: "MAP example.com 192.168.1.1", - }); - - expect(result.pdf).toBeInstanceOf(Buffer); - expect(mockPage.setRequestInterception).toHaveBeenCalledWith(true); - }); - - it("should block requests to non-target hosts", async () => { - const mockPage = createMockPage(); - mockPage.goto = vi.fn().mockResolvedValue(undefined); - mockPage.pdf = vi.fn().mockResolvedValue(Buffer.from("%PDF-1.4 test")); - - let requestHandler: any; - mockPage.on = vi.fn().mockImplementation((event: string, handler: any) => { - if (event === "request") { - requestHandler = handler; - } - }); - - const mockRequest = { - url: () => "http://malicious.com/evil", - abort: vi.fn().mockResolvedValue(undefined), - }; - - // Replace the mock browser's newPage - const mockBrowser = mockBrowsers[0]; - mockBrowser.newPage = vi.fn().mockResolvedValue(mockPage); - - await browserService.renderUrlPdf("http://example.com/test", { - hostResolverRules: "MAP example.com 192.168.1.1", - }); - - // Simulate a request to a different host - if (requestHandler) { - requestHandler(mockRequest); - expect(mockRequest.abort).toHaveBeenCalledWith("blockedbyclient"); - } - }); - }); - - describe("PDF generation timeout", () => { - it("should timeout during PDF generation when page hangs", async () => { - vi.useFakeTimers(); - - const mockPage = createMockPage(); - // Make setContent hang forever - mockPage.setContent = vi.fn().mockImplementation(() => new Promise(() => {})); - - const mockBrowser = mockBrowsers[0]; - mockBrowser.newPage = vi.fn().mockResolvedValue(mockPage); - - const renderPromise = browserService.renderPdf("Hanging test"); - - // Fast-forward past the timeout - vi.advanceTimersByTime(30_000); - - await expect(renderPromise).rejects.toThrow("PDF_TIMEOUT"); - - vi.useRealTimers(); - }); - - it("should timeout during URL navigation when page hangs", async () => { - vi.useFakeTimers(); - - const mockPage = createMockPage(); - // Make goto hang forever - mockPage.goto = vi.fn().mockImplementation(() => new Promise(() => {})); - - const mockBrowser = mockBrowsers[0]; - mockBrowser.newPage = vi.fn().mockResolvedValue(mockPage); - - const renderPromise = browserService.renderUrlPdf("http://example.com/hanging"); - - // Fast-forward past the timeout - vi.advanceTimersByTime(30_000); - - await expect(renderPromise).rejects.toThrow("PDF_TIMEOUT"); - - vi.useRealTimers(); - }); - }); - - describe("buildPdfOptions edge cases", () => { - it("should handle all optional parameters being set", () => { - const options = { - format: "Legal" as const, - landscape: true, - margin: { top: "1in", right: "0.5in", bottom: "1in", left: "0.5in" }, - printBackground: false, - headerTemplate: "
Header
", - footerTemplate: "
Footer
", - displayHeaderFooter: true, - scale: 0.8, - pageRanges: "1-3", - preferCSSPageSize: true, - width: "8.5in", - height: "11in", - }; - - const result = browserService.buildPdfOptions(options); - - expect(result).toEqual({ - format: "Legal", - landscape: true, - margin: { top: "1in", right: "0.5in", bottom: "1in", left: "0.5in" }, - printBackground: false, - headerTemplate: "
Header
", - footerTemplate: "
Footer
", - displayHeaderFooter: true, - scale: 0.8, - pageRanges: "1-3", - preferCSSPageSize: true, - width: "8.5in", - height: "11in", - }); - }); - - it("should handle empty string values correctly", () => { - const options = { - headerTemplate: "", - footerTemplate: "", - pageRanges: "", - width: "", - height: "", - }; - - const result = browserService.buildPdfOptions(options); - - expect(result.headerTemplate).toBe(""); - expect(result.footerTemplate).toBe(""); - expect(result.pageRanges).toBe(""); - expect(result.width).toBe(""); - expect(result.height).toBe(""); - }); - }); -}); \ No newline at end of file diff --git a/src/__tests__/db-retry-logic.test.ts b/src/__tests__/db-retry-logic.test.ts deleted file mode 100644 index 6a1dd82..0000000 --- a/src/__tests__/db-retry-logic.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; - -// Mock logger -vi.mock("../services/logger.js", () => ({ - default: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, -})); - -// Mock pg -const mockQuery = vi.fn(); -const mockConnect = vi.fn(); -const mockRelease = vi.fn(); -const mockClient = { - query: mockQuery, - release: mockRelease, -}; - -const mockPool = { - connect: mockConnect, - on: vi.fn(), -}; - -vi.mock("pg", () => ({ - Pool: vi.fn().mockImplementation(() => mockPool), -})); - -// Mock error utilities -vi.mock("../utils/errors.js", () => ({ - isTransientError: vi.fn(), - errorMessage: vi.fn(), - errorCode: vi.fn(), -})); - -describe("Database Retry Logic", () => { - let dbService: any; - let isTransientError: any; - let errorMessage: any; - let errorCode: any; - - beforeEach(async () => { - vi.clearAllMocks(); - mockQuery.mockReset(); - mockConnect.mockReset(); - mockRelease.mockReset(); - - // Get the mocked functions - const errorUtils = await import("../utils/errors.js"); - isTransientError = errorUtils.isTransientError; - errorMessage = errorUtils.errorMessage; - errorCode = errorUtils.errorCode; - - // Setup default mocks - isTransientError.mockImplementation((err: any) => { - return err.code === "ENOTFOUND" || err.code === "ECONNRESET" || err.code === "ETIMEDOUT"; - }); - errorMessage.mockImplementation((err: any) => err.message || "Unknown error"); - errorCode.mockImplementation((err: any) => err.code || "UNKNOWN"); - - mockConnect.mockResolvedValue(mockClient); - mockQuery.mockResolvedValue({ rows: [], rowCount: 0 }); - - // Import the service after mocks are set up - dbService = await import("../services/db.js"); - }); - - describe("queryWithRetry", () => { - it("should succeed on first attempt for successful queries", async () => { - const expectedResult = { rows: [{ id: 1, name: "test" }], rowCount: 1 }; - mockQuery.mockResolvedValueOnce(expectedResult); - - const result = await dbService.queryWithRetry("SELECT * FROM test", []); - - expect(result).toEqual(expectedResult); - expect(mockConnect).toHaveBeenCalledTimes(1); - expect(mockQuery).toHaveBeenCalledTimes(1); - expect(mockRelease).toHaveBeenCalledWith(); // Normal release - }); - - it("should retry on transient errors and eventually succeed", async () => { - vi.useFakeTimers(); - - const transientError = new Error("Connection lost"); - transientError.code = "ECONNRESET"; - const expectedResult = { rows: [{ id: 1 }], rowCount: 1 }; - - // First two calls fail, third succeeds - mockQuery - .mockRejectedValueOnce(transientError) - .mockRejectedValueOnce(transientError) - .mockResolvedValueOnce(expectedResult); - - const queryPromise = dbService.queryWithRetry("SELECT * FROM test", [], 3); - - // Advance timers to handle retry delays - vi.advanceTimersByTime(1000); // First retry delay - vi.advanceTimersByTime(2000); // Second retry delay - - const result = await queryPromise; - - expect(result).toEqual(expectedResult); - expect(mockConnect).toHaveBeenCalledTimes(3); - expect(mockQuery).toHaveBeenCalledTimes(3); - // Should have called release(true) for failed attempts to destroy bad connections - expect(mockRelease).toHaveBeenCalledWith(true); - expect(mockRelease).toHaveBeenCalledWith(); // Final successful release - - vi.useRealTimers(); - }); - - it("should fail immediately on non-transient errors", async () => { - const nonTransientError = new Error("Syntax error"); - nonTransientError.code = "42601"; // PostgreSQL syntax error - isTransientError.mockReturnValue(false); - - mockQuery.mockRejectedValueOnce(nonTransientError); - - await expect(dbService.queryWithRetry("INVALID SQL", [])).rejects.toThrow("Syntax error"); - - expect(mockConnect).toHaveBeenCalledTimes(1); - expect(mockQuery).toHaveBeenCalledTimes(1); - expect(mockRelease).toHaveBeenCalledWith(true); // Should destroy the connection even for non-transient errors - }); - - it("should fail after exhausting all retries", async () => { - vi.useFakeTimers(); - - const transientError = new Error("Network timeout"); - transientError.code = "ETIMEDOUT"; - - mockQuery.mockRejectedValue(transientError); - - const queryPromise = dbService.queryWithRetry("SELECT * FROM test", [], 2); - - // Advance timers for all retry attempts - vi.advanceTimersByTime(1000); // First retry - vi.advanceTimersByTime(2000); // Second retry - - await expect(queryPromise).rejects.toThrow("Network timeout"); - - expect(mockConnect).toHaveBeenCalledTimes(3); // Initial + 2 retries - expect(mockQuery).toHaveBeenCalledTimes(3); - - vi.useRealTimers(); - }); - - it("should handle client connect failures", async () => { - const connectError = new Error("Connection refused"); - connectError.code = "ECONNREFUSED"; - - mockConnect.mockRejectedValueOnce(connectError); - - await expect(dbService.queryWithRetry("SELECT 1", [])).rejects.toThrow("Connection refused"); - - expect(mockConnect).toHaveBeenCalledTimes(1); - expect(mockQuery).not.toHaveBeenCalled(); - }); - - it("should handle client release failures gracefully", async () => { - const expectedResult = { rows: [], rowCount: 0 }; - mockQuery.mockResolvedValueOnce(expectedResult); - mockRelease.mockImplementation(() => { - throw new Error("Release failed"); - }); - - // Should not throw despite release failure - const result = await dbService.queryWithRetry("SELECT 1", []); - - expect(result).toEqual(expectedResult); - }); - }); - - describe("connectWithRetry", () => { - it("should succeed on first attempt for healthy connections", async () => { - mockQuery.mockResolvedValueOnce({ rows: [{ "?column?": 1 }], rowCount: 1 }); - - const client = await dbService.connectWithRetry(); - - expect(client).toBe(mockClient); - expect(mockConnect).toHaveBeenCalledTimes(1); - expect(mockQuery).toHaveBeenCalledWith("SELECT 1"); // Validation query - }); - - it("should retry when connection validation fails", async () => { - vi.useFakeTimers(); - - const validationError = new Error("Connection timeout"); - validationError.code = "ETIMEDOUT"; - - // First connect succeeds, but validation fails - // Second connect and validation succeed - mockQuery - .mockRejectedValueOnce(validationError) - .mockResolvedValueOnce({ rows: [{ "?column?": 1 }], rowCount: 1 }); - - const connectPromise = dbService.connectWithRetry(2); - - // Advance timer for retry delay - vi.advanceTimersByTime(1000); - - const client = await connectPromise; - - expect(client).toBe(mockClient); - expect(mockConnect).toHaveBeenCalledTimes(2); - expect(mockQuery).toHaveBeenCalledTimes(2); - expect(mockRelease).toHaveBeenCalledWith(true); // First connection destroyed due to validation failure - - vi.useRealTimers(); - }); - - it("should fail immediately on non-transient connect errors", async () => { - const nonTransientError = new Error("Authentication failed"); - nonTransientError.code = "28P01"; // PostgreSQL auth error - isTransientError.mockReturnValue(false); - - mockConnect.mockRejectedValueOnce(nonTransientError); - - await expect(dbService.connectWithRetry()).rejects.toThrow("Authentication failed"); - - expect(mockConnect).toHaveBeenCalledTimes(1); - }); - - it("should fail after exhausting connect retries", async () => { - vi.useFakeTimers(); - - const transientError = new Error("Connection timeout"); - transientError.code = "ETIMEDOUT"; - - mockConnect.mockRejectedValue(transientError); - - const connectPromise = dbService.connectWithRetry(2); - - // Advance timers for retry delays - vi.advanceTimersByTime(1000); // First retry - vi.advanceTimersByTime(2000); // Second retry - - await expect(connectPromise).rejects.toThrow("Connection timeout"); - - expect(mockConnect).toHaveBeenCalledTimes(3); // Initial + 2 retries - - vi.useRealTimers(); - }); - - it("should fail on non-transient validation errors", async () => { - const validationError = new Error("Permission denied"); - validationError.code = "42501"; // PostgreSQL permission error - isTransientError.mockReturnValue(false); - - mockQuery.mockRejectedValueOnce(validationError); - - await expect(dbService.connectWithRetry()).rejects.toThrow("Permission denied"); - - expect(mockConnect).toHaveBeenCalledTimes(1); - expect(mockRelease).toHaveBeenCalledWith(true); // Connection destroyed due to validation failure - }); - - it("should handle release failure during validation error cleanup", async () => { - const validationError = new Error("Validation failed"); - validationError.code = "ETIMEDOUT"; - - mockQuery.mockRejectedValueOnce(validationError); - mockRelease.mockImplementation(() => { - throw new Error("Release failed"); - }); - - // Should still throw the validation error, not the release error - await expect(dbService.connectWithRetry(1)).rejects.toThrow("Validation failed"); - }); - }); - - describe("cleanupStaleData", () => { - it("should clean up expired verifications and orphaned usage", async () => { - // Mock the cleanup queries - mockQuery - .mockResolvedValueOnce({ rowCount: 3 }) // expired verifications - .mockResolvedValueOnce({ rowCount: 7 }); // orphaned usage - - const result = await dbService.cleanupStaleData(); - - expect(result).toEqual({ - expiredVerifications: 3, - orphanedUsage: 7, - }); - - expect(mockQuery).toHaveBeenCalledWith( - "DELETE FROM pending_verifications WHERE expires_at < NOW() RETURNING email" - ); - expect(mockQuery).toHaveBeenCalledWith( - expect.stringContaining("DELETE FROM usage") - ); - }); - - it("should handle null rowCount gracefully", async () => { - // Mock queries returning null rowCount - mockQuery - .mockResolvedValueOnce({ rowCount: null }) - .mockResolvedValueOnce({ rowCount: null }); - - const result = await dbService.cleanupStaleData(); - - expect(result).toEqual({ - expiredVerifications: 0, - orphanedUsage: 0, - }); - }); - - it("should handle database errors during cleanup", async () => { - const dbError = new Error("Table does not exist"); - mockQuery.mockRejectedValueOnce(dbError); - - await expect(dbService.cleanupStaleData()).rejects.toThrow("Table does not exist"); - }); - }); - - describe("initDatabase", () => { - it("should create all required tables", async () => { - mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 }); - - await dbService.initDatabase(); - - expect(mockConnect).toHaveBeenCalledTimes(1); - expect(mockQuery).toHaveBeenCalledWith( - expect.stringContaining("CREATE TABLE IF NOT EXISTS api_keys") - ); - expect(mockRelease).toHaveBeenCalledTimes(1); - }); - - it("should handle table creation failures", async () => { - const createError = new Error("Permission denied to create table"); - mockQuery.mockRejectedValueOnce(createError); - - await expect(dbService.initDatabase()).rejects.toThrow("Permission denied to create table"); - - expect(mockRelease).toHaveBeenCalledTimes(1); // Should still release the client - }); - }); -}); \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index 82c0e20..b5d47a3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,11 +9,5 @@ export default defineConfig({ ...configDefaults.exclude, "**/dist/**", ], - coverage: { - provider: 'v8', - reporter: ['text', 'text-summary'], - include: ['src/**/*.ts'], - exclude: ['src/__tests__/**', 'src/**/*.test.ts'], - }, }, });