Skip to content

Commit b98ca1d

Browse files
authored
Initial version and tests (#2)
* Github Actions * Require response to be Json-compatible * First working version and some tests * Fix tests
1 parent 27dccb1 commit b98ca1d

21 files changed

+734
-6676
lines changed

.eslintrc

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,13 @@
44
"project": "tsconfig.eslint.json"
55
},
66
"plugins": ["@typeofweb/eslint-plugin"],
7-
"extends": ["plugin:@typeofweb/recommended"]
7+
"extends": ["plugin:@typeofweb/recommended"],
8+
"overrides": [
9+
{
10+
"files": ["__tests__/**/*.ts"],
11+
"rules": {
12+
"@typescript-eslint/consistent-type-assertions": "off"
13+
}
14+
}
15+
]
816
}

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist
33
coverage/
44
.clinic/
55
.vscode/snipsnap.code-snippets
6+
*.log

__tests__/handle.test.ts

+277
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { number, object, string } from '@typeofweb/schema';
2+
3+
import { createApp } from '../src';
4+
5+
describe('handler', () => {
6+
describe('validation', () => {
7+
it('should error on invalid parameters', async () => {
8+
const app = await createApp({
9+
hostname: 'localhost',
10+
port: 3000,
11+
cors: [],
12+
}).route({
13+
path: '/users/:userId/invoices/:invoiceId',
14+
method: 'get',
15+
validation: {
16+
params: {
17+
userId: number(),
18+
invoiceId: string(),
19+
},
20+
},
21+
handler: () => null,
22+
});
23+
24+
const result = await app.inject({
25+
method: 'get',
26+
path: '/users/aaa/invoices/bbb',
27+
});
28+
expect(result.statusCode).toEqual(400);
29+
expect(result.body).toMatchInlineSnapshot(`
30+
Object {
31+
"body": Object {
32+
"details": Object {
33+
"errors": Array [
34+
Object {
35+
"error": Object {
36+
"expected": "number",
37+
"got": "aaa",
38+
},
39+
"path": "userId",
40+
},
41+
],
42+
"expected": "object",
43+
"got": Object {
44+
"invoiceId": "bbb",
45+
"userId": "aaa",
46+
},
47+
},
48+
"name": "ValidationError",
49+
},
50+
"message": "BadRequest",
51+
"name": "HttpError",
52+
}
53+
`);
54+
});
55+
56+
it('should error on invalid query', async () => {
57+
const app = await createApp({
58+
hostname: 'localhost',
59+
port: 3000,
60+
cors: [],
61+
}).route({
62+
path: '/users',
63+
method: 'get',
64+
validation: {
65+
query: {
66+
aaa: string(),
67+
},
68+
},
69+
handler: () => null,
70+
});
71+
72+
const result = await app.inject({
73+
method: 'get',
74+
path: '/users',
75+
});
76+
expect(result.statusCode).toEqual(400);
77+
expect(result.body).toMatchInlineSnapshot(`
78+
Object {
79+
"body": Object {
80+
"details": Object {
81+
"errors": Array [
82+
Object {
83+
"error": Object {
84+
"expected": "string",
85+
},
86+
"path": "aaa",
87+
},
88+
],
89+
"expected": "object",
90+
"got": Object {},
91+
},
92+
"name": "ValidationError",
93+
},
94+
"message": "BadRequest",
95+
"name": "HttpError",
96+
}
97+
`);
98+
});
99+
100+
it('should error on invalid payload', async () => {
101+
const app = await createApp({
102+
hostname: 'localhost',
103+
port: 3000,
104+
cors: [],
105+
}).route({
106+
path: '/users',
107+
method: 'post',
108+
validation: {
109+
payload: object({
110+
aaa: string(),
111+
})(),
112+
},
113+
handler: () => null,
114+
});
115+
116+
const result = await app.inject({
117+
method: 'post',
118+
path: '/users',
119+
payload: { bbb: 123 },
120+
});
121+
expect(result.statusCode).toEqual(400);
122+
expect(result.body).toMatchInlineSnapshot(`
123+
Object {
124+
"body": Object {
125+
"details": Object {
126+
"errors": Array [
127+
Object {
128+
"error": Object {
129+
"expected": "unknownKey",
130+
"got": 123,
131+
},
132+
"path": "bbb",
133+
},
134+
Object {
135+
"error": Object {
136+
"expected": "string",
137+
},
138+
"path": "aaa",
139+
},
140+
],
141+
"expected": "object",
142+
"got": Object {
143+
"bbb": 123,
144+
},
145+
},
146+
"name": "ValidationError",
147+
},
148+
"message": "BadRequest",
149+
"name": "HttpError",
150+
}
151+
`);
152+
});
153+
154+
it('should return 500 on invalid response', async () => {
155+
const app = await createApp({
156+
hostname: 'localhost',
157+
port: 3000,
158+
cors: [],
159+
}).route({
160+
path: '/users',
161+
method: 'get',
162+
validation: {
163+
response: number(),
164+
},
165+
// @ts-expect-error
166+
handler: () => null,
167+
});
168+
169+
const result = await app.inject({
170+
method: 'get',
171+
path: '/users',
172+
});
173+
expect(result.statusCode).toEqual(500);
174+
expect(result.body).toMatchInlineSnapshot(`
175+
Object {
176+
"body": Object {
177+
"expected": "number",
178+
"got": null,
179+
},
180+
"message": "Invalid type! Expected number but got null!",
181+
"name": "HttpError",
182+
}
183+
`);
184+
});
185+
186+
it('should parse and validate query, params and payload', async () => {
187+
const app = await createApp({
188+
hostname: 'localhost',
189+
port: 3000,
190+
cors: [],
191+
}).route({
192+
path: '/users/:userId/invoices/:invoiceId',
193+
method: 'post',
194+
validation: {
195+
params: {
196+
userId: number(),
197+
invoiceId: string(),
198+
},
199+
query: {
200+
search: string(),
201+
},
202+
payload: object({
203+
id: number(),
204+
})(),
205+
},
206+
handler: (request) => {
207+
expect(request.params).toEqual({ userId: 123, invoiceId: 'aaaa-bbb' });
208+
expect(request.query).toEqual({ search: 'kkk' });
209+
expect(request.payload).toEqual({ id: 123 });
210+
return null;
211+
},
212+
});
213+
214+
await app.inject({
215+
method: 'post',
216+
path: '/users/123/invoices/aaaa-bbb?search=kkk',
217+
payload: { id: 123 },
218+
});
219+
expect.assertions(3);
220+
});
221+
});
222+
223+
describe('happy path', () => {
224+
it('should return data when all validation passes', async () => {
225+
const app = await createApp({
226+
hostname: 'localhost',
227+
port: 3000,
228+
cors: [],
229+
}).route({
230+
path: '/users/:userId/invoices/:invoiceId',
231+
method: 'post',
232+
validation: {
233+
params: {
234+
userId: number(),
235+
invoiceId: string(),
236+
},
237+
query: {
238+
search: string(),
239+
},
240+
payload: object({
241+
id: number(),
242+
})(),
243+
},
244+
handler: () => {
245+
return { message: 'Wszystko ok!' };
246+
},
247+
});
248+
249+
const result = await app.inject({
250+
method: 'post',
251+
path: '/users/123/invoices/aaaa-bbb?search=kkk',
252+
payload: { id: 123 },
253+
});
254+
expect(result.statusCode).toEqual(200);
255+
expect(result.body).toEqual({ message: 'Wszystko ok!' });
256+
});
257+
258+
it('should return 204 on empty response', async () => {
259+
const app = await createApp({
260+
hostname: 'localhost',
261+
port: 3000,
262+
cors: [],
263+
}).route({
264+
path: '/users',
265+
method: 'get',
266+
validation: {},
267+
handler: () => null,
268+
});
269+
270+
const result = await app.inject({
271+
method: 'get',
272+
path: '/users',
273+
});
274+
expect(result.statusCode).toEqual(204);
275+
});
276+
});
277+
});

