@flink-app/test-utils 0.12.1-alpha.4 → 0.12.1-alpha.40
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/dist/http.js +19 -5
- package/package.json +4 -4
- package/readme.md +554 -5
- package/src/http.ts +36 -10
package/dist/http.js
CHANGED
|
@@ -59,6 +59,7 @@ var gotOpts = {
|
|
|
59
59
|
throwHttpErrors: false,
|
|
60
60
|
json: true,
|
|
61
61
|
retry: 0,
|
|
62
|
+
followRedirect: false,
|
|
62
63
|
};
|
|
63
64
|
/**
|
|
64
65
|
* Initializes test flink app.
|
|
@@ -92,7 +93,7 @@ function get(path_1) {
|
|
|
92
93
|
exports.get = get;
|
|
93
94
|
function post(path_1, body_1) {
|
|
94
95
|
return __awaiter(this, arguments, void 0, function (path, body, opts) {
|
|
95
|
-
var headers, res, err_1;
|
|
96
|
+
var headers, isRawString, res, res, err_1;
|
|
96
97
|
if (opts === void 0) { opts = {}; }
|
|
97
98
|
return __generator(this, function (_a) {
|
|
98
99
|
switch (_a.label) {
|
|
@@ -103,16 +104,29 @@ function post(path_1, body_1) {
|
|
|
103
104
|
headers = _a.sent();
|
|
104
105
|
_a.label = 2;
|
|
105
106
|
case 2:
|
|
106
|
-
_a.trys.push([2,
|
|
107
|
-
|
|
107
|
+
_a.trys.push([2, 7, , 8]);
|
|
108
|
+
isRawString = typeof body === "string";
|
|
109
|
+
if (!isRawString) return [3 /*break*/, 4];
|
|
110
|
+
return [4 /*yield*/, got_1.default.post(getUrl(path, opts.qs), {
|
|
111
|
+
throwHttpErrors: false,
|
|
112
|
+
retry: 0,
|
|
113
|
+
followRedirect: false,
|
|
114
|
+
body: body,
|
|
115
|
+
headers: __assign(__assign({}, headers), { "Content-Type": "application/json" }),
|
|
116
|
+
})];
|
|
108
117
|
case 3:
|
|
118
|
+
res = _a.sent();
|
|
119
|
+
return [2 /*return*/, __assign({ status: res.statusCode }, JSON.parse(res.body || "{}"))];
|
|
120
|
+
case 4: return [4 /*yield*/, got_1.default.post(getUrl(path, opts.qs), __assign(__assign({}, gotOpts), { body: body, headers: headers }))];
|
|
121
|
+
case 5:
|
|
109
122
|
res = _a.sent();
|
|
110
123
|
return [2 /*return*/, __assign({ status: res.statusCode }, res.body)];
|
|
111
|
-
case
|
|
124
|
+
case 6: return [3 /*break*/, 8];
|
|
125
|
+
case 7:
|
|
112
126
|
err_1 = _a.sent();
|
|
113
127
|
console.error(err_1);
|
|
114
128
|
throw err_1;
|
|
115
|
-
case
|
|
129
|
+
case 8: return [2 /*return*/];
|
|
116
130
|
}
|
|
117
131
|
});
|
|
118
132
|
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/test-utils",
|
|
3
|
-
"version": "0.12.1-alpha.
|
|
3
|
+
"version": "0.12.1-alpha.40",
|
|
4
4
|
"description": "Test utils for Flink",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\"",
|
|
7
|
-
"
|
|
7
|
+
"prepare": "tsc",
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"watch": "tsc-watch"
|
|
10
10
|
},
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"qs": "^6.7.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@flink-app/flink": "^0.12.1-alpha.
|
|
23
|
+
"@flink-app/flink": "^0.12.1-alpha.40",
|
|
24
24
|
"@types/express": "^4.17.12",
|
|
25
25
|
"@types/got": "^9.6.12",
|
|
26
26
|
"@types/node": "22.13.10",
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"tsc-watch": "^4.2.9",
|
|
31
31
|
"typescript": "5.4.5"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "456502f273fe9473df05b71a803f3eda1a2f8931"
|
|
34
34
|
}
|
package/readme.md
CHANGED
|
@@ -1,13 +1,562 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flink-app/test-utils
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A comprehensive testing utility library for Flink applications. This package provides helper functions and mocks to simplify writing tests for your Flink handlers, repositories, and plugins.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- HTTP testing utilities with automatic JSON handling
|
|
8
|
+
- Mock request objects for unit testing handlers
|
|
9
|
+
- No-op authentication plugin for testing
|
|
10
|
+
- Type-safe test helpers
|
|
11
|
+
- Support for query strings, headers, and authentication
|
|
12
|
+
- Integration testing support
|
|
13
|
+
- Works with Jasmine, Jest, and other testing frameworks
|
|
4
14
|
|
|
5
15
|
## Installation
|
|
6
16
|
|
|
7
|
-
|
|
17
|
+
```bash
|
|
18
|
+
npm install --save-dev @flink-app/test-utils
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## HTTP Testing Utilities
|
|
22
|
+
|
|
23
|
+
The HTTP utilities allow you to make test requests to your running Flink app without needing external HTTP clients in most cases.
|
|
24
|
+
|
|
25
|
+
### Setup
|
|
26
|
+
|
|
27
|
+
Initialize the test utilities with your Flink app instance:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { FlinkApp } from "@flink-app/flink";
|
|
31
|
+
import * as http from "@flink-app/test-utils";
|
|
32
|
+
|
|
33
|
+
describe("My API", () => {
|
|
34
|
+
let app: FlinkApp<AppContext>;
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
app = new FlinkApp<AppContext>({
|
|
38
|
+
name: "Test App",
|
|
39
|
+
port: 3001,
|
|
40
|
+
// ... your configuration
|
|
41
|
+
});
|
|
42
|
+
await app.start();
|
|
8
43
|
|
|
44
|
+
// Initialize test utilities
|
|
45
|
+
http.init(app);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterAll(async () => {
|
|
49
|
+
await app.stop();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Your tests here
|
|
53
|
+
});
|
|
9
54
|
```
|
|
10
|
-
|
|
55
|
+
|
|
56
|
+
### GET Requests
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { get } from "@flink-app/test-utils";
|
|
60
|
+
|
|
61
|
+
it("should get all items", async () => {
|
|
62
|
+
const response = await get("/items");
|
|
63
|
+
|
|
64
|
+
expect(response.status).toBe(200);
|
|
65
|
+
expect(response.data).toBeDefined();
|
|
66
|
+
expect(response.data.items).toBeInstanceOf(Array);
|
|
67
|
+
});
|
|
11
68
|
```
|
|
12
69
|
|
|
13
|
-
|
|
70
|
+
### POST Requests
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { post } from "@flink-app/test-utils";
|
|
74
|
+
|
|
75
|
+
it("should create a new item", async () => {
|
|
76
|
+
const newItem = {
|
|
77
|
+
name: "Test Item",
|
|
78
|
+
description: "A test item",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const response = await post("/items", newItem);
|
|
82
|
+
|
|
83
|
+
expect(response.status).toBe(200);
|
|
84
|
+
expect(response.data.item._id).toBeDefined();
|
|
85
|
+
expect(response.data.item.name).toBe("Test Item");
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### PUT Requests
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { put } from "@flink-app/test-utils";
|
|
93
|
+
|
|
94
|
+
it("should update an item", async () => {
|
|
95
|
+
const updates = {
|
|
96
|
+
name: "Updated Name",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const response = await put("/items/123", updates);
|
|
100
|
+
|
|
101
|
+
expect(response.status).toBe(200);
|
|
102
|
+
expect(response.data.item.name).toBe("Updated Name");
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### DELETE Requests
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { del } from "@flink-app/test-utils";
|
|
110
|
+
|
|
111
|
+
it("should delete an item", async () => {
|
|
112
|
+
const response = await del("/items/123");
|
|
113
|
+
|
|
114
|
+
expect(response.status).toBe(200);
|
|
115
|
+
expect(response.success).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Request Options
|
|
120
|
+
|
|
121
|
+
All HTTP methods support an optional options object:
|
|
122
|
+
|
|
123
|
+
### Query String Parameters
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const response = await get("/items", {
|
|
127
|
+
qs: {
|
|
128
|
+
limit: "10",
|
|
129
|
+
offset: "0",
|
|
130
|
+
category: "electronics",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
// Requests: /items?limit=10&offset=0&category=electronics
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Custom Headers
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const response = await post("/items", newItem, {
|
|
140
|
+
headers: {
|
|
141
|
+
"X-Custom-Header": "value",
|
|
142
|
+
"X-Request-ID": "12345",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Authentication
|
|
148
|
+
|
|
149
|
+
The test utilities support automatic authentication using the app's auth plugin:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const user = {
|
|
153
|
+
_id: "user123",
|
|
154
|
+
username: "testuser",
|
|
155
|
+
roles: ["admin"],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const response = await get("/admin/users", {
|
|
159
|
+
user: user,
|
|
160
|
+
});
|
|
161
|
+
// Automatically adds: Authorization: Bearer <token>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Combining Options
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const response = await post(
|
|
168
|
+
"/items",
|
|
169
|
+
{ name: "New Item" },
|
|
170
|
+
{
|
|
171
|
+
qs: { draft: "true" },
|
|
172
|
+
headers: { "X-Request-ID": "123" },
|
|
173
|
+
user: currentUser,
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Mock Request Objects
|
|
179
|
+
|
|
180
|
+
For unit testing handlers without a running server:
|
|
181
|
+
|
|
182
|
+
### mockReq
|
|
183
|
+
|
|
184
|
+
Create a mock FlinkRequest object:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { mockReq } from "@flink-app/test-utils";
|
|
188
|
+
|
|
189
|
+
it("should handle request correctly", async () => {
|
|
190
|
+
const req = mockReq({
|
|
191
|
+
body: { name: "Test" },
|
|
192
|
+
params: { id: "123" },
|
|
193
|
+
query: { include: "details" },
|
|
194
|
+
headers: { "content-type": "application/json" },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const result = await myHandler(req, ctx);
|
|
198
|
+
|
|
199
|
+
expect(result.status).toBe(200);
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Mock Request Properties
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const req = mockReq<RequestBody, Params, Query>({
|
|
207
|
+
body: { name: "Test" }, // Request body
|
|
208
|
+
params: { id: "123" }, // URL parameters
|
|
209
|
+
query: { page: "1" }, // Query string
|
|
210
|
+
headers: { // HTTP headers
|
|
211
|
+
"content-type": "application/json",
|
|
212
|
+
"authorization": "Bearer token",
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Mock request automatically provides:
|
|
217
|
+
// - req.get(headerName) method
|
|
218
|
+
// - JSON serialization of body/params
|
|
219
|
+
// - Default empty objects for omitted properties
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Authentication Mocking
|
|
223
|
+
|
|
224
|
+
### noOpAuthPlugin
|
|
225
|
+
|
|
226
|
+
A no-op authentication plugin that allows all requests:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { noOpAuthPlugin } from "@flink-app/test-utils";
|
|
230
|
+
|
|
231
|
+
const app = new FlinkApp<AppContext>({
|
|
232
|
+
name: "Test App",
|
|
233
|
+
port: 3001,
|
|
234
|
+
plugins: [],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Set the no-op auth plugin for testing
|
|
238
|
+
app.auth = noOpAuthPlugin();
|
|
239
|
+
|
|
240
|
+
await app.start();
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
This plugin:
|
|
244
|
+
- Always authenticates successfully
|
|
245
|
+
- Returns a mock token: `"mock-token"`
|
|
246
|
+
- Useful for testing handlers without setting up real authentication
|
|
247
|
+
|
|
248
|
+
## Complete Testing Example
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { FlinkApp } from "@flink-app/flink";
|
|
252
|
+
import * as http from "@flink-app/test-utils";
|
|
253
|
+
import { mockReq, noOpAuthPlugin } from "@flink-app/test-utils";
|
|
254
|
+
|
|
255
|
+
describe("Todo API", () => {
|
|
256
|
+
let app: FlinkApp<AppContext>;
|
|
257
|
+
|
|
258
|
+
beforeAll(async () => {
|
|
259
|
+
app = new FlinkApp<AppContext>({
|
|
260
|
+
name: "Test App",
|
|
261
|
+
port: 3001,
|
|
262
|
+
mongo: {
|
|
263
|
+
url: "mongodb://localhost:27017/test",
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
app.auth = noOpAuthPlugin();
|
|
268
|
+
await app.start();
|
|
269
|
+
http.init(app);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
afterAll(async () => {
|
|
273
|
+
await app.stop();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("GET /todos", () => {
|
|
277
|
+
it("should return all todos", async () => {
|
|
278
|
+
const response = await http.get("/todos");
|
|
279
|
+
|
|
280
|
+
expect(response.status).toBe(200);
|
|
281
|
+
expect(response.data.todos).toBeInstanceOf(Array);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should filter todos by status", async () => {
|
|
285
|
+
const response = await http.get("/todos", {
|
|
286
|
+
qs: { status: "completed" },
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(response.status).toBe(200);
|
|
290
|
+
expect(response.data.todos.every((t) => t.status === "completed")).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("POST /todos", () => {
|
|
295
|
+
it("should create a new todo", async () => {
|
|
296
|
+
const newTodo = {
|
|
297
|
+
title: "Test Todo",
|
|
298
|
+
description: "Test Description",
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const response = await http.post("/todos", newTodo);
|
|
302
|
+
|
|
303
|
+
expect(response.status).toBe(200);
|
|
304
|
+
expect(response.data.todo._id).toBeDefined();
|
|
305
|
+
expect(response.data.todo.title).toBe("Test Todo");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should require authentication", async () => {
|
|
309
|
+
const response = await http.post("/todos", {
|
|
310
|
+
title: "Test",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// With noOpAuthPlugin, this would pass
|
|
314
|
+
// With real auth, this would return 401
|
|
315
|
+
expect(response.status).toBe(200);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should validate required fields", async () => {
|
|
319
|
+
const response = await http.post("/todos", {});
|
|
320
|
+
|
|
321
|
+
expect(response.status).toBe(400);
|
|
322
|
+
expect(response.error).toBe("validation_error");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("PUT /todos/:id", () => {
|
|
327
|
+
let todoId: string;
|
|
328
|
+
|
|
329
|
+
beforeEach(async () => {
|
|
330
|
+
const created = await http.post("/todos", {
|
|
331
|
+
title: "Todo to Update",
|
|
332
|
+
});
|
|
333
|
+
todoId = created.data.todo._id;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should update a todo", async () => {
|
|
337
|
+
const response = await http.put(`/todos/${todoId}`, {
|
|
338
|
+
title: "Updated Title",
|
|
339
|
+
status: "completed",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(response.status).toBe(200);
|
|
343
|
+
expect(response.data.todo.title).toBe("Updated Title");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe("DELETE /todos/:id", () => {
|
|
348
|
+
it("should delete a todo", async () => {
|
|
349
|
+
const created = await http.post("/todos", {
|
|
350
|
+
title: "Todo to Delete",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const response = await http.del(`/todos/${created.data.todo._id}`);
|
|
354
|
+
|
|
355
|
+
expect(response.status).toBe(200);
|
|
356
|
+
expect(response.success).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Unit Testing Handlers
|
|
363
|
+
|
|
364
|
+
Test individual handlers without a running server:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { mockReq } from "@flink-app/test-utils";
|
|
368
|
+
import GetTodoById from "./handlers/GetTodoById";
|
|
369
|
+
|
|
370
|
+
describe("GetTodoById Handler", () => {
|
|
371
|
+
let mockContext: AppContext;
|
|
372
|
+
|
|
373
|
+
beforeEach(() => {
|
|
374
|
+
mockContext = {
|
|
375
|
+
repos: {
|
|
376
|
+
todoRepo: {
|
|
377
|
+
findById: jasmine.createSpy("findById"),
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
} as any;
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should return todo when found", async () => {
|
|
384
|
+
const mockTodo = {
|
|
385
|
+
_id: "123",
|
|
386
|
+
title: "Test Todo",
|
|
387
|
+
status: "active",
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
(mockContext.repos.todoRepo.findById as any).and.returnValue(
|
|
391
|
+
Promise.resolve(mockTodo)
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const req = mockReq({
|
|
395
|
+
params: { id: "123" },
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const result = await GetTodoById(req, mockContext);
|
|
399
|
+
|
|
400
|
+
expect(result.status).toBe(200);
|
|
401
|
+
expect(result.data.todo).toEqual(mockTodo);
|
|
402
|
+
expect(mockContext.repos.todoRepo.findById).toHaveBeenCalledWith("123");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should return 404 when not found", async () => {
|
|
406
|
+
(mockContext.repos.todoRepo.findById as any).and.returnValue(
|
|
407
|
+
Promise.resolve(null)
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const req = mockReq({
|
|
411
|
+
params: { id: "999" },
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const result = await GetTodoById(req, mockContext);
|
|
415
|
+
|
|
416
|
+
expect(result.status).toBe(404);
|
|
417
|
+
expect(result.error).toBe("not_found");
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Response Type
|
|
423
|
+
|
|
424
|
+
All HTTP methods return a `FlinkResponse` object:
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
interface FlinkResponse<T> {
|
|
428
|
+
status: number; // HTTP status code
|
|
429
|
+
success?: boolean; // Success flag (if present in response)
|
|
430
|
+
data?: T; // Response data (if present)
|
|
431
|
+
error?: string; // Error code (if error occurred)
|
|
432
|
+
message?: string; // Error message (if error occurred)
|
|
433
|
+
// ... any other fields from the response
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## TypeScript Support
|
|
438
|
+
|
|
439
|
+
The test utilities are fully typed:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
interface CreateTodoRequest {
|
|
443
|
+
title: string;
|
|
444
|
+
description?: string;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
interface CreateTodoResponse {
|
|
448
|
+
todo: {
|
|
449
|
+
_id: string;
|
|
450
|
+
title: string;
|
|
451
|
+
description?: string;
|
|
452
|
+
createdAt: string;
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const response = await post<CreateTodoRequest, CreateTodoResponse>(
|
|
457
|
+
"/todos",
|
|
458
|
+
{ title: "New Todo" }
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// response.data is typed as CreateTodoResponse
|
|
462
|
+
expect(response.data?.todo._id).toBeDefined();
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Best Practices
|
|
466
|
+
|
|
467
|
+
1. **Initialize once**: Call `http.init(app)` once in `beforeAll`, not before each test
|
|
468
|
+
2. **Clean up**: Always call `app.stop()` in `afterAll`
|
|
469
|
+
3. **Use separate database**: Use a test database, not your production database
|
|
470
|
+
4. **Reset data**: Clear test data between tests if needed
|
|
471
|
+
5. **Mock external services**: Use mocks for external API calls
|
|
472
|
+
6. **Test error cases**: Don't just test happy paths
|
|
473
|
+
7. **Use authentication**: Test both authenticated and unauthenticated scenarios
|
|
474
|
+
|
|
475
|
+
## Working with Different Test Frameworks
|
|
476
|
+
|
|
477
|
+
### Jasmine
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
describe("My tests", () => {
|
|
481
|
+
let app: FlinkApp<AppContext>;
|
|
482
|
+
|
|
483
|
+
beforeAll(async () => {
|
|
484
|
+
app = createTestApp();
|
|
485
|
+
await app.start();
|
|
486
|
+
http.init(app);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
afterAll(async () => {
|
|
490
|
+
await app.stop();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("should work", async () => {
|
|
494
|
+
const response = await http.get("/endpoint");
|
|
495
|
+
expect(response.status).toBe(200);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Jest
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
describe("My tests", () => {
|
|
504
|
+
let app: FlinkApp<AppContext>;
|
|
505
|
+
|
|
506
|
+
beforeAll(async () => {
|
|
507
|
+
app = createTestApp();
|
|
508
|
+
await app.start();
|
|
509
|
+
http.init(app);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
afterAll(async () => {
|
|
513
|
+
await app.stop();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("should work", async () => {
|
|
517
|
+
const response = await http.get("/endpoint");
|
|
518
|
+
expect(response.status).toBe(200);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## Troubleshooting
|
|
524
|
+
|
|
525
|
+
### Port Already in Use
|
|
526
|
+
|
|
527
|
+
Use a different port for testing:
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
const app = new FlinkApp<AppContext>({
|
|
531
|
+
port: 3001, // Different from dev/prod port
|
|
532
|
+
// ...
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Tests Hanging
|
|
537
|
+
|
|
538
|
+
Make sure to call `app.stop()` in `afterAll`:
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
afterAll(async () => {
|
|
542
|
+
await app.stop();
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Authentication Errors
|
|
547
|
+
|
|
548
|
+
Use `noOpAuthPlugin()` for testing or provide valid user objects:
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
app.auth = noOpAuthPlugin();
|
|
552
|
+
|
|
553
|
+
// OR
|
|
554
|
+
|
|
555
|
+
const response = await http.get("/endpoint", {
|
|
556
|
+
user: { _id: "123", roles: ["admin"] },
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
## License
|
|
561
|
+
|
|
562
|
+
MIT
|
package/src/http.ts
CHANGED
|
@@ -9,6 +9,7 @@ const gotOpts: GotJSONOptions = {
|
|
|
9
9
|
throwHttpErrors: false,
|
|
10
10
|
json: true,
|
|
11
11
|
retry: 0,
|
|
12
|
+
followRedirect: false,
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export type HttpOpts = {
|
|
@@ -59,16 +60,41 @@ export async function post<Req = any, Res = any>(path: string, body: Req, opts:
|
|
|
59
60
|
const headers = await setAuthHeader(opts.user, opts.headers);
|
|
60
61
|
|
|
61
62
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
// If body is a string, send it as raw body without JSON encoding
|
|
64
|
+
// This is needed for webhook signature validation where the signature
|
|
65
|
+
// is computed on the raw JSON string
|
|
66
|
+
const isRawString = typeof body === "string";
|
|
67
|
+
|
|
68
|
+
if (isRawString) {
|
|
69
|
+
// Send raw string body
|
|
70
|
+
const res = await got.post(getUrl(path, opts.qs), {
|
|
71
|
+
throwHttpErrors: false,
|
|
72
|
+
retry: 0,
|
|
73
|
+
followRedirect: false,
|
|
74
|
+
body: body as any,
|
|
75
|
+
headers: {
|
|
76
|
+
...headers,
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
status: res.statusCode,
|
|
83
|
+
...JSON.parse((res.body as any) || "{}"),
|
|
84
|
+
};
|
|
85
|
+
} else {
|
|
86
|
+
// Send JSON body
|
|
87
|
+
const res = await got.post(getUrl(path, opts.qs), {
|
|
88
|
+
...gotOpts,
|
|
89
|
+
body: body as any,
|
|
90
|
+
headers,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
status: res.statusCode,
|
|
95
|
+
...res.body,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
72
98
|
} catch (err) {
|
|
73
99
|
console.error(err);
|
|
74
100
|
throw err;
|