forked from OphirPay/OphirPay
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi.spec.ts
More file actions
173 lines (143 loc) · 6.68 KB
/
Copy pathapi.spec.ts
File metadata and controls
173 lines (143 loc) · 6.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// SPDX-License-Identifier: MIT
import { test, expect } from "@playwright/test";
const BASE_URL = process.env.E2E_BASE_URL || "http://localhost:3000";
// Security model under test:
// • Public endpoints: /api/health, /api/metrics, /api/events
// • All data routes are auth-gated → 401 without a session/API key
// (wallet session cookie, Authorization: Bearer, or X-API-Key).
// ── Health ─────────────────────────────────────────────────────
test.describe("GET /api/health", () => {
test("reports service status (200 healthy / 503 degraded)", async ({
request,
}) => {
// The health endpoint is designed to return 503 when a dependency is
// degraded (e.g. the database is unreachable), while still reporting the
// full service status in the body. Accept either contract so CI stays
// stable against the live deployment.
const res = await request.get(`${BASE_URL}/api/health`);
expect([200, 503]).toContain(res.status());
const json = await res.json();
expect(json.data.version).toBeDefined();
expect(json.data.services.database.status).toBeDefined();
expect(json.data.services.stellar).toBeDefined();
expect(json.data.uptime).toBeGreaterThan(0);
});
});
// ── Metrics ────────────────────────────────────────────────────
test.describe("GET /api/metrics", () => {
test("returns Prometheus text format", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/metrics`);
expect(res.status()).toBe(200);
const text = await res.text();
expect(text).toContain("ophirpay_http_requests_total");
expect(text).toContain("ophirpay_payments_created_total");
expect(text).toContain("ophirpay_info");
});
});
// ── Auth gate: data routes require a session or API key ────────
test.describe("Auth gate on data routes", () => {
test("GET /api/payments returns 401 without auth", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/payments?page=1&limit=10`);
expect(res.status()).toBe(401);
});
test("GET /api/payments rejects bad pagination only after auth", async ({
request,
}) => {
// Auth check runs before validation, so an unauthenticated request
// with invalid pagination still returns 401 (not 400).
const res = await request.get(`${BASE_URL}/api/payments?page=0&limit=500`);
expect(res.status()).toBe(401);
});
test("GET /api/recurring returns 401 without auth", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/recurring?page=1&limit=5`);
expect(res.status()).toBe(401);
});
test("GET /api/analytics returns 401 without auth", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/analytics`);
expect(res.status()).toBe(401);
});
test("GET /api/requests returns 401 without auth", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/requests`);
expect(res.status()).toBe(401);
});
test("GET /api/hooks returns 401 without auth", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/hooks`);
expect(res.status()).toBe(401);
});
test("GET /api/refunds returns 401 without auth", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/refunds`);
expect(res.status()).toBe(401);
});
});
// ── Batches (auth-gated) ───────────────────────────────────────
test.describe("POST /api/batches", () => {
test("returns 401 without auth", async ({ request }) => {
const res = await request.post(`${BASE_URL}/api/batches`, {
data: { name: "Unauthed batch", recipients: [] },
});
expect(res.status()).toBe(401);
});
});
// ── Webhooks (auth-gated) ─────────────────────────────────────
test.describe("GET /api/webhooks", () => {
test("returns 401 without API key", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/webhooks`);
expect(res.status()).toBe(401);
const json = await res.json();
expect(json.success).toBe(false);
expect(json.error.code).toBe("UNAUTHORIZED");
});
});
// ── API Keys (auth-gated) ──────────────────────────────────────
test.describe("GET /api/keys", () => {
test("returns 401 without API key", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/keys`);
expect(res.status()).toBe(401);
});
});
// ── CORS & Security Headers ────────────────────────────────────
test.describe("Security headers on API", () => {
test("API returns security headers", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/health`);
expect(res.headers()["x-content-type-options"]).toBe("nosniff");
expect(res.headers()["x-frame-options"]).toBe("DENY");
expect(res.headers()["referrer-policy"]).toBeDefined();
});
});
// ── Not Found ──────────────────────────────────────────────────
test.describe("404 handling", () => {
test("unknown API route returns proper error", async ({ request }) => {
const res = await request.get(`${BASE_URL}/api/nonexistent`);
// Next.js returns 404 for unknown routes
expect(res.status()).toBeGreaterThanOrEqual(400);
});
});
// ── SSE Events ─────────────────────────────────────────────────
test.describe("GET /api/events", () => {
test("opens an SSE stream", async ({ page }) => {
// SSE streams never end, so APIRequestContext (which waits for the full
// body) cannot be used. Verify the stream opens via a real EventSource.
await page.goto("/");
const connected = await page.evaluate(
() =>
new Promise<boolean>((resolve) => {
const es = new EventSource("/api/events");
const timer = setTimeout(() => {
es.close();
resolve(false);
}, 15000);
es.onopen = () => {
clearTimeout(timer);
es.close();
resolve(true);
};
es.onerror = () => {
clearTimeout(timer);
es.close();
resolve(false);
};
})
);
expect(connected).toBe(true);
});
});