__tests__/handler.test-d.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { array, number, object, optional, string } from '@typeofweb/schema';
2+
import { expectType } from 'tsd';
3+
4+
import { createApp } from '../src';
5+
6+
const app = createApp({
7+
hostname: 'localhost',
8+
port: 3000,
9+
cors: [],
10+
});
11+
12+
void app.route({
13+
path: '/users/:userId/invoices/:invoiceId',
14+
method: 'get',
15+
validation: {
16+
// @ts-expect-error Missing userId and invoiceId
17+
params: {},
18+
},
19+
handler: () => null,
20+
});
21+
22+
void app.route({
23+
path: '/users/:userId/invoices/:invoiceId',
24+
method: 'get',
25+
validation: {
26+
// @ts-expect-error Missing invoiceId
27+
params: {
28+
userId: number(),
29+
},
30+
},
31+
handler: () => null,
32+
});
33+
34+
void app.route({
35+
path: '/users/:userId/invoices/:invoiceId',
36+
method: 'get',
37+
validation: {
38+
params: {
39+
userId: number(),
40+
invoiceId: string(),
41+
},
42+
},
43+
handler: () => null,
44+
});
45+
46+
void app.route({
47+
path: '/dsa',
48+
method: 'get',
49+
validation: {
50+
response: number(),
51+
},
52+
// @ts-expect-error number
53+
handler: () => null,
54+
});
55+
56+
void app.route({
57+
path: '/users/:userId/invoices/:invoiceId',
58+
method: 'post',
59+
validation: {
60+
params: {
61+
userId: number(),
62+
invoiceId: string(),
63+
},
64+
query: {
65+
search: optional(string()),
66+
},
67+
payload: object({ id: optional(number()) })(),
68+
response: array(number())(),
69+
},
70+
handler(request) {
71+
expectType<number>(request.params.userId);
72+
expectType<string>(request.params.invoiceId);
73+
expectType<{ readonly search: string | undefined }>(request.query);
74+
expectType<{ readonly id?: number }>(request.payload);
75+
76+
return [1, 2, 3];
77+
},
78+
});

0 commit comments

Comments
 (0)