@flink-app/test-utils 0.12.1-alpha.7 → 0.13.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @flink-app/test-utils
2
+
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Migrate to pnpm and streamlines build process.
package/dist/http.js CHANGED
@@ -20,8 +20,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
20
20
  });
21
21
  };
22
22
  var __generator = (this && this.__generator) || function (thisArg, body) {
23
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
24
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
25
  function verb(n) { return function (v) { return step([n, v]); }; }
26
26
  function step(op) {
27
27
  if (f) throw new TypeError("Generator is already executing.");
@@ -50,7 +50,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
50
50
  return (mod && mod.__esModule) ? mod : { "default": mod };
51
51
  };
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
- exports.del = exports.put = exports.post = exports.get = exports.init = void 0;
53
+ exports.init = init;
54
+ exports.get = get;
55
+ exports.post = post;
56
+ exports.put = put;
57
+ exports.del = del;
54
58
  var got_1 = __importDefault(require("got"));
55
59
  var qs_1 = __importDefault(require("qs"));
56
60
  var app;
@@ -59,6 +63,7 @@ var gotOpts = {
59
63
  throwHttpErrors: false,
60
64
  json: true,
61
65
  retry: 0,
66
+ followRedirect: false,
62
67
  };
63
68
  /**
64
69
  * Initializes test flink app.
@@ -69,7 +74,6 @@ function init(_app, host) {
69
74
  app = _app;
70
75
  baseUrl = "http://".concat(host, ":").concat(app.port);
71
76
  }
72
- exports.init = init;
73
77
  function get(path_1) {
74
78
  return __awaiter(this, arguments, void 0, function (path, opts) {
75
79
  var headers, res;
@@ -89,10 +93,9 @@ function get(path_1) {
89
93
  });
90
94
  });
91
95
  }
92
- exports.get = get;
93
96
  function post(path_1, body_1) {
94
97
  return __awaiter(this, arguments, void 0, function (path, body, opts) {
95
- var headers, res, err_1;
98
+ var headers, isRawString, res, res, err_1;
96
99
  if (opts === void 0) { opts = {}; }
97
100
  return __generator(this, function (_a) {
98
101
  switch (_a.label) {
@@ -103,21 +106,33 @@ function post(path_1, body_1) {
103
106
  headers = _a.sent();
104
107
  _a.label = 2;
105
108
  case 2:
106
- _a.trys.push([2, 4, , 5]);
107
- return [4 /*yield*/, got_1.default.post(getUrl(path, opts.qs), __assign(__assign({}, gotOpts), { body: body, headers: headers }))];
109
+ _a.trys.push([2, 7, , 8]);
110
+ isRawString = typeof body === "string";
111
+ if (!isRawString) return [3 /*break*/, 4];
112
+ return [4 /*yield*/, got_1.default.post(getUrl(path, opts.qs), {
113
+ throwHttpErrors: false,
114
+ retry: 0,
115
+ followRedirect: false,
116
+ body: body,
117
+ headers: __assign(__assign({}, headers), { "Content-Type": "application/json" }),
118
+ })];
108
119
  case 3:
120
+ res = _a.sent();
121
+ return [2 /*return*/, __assign({ status: res.statusCode }, JSON.parse(res.body || "{}"))];
122
+ case 4: return [4 /*yield*/, got_1.default.post(getUrl(path, opts.qs), __assign(__assign({}, gotOpts), { body: body, headers: headers }))];
123
+ case 5:
109
124
  res = _a.sent();
110
125
  return [2 /*return*/, __assign({ status: res.statusCode }, res.body)];
111
- case 4:
126
+ case 6: return [3 /*break*/, 8];
127
+ case 7:
112
128
  err_1 = _a.sent();
113
129
  console.error(err_1);
114
130
  throw err_1;
115
- case 5: return [2 /*return*/];
131
+ case 8: return [2 /*return*/];
116
132
  }
117
133
  });
118
134
  });
119
135
  }
120
- exports.post = post;
121
136
  function put(path_1, body_1) {
122
137
  return __awaiter(this, arguments, void 0, function (path, body, opts) {
123
138
  var headers, res;
@@ -137,7 +152,6 @@ function put(path_1, body_1) {
137
152
  });
138
153
  });
139
154
  }
140
- exports.put = put;
141
155
  function del(path_1) {
142
156
  return __awaiter(this, arguments, void 0, function (path, opts) {
143
157
  var headers, res;
@@ -157,7 +171,6 @@ function del(path_1) {
157
171
  });
158
172
  });
159
173
  }
