Skip to content

Commit 43f13ab

Browse files
committed
feat: more tests\
1 parent cba5451 commit 43f13ab

File tree

5 files changed

+840
-116
lines changed

5 files changed

+840
-116
lines changed

apps/basket/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
"packageManager": "[email protected]",
55
"scripts": {
66
"dev": "bun --watch run src/index.ts --port 3002",
7-
"start": "bun run src/index.ts --port 3002"
7+
"start": "bun run src/index.ts --port 3002",
8+
"test": "bun test",
9+
"test:watch": "bun test --watch",
10+
"test:routes": "bun test src/routes/*.test.ts",
11+
"test:utils": "bun test src/utils/*.test.ts",
12+
"test:coverage": "bun test --coverage"
813
},
914
"dependencies": {
1015
"@clickhouse/client": "catalog:",

apps/basket/src/index.test.ts

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
import { beforeEach, describe, expect, it, mock } from 'bun:test';
2+
import app from './index';
3+
4+
// Mock external dependencies
5+
const mockLogger = {
6+
info: mock(() => {}),
7+
warn: mock(() => {}),
8+
error: mock(() => {}),
9+
};
10+
11+
const mockClickHouse = {
12+
insert: mock(() => Promise.resolve()),
13+
};
14+
15+
const mockRedis = {
16+
get: mock(() => Promise.resolve(null)),
17+
setex: mock(() => Promise.resolve()),
18+
exists: mock(() => Promise.resolve(false)),
19+
};
20+
21+
const mockAutumn = {
22+
check: mock(() => Promise.resolve({ data: { allowed: true } })),
23+
};
24+
25+
const mockDb = {
26+
query: {
27+
websites: {
28+
findFirst: mock(() => Promise.resolve({
29+
id: 'test-client-id',
30+
domain: 'example.com',
31+
status: 'ACTIVE',
32+
userId: 'test-user-id',
33+
organizationId: null,
34+
})),
35+
},
36+
},
37+
};
38+
39+
// Mock modules
40+
mock.module('./lib/logger', () => ({
41+
logger: mockLogger,
42+
}));
43+
44+
mock.module('@databuddy/db', () => ({
45+
clickHouse: mockClickHouse,
46+
db: mockDb,
47+
}));
48+
49+
mock.module('@databuddy/redis', () => ({
50+
redis: mockRedis,
51+
}));
52+
53+
mock.module('autumn-js', () => ({
54+
Autumn: mockAutumn,
55+
}));
56+
57+
mock.module('./routes/basket', () => ({
58+
default: {
59+
fetch: mock(() => Promise.resolve(new Response(JSON.stringify({ status: 'success' }), { status: 200 }))),
60+
},
61+
}));
62+
63+
mock.module('./routes/email', () => ({
64+
default: {
65+
fetch: mock(() => Promise.resolve(new Response(JSON.stringify({ status: 'success' }), { status: 200 }))),
66+
},
67+
}));
68+
69+
mock.module('./routes/stripe', () => ({
70+
default: {
71+
fetch: mock(() => Promise.resolve(new Response(JSON.stringify({ status: 'success' }), { status: 200 }))),
72+
},
73+
}));
74+
75+
describe('Basket App', () => {
76+
beforeEach(() => {
77+
// Reset all mocks
78+
mockLogger.info.mockClear();
79+
mockLogger.warn.mockClear();
80+
mockLogger.error.mockClear();
81+
mockClickHouse.insert.mockClear();
82+
mockRedis.get.mockClear();
83+
mockRedis.setex.mockClear();
84+
mockRedis.exists.mockClear();
85+
mockAutumn.check.mockClear();
86+
});
87+
88+
describe('App Initialization', () => {
89+
it('should initialize without errors', () => {
90+
expect(app).toBeDefined();
91+
expect(app.fetch).toBeDefined();
92+
expect(app.port).toBeDefined();
93+
});
94+
95+
it('should have correct port configuration', () => {
96+
expect(app.port).toBe(4000);
97+
});
98+
99+
it('should export fetch function', () => {
100+
expect(typeof app.fetch).toBe('function');
101+
});
102+
});
103+
104+
describe('Health Endpoint', () => {
105+
it('should respond to health check', async () => {
106+
const response = await app.fetch(new Request('http://localhost:4000/health'));
107+
expect(response.status).toBe(200);
108+
109+
const data = await response.json();
110+
expect(data).toEqual({
111+
status: 'ok',
112+
version: '1.0.0',
113+
});
114+
});
115+
116+
it('should handle multiple health checks', async () => {
117+
const requests = Array(5).fill(null).map(() =>
118+
app.fetch(new Request('http://localhost:4000/health'))
119+
);
120+
121+
const responses = await Promise.all(requests);
122+
123+
for (const response of responses) {
124+
expect(response.status).toBe(200);
125+
const data = await response.json();
126+
expect(data.status).toBe('ok');
127+
}
128+
});
129+
});
130+
131+
describe('CORS Configuration', () => {
132+
it('should handle OPTIONS requests', async () => {
133+
const response = await app.fetch(new Request('http://localhost:4000/', {
134+
method: 'OPTIONS',
135+
headers: {
136+
'Origin': 'https://example.com',
137+
},
138+
}));
139+
140+
expect(response.status).toBe(204);
141+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://example.com');
142+
expect(response.headers.get('Access-Control-Allow-Methods')).toContain('POST');
143+
expect(response.headers.get('Access-Control-Allow-Headers')).toContain('Content-Type');
144+
expect(response.headers.get('Access-Control-Allow-Credentials')).toBe('true');
145+
});
146+
147+
it('should set CORS headers for requests with origin', async () => {
148+
const response = await app.fetch(new Request('http://localhost:4000/health', {
149+
headers: {
150+
'Origin': 'https://example.com',
151+
},
152+
}));
153+
154+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://example.com');
155+
expect(response.headers.get('Access-Control-Allow-Credentials')).toBe('true');
156+
});
157+
158+
it('should handle requests without origin', async () => {
159+
const response = await app.fetch(new Request('http://localhost:4000/health'));
160+
expect(response.status).toBe(200);
161+
// Should not crash when no origin is provided
162+
});
163+
164+
it('should include custom headers in CORS', async () => {
165+
const response = await app.fetch(new Request('http://localhost:4000/', {
166+
method: 'OPTIONS',
167+
headers: {
168+
'Origin': 'https://example.com',
169+
},
170+
}));
171+
172+
const allowedHeaders = response.headers.get('Access-Control-Allow-Headers');
173+
expect(allowedHeaders).toContain('databuddy-client-id');
174+
expect(allowedHeaders).toContain('databuddy-sdk-name');
175+
expect(allowedHeaders).toContain('databuddy-sdk-version');
176+
});
177+
});
178+
179+
describe('Error Handling', () => {
180+
it('should handle malformed requests gracefully', async () => {
181+
const response = await app.fetch(new Request('http://localhost:4000/', {
182+
method: 'POST',
183+
headers: {
184+
'Content-Type': 'application/json',
185+
},
186+
body: 'invalid json',
187+
}));
188+
189+
// Should not crash the server
190+
expect([200, 400, 500]).toContain(response.status);
191+
});
192+
193+
it('should handle requests to non-existent endpoints', async () => {
194+
const response = await app.fetch(new Request('http://localhost:4000/non-existent'));
195+
// Should handle gracefully without crashing
196+
expect([200, 404, 405]).toContain(response.status);
197+
});
198+
199+
it('should handle requests with invalid methods', async () => {
200+
const response = await app.fetch(new Request('http://localhost:4000/health', {
201+
method: 'DELETE',
202+
}));
203+
204+
expect([200, 404, 405]).toContain(response.status);
205+
});
206+
});
207+
208+
describe('Route Integration', () => {
209+
it('should have basket routes available', () => {
210+
expect(app).toBeDefined();
211+
expect(app.fetch).toBeDefined();
212+
});
213+
214+
it('should have email routes available', () => {
215+
expect(app).toBeDefined();
216+
expect(app.fetch).toBeDefined();
217+
});
218+
219+
it('should have stripe routes available', () => {
220+
expect(app).toBeDefined();
221+
expect(app.fetch).toBeDefined();
222+
});
223+
});
224+
225+
describe('Performance', () => {
226+
it('should respond quickly to health checks', async () => {
227+
const start = Date.now();
228+
const response = await app.fetch(new Request('http://localhost:4000/health'));
229+
const duration = Date.now() - start;
230+
231+
expect(response.status).toBe(200);
232+
expect(duration).toBeLessThan(100); // Should be very fast
233+
});
234+
235+
it('should handle concurrent requests', async () => {
236+
const requests = Array(10).fill(null).map(() =>
237+
app.fetch(new Request('http://localhost:4000/health'))
238+
);
239+
240+
const start = Date.now();
241+
const responses = await Promise.all(requests);
242+
const duration = Date.now() - start;
243+
244+
for (const response of responses) {
245+
expect(response.status).toBe(200);
246+
}
247+
248+
expect(duration).toBeLessThan(500); // Should handle 10 requests in under 500ms
249+
});
250+
251+
it('should handle rapid successive requests', async () => {
252+
const start = Date.now();
253+
254+
for (let i = 0; i < 5; i++) {
255+
const response = await app.fetch(new Request('http://localhost:4000/health'));
256+
expect(response.status).toBe(200);
257+
}
258+
259+
const duration = Date.now() - start;
260+
expect(duration).toBeLessThan(200); // Should handle 5 requests in under 200ms
261+
});
262+
});
263+
264+
describe('Middleware', () => {
265+
it('should apply error handling middleware', async () => {
266+
// Test that errors are caught and handled
267+
const response = await app.fetch(new Request('http://localhost:4000/', {
268+
method: 'POST',
269+
headers: {
270+
'Content-Type': 'application/json',
271+
},
272+
body: 'invalid json',
273+
}));
274+
275+
// Should not crash the server
276+
expect([200, 400, 500]).toContain(response.status);
277+
});
278+
279+
it('should apply CORS middleware', async () => {
280+
const response = await app.fetch(new Request('http://localhost:4000/health', {
281+
headers: {
282+
'Origin': 'https://example.com',
283+
},
284+
}));
285+
286+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://example.com');
287+
});
288+
289+
it('should handle preflight requests', async () => {
290+
const response = await app.fetch(new Request('http://localhost:4000/', {
291+
method: 'OPTIONS',
292+
headers: {
293+
'Origin': 'https://example.com',
294+
'Access-Control-Request-Method': 'POST',
295+
'Access-Control-Request-Headers': 'Content-Type',
296+
},
297+
}));
298+
299+
expect(response.status).toBe(204);
300+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://example.com');
301+
expect(response.headers.get('Access-Control-Allow-Methods')).toContain('POST');
302+
});
303+
});
304+
305+
describe('Environment Configuration', () => {
306+
it('should work with default port', () => {
307+
expect(app.port).toBe(4000);
308+
});
309+
310+
it('should handle missing environment variables gracefully', async () => {
311+
// Test that the app works even without optional env vars
312+
const response = await app.fetch(new Request('http://localhost:4000/health'));
313+
expect(response.status).toBe(200);
314+
});
315+
});
316+
317+
describe('Server Lifecycle', () => {
318+
it('should maintain state across requests', async () => {
319+
const response1 = await app.fetch(new Request('http://localhost:4000/health'));
320+
const response2 = await app.fetch(new Request('http://localhost:4000/health'));
321+
322+
expect(response1.status).toBe(200);
323+
expect(response2.status).toBe(200);
324+
325+
const data1 = await response1.json();
326+
const data2 = await response2.json();
327+
328+
expect(data1).toEqual(data2);
329+
});
330+
331+
it('should handle server restarts gracefully', async () => {
332+
// Test that the server can handle being "restarted"
333+
const response1 = await app.fetch(new Request('http://localhost:4000/health'));
334+
expect(response1.status).toBe(200);
335+
336+
// Simulate restart
337+
const response2 = await app.fetch(new Request('http://localhost:4000/health'));
338+
expect(response2.status).toBe(200);
339+
340+
const data1 = await response1.json();
341+
const data2 = await response2.json();
342+
expect(data1).toEqual(data2);
343+
});
344+
});
345+
});

0 commit comments

Comments
 (0)