Files
rentall-app/backend/tests/unit/routes/stripe.test.js
jackiettran 5d3c124d3e text changes
2026-01-21 19:20:07 -05:00

820 lines
25 KiB
JavaScript

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" });
});
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" });
});
});
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" });
});
});
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" });
});
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);
});
});
});