forked from UKSOURCE/ipv6
paypal
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Property-based tests for the webhook handler
|
||||
* Feature: paypal-payment-integration
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as fc from "fast-check";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
const mockVerifyWebhookSignature = vi.fn();
|
||||
const mockMarkPaymentPaid = vi.fn();
|
||||
|
||||
vi.mock("@/lib/paypal", () => ({
|
||||
verifyWebhookSignature: (...args: unknown[]) => mockVerifyWebhookSignature(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/summit-requests", () => ({
|
||||
markPaymentPaid: (...args: unknown[]) => mockMarkPaymentPaid(...args),
|
||||
}));
|
||||
|
||||
function makeRequest(body: unknown, headers: Record<string, string> = {}): NextRequest {
|
||||
const defaultHeaders: Record<string, string> = {
|
||||
"paypal-transmission-id": "tx-123",
|
||||
"paypal-transmission-time": "2026-01-01T00:00:00Z",
|
||||
"paypal-cert-url": "https://api.paypal.com/cert",
|
||||
"paypal-auth-algo": "SHA256withRSA",
|
||||
"paypal-transmission-sig": "sig-abc",
|
||||
"content-type": "application/json",
|
||||
...headers,
|
||||
};
|
||||
return new NextRequest("http://localhost/api/payments/webhook", {
|
||||
method: "POST",
|
||||
headers: defaultHeaders,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Property 8: Webhook với signature không hợp lệ → HTTP 401
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("webhook handler — signature validation", () => {
|
||||
beforeEach(() => {
|
||||
mockVerifyWebhookSignature.mockReset();
|
||||
mockMarkPaymentPaid.mockReset();
|
||||
process.env.PAYPAL_WEBHOOK_ID = "test-webhook-id";
|
||||
});
|
||||
|
||||
it(
|
||||
"Feature: paypal-payment-integration, Property 8: Webhook signature không hợp lệ → 401",
|
||||
async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
fc.record({
|
||||
event_type: fc.string(),
|
||||
resource: fc.record({ id: fc.string() }),
|
||||
}),
|
||||
async (payload) => {
|
||||
mockVerifyWebhookSignature.mockResolvedValue(false);
|
||||
mockMarkPaymentPaid.mockResolvedValue(undefined);
|
||||
|
||||
const { POST } = await import("../route");
|
||||
const req = makeRequest(payload);
|
||||
const res = await POST(req);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(mockMarkPaymentPaid).not.toHaveBeenCalled();
|
||||
}
|
||||
),
|
||||
{ numRuns: 30 }
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
"Feature: paypal-payment-integration, Property 8b: Webhook thiếu headers → 401",
|
||||
async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
fc.record({
|
||||
event_type: fc.string(),
|
||||
resource: fc.record({ id: fc.string() }),
|
||||
}),
|
||||
async (payload) => {
|
||||
// Missing all PayPal signature headers
|
||||
const { POST } = await import("../route");
|
||||
const req = makeRequest(payload, {
|
||||
"paypal-transmission-id": "",
|
||||
"paypal-transmission-time": "",
|
||||
"paypal-cert-url": "",
|
||||
"paypal-auth-algo": "",
|
||||
"paypal-transmission-sig": "",
|
||||
});
|
||||
const res = await POST(req);
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
}
|
||||
),
|
||||
{ numRuns: 20 }
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Property 9: Webhook idempotent — valid signature, already paid → 200
|
||||
// Property 10: Webhook hợp lệ cập nhật trạng thái sang paid
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("webhook handler — PAYMENT.CAPTURE.COMPLETED", () => {
|
||||
beforeEach(() => {
|
||||
mockVerifyWebhookSignature.mockReset();
|
||||
mockMarkPaymentPaid.mockReset();
|
||||
process.env.PAYPAL_WEBHOOK_ID = "test-webhook-id";
|
||||
});
|
||||
|
||||
it(
|
||||
"Feature: paypal-payment-integration, Property 10: Webhook hợp lệ cập nhật trạng thái sang paid",
|
||||
async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
fc.string({ minLength: 1, maxLength: 30 }),
|
||||
fc.string({ minLength: 1, maxLength: 30 }),
|
||||
async (orderId, captureId) => {
|
||||
mockVerifyWebhookSignature.mockResolvedValue(true);
|
||||
mockMarkPaymentPaid.mockResolvedValue(undefined);
|
||||
|
||||
const payload = {
|
||||
event_type: "PAYMENT.CAPTURE.COMPLETED",
|
||||
resource: {
|
||||
id: captureId,
|
||||
supplementary_data: {
|
||||
related_ids: { order_id: orderId },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { POST } = await import("../route");
|
||||
const req = makeRequest(payload);
|
||||
const res = await POST(req);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockMarkPaymentPaid).toHaveBeenCalledWith(orderId, captureId);
|
||||
}
|
||||
),
|
||||
{ numRuns: 30 }
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
"Feature: paypal-payment-integration, Property 9: Webhook idempotent — trả về 200 kể cả khi đã paid",
|
||||
async () => {
|
||||
// markPaymentPaid is idempotent (no-op if already paid) — webhook should still return 200
|
||||
mockVerifyWebhookSignature.mockResolvedValue(true);
|
||||
mockMarkPaymentPaid.mockResolvedValue(undefined); // no-op
|
||||
|
||||
const payload = {
|
||||
event_type: "PAYMENT.CAPTURE.COMPLETED",
|
||||
resource: {
|
||||
id: "capture-already-done",
|
||||
supplementary_data: { related_ids: { order_id: "order-already-paid" } },
|
||||
},
|
||||
};
|
||||
|
||||
const { POST } = await import("../route");
|
||||
const req = makeRequest(payload);
|
||||
const res = await POST(req);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user