@thalorlabs/api-test-suite 1.0.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/.prettierrc ADDED
@@ -0,0 +1 @@
1
+ "@thalorlabs/eslint-plugin-dev-config/prettier"
package/CLAUDE.md ADDED
@@ -0,0 +1,22 @@
1
+ # @thalorlabs/api-test-suite
2
+
3
+ ## What this repo is
4
+
5
+ Standardised integration test suite for ThalorLabs microservices. Provides `runStandardApiTests()` which generates the 90% of tests every MS needs — routing, auth, health, error contract.
6
+
7
+ ## Build & publish
8
+
9
+ ```bash
10
+ npm run build # tsc → dist/
11
+ npm version patch # or minor/major
12
+ npm publish --access public
13
+ ```
14
+
15
+ ## What it tests
16
+
17
+ - Route exists and returns expected status with valid auth
18
+ - Unknown routes return 404
19
+ - Missing/wrong/empty API key returns 401
20
+ - GET /health and GET /alive return 200 without auth
21
+ - Error responses include status field, no stack traces leaked
22
+ - Response is JSON
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @thalorlabs/api-test-suite
2
+
3
+ Standardised integration tests for ThalorLabs microservices. One function call gives you the 90% of tests every MS needs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -D @thalorlabs/api-test-suite
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { runStandardApiTests } from '@thalorlabs/api-test-suite';
15
+ import { app } from '../src/index';
16
+
17
+ runStandardApiTests({
18
+ app,
19
+ route: '/api/v1/jokes',
20
+ method: 'GET',
21
+ validApiKey: 'test-api-key',
22
+ });
23
+ ```
24
+
25
+ ## What it tests
26
+
27
+ | Category | Tests |
28
+ |----------|-------|
29
+ | **Routing** | Route returns expected status, unknown routes return 404, response is JSON |
30
+ | **Authentication** | Missing key → 401, wrong key → 401, empty key → 401 |
31
+ | **Health** | GET /health returns 200 without auth, GET /alive returns 200 |
32
+ | **Error contract** | Error responses include status field, no stack traces leaked |
33
+
34
+ ## Config
35
+
36
+ | Option | Type | Default | Description |
37
+ |--------|------|---------|-------------|
38
+ | `app` | Express | required | The Express app instance |
39
+ | `route` | string | required | Route to test (e.g. '/api/v1/jokes') |
40
+ | `method` | string | 'GET' | HTTP method |
41
+ | `validApiKey` | string | required | Valid API key for auth tests |
42
+ | `requiresAuth` | boolean | true | Whether the route requires auth |
43
+ | `expectedStatus` | number | 200 | Expected success status code |
@@ -0,0 +1,2 @@
1
+ export { runStandardApiTests } from './standardTests';
2
+ export type { StandardApiTestConfig } from './standardTests';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runStandardApiTests = void 0;
4
+ var standardTests_1 = require("./standardTests");
5
+ Object.defineProperty(exports, "runStandardApiTests", { enumerable: true, get: function () { return standardTests_1.runStandardApiTests; } });
@@ -0,0 +1,40 @@
1
+ import { Express } from 'express';
2
+ export interface StandardApiTestConfig {
3
+ /** The Express app instance */
4
+ app: Express;
5
+ /** The route to test (e.g. '/api/v1/jokes') */
6
+ route: string;
7
+ /** HTTP method (default: 'GET') */
8
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
9
+ /** Valid API key for auth tests */
10
+ validApiKey: string;
11
+ /** Whether the route requires auth (default: true) */
12
+ requiresAuth?: boolean;
13
+ /** Expected success status code (default: 200) */
14
+ expectedStatus?: number;
15
+ }
16
+ /**
17
+ * Runs standardised integration tests for a ThalorLabs microservice route.
18
+ *
19
+ * Covers:
20
+ * - Route exists and returns expected status
21
+ * - Authentication (missing key, wrong key)
22
+ * - Unknown routes return 404
23
+ * - Health endpoint returns 200 without auth
24
+ * - Response is JSON
25
+ * - Error responses don't leak stack traces
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { runStandardApiTests } from '@thalorlabs/api-test-suite';
30
+ * import { app } from '../src/index';
31
+ *
32
+ * runStandardApiTests({
33
+ * app,
34
+ * route: '/api/v1/jokes',
35
+ * method: 'GET',
36
+ * validApiKey: 'test-key',
37
+ * });
38
+ * ```
39
+ */
40
+ export declare function runStandardApiTests(config: StandardApiTestConfig): void;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runStandardApiTests = runStandardApiTests;
7
+ const supertest_1 = __importDefault(require("supertest"));
8
+ function makeRequest(app, method, route) {
9
+ switch (method) {
10
+ case 'POST':
11
+ return (0, supertest_1.default)(app).post(route);
12
+ case 'PUT':
13
+ return (0, supertest_1.default)(app).put(route);
14
+ case 'PATCH':
15
+ return (0, supertest_1.default)(app).patch(route);
16
+ case 'DELETE':
17
+ return (0, supertest_1.default)(app).delete(route);
18
+ default:
19
+ return (0, supertest_1.default)(app).get(route);
20
+ }
21
+ }
22
+ /**
23
+ * Runs standardised integration tests for a ThalorLabs microservice route.
24
+ *
25
+ * Covers:
26
+ * - Route exists and returns expected status
27
+ * - Authentication (missing key, wrong key)
28
+ * - Unknown routes return 404
29
+ * - Health endpoint returns 200 without auth
30
+ * - Response is JSON
31
+ * - Error responses don't leak stack traces
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * import { runStandardApiTests } from '@thalorlabs/api-test-suite';
36
+ * import { app } from '../src/index';
37
+ *
38
+ * runStandardApiTests({
39
+ * app,
40
+ * route: '/api/v1/jokes',
41
+ * method: 'GET',
42
+ * validApiKey: 'test-key',
43
+ * });
44
+ * ```
45
+ */
46
+ function runStandardApiTests(config) {
47
+ const { app, route, method = 'GET', validApiKey, requiresAuth = true, expectedStatus = 200, } = config;
48
+ describe(`Standard API Tests — ${method} ${route}`, () => {
49
+ // Routing
50
+ describe('Routing', () => {
51
+ it(`${method} ${route} returns ${expectedStatus} with valid auth`, async () => {
52
+ const res = await makeRequest(app, method, route).set('x-api-key', validApiKey);
53
+ expect(res.status).toBe(expectedStatus);
54
+ });
55
+ it('unknown route returns 404', async () => {
56
+ const res = await (0, supertest_1.default)(app)
57
+ .get('/api/v1/this-route-does-not-exist')
58
+ .set('x-api-key', validApiKey);
59
+ expect(res.status).toBe(404);
60
+ });
61
+ it('response is JSON', async () => {
62
+ const res = await makeRequest(app, method, route).set('x-api-key', validApiKey);
63
+ expect(res.headers['content-type']).toMatch(/json/);
64
+ });
65
+ });
66
+ // Authentication
67
+ if (requiresAuth) {
68
+ describe('Authentication', () => {
69
+ it('returns 401 without API key', async () => {
70
+ const res = await makeRequest(app, method, route);
71
+ expect(res.status).toBe(401);
72
+ });
73
+ it('returns 401 with wrong API key', async () => {
74
+ const res = await makeRequest(app, method, route).set('x-api-key', 'wrong-key-that-should-fail');
75
+ expect(res.status).toBe(401);
76
+ });
77
+ it('returns 401 with empty API key', async () => {
78
+ const res = await makeRequest(app, method, route).set('x-api-key', '');
79
+ expect(res.status).toBe(401);
80
+ });
81
+ });
82
+ }
83
+ // Health endpoint
84
+ describe('Health', () => {
85
+ it('GET /health returns 200 without auth', async () => {
86
+ const res = await (0, supertest_1.default)(app).get('/health');
87
+ expect(res.status).toBe(200);
88
+ expect(res.body).toHaveProperty('status');
89
+ });
90
+ it('GET /alive returns 200 without auth', async () => {
91
+ const res = await (0, supertest_1.default)(app).get('/alive');
92
+ expect(res.status).toBe(200);
93
+ expect(res.body).toHaveProperty('status', 'alive');
94
+ });
95
+ });
96
+ // Error response contract
97
+ describe('Error response contract', () => {
98
+ it('error responses include status field', async () => {
99
+ const res = await makeRequest(app, method, route);
100
+ if (res.status >= 400) {
101
+ expect(res.body).toHaveProperty('status');
102
+ }
103
+ });
104
+ it('error responses do not leak stack traces', async () => {
105
+ const res = await makeRequest(app, method, route);
106
+ if (res.status >= 400) {
107
+ const body = JSON.stringify(res.body);
108
+ expect(body).not.toContain('at Object.');
109
+ expect(body).not.toContain('at Module.');
110
+ expect(body).not.toContain('node_modules');
111
+ expect(body).not.toContain('.ts:');
112
+ }
113
+ });
114
+ });
115
+ });
116
+ }
@@ -0,0 +1,3 @@
1
+ import { backend } from '@thalorlabs/eslint-plugin-dev-config';
2
+
3
+ export default backend;
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@thalorlabs/api-test-suite",
3
+ "author": "ThalorLabs",
4
+ "private": false,
5
+ "version": "1.0.0",
6
+ "description": "Standardised integration test suite for ThalorLabs microservices",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "clean": "rimraf dist",
12
+ "prebuild": "npm run clean",
13
+ "prepublishOnly": "npm run build",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "lint": "eslint src/",
17
+ "lint:fix": "eslint src/ --fix",
18
+ "format": "prettier --write \"src/**/*.ts\"",
19
+ "format:check": "prettier --check \"src/**/*.ts\""
20
+ },
21
+ "devDependencies": {
22
+ "@thalorlabs/eslint-plugin-dev-config": "^2.1.3",
23
+ "@types/express": "^5.0.0",
24
+ "@types/node": "^25.0.0",
25
+ "@types/supertest": "^6.0.0",
26
+ "express": "^4.21.0",
27
+ "prettier": "^3.0.0",
28
+ "rimraf": "^5.0.0",
29
+ "typescript": "^5.0.0"
30
+ },
31
+ "dependencies": {
32
+ "supertest": "^7.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "express": "^4.0.0"
36
+ },
37
+ "prettier": "@thalorlabs/eslint-plugin-dev-config/prettier",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { runStandardApiTests } from './standardTests';
2
+ export type { StandardApiTestConfig } from './standardTests';
@@ -0,0 +1,177 @@
1
+ import { Express } from 'express';
2
+ import request from 'supertest';
3
+
4
+ // vitest globals (describe, it, expect) are injected by the test runner.
5
+ // This package is consumed inside vitest test files — no explicit import needed.
6
+ declare const describe: (name: string, fn: () => void) => void;
7
+ declare const it: (name: string, fn: () => Promise<void> | void) => void;
8
+ declare const expect: (value: unknown) => any;
9
+
10
+ export interface StandardApiTestConfig {
11
+ /** The Express app instance */
12
+ app: Express;
13
+ /** The route to test (e.g. '/api/v1/jokes') */
14
+ route: string;
15
+ /** HTTP method (default: 'GET') */
16
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
17
+ /** Valid API key for auth tests */
18
+ validApiKey: string;
19
+ /** Whether the route requires auth (default: true) */
20
+ requiresAuth?: boolean;
21
+ /** Expected success status code (default: 200) */
22
+ expectedStatus?: number;
23
+ }
24
+
25
+ function makeRequest(
26
+ app: Express,
27
+ method: string,
28
+ route: string
29
+ ): request.Test {
30
+ switch (method) {
31
+ case 'POST':
32
+ return request(app).post(route);
33
+ case 'PUT':
34
+ return request(app).put(route);
35
+ case 'PATCH':
36
+ return request(app).patch(route);
37
+ case 'DELETE':
38
+ return request(app).delete(route);
39
+ default:
40
+ return request(app).get(route);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Runs standardised integration tests for a ThalorLabs microservice route.
46
+ *
47
+ * Covers:
48
+ * - Route exists and returns expected status
49
+ * - Authentication (missing key, wrong key)
50
+ * - Unknown routes return 404
51
+ * - Health endpoint returns 200 without auth
52
+ * - Response is JSON
53
+ * - Error responses don't leak stack traces
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * import { runStandardApiTests } from '@thalorlabs/api-test-suite';
58
+ * import { app } from '../src/index';
59
+ *
60
+ * runStandardApiTests({
61
+ * app,
62
+ * route: '/api/v1/jokes',
63
+ * method: 'GET',
64
+ * validApiKey: 'test-key',
65
+ * });
66
+ * ```
67
+ */
68
+ export function runStandardApiTests(config: StandardApiTestConfig): void {
69
+ const {
70
+ app,
71
+ route,
72
+ method = 'GET',
73
+ validApiKey,
74
+ requiresAuth = true,
75
+ expectedStatus = 200,
76
+ } = config;
77
+
78
+ describe(`Standard API Tests — ${method} ${route}`, () => {
79
+ // Routing
80
+ describe('Routing', () => {
81
+ it(`${method} ${route} returns ${expectedStatus} with valid auth`, async () => {
82
+ const res = await makeRequest(app, method, route).set(
83
+ 'x-api-key',
84
+ validApiKey
85
+ );
86
+
87
+ expect(res.status).toBe(expectedStatus);
88
+ });
89
+
90
+ it('unknown route returns 404', async () => {
91
+ const res = await request(app)
92
+ .get('/api/v1/this-route-does-not-exist')
93
+ .set('x-api-key', validApiKey);
94
+
95
+ expect(res.status).toBe(404);
96
+ });
97
+
98
+ it('response is JSON', async () => {
99
+ const res = await makeRequest(app, method, route).set(
100
+ 'x-api-key',
101
+ validApiKey
102
+ );
103
+
104
+ expect(res.headers['content-type']).toMatch(/json/);
105
+ });
106
+ });
107
+
108
+ // Authentication
109
+ if (requiresAuth) {
110
+ describe('Authentication', () => {
111
+ it('returns 401 without API key', async () => {
112
+ const res = await makeRequest(app, method, route);
113
+
114
+ expect(res.status).toBe(401);
115
+ });
116
+
117
+ it('returns 401 with wrong API key', async () => {
118
+ const res = await makeRequest(app, method, route).set(
119
+ 'x-api-key',
120
+ 'wrong-key-that-should-fail'
121
+ );
122
+
123
+ expect(res.status).toBe(401);
124
+ });
125
+
126
+ it('returns 401 with empty API key', async () => {
127
+ const res = await makeRequest(app, method, route).set(
128
+ 'x-api-key',
129
+ ''
130
+ );
131
+
132
+ expect(res.status).toBe(401);
133
+ });
134
+ });
135
+ }
136
+
137
+ // Health endpoint
138
+ describe('Health', () => {
139
+ it('GET /health returns 200 without auth', async () => {
140
+ const res = await request(app).get('/health');
141
+
142
+ expect(res.status).toBe(200);
143
+ expect(res.body).toHaveProperty('status');
144
+ });
145
+
146
+ it('GET /alive returns 200 without auth', async () => {
147
+ const res = await request(app).get('/alive');
148
+
149
+ expect(res.status).toBe(200);
150
+ expect(res.body).toHaveProperty('status', 'alive');
151
+ });
152
+ });
153
+
154
+ // Error response contract
155
+ describe('Error response contract', () => {
156
+ it('error responses include status field', async () => {
157
+ const res = await makeRequest(app, method, route);
158
+
159
+ if (res.status >= 400) {
160
+ expect(res.body).toHaveProperty('status');
161
+ }
162
+ });
163
+
164
+ it('error responses do not leak stack traces', async () => {
165
+ const res = await makeRequest(app, method, route);
166
+
167
+ if (res.status >= 400) {
168
+ const body = JSON.stringify(res.body);
169
+ expect(body).not.toContain('at Object.');
170
+ expect(body).not.toContain('at Module.');
171
+ expect(body).not.toContain('node_modules');
172
+ expect(body).not.toContain('.ts:');
173
+ }
174
+ });
175
+ });
176
+ });
177
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@thalorlabs/eslint-plugin-dev-config/tsconfig/backend.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist"
5
+ },
6
+ "include": ["src"],
7
+ "exclude": ["node_modules", "dist"]
8
+ }