@donkeylabs/server 0.3.0 → 0.3.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/LICENSE +1 -1
- package/docs/api-client.md +7 -7
- package/docs/cache.md +1 -74
- package/docs/core-services.md +4 -116
- package/docs/cron.md +1 -1
- package/docs/errors.md +2 -2
- package/docs/events.md +3 -98
- package/docs/handlers.md +13 -48
- package/docs/logger.md +3 -58
- package/docs/middleware.md +2 -2
- package/docs/plugins.md +13 -64
- package/docs/project-structure.md +4 -142
- package/docs/rate-limiter.md +4 -136
- package/docs/router.md +6 -14
- package/docs/sse.md +1 -99
- package/docs/sveltekit-adapter.md +420 -0
- package/package.json +6 -6
- package/registry.d.ts +15 -14
- package/src/core/cache.ts +0 -75
- package/src/core/cron.ts +3 -96
- package/src/core/errors.ts +78 -11
- package/src/core/events.ts +1 -47
- package/src/core/index.ts +0 -4
- package/src/core/jobs.ts +0 -112
- package/src/core/logger.ts +12 -79
- package/src/core/rate-limiter.ts +29 -108
- package/src/core/sse.ts +1 -84
- package/src/core.ts +13 -104
- package/src/generator/index.ts +551 -0
- package/src/handlers.ts +14 -110
- package/src/index.ts +19 -23
- package/src/middleware.ts +2 -5
- package/src/registry.ts +4 -0
- package/src/server.ts +354 -337
- package/README.md +0 -254
- package/cli/commands/dev.ts +0 -134
- package/cli/commands/generate.ts +0 -605
- package/cli/commands/init.ts +0 -205
- package/cli/commands/interactive.ts +0 -417
- package/cli/commands/plugin.ts +0 -192
- package/cli/commands/route.ts +0 -195
- package/cli/donkeylabs +0 -2
- package/cli/index.ts +0 -114
- package/docs/svelte-frontend.md +0 -324
- package/docs/testing.md +0 -438
- package/mcp/donkeylabs-mcp +0 -3238
- package/mcp/server.ts +0 -3238
package/docs/testing.md
DELETED
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
# Testing
|
|
2
|
-
|
|
3
|
-
Testing utilities for @donkeylabs/server plugins and routes.
|
|
4
|
-
|
|
5
|
-
## Test Harness
|
|
6
|
-
|
|
7
|
-
The framework provides `createTestHarness` for testing plugins with real services:
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
import { createTestHarness } from "@donkeylabs/server/harness";
|
|
11
|
-
import { myPlugin } from "./plugins/myPlugin";
|
|
12
|
-
import { depPlugin } from "./plugins/depPlugin";
|
|
13
|
-
|
|
14
|
-
const { manager, db, core } = await createTestHarness(myPlugin, [depPlugin]);
|
|
15
|
-
|
|
16
|
-
// Access plugin service
|
|
17
|
-
const service = manager.getServices().myPlugin;
|
|
18
|
-
|
|
19
|
-
// Use real in-memory database
|
|
20
|
-
await db.insertInto("users").values({ name: "Test" }).execute();
|
|
21
|
-
|
|
22
|
-
// Use real core services
|
|
23
|
-
core.logger.info("Test running");
|
|
24
|
-
await core.cache.set("key", "value");
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## What the Harness Provides
|
|
30
|
-
|
|
31
|
-
| Component | Description |
|
|
32
|
-
|-----------|-------------|
|
|
33
|
-
| In-memory SQLite | Real Kysely database, auto-runs migrations |
|
|
34
|
-
| Logger | Configured for `warn` level (less verbose) |
|
|
35
|
-
| Cache | Full in-memory cache service |
|
|
36
|
-
| Events | Pub/sub event system |
|
|
37
|
-
| Cron | Scheduled task service |
|
|
38
|
-
| Jobs | Background job queue |
|
|
39
|
-
| SSE | Server-sent events service |
|
|
40
|
-
| RateLimiter | Rate limiting service |
|
|
41
|
-
| Errors | Error factory functions |
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## When to Use
|
|
46
|
-
|
|
47
|
-
| Scenario | Use Harness | Use Manual Mock |
|
|
48
|
-
|----------|-------------|-----------------|
|
|
49
|
-
| Testing plugin service methods | Yes | |
|
|
50
|
-
| Testing database queries | Yes | |
|
|
51
|
-
| Testing core service integration | Yes | |
|
|
52
|
-
| Unit testing pure functions | | Yes |
|
|
53
|
-
| Testing HTTP endpoints | | Yes (real server) |
|
|
54
|
-
| Testing middleware | | Yes (mock request/response) |
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## Unit Tests
|
|
59
|
-
|
|
60
|
-
Test plugin services with the harness:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
import { describe, it, expect } from "bun:test";
|
|
64
|
-
import { createTestHarness } from "@donkeylabs/server/harness";
|
|
65
|
-
import { usersPlugin } from "../plugins/users";
|
|
66
|
-
|
|
67
|
-
describe("users plugin", () => {
|
|
68
|
-
it("creates a user", async () => {
|
|
69
|
-
const { manager, db } = await createTestHarness(usersPlugin);
|
|
70
|
-
const users = manager.getServices().users;
|
|
71
|
-
|
|
72
|
-
const user = await users.create("test@example.com", "Test User");
|
|
73
|
-
|
|
74
|
-
expect(user.email).toBe("test@example.com");
|
|
75
|
-
|
|
76
|
-
// Verify in database
|
|
77
|
-
const found = await db.selectFrom("users").selectAll().execute();
|
|
78
|
-
expect(found).toHaveLength(1);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("finds user by email", async () => {
|
|
82
|
-
const { manager, db } = await createTestHarness(usersPlugin);
|
|
83
|
-
const users = manager.getServices().users;
|
|
84
|
-
|
|
85
|
-
// Seed data directly
|
|
86
|
-
await db.insertInto("users").values({
|
|
87
|
-
email: "existing@example.com",
|
|
88
|
-
name: "Existing",
|
|
89
|
-
}).execute();
|
|
90
|
-
|
|
91
|
-
const found = await users.findByEmail("existing@example.com");
|
|
92
|
-
|
|
93
|
-
expect(found?.name).toBe("Existing");
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Testing Plugins with Dependencies
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
import { createTestHarness } from "@donkeylabs/server/harness";
|
|
104
|
-
import { notificationsPlugin } from "../plugins/notifications";
|
|
105
|
-
import { usersPlugin } from "../plugins/users";
|
|
106
|
-
import { emailPlugin } from "../plugins/email";
|
|
107
|
-
|
|
108
|
-
describe("notifications plugin", () => {
|
|
109
|
-
it("sends notification to user", async () => {
|
|
110
|
-
// notificationsPlugin depends on users and email
|
|
111
|
-
const { manager } = await createTestHarness(notificationsPlugin, [
|
|
112
|
-
usersPlugin,
|
|
113
|
-
emailPlugin({ apiKey: "test", fromAddress: "test@test.com", sandbox: true }),
|
|
114
|
-
]);
|
|
115
|
-
|
|
116
|
-
const notifications = manager.getServices().notifications;
|
|
117
|
-
|
|
118
|
-
// Dependencies are resolved and available
|
|
119
|
-
await notifications.notifyUser(1, "Hello!");
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## Testing with Events
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
import { createTestHarness } from "@donkeylabs/server/harness";
|
|
130
|
-
import { statsPlugin } from "../plugins/stats";
|
|
131
|
-
|
|
132
|
-
describe("stats plugin", () => {
|
|
133
|
-
it("records events", async () => {
|
|
134
|
-
const { manager, core } = await createTestHarness(statsPlugin);
|
|
135
|
-
const stats = manager.getServices().stats;
|
|
136
|
-
|
|
137
|
-
// Emit event that plugin listens to
|
|
138
|
-
core.events.emit("request.completed", {
|
|
139
|
-
route: "users.list",
|
|
140
|
-
duration: 50,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Give async handlers time to process
|
|
144
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
145
|
-
|
|
146
|
-
const summary = stats.getStats();
|
|
147
|
-
expect(summary.totalRequests).toBe(1);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## Integration Tests
|
|
155
|
-
|
|
156
|
-
For HTTP endpoint testing, run the actual server:
|
|
157
|
-
|
|
158
|
-
```ts
|
|
159
|
-
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
160
|
-
import { spawn } from "bun";
|
|
161
|
-
|
|
162
|
-
const BASE_URL = "http://localhost:3001";
|
|
163
|
-
let serverProcess: ReturnType<typeof spawn>;
|
|
164
|
-
|
|
165
|
-
beforeAll(async () => {
|
|
166
|
-
// Start server on test port
|
|
167
|
-
serverProcess = spawn({
|
|
168
|
-
cmd: ["bun", "run", "src/index.ts"],
|
|
169
|
-
env: { ...process.env, PORT: "3001" },
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Wait for server to be ready
|
|
173
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
afterAll(() => {
|
|
177
|
-
serverProcess.kill();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
describe("users API", () => {
|
|
181
|
-
it("POST /users.create returns user", async () => {
|
|
182
|
-
const res = await fetch(`${BASE_URL}/users.create`, {
|
|
183
|
-
method: "POST",
|
|
184
|
-
headers: { "Content-Type": "application/json" },
|
|
185
|
-
body: JSON.stringify({ email: "test@example.com", name: "Test" }),
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
expect(res.ok).toBe(true);
|
|
189
|
-
const user = await res.json();
|
|
190
|
-
expect(user.email).toBe("test@example.com");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("returns validation error for invalid input", async () => {
|
|
194
|
-
const res = await fetch(`${BASE_URL}/users.create`, {
|
|
195
|
-
method: "POST",
|
|
196
|
-
headers: { "Content-Type": "application/json" },
|
|
197
|
-
body: JSON.stringify({ email: "not-an-email" }),
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
expect(res.status).toBe(400);
|
|
201
|
-
const error = await res.json();
|
|
202
|
-
expect(error.error).toBe("Validation Failed");
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
|
-
## Testing Middleware
|
|
210
|
-
|
|
211
|
-
Test middleware in isolation:
|
|
212
|
-
|
|
213
|
-
```ts
|
|
214
|
-
import { describe, it, expect, vi } from "bun:test";
|
|
215
|
-
import { authMiddleware } from "./middleware/auth";
|
|
216
|
-
|
|
217
|
-
describe("auth middleware", () => {
|
|
218
|
-
it("rejects request without token when required", async () => {
|
|
219
|
-
const req = new Request("http://test", {
|
|
220
|
-
method: "POST",
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
const ctx = { user: undefined } as any;
|
|
224
|
-
const next = vi.fn();
|
|
225
|
-
|
|
226
|
-
const response = await authMiddleware.execute(
|
|
227
|
-
req,
|
|
228
|
-
ctx,
|
|
229
|
-
next,
|
|
230
|
-
{ required: true }
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
expect(response.status).toBe(401);
|
|
234
|
-
expect(next).not.toHaveBeenCalled();
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("allows request with valid token", async () => {
|
|
238
|
-
const req = new Request("http://test", {
|
|
239
|
-
method: "POST",
|
|
240
|
-
headers: { Authorization: "Bearer valid-token" },
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
const ctx = { user: undefined } as any;
|
|
244
|
-
const next = vi.fn().mockResolvedValue(Response.json({ ok: true }));
|
|
245
|
-
|
|
246
|
-
// Mock token validation
|
|
247
|
-
vi.mock("./auth-service", () => ({
|
|
248
|
-
verifyToken: vi.fn().mockResolvedValue({ id: 1, name: "Test" }),
|
|
249
|
-
}));
|
|
250
|
-
|
|
251
|
-
const response = await authMiddleware.execute(
|
|
252
|
-
req,
|
|
253
|
-
ctx,
|
|
254
|
-
next,
|
|
255
|
-
{ required: true }
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
expect(next).toHaveBeenCalled();
|
|
259
|
-
expect(ctx.user).toBeDefined();
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
---
|
|
265
|
-
|
|
266
|
-
## Testing Handlers
|
|
267
|
-
|
|
268
|
-
Test custom handlers directly:
|
|
269
|
-
|
|
270
|
-
```ts
|
|
271
|
-
import { describe, it, expect } from "bun:test";
|
|
272
|
-
import { EchoHandler } from "./handlers/echo";
|
|
273
|
-
|
|
274
|
-
describe("EchoHandler", () => {
|
|
275
|
-
it("echoes request body", async () => {
|
|
276
|
-
const req = new Request("http://test", {
|
|
277
|
-
method: "POST",
|
|
278
|
-
body: JSON.stringify({ hello: "world" }),
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
const mockHandle = async (body: any) => ({ echo: body });
|
|
282
|
-
const ctx = {} as any;
|
|
283
|
-
|
|
284
|
-
const response = await EchoHandler.execute(req, {}, mockHandle, ctx);
|
|
285
|
-
const json = await response.json();
|
|
286
|
-
|
|
287
|
-
expect(json).toEqual({ echo: { hello: "world" } });
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("handles invalid JSON", async () => {
|
|
291
|
-
const req = new Request("http://test", {
|
|
292
|
-
method: "POST",
|
|
293
|
-
body: "not json",
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
const mockHandle = async () => ({});
|
|
297
|
-
const ctx = {} as any;
|
|
298
|
-
|
|
299
|
-
const response = await EchoHandler.execute(req, {}, mockHandle, ctx);
|
|
300
|
-
|
|
301
|
-
expect(response.status).toBe(400);
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## Route File Organization
|
|
309
|
-
|
|
310
|
-
The starter project organizes tests alongside route handlers:
|
|
311
|
-
|
|
312
|
-
```
|
|
313
|
-
routes/<namespace>/<route>/
|
|
314
|
-
├── index.ts # Route definition
|
|
315
|
-
├── schema.ts # Zod schemas (Input, Output)
|
|
316
|
-
├── models/
|
|
317
|
-
│ └── model.ts # Handler class
|
|
318
|
-
└── tests/
|
|
319
|
-
├── unit.test.ts # Unit tests (harness)
|
|
320
|
-
└── integ.test.ts # Integration tests (real HTTP)
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
Example test structure:
|
|
324
|
-
|
|
325
|
-
```ts
|
|
326
|
-
// routes/health/ping/tests/unit.test.ts
|
|
327
|
-
import { describe, it, expect } from "bun:test";
|
|
328
|
-
import { PingModel } from "../models/model";
|
|
329
|
-
|
|
330
|
-
describe("PingModel", () => {
|
|
331
|
-
it("returns pong message", () => {
|
|
332
|
-
const ctx = {} as any; // Minimal mock
|
|
333
|
-
const model = new PingModel(ctx);
|
|
334
|
-
|
|
335
|
-
const result = model.handle({ message: "ping" });
|
|
336
|
-
|
|
337
|
-
expect(result.response).toBe("pong");
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
```ts
|
|
343
|
-
// routes/health/ping/tests/integ.test.ts
|
|
344
|
-
import { describe, it, expect } from "bun:test";
|
|
345
|
-
|
|
346
|
-
describe("health.ping endpoint", () => {
|
|
347
|
-
it("responds to ping", async () => {
|
|
348
|
-
const res = await fetch("http://localhost:3000/health.ping", {
|
|
349
|
-
method: "POST",
|
|
350
|
-
headers: { "Content-Type": "application/json" },
|
|
351
|
-
body: JSON.stringify({ message: "ping" }),
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
expect(res.ok).toBe(true);
|
|
355
|
-
const data = await res.json();
|
|
356
|
-
expect(data.response).toBe("pong");
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
---
|
|
362
|
-
|
|
363
|
-
## Best Practices
|
|
364
|
-
|
|
365
|
-
### 1. Use Harness for Plugin Tests
|
|
366
|
-
|
|
367
|
-
```ts
|
|
368
|
-
// Good - uses harness for real DB
|
|
369
|
-
const { manager, db } = await createTestHarness(usersPlugin);
|
|
370
|
-
const users = manager.getServices().users;
|
|
371
|
-
await users.create("test@test.com", "Test");
|
|
372
|
-
|
|
373
|
-
// Bad - manual mocking that may miss real behavior
|
|
374
|
-
const mockDb = { insertInto: vi.fn() };
|
|
375
|
-
const users = createUsersService({ db: mockDb });
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
### 2. Isolate Each Test
|
|
379
|
-
|
|
380
|
-
```ts
|
|
381
|
-
// Good - each test gets fresh harness
|
|
382
|
-
describe("users", () => {
|
|
383
|
-
it("test 1", async () => {
|
|
384
|
-
const { manager } = await createTestHarness(usersPlugin);
|
|
385
|
-
// ...
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("test 2", async () => {
|
|
389
|
-
const { manager } = await createTestHarness(usersPlugin);
|
|
390
|
-
// Clean slate, no data from test 1
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
### 3. Test Error Cases
|
|
396
|
-
|
|
397
|
-
```ts
|
|
398
|
-
it("throws on duplicate email", async () => {
|
|
399
|
-
const { manager, db } = await createTestHarness(usersPlugin);
|
|
400
|
-
const users = manager.getServices().users;
|
|
401
|
-
|
|
402
|
-
await users.create("test@test.com", "First");
|
|
403
|
-
|
|
404
|
-
await expect(users.create("test@test.com", "Second"))
|
|
405
|
-
.rejects.toThrow();
|
|
406
|
-
});
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### 4. Use Descriptive Test Names
|
|
410
|
-
|
|
411
|
-
```ts
|
|
412
|
-
// Good - describes scenario and expectation
|
|
413
|
-
it("returns empty array when no users exist", async () => {});
|
|
414
|
-
it("throws NotFound when user does not exist", async () => {});
|
|
415
|
-
it("updates only provided fields", async () => {});
|
|
416
|
-
|
|
417
|
-
// Bad - vague
|
|
418
|
-
it("works", async () => {});
|
|
419
|
-
it("handles edge case", async () => {});
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
---
|
|
423
|
-
|
|
424
|
-
## Running Tests
|
|
425
|
-
|
|
426
|
-
```sh
|
|
427
|
-
# Run all tests
|
|
428
|
-
bun test
|
|
429
|
-
|
|
430
|
-
# Run specific test file
|
|
431
|
-
bun test src/plugins/users/tests/unit.test.ts
|
|
432
|
-
|
|
433
|
-
# Run tests matching pattern
|
|
434
|
-
bun test --filter "users"
|
|
435
|
-
|
|
436
|
-
# Watch mode
|
|
437
|
-
bun test --watch
|
|
438
|
-
```
|