const request = require("supertest"); const express = require("express"); const jwt = require("jsonwebtoken"); // Mock dependencies jest.mock("jsonwebtoken"); jest.mock("../../../models", () => ({ User: { findByPk: jest.fn(), create: jest.fn(), findOne: jest.fn(), }, Item: {}, })); jest.mock("../../../services/stripeService", () => ({ getCheckoutSession: jest.fn(), createConnectedAccount: jest.fn(), createAccountLink: jest.fn(), getAccountStatus: jest.fn(), createCustomer: jest.fn(), createSetupCheckoutSession: jest.fn(), })); // Mock auth middleware jest.mock("../../../middleware/auth", () => ({ authenticateToken: (req, res, next) => { // Mock authenticated user if (req.headers.authorization) { req.user = { id: 1 }; next(); } else { res.status(401).json({ error: "No token provided" }); } }, requireVerifiedEmail: (req, res, next) => next(), })); // Mock logger jest.mock("../../../utils/logger", () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), withRequestId: jest.fn(() => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), })), })); const { User } = require("../../../models"); const StripeService = require("../../../services/stripeService"); const stripeRoutes = require("../../../routes/stripe"); // Set up Express app for testing const app = express(); app.use(express.json()); app.use("/stripe", stripeRoutes); // Error handler middleware app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); describe("Stripe Routes", () => { let consoleSpy, consoleErrorSpy; beforeEach(() => { jest.clearAllMocks(); // Set up console spies consoleSpy = jest.spyOn(console, "log").mockImplementation(); consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); }); afterEach(() => { consoleSpy.mockRestore(); consoleErrorSpy.mockRestore(); }); describe("GET /checkout-session/:sessionId", () => { it("should retrieve checkout session successfully", async () => { const mockSession = { status: "complete", payment_status: "paid", customer_details: { email: "test@example.com", }, setup_intent: { id: "seti_123456789", status: "succeeded", }, metadata: { userId: "1", }, }; StripeService.getCheckoutSession.mockResolvedValue(mockSession); const response = await request(app).get( "/stripe/checkout-session/cs_123456789" ); expect(response.status).toBe(200); expect(response.body).toEqual({ status: "complete", payment_status: "paid", customer_email: "test@example.com", setup_intent: { id: "seti_123456789", status: "succeeded", }, metadata: { userId: "1", }, }); expect(StripeService.getCheckoutSession).toHaveBeenCalledWith( "cs_123456789" ); }); it("should handle missing customer_details gracefully", async () => { const mockSession = { status: "complete", payment_status: "paid", customer_details: null, setup_intent: null, metadata: {}, }; StripeService.getCheckoutSession.mockResolvedValue(mockSession); const response = await request(app).get( "/stripe/checkout-session/cs_123456789" ); expect(response.status).toBe(200); expect(response.body).toEqual({ status: "complete", payment_status: "paid", customer_email: undefined, setup_intent: null, metadata: {}, }); }); it("should handle checkout session retrieval errors", async () => { const error = new Error("Session not found"); StripeService.getCheckoutSession.mockRejectedValue(error); const response = await request(app).get( "/stripe/checkout-session/invalid_session" ); expect(response.status).toBe(500); expect(response.body).toEqual({ error: "Session not found" }); }); it("should handle missing session ID", async () => { const error = new Error("Invalid session ID"); StripeService.getCheckoutSession.mockRejectedValue(error); const response = await request(app).get("/stripe/checkout-session/"); expect(response.status).toBe(404); }); }); describe("POST /accounts", () => { const mockUser = { id: 1, email: "test@example.com", stripeConnectedAccountId: null, update: jest.fn(), }; beforeEach(() => { mockUser.update.mockReset(); mockUser.stripeConnectedAccountId = null; }); it("should create connected account successfully", async () => { const mockAccount = { id: "acct_123456789", email: "test@example.com", country: "US", }; User.findByPk.mockResolvedValue(mockUser); StripeService.createConnectedAccount.mockResolvedValue(mockAccount); mockUser.update.mockResolvedValue(mockUser); const response = await request(app) .post("/stripe/accounts") .set("Authorization", "Bearer valid_token"); expect(response.status).toBe(200); expect(response.body).toEqual({ stripeConnectedAccountId: "acct_123456789", success: true, }); expect(User.findByPk).toHaveBeenCalledWith(1); expect(StripeService.createConnectedAccount).toHaveBeenCalledWith({ email: "test@example.com", country: "US", }); expect(mockUser.update).toHaveBeenCalledWith({ stripeConnectedAccountId: "acct_123456789", }); }); it("should return error if user not found", async () => { User.findByPk.mockResolvedValue(null); const response = await request(app) .post("/stripe/accounts") .set("Authorization", "Bearer valid_token"); expect(response.status).toBe(404); expect(response.body).toEqual({ error: "User not found" }); expect(StripeService.createConnectedAccount).not.toHaveBeenCalled(); }); it("should return error if user already has connected account", async () => { const userWithAccount = { ...mockUser, stripeConnectedAccountId: "acct_existing", }; User.findByPk.mockResolvedValue(userWithAccount); const response = await request(app) .post("/stripe/accounts") .set("Authorization", "Bearer valid_token"); expect(response.status).toBe(400); expect(response.body).toEqual({ error: "User already has a connected account", }); expect(StripeService.createConnectedAccount).not.toHaveBeenCalled(); }); it("should require authentication", async () => { const response = await request(app).post("/stripe/accounts"); expect(response.status).toBe(401); expect(response.body).toEqual({ error: "No token provided" }); }); it("should handle Stripe account creation errors", async () => { const error = new Error("Invalid email address"); User.findByPk.mockResolvedValue(mockUser); StripeService.createConnectedAccount.mockRejectedValue(error); const response = await request(app) .post("/stripe/accounts") .set("Authorization", "Bearer valid_token"); expect(response.status).toBe(500); expect(response.body).toEqual({ error: "Invalid email address" }); // Note: route uses logger instead of console.error }); it("should handle database update errors", async () => { const mockAccount = { id: "acct_123456789" }; const dbError = new Error("Database update failed"); User.findByPk.mockResolvedValue(mockUser); StripeService.createConnectedAccount.mockResolvedValue(mockAccount); mockUser.update.mockRejectedValue(dbError); const response = await request(app) .post("/stripe/accounts") .set("Authorization", "Bearer valid_token"); expect(response.status).toBe(500); expect(response.body).toEqual({ error: "Database update failed" }); }); }); describe("POST /account-links", () => { const mockUser = { id: 1, stripeConnectedAccountId: "acct_123456789", }; it("should create account link successfully", async () => { const mockAccountLink = { url: "https://connect.stripe.com/setup/e/acct_123456789", expires_at: Date.now() + 3600, }; User.findByPk.mockResolvedValue(mockUser); StripeService.createAccountLink.mockResolvedValue(mockAccountLink); const response = await request(app) .post("/stripe/account-links") .set("Authorization", "Bearer valid_token") .send({ refreshUrl: "http://localhost:3000/refresh", returnUrl: "http://localhost:3000/return", }); expect(response.status).toBe(200); expect(response.body).toEqual({ url: mockAccountLink.url, expiresAt: mockAccountLink.expires_at, }); expect(StripeService.createAccountLink).toHaveBeenCalledWith( "acct_123456789", "http://localhost:3000/refresh", "http://localhost:3000/return" ); }); it("should return error if no connected account found", async () => { const userWithoutAccount = { id: 1, stripeConnectedAccountId: null, }; User.findByPk.mockResolvedValue(userWithoutAccount); const response = await request(app) .post("/stripe/account-links") .set("Authorization", "Bearer valid_token") .send({ refreshUrl: "http://localhost:3000/refresh", returnUrl: "http://localhost:3000/return", }); expect(response.status).toBe(400); expect(response.body).toEqual({ error: "No connected account found" }); expect(StripeService.createAccountLink).not.toHaveBeenCalled(); }); it("should return error if user not found", async () => { User.findByPk.mockResolvedValue(null); const response = await request(app) .post("/stripe/account-links") .set("Authorization", "Bearer valid_token") .send({ refreshUrl: "http://localhost:3000/refresh", returnUrl: "http://localhost:3000/return", }); expect(response.status).toBe(400); expect(response.body).toEqual({ error: "No connected account found" }); }); it("should validate required URLs", async () => { User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post("/stripe/account-links") .set("Authorization", "Bearer valid_token") .send({ refreshUrl: "http://localhost:3000/refresh", // Missing returnUrl }); expect(response.status).toBe(400); expect(response.body).toEqual({ error: "refreshUrl and returnUrl are required", }); expect(StripeService.createAccountLink).not.toHaveBeenCalled(); }); it("should validate both URLs are provided", async () => { User.findByPk.mockResolvedValue(mockUser); const response = await request(app) .post("/stripe/account-links") .set("Authorization", "Bearer valid_token") .send({ returnUrl: "http://localhost:3000/return", // Missing refreshUrl }); expect(response.status).toBe(400); expect(response.body).toEqual({ error: "refreshUrl and returnUrl are required", }); }); it("should require authentication", async () => { const response = await request(app).post("/stripe/account-links").send({ refreshUrl: "http://localhost:3000/refresh", returnUrl: "http://localhost:3000/return", }); expect(response.status).toBe(401); }); it("should handle Stripe account link creation errors", async () => { const error = new Error("Account not found"); User.findByPk.mockResolvedValue(mockUser); StripeService.createAccountLink.mockRejectedValue(error); const response = await request(app) .post("/stripe/account-links") .set("Authorization", "Bearer valid_token") .send({ refreshUrl: "http://localhost:3000/refresh", returnUrl: "http://localhost:3000/return", }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: "Account not found" }); // Note: route uses logger instead of console.error }); }); describe("GET /account-status", () => { const mockUser = { id: 1, stripeConnectedAccountId: "acct_123456789", stripePayoutsEnabled: true, stripeRequirementsCurrentlyDue: [], stripeRequirementsPastDue: [], stripeDisabledReason: null, update: jest.fn().mockResolvedValue(true), }; it("should get account status successfully", async () => { const mockAccountStatus = { id: "acct_123456789", details_submitted: true, payouts_enabled: true, capabilities: { transfers: { status: "active" }, }, requirements: { pending_verification: [], currently_due: [], past_due: [], }, }; User.findByPk.mockResolvedValue(mockUser); StripeService.getAccountStatus.mockResolvedValue(mockAccountStatus); const response = await request(app) .get("/stripe/account-status") .set("Authorization", "Bearer valid_token"); expect(response.status).toBe(200); expect(response.body).toEqual({ accountId: "acct_123456789", detailsSubmitted: true, payoutsEnabled: true, capabilities: { transfers: { status: "active" }, }, requirements: { pending_verification: [], currently_due: [], past_due: [], }, }); expect(StripeService.getAccountStatus).toHaveBeenCalledWith( "acct_123456789" ); }); it("should return error if no connected account found", async () => { const userWithoutAccount = { id: 1, stripeConnectedAccountId: null, }; User.findByPk.mockResolvedValue(userWithoutAccount); const response = await request(app) .get("/stripe/account-status") .set("Authorization", "Bearer valid_token"); expect(response.status).toBe(400); expect(response.body).toEqual({ error: "No connected account found" }); expect(StripeService.getAccountStatus).not.toHaveBeenCalled(); }); it("should return error if user not found", async () => { User.findByPk.mockResolvedValue(null); const response = await request(app) .get("/stripe/account-status") .set("Authorization", "Bearer valid_token"); expect(response.status).toBe(400); expect(response.body).toEqual({ error: "No connected account found" }); }); it("should require authentication", async () => { const response = await request(app).get("/stripe/account-status"); expect(response.status).toBe(401); }); it("should handle Stripe account status retrieval errors", async () => { const error = new Error("Account not found"); User.findByPk.mockResolvedValue(mockUser); StripeService.getAccountStatus.mockRejectedValue(error); const response = await request(app) .get("/stripe/account-status") .set("Authorization", "Bearer valid_token"); expect(response.status).toBe(500); expect(response.body).toEqual({ error: "Account not found" }); // Note: route uses logger instead of console.error }); }); describe("POST /create-setup-checkout-session", () => { const mockUser = { id: 1, email: "test@example.com", firstName: "John", lastName: "Doe", stripeCustomerId: null, update: jest.fn(), }; beforeEach(() => { mockUser.update.mockReset(); mockUser.stripeCustomerId = null; }); it("should create setup checkout session for new customer", async () => { const mockCustomer = { id: "cus_123456789", email: "test@example.com", }; const mockSession = { id: "cs_123456789", client_secret: "cs_123456789_secret_test", }; const rentalData = { itemId: "123", startDate: "2023-12-01", endDate: "2023-12-03", }; User.findByPk.mockResolvedValue(mockUser); StripeService.createCustomer.mockResolvedValue(mockCustomer); StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession); mockUser.update.mockResolvedValue(mockUser); const response = await request(app) .post("/stripe/create-setup-checkout-session") .set("Authorization", "Bearer valid_token") .send({ rentalData }); expect(response.status).toBe(200); expect(response.body).toEqual({ clientSecret: "cs_123456789_secret_test", sessionId: "cs_123456789", }); expect(User.findByPk).toHaveBeenCalledWith(1); expect(StripeService.createCustomer).toHaveBeenCalledWith({ email: "test@example.com", name: "John Doe", metadata: { userId: "1", }, }); expect(mockUser.update).toHaveBeenCalledWith({ stripeCustomerId: "cus_123456789", }); expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({ customerId: "cus_123456789", metadata: { rentalData: JSON.stringify(rentalData), }, }); }); it("should use existing customer ID if available", async () => { const userWithCustomer = { ...mockUser, stripeCustomerId: "cus_existing123", }; const mockSession = { id: "cs_123456789", client_secret: "cs_123456789_secret_test", }; User.findByPk.mockResolvedValue(userWithCustomer); StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession); const response = await request(app) .post("/stripe/create-setup-checkout-session") .set("Authorization", "Bearer valid_token") .send({}); expect(response.status).toBe(200); expect(response.body).toEqual({ clientSecret: "cs_123456789_secret_test", sessionId: "cs_123456789", }); expect(StripeService.createCustomer).not.toHaveBeenCalled(); expect(userWithCustomer.update).not.toHaveBeenCalled(); expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({ customerId: "cus_existing123", metadata: {}, }); }); it("should handle session without rental data", async () => { const mockCustomer = { id: "cus_123456789", }; const mockSession = { id: "cs_123456789", client_secret: "cs_123456789_secret_test", }; User.findByPk.mockResolvedValue(mockUser); StripeService.createCustomer.mockResolvedValue(mockCustomer); StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession); mockUser.update.mockResolvedValue(mockUser); const response = await request(app) .post("/stripe/create-setup-checkout-session") .set("Authorization", "Bearer valid_token") .send({}); expect(response.status).toBe(200); expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({ customerId: "cus_123456789", metadata: {}, }); }); it("should return error if user not found", async () => { User.findByPk.mockResolvedValue(null); const response = await request(app) .post("/stripe/create-setup-checkout-session") .set("Authorization", "Bearer valid_token") .send({}); expect(response.status).toBe(404); expect(response.body).toEqual({ error: "User not found" }); expect(StripeService.createCustomer).not.toHaveBeenCalled(); expect(StripeService.createSetupCheckoutSession).not.toHaveBeenCalled(); }); it("should require authentication", async () => { const response = await request(app) .post("/stripe/create-setup-checkout-session") .send({}); expect(response.status).toBe(401); }); it("should handle customer creation errors", async () => { const error = new Error("Invalid email address"); User.findByPk.mockResolvedValue(mockUser); StripeService.createCustomer.mockRejectedValue(error); const response = await request(app) .post("/stripe/create-setup-checkout-session") .set("Authorization", "Bearer valid_token") .send({}); expect(response.status).toBe(500); expect(response.body).toEqual({ error: "Invalid email address" }); // Note: route uses logger.withRequestId().error() instead of console.error }); it("should handle database update errors", async () => { const mockCustomer = { id: "cus_123456789" }; const dbError = new Error("Database update failed"); User.findByPk.mockResolvedValue(mockUser); StripeService.createCustomer.mockResolvedValue(mockCustomer); mockUser.update.mockRejectedValue(dbError); const response = await request(app) .post("/stripe/create-setup-checkout-session") .set("Authorization", "Bearer valid_token") .send({}); expect(response.status).toBe(500); expect(response.body).toEqual({ error: "Database update failed" }); }); it("should handle session creation errors", async () => { const mockCustomer = { id: "cus_123456789" }; const sessionError = new Error("Session creation failed"); User.findByPk.mockResolvedValue(mockUser); StripeService.createCustomer.mockResolvedValue(mockCustomer); mockUser.update.mockResolvedValue(mockUser); StripeService.createSetupCheckoutSession.mockRejectedValue(sessionError); const response = await request(app) .post("/stripe/create-setup-checkout-session") .set("Authorization", "Bearer valid_token") .send({}); expect(response.status).toBe(500); expect(response.body).toEqual({ error: "Session creation failed" }); }); it("should handle complex rental data", async () => { const mockCustomer = { id: "cus_123456789" }; const mockSession = { id: "cs_123456789", client_secret: "cs_123456789_secret_test", }; const complexRentalData = { itemId: "123", startDate: "2023-12-01", endDate: "2023-12-03", totalAmount: 150.0, additionalServices: ["cleaning", "delivery"], notes: "Special instructions", }; User.findByPk.mockResolvedValue(mockUser); StripeService.createCustomer.mockResolvedValue(mockCustomer); StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession); mockUser.update.mockResolvedValue(mockUser); const response = await request(app) .post("/stripe/create-setup-checkout-session") .set("Authorization", "Bearer valid_token") .send({ rentalData: complexRentalData }); expect(response.status).toBe(200); expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({ customerId: "cus_123456789", metadata: { rentalData: JSON.stringify(complexRentalData), }, }); }); }); describe("Error handling and edge cases", () => { it("should handle malformed JSON in rental data", async () => { const mockUser = { id: 1, email: "test@example.com", firstName: "John", lastName: "Doe", stripeCustomerId: "cus_123456789", }; User.findByPk.mockResolvedValue(mockUser); // This should work fine as Express will parse valid JSON const response = await request(app) .post("/stripe/create-setup-checkout-session") .set("Authorization", "Bearer valid_token") .set("Content-Type", "application/json") .send('{"rentalData":{"itemId":"123"}}'); expect(response.status).toBe(200); }); it("should handle very large session IDs", async () => { const longSessionId = "cs_" + "a".repeat(100); const error = new Error("Session ID too long"); StripeService.getCheckoutSession.mockRejectedValue(error); const response = await request(app).get( `/stripe/checkout-session/${longSessionId}` ); expect(response.status).toBe(500); expect(response.body).toEqual({ error: "Session ID too long" }); }); it("should handle concurrent requests for same user", async () => { const mockUser = { id: 1, email: "test@example.com", stripeConnectedAccountId: null, update: jest.fn().mockResolvedValue({}), }; const mockAccount = { id: "acct_123456789" }; User.findByPk.mockResolvedValue(mockUser); StripeService.createConnectedAccount.mockResolvedValue(mockAccount); // Simulate concurrent requests const [response1, response2] = await Promise.all([ request(app) .post("/stripe/accounts") .set("Authorization", "Bearer valid_token"), request(app) .post("/stripe/accounts") .set("Authorization", "Bearer valid_token"), ]); // Both should succeed (in this test scenario) expect(response1.status).toBe(200); expect(response2.status).toBe(200); }); }); });