@thebookingkit/server 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -0
- package/.turbo/turbo-test.log +20 -0
- package/CHANGELOG.md +9 -0
- package/dist/__tests__/api.test.d.ts +2 -0
- package/dist/__tests__/api.test.d.ts.map +1 -0
- package/dist/__tests__/api.test.js +280 -0
- package/dist/__tests__/api.test.js.map +1 -0
- package/dist/__tests__/auth.test.d.ts +2 -0
- package/dist/__tests__/auth.test.d.ts.map +1 -0
- package/dist/__tests__/auth.test.js +78 -0
- package/dist/__tests__/auth.test.js.map +1 -0
- package/dist/__tests__/concurrent-booking.test.d.ts +2 -0
- package/dist/__tests__/concurrent-booking.test.d.ts.map +1 -0
- package/dist/__tests__/concurrent-booking.test.js +111 -0
- package/dist/__tests__/concurrent-booking.test.js.map +1 -0
- package/dist/__tests__/multi-tenancy.test.d.ts +2 -0
- package/dist/__tests__/multi-tenancy.test.d.ts.map +1 -0
- package/dist/__tests__/multi-tenancy.test.js +196 -0
- package/dist/__tests__/multi-tenancy.test.js.map +1 -0
- package/dist/__tests__/serialization-retry.test.d.ts +2 -0
- package/dist/__tests__/serialization-retry.test.d.ts.map +1 -0
- package/dist/__tests__/serialization-retry.test.js +53 -0
- package/dist/__tests__/serialization-retry.test.js.map +1 -0
- package/dist/__tests__/webhooks.test.d.ts +2 -0
- package/dist/__tests__/webhooks.test.d.ts.map +1 -0
- package/dist/__tests__/webhooks.test.js +286 -0
- package/dist/__tests__/webhooks.test.js.map +1 -0
- package/dist/__tests__/workflows.test.d.ts +2 -0
- package/dist/__tests__/workflows.test.d.ts.map +1 -0
- package/dist/__tests__/workflows.test.js +299 -0
- package/dist/__tests__/workflows.test.js.map +1 -0
- package/dist/adapters/calendar-adapter.d.ts +47 -0
- package/dist/adapters/calendar-adapter.d.ts.map +1 -0
- package/dist/adapters/calendar-adapter.js +2 -0
- package/dist/adapters/calendar-adapter.js.map +1 -0
- package/dist/adapters/email-adapter.d.ts +65 -0
- package/dist/adapters/email-adapter.d.ts.map +1 -0
- package/dist/adapters/email-adapter.js +40 -0
- package/dist/adapters/email-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/job-adapter.d.ts +26 -0
- package/dist/adapters/job-adapter.d.ts.map +1 -0
- package/dist/adapters/job-adapter.js +13 -0
- package/dist/adapters/job-adapter.js.map +1 -0
- package/dist/adapters/payment-adapter.d.ts +106 -0
- package/dist/adapters/payment-adapter.d.ts.map +1 -0
- package/dist/adapters/payment-adapter.js +8 -0
- package/dist/adapters/payment-adapter.js.map +1 -0
- package/dist/adapters/sms-adapter.d.ts +33 -0
- package/dist/adapters/sms-adapter.d.ts.map +1 -0
- package/dist/adapters/sms-adapter.js +8 -0
- package/dist/adapters/sms-adapter.js.map +1 -0
- package/dist/adapters/storage-adapter.d.ts +12 -0
- package/dist/adapters/storage-adapter.d.ts.map +1 -0
- package/dist/adapters/storage-adapter.js +2 -0
- package/dist/adapters/storage-adapter.js.map +1 -0
- package/dist/api.d.ts +223 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +271 -0
- package/dist/api.js.map +1 -0
- package/dist/auth.d.ts +71 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +81 -0
- package/dist/auth.js.map +1 -0
- package/dist/booking-tokens.d.ts +23 -0
- package/dist/booking-tokens.d.ts.map +1 -0
- package/dist/booking-tokens.js +52 -0
- package/dist/booking-tokens.js.map +1 -0
- package/dist/email-templates.d.ts +36 -0
- package/dist/email-templates.d.ts.map +1 -0
- package/dist/email-templates.js +112 -0
- package/dist/email-templates.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/multi-tenancy.d.ts +132 -0
- package/dist/multi-tenancy.d.ts.map +1 -0
- package/dist/multi-tenancy.js +188 -0
- package/dist/multi-tenancy.js.map +1 -0
- package/dist/notification-jobs.d.ts +143 -0
- package/dist/notification-jobs.d.ts.map +1 -0
- package/dist/notification-jobs.js +278 -0
- package/dist/notification-jobs.js.map +1 -0
- package/dist/serialization-retry.d.ts +28 -0
- package/dist/serialization-retry.d.ts.map +1 -0
- package/dist/serialization-retry.js +71 -0
- package/dist/serialization-retry.js.map +1 -0
- package/dist/webhooks.d.ts +164 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +228 -0
- package/dist/webhooks.js.map +1 -0
- package/dist/workflows.d.ts +169 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +251 -0
- package/dist/workflows.js.map +1 -0
- package/package.json +32 -0
- package/src/__tests__/api.test.ts +354 -0
- package/src/__tests__/auth.test.ts +111 -0
- package/src/__tests__/concurrent-booking.test.ts +170 -0
- package/src/__tests__/multi-tenancy.test.ts +267 -0
- package/src/__tests__/serialization-retry.test.ts +76 -0
- package/src/__tests__/webhooks.test.ts +412 -0
- package/src/__tests__/workflows.test.ts +422 -0
- package/src/adapters/calendar-adapter.ts +49 -0
- package/src/adapters/email-adapter.ts +108 -0
- package/src/adapters/index.ts +36 -0
- package/src/adapters/job-adapter.ts +26 -0
- package/src/adapters/payment-adapter.ts +118 -0
- package/src/adapters/sms-adapter.ts +35 -0
- package/src/adapters/storage-adapter.ts +11 -0
- package/src/api.ts +446 -0
- package/src/auth.ts +146 -0
- package/src/booking-tokens.ts +61 -0
- package/src/email-templates.ts +140 -0
- package/src/index.ts +192 -0
- package/src/multi-tenancy.ts +301 -0
- package/src/notification-jobs.ts +428 -0
- package/src/serialization-retry.ts +94 -0
- package/src/webhooks.ts +378 -0
- package/src/workflows.ts +441 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
> @thebookingkit/server@0.1.0 test
|
|
3
|
+
> vitest run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
RUN v3.2.4 /Users/zain/Desktop/slotkit/packages/server
|
|
7
|
+
|
|
8
|
+
✓ src/__tests__/multi-tenancy.test.ts (26 tests) 7ms
|
|
9
|
+
✓ src/__tests__/webhooks.test.ts (35 tests) 10ms
|
|
10
|
+
✓ src/__tests__/workflows.test.ts (37 tests) 14ms
|
|
11
|
+
✓ src/__tests__/api.test.ts (40 tests) 15ms
|
|
12
|
+
✓ src/__tests__/auth.test.ts (8 tests) 15ms
|
|
13
|
+
✓ src/__tests__/serialization-retry.test.ts (6 tests) 106ms
|
|
14
|
+
✓ src/__tests__/concurrent-booking.test.ts (5 tests) 167ms
|
|
15
|
+
|
|
16
|
+
Test Files 7 passed (7)
|
|
17
|
+
Tests 157 passed (157)
|
|
18
|
+
Start at 01:24:25
|
|
19
|
+
Duration 700ms (transform 315ms, setup 0ms, collect 1.10s, tests 335ms, environment 1ms, prepare 475ms)
|
|
20
|
+
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/api.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { createErrorResponse, createSuccessResponse, createPaginatedResponse, generateApiKey, hashApiKey, verifyApiKey, hasScope, isKeyExpired, checkRateLimit, encodeCursor, decodeCursor, validateSlotQueryParams, parseSortParam, API_ERROR_CODES, } from "../api.js";
|
|
3
|
+
// Set the required env var for API key hashing in tests
|
|
4
|
+
process.env.SLOTKIT_API_KEY_SECRET = "test-secret-for-unit-tests-only";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Response Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
describe("createErrorResponse", () => {
|
|
9
|
+
it("creates a standard error envelope", () => {
|
|
10
|
+
const response = createErrorResponse("NOT_FOUND", "Booking not found");
|
|
11
|
+
expect(response).toEqual({
|
|
12
|
+
error: { code: "NOT_FOUND", message: "Booking not found" },
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
it("includes details when provided", () => {
|
|
16
|
+
const response = createErrorResponse("VALIDATION_ERROR", "Invalid input", { field: "email" });
|
|
17
|
+
expect(response.error.details).toEqual({ field: "email" });
|
|
18
|
+
});
|
|
19
|
+
it("omits details when not provided", () => {
|
|
20
|
+
const response = createErrorResponse("UNAUTHORIZED", "Invalid key");
|
|
21
|
+
expect(response.error.details).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("createSuccessResponse", () => {
|
|
25
|
+
it("wraps data in a success envelope", () => {
|
|
26
|
+
const response = createSuccessResponse({ id: "bk-1" });
|
|
27
|
+
expect(response).toEqual({ data: { id: "bk-1" } });
|
|
28
|
+
});
|
|
29
|
+
it("includes meta when provided", () => {
|
|
30
|
+
const response = createSuccessResponse([], {
|
|
31
|
+
nextCursor: "abc",
|
|
32
|
+
hasMore: true,
|
|
33
|
+
});
|
|
34
|
+
expect(response.meta?.nextCursor).toBe("abc");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("createPaginatedResponse", () => {
|
|
38
|
+
it("creates paginated response with cursor", () => {
|
|
39
|
+
const response = createPaginatedResponse(["a", "b"], "cursor123", 10);
|
|
40
|
+
expect(response.data).toEqual(["a", "b"]);
|
|
41
|
+
expect(response.meta.nextCursor).toBe("cursor123");
|
|
42
|
+
expect(response.meta.hasMore).toBe(true);
|
|
43
|
+
expect(response.meta.total).toBe(10);
|
|
44
|
+
});
|
|
45
|
+
it("marks hasMore false when nextCursor is null", () => {
|
|
46
|
+
const response = createPaginatedResponse(["a"], null);
|
|
47
|
+
expect(response.meta.hasMore).toBe(false);
|
|
48
|
+
expect(response.meta.nextCursor).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// API Key Management
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
describe("generateApiKey", () => {
|
|
55
|
+
it("generates a key with the given prefix", () => {
|
|
56
|
+
const { key } = generateApiKey("sk_live_");
|
|
57
|
+
expect(key).toMatch(/^sk_live_[0-9a-f]{64}$/);
|
|
58
|
+
});
|
|
59
|
+
it("generates a display prefix", () => {
|
|
60
|
+
const { prefix } = generateApiKey("sk_live_");
|
|
61
|
+
expect(prefix).toContain("...");
|
|
62
|
+
});
|
|
63
|
+
it("generates a 64-char hex hash", () => {
|
|
64
|
+
const { hash } = generateApiKey();
|
|
65
|
+
expect(hash).toMatch(/^[0-9a-f]{64}$/);
|
|
66
|
+
});
|
|
67
|
+
it("generates unique keys each time", () => {
|
|
68
|
+
const { key: k1 } = generateApiKey();
|
|
69
|
+
const { key: k2 } = generateApiKey();
|
|
70
|
+
expect(k1).not.toBe(k2);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe("hashApiKey", () => {
|
|
74
|
+
it("throws when SLOTKIT_API_KEY_SECRET is missing", () => {
|
|
75
|
+
const original = process.env.SLOTKIT_API_KEY_SECRET;
|
|
76
|
+
delete process.env.SLOTKIT_API_KEY_SECRET;
|
|
77
|
+
try {
|
|
78
|
+
expect(() => hashApiKey("sk_live_test")).toThrow("SLOTKIT_API_KEY_SECRET environment variable is required");
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
process.env.SLOTKIT_API_KEY_SECRET = original;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
it("accepts an explicit secret parameter", () => {
|
|
85
|
+
const hash = hashApiKey("sk_live_test", "my-explicit-secret");
|
|
86
|
+
expect(hash).toMatch(/^[0-9a-f]{64}$/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe("verifyApiKey", () => {
|
|
90
|
+
it("returns true for correct key", () => {
|
|
91
|
+
const { key, hash } = generateApiKey();
|
|
92
|
+
expect(verifyApiKey(key, hash)).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it("returns false for tampered key", () => {
|
|
95
|
+
const { hash } = generateApiKey();
|
|
96
|
+
expect(verifyApiKey("sk_live_wrong", hash)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
it("returns false for wrong hash", () => {
|
|
99
|
+
const { key } = generateApiKey();
|
|
100
|
+
const { hash: otherHash } = generateApiKey();
|
|
101
|
+
expect(verifyApiKey(key, otherHash)).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe("hasScope", () => {
|
|
105
|
+
it("returns true when scope is present", () => {
|
|
106
|
+
const scopes = ["read:bookings", "write:bookings"];
|
|
107
|
+
expect(hasScope(scopes, "read:bookings")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it("returns false when scope is absent", () => {
|
|
110
|
+
const scopes = ["read:bookings"];
|
|
111
|
+
expect(hasScope(scopes, "write:bookings")).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
it("admin scope grants all permissions", () => {
|
|
114
|
+
const scopes = ["admin"];
|
|
115
|
+
expect(hasScope(scopes, "read:bookings")).toBe(true);
|
|
116
|
+
expect(hasScope(scopes, "write:event-types")).toBe(true);
|
|
117
|
+
expect(hasScope(scopes, "write:webhooks")).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe("isKeyExpired", () => {
|
|
121
|
+
it("returns false for no expiry", () => {
|
|
122
|
+
expect(isKeyExpired(undefined)).toBe(false);
|
|
123
|
+
expect(isKeyExpired(null)).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it("returns true for past expiry", () => {
|
|
126
|
+
expect(isKeyExpired(new Date(Date.now() - 1000))).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
it("returns false for future expiry", () => {
|
|
129
|
+
expect(isKeyExpired(new Date(Date.now() + 86400000))).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Rate Limiting
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
describe("checkRateLimit", () => {
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
vi.useFakeTimers();
|
|
138
|
+
vi.setSystemTime(new Date("2026-03-15T14:00:00.000Z"));
|
|
139
|
+
});
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
vi.useRealTimers();
|
|
142
|
+
});
|
|
143
|
+
it("allows first request", () => {
|
|
144
|
+
const { result } = checkRateLimit(null, 120);
|
|
145
|
+
expect(result.allowed).toBe(true);
|
|
146
|
+
expect(result.remaining).toBe(119);
|
|
147
|
+
expect(result.limit).toBe(120);
|
|
148
|
+
});
|
|
149
|
+
it("tracks requests within a window", () => {
|
|
150
|
+
const { newState } = checkRateLimit(null, 120);
|
|
151
|
+
const { result } = checkRateLimit(newState, 120);
|
|
152
|
+
expect(result.allowed).toBe(true);
|
|
153
|
+
expect(result.remaining).toBe(118);
|
|
154
|
+
});
|
|
155
|
+
it("blocks requests when limit exceeded", () => {
|
|
156
|
+
let state = checkRateLimit(null, 2).newState;
|
|
157
|
+
state = checkRateLimit(state, 2).newState;
|
|
158
|
+
const { result } = checkRateLimit(state, 2);
|
|
159
|
+
expect(result.allowed).toBe(false);
|
|
160
|
+
expect(result.remaining).toBe(0);
|
|
161
|
+
});
|
|
162
|
+
it("resets counter in new window", () => {
|
|
163
|
+
let state = checkRateLimit(null, 2).newState;
|
|
164
|
+
state = checkRateLimit(state, 2).newState;
|
|
165
|
+
// Advance to next minute
|
|
166
|
+
vi.advanceTimersByTime(60 * 1000);
|
|
167
|
+
const { result } = checkRateLimit(state, 2);
|
|
168
|
+
expect(result.allowed).toBe(true);
|
|
169
|
+
expect(result.remaining).toBe(1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Cursor Pagination
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
describe("encodeCursor / decodeCursor", () => {
|
|
176
|
+
it("round-trips cursor data", () => {
|
|
177
|
+
const data = { id: "bk-1", createdAt: "2026-03-15T14:00:00Z" };
|
|
178
|
+
const cursor = encodeCursor(data);
|
|
179
|
+
const decoded = decodeCursor(cursor);
|
|
180
|
+
expect(decoded).toEqual(data);
|
|
181
|
+
});
|
|
182
|
+
it("returns null for invalid cursor", () => {
|
|
183
|
+
expect(decodeCursor("not-valid-base64!!!")).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
it("returns null for non-JSON cursor", () => {
|
|
186
|
+
const cursor = Buffer.from("not json").toString("base64url");
|
|
187
|
+
expect(decodeCursor(cursor)).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// validateSlotQueryParams
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
describe("validateSlotQueryParams", () => {
|
|
194
|
+
const validParams = {
|
|
195
|
+
providerId: "prov-1",
|
|
196
|
+
start: "2026-03-15",
|
|
197
|
+
end: "2026-04-15",
|
|
198
|
+
timezone: "America/New_York",
|
|
199
|
+
};
|
|
200
|
+
it("accepts valid params", () => {
|
|
201
|
+
const result = validateSlotQueryParams(validParams);
|
|
202
|
+
expect(result.valid).toBe(true);
|
|
203
|
+
expect(result.errors).toHaveLength(0);
|
|
204
|
+
});
|
|
205
|
+
it("accepts teamId instead of providerId", () => {
|
|
206
|
+
const result = validateSlotQueryParams({
|
|
207
|
+
teamId: "team-1",
|
|
208
|
+
start: "2026-03-15",
|
|
209
|
+
end: "2026-04-15",
|
|
210
|
+
});
|
|
211
|
+
expect(result.valid).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
it("rejects missing providerId and teamId", () => {
|
|
214
|
+
const result = validateSlotQueryParams({
|
|
215
|
+
start: "2026-03-15",
|
|
216
|
+
end: "2026-04-15",
|
|
217
|
+
});
|
|
218
|
+
expect(result.valid).toBe(false);
|
|
219
|
+
expect(result.errors[0].field).toBe("providerId");
|
|
220
|
+
});
|
|
221
|
+
it("rejects missing start date", () => {
|
|
222
|
+
const result = validateSlotQueryParams({
|
|
223
|
+
providerId: "p1",
|
|
224
|
+
end: "2026-04-15",
|
|
225
|
+
});
|
|
226
|
+
expect(result.valid).toBe(false);
|
|
227
|
+
expect(result.errors.some((e) => e.field === "start")).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
it("rejects invalid start date", () => {
|
|
230
|
+
const result = validateSlotQueryParams({
|
|
231
|
+
providerId: "p1",
|
|
232
|
+
start: "not-a-date",
|
|
233
|
+
end: "2026-04-15",
|
|
234
|
+
});
|
|
235
|
+
expect(result.valid).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
it("rejects end before start", () => {
|
|
238
|
+
const result = validateSlotQueryParams({
|
|
239
|
+
providerId: "p1",
|
|
240
|
+
start: "2026-04-15",
|
|
241
|
+
end: "2026-03-15",
|
|
242
|
+
});
|
|
243
|
+
expect(result.valid).toBe(false);
|
|
244
|
+
expect(result.errors.some((e) => e.message.includes("after start"))).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// parseSortParam
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
describe("parseSortParam", () => {
|
|
251
|
+
const allowedFields = ["createdAt", "startsAt", "status"];
|
|
252
|
+
it("parses ascending sort", () => {
|
|
253
|
+
const result = parseSortParam("startsAt", allowedFields);
|
|
254
|
+
expect(result).toEqual({ field: "startsAt", direction: "asc" });
|
|
255
|
+
});
|
|
256
|
+
it("parses descending sort with leading minus", () => {
|
|
257
|
+
const result = parseSortParam("-createdAt", allowedFields);
|
|
258
|
+
expect(result).toEqual({ field: "createdAt", direction: "desc" });
|
|
259
|
+
});
|
|
260
|
+
it("returns null for invalid field", () => {
|
|
261
|
+
expect(parseSortParam("unknownField", allowedFields)).toBeNull();
|
|
262
|
+
});
|
|
263
|
+
it("returns null for undefined input", () => {
|
|
264
|
+
expect(parseSortParam(undefined, allowedFields)).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// API_ERROR_CODES
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
describe("API_ERROR_CODES", () => {
|
|
271
|
+
it("exports all standard error codes", () => {
|
|
272
|
+
expect(API_ERROR_CODES.NOT_FOUND).toBe("NOT_FOUND");
|
|
273
|
+
expect(API_ERROR_CODES.UNAUTHORIZED).toBe("UNAUTHORIZED");
|
|
274
|
+
expect(API_ERROR_CODES.FORBIDDEN).toBe("FORBIDDEN");
|
|
275
|
+
expect(API_ERROR_CODES.VALIDATION_ERROR).toBe("VALIDATION_ERROR");
|
|
276
|
+
expect(API_ERROR_CODES.RATE_LIMITED).toBe("RATE_LIMITED");
|
|
277
|
+
expect(API_ERROR_CODES.CONFLICT).toBe("CONFLICT");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
//# sourceMappingURL=api.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.test.js","sourceRoot":"","sources":["../../src/__tests__/api.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/auth.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { withAuth, assertProviderOwnership, assertCustomerAccess, ForbiddenError, } from "../index.js";
|
|
3
|
+
function mockAdapter(user) {
|
|
4
|
+
return {
|
|
5
|
+
getCurrentUser: vi.fn().mockResolvedValue(user),
|
|
6
|
+
getSession: vi.fn().mockResolvedValue(user ? { user, expires: new Date() } : null),
|
|
7
|
+
verifyToken: vi.fn().mockResolvedValue(user),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function mockRequest(headers = {}) {
|
|
11
|
+
return new Request("http://localhost/api/test", {
|
|
12
|
+
headers: new Headers(headers),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
describe("withAuth", () => {
|
|
16
|
+
it("injects user and calls handler on valid session", async () => {
|
|
17
|
+
const user = { id: "user1", email: "test@test.com" };
|
|
18
|
+
const adapter = mockAdapter(user);
|
|
19
|
+
const handler = vi.fn().mockResolvedValue(Response.json({ ok: true }));
|
|
20
|
+
const wrapped = withAuth(adapter, handler);
|
|
21
|
+
const response = await wrapped(mockRequest());
|
|
22
|
+
expect(response.status).toBe(200);
|
|
23
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
24
|
+
const calledReq = handler.mock.calls[0][0];
|
|
25
|
+
expect(calledReq.user).toEqual(user);
|
|
26
|
+
});
|
|
27
|
+
it("returns 401 when no user is authenticated", async () => {
|
|
28
|
+
const adapter = mockAdapter(null);
|
|
29
|
+
// verifyToken also returns null
|
|
30
|
+
adapter.verifyToken = vi.fn().mockResolvedValue(null);
|
|
31
|
+
const handler = vi.fn();
|
|
32
|
+
const wrapped = withAuth(adapter, handler);
|
|
33
|
+
const response = await wrapped(mockRequest());
|
|
34
|
+
expect(response.status).toBe(401);
|
|
35
|
+
expect(handler).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
it("falls back to Bearer token when session returns null", async () => {
|
|
38
|
+
const user = { id: "user2", email: "api@test.com" };
|
|
39
|
+
const adapter = mockAdapter(null);
|
|
40
|
+
adapter.getCurrentUser = vi.fn().mockResolvedValue(null);
|
|
41
|
+
adapter.verifyToken = vi.fn().mockResolvedValue(user);
|
|
42
|
+
const handler = vi.fn().mockResolvedValue(Response.json({ ok: true }));
|
|
43
|
+
const wrapped = withAuth(adapter, handler);
|
|
44
|
+
const response = await wrapped(mockRequest({ Authorization: "Bearer test-token-123" }));
|
|
45
|
+
expect(response.status).toBe(200);
|
|
46
|
+
expect(adapter.verifyToken).toHaveBeenCalledWith("test-token-123");
|
|
47
|
+
});
|
|
48
|
+
it("returns 403 when role does not match", async () => {
|
|
49
|
+
const user = {
|
|
50
|
+
id: "user3",
|
|
51
|
+
email: "member@test.com",
|
|
52
|
+
role: "customer",
|
|
53
|
+
};
|
|
54
|
+
const adapter = mockAdapter(user);
|
|
55
|
+
const handler = vi.fn();
|
|
56
|
+
const wrapped = withAuth(adapter, handler, { requiredRole: "admin" });
|
|
57
|
+
const response = await wrapped(mockRequest());
|
|
58
|
+
expect(response.status).toBe(403);
|
|
59
|
+
expect(handler).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("assertProviderOwnership", () => {
|
|
63
|
+
it("does not throw when user IDs match", () => {
|
|
64
|
+
expect(() => assertProviderOwnership("user1", "user1")).not.toThrow();
|
|
65
|
+
});
|
|
66
|
+
it("throws ForbiddenError when user IDs do not match", () => {
|
|
67
|
+
expect(() => assertProviderOwnership("user1", "user2")).toThrow(ForbiddenError);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("assertCustomerAccess", () => {
|
|
71
|
+
it("does not throw when emails match", () => {
|
|
72
|
+
expect(() => assertCustomerAccess("a@test.com", "a@test.com")).not.toThrow();
|
|
73
|
+
});
|
|
74
|
+
it("throws ForbiddenError when emails do not match", () => {
|
|
75
|
+
expect(() => assertCustomerAccess("a@test.com", "b@test.com")).toThrow(ForbiddenError);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
//# sourceMappingURL=auth.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.test.js","sourceRoot":"","sources":["../../src/__tests__/auth.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrent-booking.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/concurrent-booking.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { withSerializableRetry, BookingConflictError, SerializationRetryExhaustedError, } from "../index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Concurrent booking simulation tests.
|
|
5
|
+
*
|
|
6
|
+
* These tests simulate the load test from E01-S04:
|
|
7
|
+
* "50 concurrent booking attempts for the same slot yields exactly 1 confirmed
|
|
8
|
+
* booking, 0 unhandled serialization errors, and all other callers receive
|
|
9
|
+
* a clear conflict response within 2 seconds."
|
|
10
|
+
*
|
|
11
|
+
* Since we can't connect to a real Postgres in unit tests, we simulate the
|
|
12
|
+
* database behavior with an in-memory "slot lock" that mimics the EXCLUDE
|
|
13
|
+
* constraint (SQLSTATE 23P01) and serialization failures (SQLSTATE 40001).
|
|
14
|
+
*/
|
|
15
|
+
/** Simulates a Postgres error with a SQLSTATE code */
|
|
16
|
+
class PgError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(message, code) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "PgError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
describe("Concurrent Booking Simulation (E01-S04 Load Test)", () => {
|
|
25
|
+
it("50 concurrent attempts for the same slot: exactly 1 success, rest get conflict", async () => {
|
|
26
|
+
// Simulate a database slot that can only be booked once.
|
|
27
|
+
// The first successful INSERT wins; subsequent attempts get exclusion_violation.
|
|
28
|
+
let slotBooked = false;
|
|
29
|
+
let serializationFailureCount = 0;
|
|
30
|
+
const maxSimulatedSerializationFailures = 5; // simulate some contention
|
|
31
|
+
const attemptBooking = async (attemptId) => {
|
|
32
|
+
return withSerializableRetry(async () => {
|
|
33
|
+
// Simulate random serialization failures under contention
|
|
34
|
+
if (!slotBooked &&
|
|
35
|
+
serializationFailureCount < maxSimulatedSerializationFailures &&
|
|
36
|
+
Math.random() < 0.3) {
|
|
37
|
+
serializationFailureCount++;
|
|
38
|
+
throw new PgError("serialization failure", "40001");
|
|
39
|
+
}
|
|
40
|
+
// Simulate the EXCLUDE constraint check
|
|
41
|
+
if (slotBooked) {
|
|
42
|
+
throw new PgError("conflicting key value violates exclusion constraint", "23P01");
|
|
43
|
+
}
|
|
44
|
+
// Race: first writer wins
|
|
45
|
+
slotBooked = true;
|
|
46
|
+
return `booking-${attemptId}`;
|
|
47
|
+
}, { maxRetries: 3, baseDelayMs: 10 });
|
|
48
|
+
};
|
|
49
|
+
const CONCURRENCY = 50;
|
|
50
|
+
const results = await Promise.allSettled(Array.from({ length: CONCURRENCY }, (_, i) => attemptBooking(i)));
|
|
51
|
+
const successes = results.filter((r) => r.status === "fulfilled");
|
|
52
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
53
|
+
// Exactly 1 booking should succeed
|
|
54
|
+
expect(successes.length).toBe(1);
|
|
55
|
+
// All failures should be BookingConflictError (not unhandled errors)
|
|
56
|
+
for (const failure of failures) {
|
|
57
|
+
if (failure.status === "rejected") {
|
|
58
|
+
expect(failure.reason).toBeInstanceOf(BookingConflictError);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Zero unhandled serialization errors
|
|
62
|
+
const serializationErrors = failures.filter((f) => f.status === "rejected" &&
|
|
63
|
+
f.reason instanceof SerializationRetryExhaustedError);
|
|
64
|
+
expect(serializationErrors.length).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
it("completes all 50 attempts within 2 seconds", async () => {
|
|
67
|
+
let slotBooked = false;
|
|
68
|
+
const attemptBooking = async (attemptId) => {
|
|
69
|
+
return withSerializableRetry(async () => {
|
|
70
|
+
if (slotBooked) {
|
|
71
|
+
throw new PgError("conflicting key value violates exclusion constraint", "23P01");
|
|
72
|
+
}
|
|
73
|
+
slotBooked = true;
|
|
74
|
+
return `booking-${attemptId}`;
|
|
75
|
+
}, { maxRetries: 3, baseDelayMs: 10 });
|
|
76
|
+
};
|
|
77
|
+
const start = Date.now();
|
|
78
|
+
await Promise.allSettled(Array.from({ length: 50 }, (_, i) => attemptBooking(i)));
|
|
79
|
+
const elapsed = Date.now() - start;
|
|
80
|
+
expect(elapsed).toBeLessThan(2000);
|
|
81
|
+
});
|
|
82
|
+
it("serialization failures are retried and can succeed on a different slot", async () => {
|
|
83
|
+
let attempt = 0;
|
|
84
|
+
const result = await withSerializableRetry(async () => {
|
|
85
|
+
attempt++;
|
|
86
|
+
if (attempt <= 2) {
|
|
87
|
+
throw new PgError("serialization failure", "40001");
|
|
88
|
+
}
|
|
89
|
+
return "booking-success";
|
|
90
|
+
}, { maxRetries: 3, baseDelayMs: 10 });
|
|
91
|
+
expect(result).toBe("booking-success");
|
|
92
|
+
expect(attempt).toBe(3);
|
|
93
|
+
});
|
|
94
|
+
it("retries are exhausted after maxRetries serialization failures", async () => {
|
|
95
|
+
const fn = () => withSerializableRetry(async () => {
|
|
96
|
+
throw new PgError("serialization failure", "40001");
|
|
97
|
+
}, { maxRetries: 3, baseDelayMs: 10 });
|
|
98
|
+
await expect(fn()).rejects.toThrow(SerializationRetryExhaustedError);
|
|
99
|
+
});
|
|
100
|
+
it("exclusion violation (slot taken) is never retried", async () => {
|
|
101
|
+
let callCount = 0;
|
|
102
|
+
const fn = () => withSerializableRetry(async () => {
|
|
103
|
+
callCount++;
|
|
104
|
+
throw new PgError("exclusion constraint", "23P01");
|
|
105
|
+
}, { maxRetries: 3, baseDelayMs: 10 });
|
|
106
|
+
await expect(fn()).rejects.toThrow(BookingConflictError);
|
|
107
|
+
// Should NOT have retried — only 1 call
|
|
108
|
+
expect(callCount).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
//# sourceMappingURL=concurrent-booking.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrent-booking.test.js","sourceRoot":"","sources":["../../src/__tests__/concurrent-booking.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multi-tenancy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/multi-tenancy.test.ts"],"names":[],"mappings":""}
|