test: comprehensive test suite for Manoon storefront
Add complete testing infrastructure with Vitest: Testing Stack: - Vitest for unit/integration tests - @testing-library/react for component tests - @playwright/test for E2E (installed, ready to configure) - MSW for API mocking Test Coverage: 1. Webhook Handler Tests (src/__tests__/integration/api/webhooks/saleor.test.ts) - ORDER_CONFIRMED: Emails + analytics - ORDER_CREATED: No duplicates - ORDER_FULFILLED: Tracking info - ORDER_CANCELLED: Cancellation reason - ORDER_FULLY_PAID: Payment confirmation - Error handling (400/500 responses) - Currency handling (RSD preservation) 2. OrderNotificationService Tests - Email sending in all 4 languages (SR, EN, DE, FR) - Price formatting verification - Admin vs Customer templates - Address formatting - Tracking info handling 3. AnalyticsService Tests - Revenue tracking with correct currency - Duplicate prevention verification - Error handling (doesn't break flow) - Singleton pattern 4. Utility Tests - formatPrice: RSD, EUR, USD formatting - Decimal and zero handling Fixtures: - Realistic order data in src/__tests__/fixtures/orders.ts - Multiple scenarios (with tracking, cancelled, etc.) Scripts: - npm test: Run tests in watch mode - npm run test:run: Run once - npm run test:coverage: Generate coverage report - npm run test:e2e: Run Playwright tests Coverage target: 80%+ for critical paths
This commit is contained in:
252
src/__tests__/unit/services/OrderNotificationService.test.ts
Normal file
252
src/__tests__/unit/services/OrderNotificationService.test.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { orderNotificationService } from "@/lib/services/OrderNotificationService";
|
||||
import { sendEmail } from "@/lib/resend";
|
||||
import { mockOrderConverted } from "../../fixtures/orders";
|
||||
|
||||
// Mock the resend module
|
||||
vi.mock("@/lib/resend", () => ({
|
||||
sendEmail: vi.fn().mockResolvedValue({ id: "test-email-id" }),
|
||||
ADMIN_EMAILS: ["me@hytham.me", "tamara@hytham.me"],
|
||||
}));
|
||||
|
||||
describe("OrderNotificationService", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("sendOrderConfirmation", () => {
|
||||
it("should send customer order confirmation in correct language (EN)", async () => {
|
||||
const order = { ...mockOrderConverted, languageCode: "EN" };
|
||||
|
||||
await orderNotificationService.sendOrderConfirmation(order);
|
||||
|
||||
expect(sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "test@hytham.me",
|
||||
subject: "Order Confirmation #1524",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should send customer order confirmation in Serbian (SR)", async () => {
|
||||
const order = { ...mockOrderConverted, languageCode: "SR" };
|
||||
|
||||
await orderNotificationService.sendOrderConfirmation(order);
|
||||
|
||||
expect(sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "test@hytham.me",
|
||||
subject: "Potvrda narudžbine #1524",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should send customer order confirmation in German (DE)", async () => {
|
||||
const order = { ...mockOrderConverted, languageCode: "DE" };
|
||||
|
||||
await orderNotificationService.sendOrderConfirmation(order);
|
||||
|
||||
expect(sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "test@hytham.me",
|
||||
subject: "Bestellbestätigung #1524",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should send customer order confirmation in French (FR)", async () => {
|
||||
const order = { ...mockOrderConverted, languageCode: "FR" };
|
||||
|
||||
await orderNotificationService.sendOrderConfirmation(order);
|
||||
|
||||
expect(sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "test@hytham.me",
|
||||
subject: "Confirmation de commande #1524",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should format price correctly", async () => {
|
||||
const order = {
|
||||
...mockOrderConverted,
|
||||
total: { gross: { amount: 5479, currency: "RSD" } },
|
||||
};
|
||||
|
||||
await orderNotificationService.sendOrderConfirmation(order);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.total).toBe("5.479,00 RSD");
|
||||
});
|
||||
|
||||
it("should handle missing variant name gracefully", async () => {
|
||||
const order = {
|
||||
...mockOrderConverted,
|
||||
lines: [
|
||||
{
|
||||
...mockOrderConverted.lines[0],
|
||||
variantName: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await orderNotificationService.sendOrderConfirmation(order);
|
||||
|
||||
expect(sendEmail).toHaveBeenCalled();
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.items[0].name).toBe("Manoon Anti-age Serum");
|
||||
});
|
||||
|
||||
it("should include variant name when present", async () => {
|
||||
await orderNotificationService.sendOrderConfirmation(mockOrderConverted);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.items[0].name).toBe("Manoon Anti-age Serum (50ml)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendOrderConfirmationToAdmin", () => {
|
||||
it("should send admin notification with order details", async () => {
|
||||
await orderNotificationService.sendOrderConfirmationToAdmin(mockOrderConverted);
|
||||
|
||||
expect(sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: ["me@hytham.me", "tamara@hytham.me"],
|
||||
subject: "[Admin] 🎉 New Order #1524 - 10.000,00 RSD",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should always use English for admin emails", async () => {
|
||||
const order = { ...mockOrderConverted, languageCode: "SR" };
|
||||
|
||||
await orderNotificationService.sendOrderConfirmationToAdmin(order);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.language).toBe("en");
|
||||
});
|
||||
|
||||
it("should include all order details in admin email", async () => {
|
||||
await orderNotificationService.sendOrderConfirmationToAdmin(mockOrderConverted);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.isAdmin).toBe(true);
|
||||
expect(callArgs.react.props.customerEmail).toBe("test@hytham.me");
|
||||
expect(callArgs.react.props.phone).toBe("+38160123456");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendOrderShipped", () => {
|
||||
it("should send shipping confirmation with tracking", async () => {
|
||||
await orderNotificationService.sendOrderShipped(
|
||||
mockOrderConverted,
|
||||
"TRK123",
|
||||
"https://track.com/TRK123"
|
||||
);
|
||||
|
||||
expect(sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "test@hytham.me",
|
||||
subject: "Your Order #1524 Has Shipped!",
|
||||
})
|
||||
);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.trackingNumber).toBe("TRK123");
|
||||
expect(callArgs.react.props.trackingUrl).toBe("https://track.com/TRK123");
|
||||
});
|
||||
|
||||
it("should handle missing tracking info", async () => {
|
||||
await orderNotificationService.sendOrderShipped(mockOrderConverted);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.trackingNumber).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendOrderCancelled", () => {
|
||||
it("should send cancellation email with reason", async () => {
|
||||
await orderNotificationService.sendOrderCancelled(
|
||||
mockOrderConverted,
|
||||
"Out of stock"
|
||||
);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.reason).toBe("Out of stock");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendOrderPaid", () => {
|
||||
it("should send payment confirmation", async () => {
|
||||
await orderNotificationService.sendOrderPaid(mockOrderConverted);
|
||||
|
||||
expect(sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "test@hytham.me",
|
||||
subject: "Payment Received for Order #1524!",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatPrice", () => {
|
||||
it("should format prices correctly for RSD", () => {
|
||||
// This is tested indirectly through the email calls above
|
||||
// The formatPrice function is in utils.ts
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCustomerName", () => {
|
||||
it("should extract name from user object", async () => {
|
||||
const order = {
|
||||
...mockOrderConverted,
|
||||
user: { firstName: "John", lastName: "Doe" },
|
||||
billingAddress: { firstName: "Jane", lastName: "Smith" },
|
||||
};
|
||||
|
||||
await orderNotificationService.sendOrderConfirmation(order);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.customerName).toBe("John Doe");
|
||||
});
|
||||
|
||||
it("should fallback to billing address if user not available", async () => {
|
||||
const order = {
|
||||
...mockOrderConverted,
|
||||
user: undefined,
|
||||
billingAddress: { firstName: "Jane", lastName: "Smith" },
|
||||
};
|
||||
|
||||
await orderNotificationService.sendOrderConfirmation(order);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.customerName).toBe("Jane Smith");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatAddress", () => {
|
||||
it("should format address with all fields", async () => {
|
||||
await orderNotificationService.sendOrderConfirmation(mockOrderConverted);
|
||||
|
||||
const callArgs = vi.mocked(sendEmail).mock.calls[0][0];
|
||||
expect(callArgs.react.props.shippingAddress).toContain("123 Test Street");
|
||||
expect(callArgs.react.props.shippingAddress).toContain("Belgrade");
|
||||
expect(callArgs.react.props.shippingAddress).toContain("11000");
|
||||
});
|
||||
|
||||
it("should handle missing address fields", async () => {
|
||||
const order = {
|
||||
...mockOrderConverted,
|
||||
shippingAddress: {
|
||||
firstName: "Test",
|
||||
lastName: "Customer",
|
||||
city: "Belgrade",
|
||||
},
|
||||
};
|
||||
|
||||
await orderNotificationService.sendOrderConfirmation(order);
|
||||
|
||||
expect(sendEmail).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user