160
- exports.del = del;
161
174
  function validateApp() {
162
175
  if (!app) {
163
176
  throw new Error("App not initialized, run `init(app)` prior to invoking get/post/put/del");
package/dist/mocks.js CHANGED
@@ -20,8 +20,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
20
20
  });
21
21
  };
22
22
  var __generator = (this && this.__generator) || function (thisArg, body) {
23
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
24
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
25
  function verb(n) { return function (v) { return step([n, v]); }; }
26
26
  function step(op) {
27
27
  if (f) throw new TypeError("Generator is already executing.");
@@ -47,7 +47,8 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
47
47
  }
48
48
  };
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.noOpAuthPlugin = exports.mockReq = void 0;
50
+ exports.mockReq = mockReq;
51
+ exports.noOpAuthPlugin = noOpAuthPlugin;
51
52
  /**
52
53
  * Creates a mocked FlinkRequest object where only essential properties are required.
53
54
  * Will convert req body to JSON to ensure the body is a plain object.
@@ -64,7 +65,6 @@ function mockReq(req) {
64
65
  req.params = req.params ? JSON.parse(JSON.stringify(req.params)) : {};
65
66
  return aMockReq;
66
67
  }
67
- exports.mockReq = mockReq;
68
68
  /**
69
69
  * Auth plugin used for testing handlers.
70
70
  * Will allow all requests.
@@ -81,4 +81,3 @@ function noOpAuthPlugin() {
81
81
  }); }); },
82
82
  };
83
83
  }
84
- exports.noOpAuthPlugin = noOpAuthPlugin;
package/package.json CHANGED
@@ -1,34 +1,31 @@
1
1
  {
2
- "name": "@flink-app/test-utils",
3
- "version": "0.12.1-alpha.7",
4
- "description": "Test utils for Flink",
5
- "scripts": {
6
- "test": "echo \"Error: no test specified\"",
7
- "prepublish": "tsc",
8
- "build": "tsc",
9
- "watch": "tsc-watch"
10
- },
11
- "author": "joel@frost.se",
12
- "license": "MIT",
13
- "types": "dist/index.d.ts",
14
- "main": "dist/index.js",
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
- "dependencies": {
19
- "got": "^9.6.0",
20
- "qs": "^6.7.0"
21
- },
22
- "devDependencies": {
23
- "@flink-app/flink": "^0.12.1-alpha.7",
24
- "@types/express": "^4.17.12",
25
- "@types/got": "^9.6.12",
26
- "@types/node": "22.13.10",
27
- "jasmine-spec-reporter": "^7.0.0",
28
- "nodemon": "^2.0.7",
29
- "ts-node": "^9.1.1",
30
- "tsc-watch": "^4.2.9",
31
- "typescript": "5.4.5"
32
- },
33
- "gitHead": "cd86dec5f9b3bf23c01374a729bb02d8a48c2869"
34
- }
2
+ "name": "@flink-app/test-utils",
3
+ "version": "0.13.0",
4
+ "description": "Test utils for Flink",
5
+ "author": "joel@frost.se",
6
+ "license": "MIT",
7
+ "types": "dist/index.d.ts",
8
+ "main": "dist/index.js",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "dependencies": {
13
+ "got": "^9.6.0",
14
+ "qs": "^6.7.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/got": "^9.6.12",
18
+ "@types/node": "22.13.10",
19
+ "@types/qs": "^6.9.7",
20
+ "ts-node": "^10.9.2",
21
+ "tsc-watch": "^4.2.9",
22
+ "@flink-app/flink": "0.13.0"
23
+ },
24
+ "gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
25
+ "scripts": {
26
+ "test": "echo \"Error: no test specified\"",
27
+ "build": "tsc",
28
+ "watch": "tsc-watch",
29
+ "clean": "rimraf dist .flink"
30
+ }
31
+ }
package/readme.md CHANGED
@@ -1,13 +1,562 @@
1
- # Flink test utils
1
+ # @flink-app/test-utils
2
2
 
3
- A helper library to simplify writing tests for a Flink app.
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
- Install plugin to your flink app project:
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
- npm i -D @flink-app/test-utils
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
- Read docs for more details.
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
- const res = await got.post(getUrl(path, opts.qs), {
63
- ...gotOpts,
64
- body: body as any,
65
- headers,
66
- });
67
-
68
- return {
69
- status: res.statusCode,
70
- ...res.body,
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;
package/tsconfig.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "noEmit": false,
16
16
  "declaration": true,
17
17
  "experimentalDecorators": true,
18
- "checkJs": true,
18
+ "checkJs": false,
19
19
  "outDir": "dist"
20
20
  },
21
21
  "include": ["./src/**/*.ts", "./.flink/**/*.ts"],