@terreno/api 0.0.11-beta.1 → 0.0.12
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/biome.jsonc +1 -1
- package/dist/api.arrayOperations.test.d.ts +1 -0
- package/dist/api.arrayOperations.test.js +868 -0
- package/dist/api.errors.test.d.ts +1 -0
- package/dist/api.errors.test.js +175 -0
- package/dist/api.hooks.test.d.ts +1 -0
- package/dist/api.hooks.test.js +891 -0
- package/dist/api.query.test.d.ts +1 -0
- package/dist/api.query.test.js +805 -0
- package/dist/api.test.js +310 -3182
- package/dist/auth.test.js +135 -0
- package/dist/expressServer.test.d.ts +1 -0
- package/dist/expressServer.test.js +669 -0
- package/dist/notifiers/slackNotifier.d.ts +2 -1
- package/dist/notifiers/slackNotifier.js +20 -13
- package/dist/permissions.test.js +57 -0
- package/dist/populate.test.js +52 -0
- package/dist/tests.d.ts +9 -27
- package/dist/utils.test.js +66 -0
- package/package.json +2 -2
- package/src/api.arrayOperations.test.ts +690 -0
- package/src/api.errors.test.ts +156 -0
- package/src/api.hooks.test.ts +704 -0
- package/src/api.query.test.ts +538 -0
- package/src/api.test.ts +273 -2658
- package/src/auth.test.ts +72 -0
- package/src/expressServer.test.ts +579 -0
- package/src/notifiers/slackNotifier.ts +28 -17
- package/src/permissions.test.ts +70 -1
- package/src/populate.test.ts +58 -0
- package/src/utils.test.ts +26 -1
package/src/api.test.ts
CHANGED
|
@@ -1,106 +1,60 @@
|
|
|
1
1
|
import {beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
-
import * as Sentry from "@sentry/node";
|
|
3
2
|
import type express from "express";
|
|
4
|
-
import qs from "qs";
|
|
5
3
|
import supertest from "supertest";
|
|
6
4
|
import type TestAgent from "supertest/lib/agent";
|
|
7
5
|
|
|
8
6
|
import {addPopulateToQuery, modelRouter} from "./api";
|
|
9
7
|
import {addAuthRoutes, setupAuth} from "./auth";
|
|
10
|
-
import {APIError} from "./errors";
|
|
11
|
-
import {logRequests} from "./expressServer";
|
|
12
8
|
import {Permissions} from "./permissions";
|
|
13
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
authAsUser,
|
|
11
|
+
type Food,
|
|
12
|
+
FoodModel,
|
|
13
|
+
getBaseServer,
|
|
14
|
+
RequiredModel,
|
|
15
|
+
setupDb,
|
|
16
|
+
UserModel,
|
|
17
|
+
} from "./tests";
|
|
14
18
|
import {AdminOwnerTransformer} from "./transformers";
|
|
15
19
|
|
|
16
20
|
describe("@terreno/api", () => {
|
|
17
21
|
let server: TestAgent;
|
|
18
22
|
let app: express.Application;
|
|
19
23
|
|
|
20
|
-
describe("
|
|
24
|
+
describe("populate", () => {
|
|
25
|
+
let admin: any;
|
|
26
|
+
let notAdmin: any;
|
|
21
27
|
let agent: TestAgent;
|
|
28
|
+
let spinach: Food;
|
|
22
29
|
|
|
23
30
|
beforeEach(async () => {
|
|
24
|
-
await setupDb();
|
|
25
|
-
app = getBaseServer();
|
|
26
|
-
setupAuth(app, UserModel as any);
|
|
27
|
-
addAuthRoutes(app, UserModel as any);
|
|
28
|
-
agent = await authAsUser(app, "notAdmin");
|
|
29
|
-
});
|
|
31
|
+
[admin, notAdmin] = await setupDb();
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
list: [Permissions.IsAny],
|
|
41
|
-
read: [Permissions.IsAny],
|
|
42
|
-
update: [Permissions.IsAny],
|
|
43
|
-
},
|
|
44
|
-
preCreate: (data: any) => {
|
|
45
|
-
data.calories = 14;
|
|
46
|
-
return data;
|
|
47
|
-
},
|
|
48
|
-
preDelete: (data: any) => {
|
|
49
|
-
deleteCalled = true;
|
|
50
|
-
return data;
|
|
33
|
+
[spinach] = await Promise.all([
|
|
34
|
+
FoodModel.create({
|
|
35
|
+
calories: 1,
|
|
36
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
37
|
+
hidden: false,
|
|
38
|
+
name: "Spinach",
|
|
39
|
+
ownerId: admin._id,
|
|
40
|
+
source: {
|
|
41
|
+
name: "Brand",
|
|
51
42
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
}),
|
|
44
|
+
FoodModel.create({
|
|
45
|
+
calories: 1,
|
|
46
|
+
created: new Date("2022-12-03T00:00:20.000Z"),
|
|
47
|
+
hidden: false,
|
|
48
|
+
name: "Carrots",
|
|
49
|
+
ownerId: notAdmin._id,
|
|
50
|
+
source: {
|
|
51
|
+
name: "User",
|
|
55
52
|
},
|
|
56
|
-
})
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.post("/food")
|
|
62
|
-
.send({
|
|
63
|
-
calories: 15,
|
|
64
|
-
name: "Broccoli",
|
|
65
|
-
})
|
|
66
|
-
.expect(201);
|
|
67
|
-
const broccoli = await FoodModel.findById(res.body.data._id);
|
|
68
|
-
if (!broccoli) {
|
|
69
|
-
throw new Error("Broccoli was not created");
|
|
70
|
-
}
|
|
71
|
-
expect(broccoli.name).toBe("Broccoli");
|
|
72
|
-
// Overwritten by the pre create hook
|
|
73
|
-
expect(broccoli.calories).toBe(14);
|
|
74
|
-
|
|
75
|
-
res = await server
|
|
76
|
-
.patch(`/food/${broccoli._id}`)
|
|
77
|
-
.send({
|
|
78
|
-
name: "Broccoli2",
|
|
79
|
-
})
|
|
80
|
-
.expect(200);
|
|
81
|
-
expect(res.body.data.name).toBe("Broccoli2");
|
|
82
|
-
// Updated by the pre update hook
|
|
83
|
-
expect(res.body.data.calories).toBe(15);
|
|
84
|
-
|
|
85
|
-
await agent.delete(`/food/${broccoli._id}`).expect(204);
|
|
86
|
-
expect(deleteCalled).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("pre hooks return null", async () => {
|
|
90
|
-
const notAdmin = await UserModel.findOne({
|
|
91
|
-
email: "notAdmin@example.com",
|
|
92
|
-
});
|
|
93
|
-
const spinach = await FoodModel.create({
|
|
94
|
-
calories: 1,
|
|
95
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
96
|
-
hidden: false,
|
|
97
|
-
name: "Spinach",
|
|
98
|
-
ownerId: (notAdmin as any)._id,
|
|
99
|
-
source: {
|
|
100
|
-
name: "Brand",
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
app = getBaseServer();
|
|
56
|
+
setupAuth(app, UserModel as any);
|
|
57
|
+
addAuthRoutes(app, UserModel as any);
|
|
104
58
|
app.use(
|
|
105
59
|
"/food",
|
|
106
60
|
modelRouter(FoodModel, {
|
|
@@ -112,312 +66,69 @@ describe("@terreno/api", () => {
|
|
|
112
66
|
read: [Permissions.IsAny],
|
|
113
67
|
update: [Permissions.IsAny],
|
|
114
68
|
},
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
preUpdate: () => null,
|
|
69
|
+
populatePaths: [{fields: ["email"], path: "ownerId"}],
|
|
70
|
+
sort: "-created",
|
|
118
71
|
})
|
|
119
72
|
);
|
|
120
73
|
server = supertest(app);
|
|
121
|
-
|
|
122
|
-
const res = await server
|
|
123
|
-
.post("/food")
|
|
124
|
-
.send({
|
|
125
|
-
calories: 15,
|
|
126
|
-
name: "Broccoli",
|
|
127
|
-
})
|
|
128
|
-
.expect(403);
|
|
129
|
-
const broccoli = await FoodModel.findById(res.body._id);
|
|
130
|
-
expect(broccoli).toBeNull();
|
|
131
|
-
|
|
132
|
-
await server
|
|
133
|
-
.patch(`/food/${spinach._id}`)
|
|
134
|
-
.send({
|
|
135
|
-
name: "Broccoli",
|
|
136
|
-
})
|
|
137
|
-
.expect(403);
|
|
138
|
-
await server.delete(`/food/${spinach._id}`).expect(403);
|
|
74
|
+
agent = await authAsUser(app, "notAdmin");
|
|
139
75
|
});
|
|
140
76
|
|
|
141
|
-
it("
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
read: [Permissions.IsAny],
|
|
152
|
-
update: [Permissions.IsAny],
|
|
153
|
-
},
|
|
154
|
-
postCreate: async (data: any) => {
|
|
155
|
-
data.calories = 14;
|
|
156
|
-
await data.save();
|
|
157
|
-
return data;
|
|
158
|
-
},
|
|
159
|
-
postDelete: (data: any) => {
|
|
160
|
-
deleteCalled = true;
|
|
161
|
-
return data;
|
|
162
|
-
},
|
|
163
|
-
postUpdate: async (data: any) => {
|
|
164
|
-
data.calories = 15;
|
|
165
|
-
await data.save();
|
|
166
|
-
return data;
|
|
167
|
-
},
|
|
168
|
-
})
|
|
169
|
-
);
|
|
170
|
-
server = supertest(app);
|
|
171
|
-
|
|
172
|
-
let res = await server
|
|
173
|
-
.post("/food")
|
|
174
|
-
.send({
|
|
175
|
-
calories: 15,
|
|
176
|
-
name: "Broccoli",
|
|
177
|
-
})
|
|
178
|
-
.expect(201);
|
|
179
|
-
let broccoli = await FoodModel.findById(res.body.data._id);
|
|
180
|
-
if (!broccoli) {
|
|
181
|
-
throw new Error("Broccoli was not created");
|
|
182
|
-
}
|
|
183
|
-
expect(broccoli.name).toBe("Broccoli");
|
|
184
|
-
// Overwritten by the pre create hook
|
|
185
|
-
expect(broccoli.calories).toBe(14);
|
|
186
|
-
|
|
187
|
-
res = await server
|
|
188
|
-
.patch(`/food/${broccoli._id}`)
|
|
189
|
-
.send({
|
|
190
|
-
name: "Broccoli2",
|
|
191
|
-
})
|
|
192
|
-
.expect(200);
|
|
193
|
-
broccoli = await FoodModel.findById(res.body.data._id);
|
|
194
|
-
if (!broccoli) {
|
|
195
|
-
throw new Error("Broccoli was not update");
|
|
196
|
-
}
|
|
197
|
-
expect(broccoli.name).toBe("Broccoli2");
|
|
198
|
-
// Updated by the post update hook
|
|
199
|
-
expect(broccoli.calories).toBe(15);
|
|
200
|
-
|
|
201
|
-
await agent.delete(`/food/${broccoli._id}`).expect(204);
|
|
202
|
-
expect(deleteCalled).toBe(true);
|
|
77
|
+
it("lists with populate", async () => {
|
|
78
|
+
const res = await agent.get("/food").expect(200);
|
|
79
|
+
expect(res.body.data).toHaveLength(2);
|
|
80
|
+
const [carrots, spin] = res.body.data;
|
|
81
|
+
expect(carrots.ownerId._id).toBe(notAdmin._id.toString());
|
|
82
|
+
expect(carrots.ownerId.email).toBe(notAdmin.email);
|
|
83
|
+
expect(carrots.ownerId.name).toBeUndefined();
|
|
84
|
+
expect(spin.ownerId._id).toBe(admin._id.toString());
|
|
85
|
+
expect(spin.ownerId.email).toBe(admin.email);
|
|
86
|
+
expect(spin.ownerId.name).toBeUndefined();
|
|
203
87
|
});
|
|
204
88
|
|
|
205
|
-
it("
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
permissions: {
|
|
211
|
-
create: [Permissions.IsAny],
|
|
212
|
-
delete: [Permissions.IsAny],
|
|
213
|
-
list: [Permissions.IsAny],
|
|
214
|
-
read: [Permissions.IsAny],
|
|
215
|
-
update: [Permissions.IsAny],
|
|
216
|
-
},
|
|
217
|
-
preCreate: () => {
|
|
218
|
-
throw new APIError({
|
|
219
|
-
disableExternalErrorTracking: true,
|
|
220
|
-
status: 400,
|
|
221
|
-
title: "Custom preCreate error",
|
|
222
|
-
});
|
|
223
|
-
},
|
|
224
|
-
})
|
|
225
|
-
);
|
|
226
|
-
server = supertest(app);
|
|
227
|
-
|
|
228
|
-
const res = await server
|
|
229
|
-
.post("/food")
|
|
230
|
-
.send({
|
|
231
|
-
calories: 15,
|
|
232
|
-
name: "Broccoli",
|
|
233
|
-
})
|
|
234
|
-
.expect(400);
|
|
235
|
-
|
|
236
|
-
expect(res.body.title).toBe("Custom preCreate error");
|
|
237
|
-
expect(res.body.disableExternalErrorTracking).toBe(true);
|
|
89
|
+
it("reads with populate", async () => {
|
|
90
|
+
const res = await agent.get(`/food/${spinach._id}`).expect(200);
|
|
91
|
+
expect(res.body.data.ownerId._id).toBe(admin._id.toString());
|
|
92
|
+
expect(res.body.data.ownerId.email).toBe(admin.email);
|
|
93
|
+
expect(res.body.data.ownerId.name).toBeUndefined();
|
|
238
94
|
});
|
|
239
95
|
|
|
240
|
-
it("
|
|
241
|
-
app.use(
|
|
242
|
-
"/food",
|
|
243
|
-
modelRouter(FoodModel, {
|
|
244
|
-
allowAnonymous: true,
|
|
245
|
-
permissions: {
|
|
246
|
-
create: [Permissions.IsAny],
|
|
247
|
-
delete: [Permissions.IsAny],
|
|
248
|
-
list: [Permissions.IsAny],
|
|
249
|
-
read: [Permissions.IsAny],
|
|
250
|
-
update: [Permissions.IsAny],
|
|
251
|
-
},
|
|
252
|
-
preCreate: () => {
|
|
253
|
-
const error: any = new Error("Some custom error");
|
|
254
|
-
error.disableExternalErrorTracking = true;
|
|
255
|
-
throw error;
|
|
256
|
-
},
|
|
257
|
-
})
|
|
258
|
-
);
|
|
259
|
-
server = supertest(app);
|
|
260
|
-
|
|
96
|
+
it("creates with populate", async () => {
|
|
261
97
|
const res = await server
|
|
262
98
|
.post("/food")
|
|
263
99
|
.send({
|
|
264
100
|
calories: 15,
|
|
265
101
|
name: "Broccoli",
|
|
102
|
+
ownerId: admin._id,
|
|
266
103
|
})
|
|
267
|
-
.expect(
|
|
268
|
-
|
|
269
|
-
expect(res.body.
|
|
270
|
-
expect(res.body.
|
|
104
|
+
.expect(201);
|
|
105
|
+
expect(res.body.data.ownerId._id).toBe(admin._id.toString());
|
|
106
|
+
expect(res.body.data.ownerId.email).toBe(admin.email);
|
|
107
|
+
expect(res.body.data.ownerId.name).toBeUndefined();
|
|
271
108
|
});
|
|
272
109
|
|
|
273
|
-
it("
|
|
274
|
-
const notAdmin = await UserModel.findOne({
|
|
275
|
-
email: "notAdmin@example.com",
|
|
276
|
-
});
|
|
277
|
-
const spinach = await FoodModel.create({
|
|
278
|
-
calories: 1,
|
|
279
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
280
|
-
hidden: false,
|
|
281
|
-
name: "Spinach",
|
|
282
|
-
ownerId: (notAdmin as any)._id,
|
|
283
|
-
source: {
|
|
284
|
-
name: "Brand",
|
|
285
|
-
},
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
app.use(
|
|
289
|
-
"/food",
|
|
290
|
-
modelRouter(FoodModel, {
|
|
291
|
-
allowAnonymous: true,
|
|
292
|
-
permissions: {
|
|
293
|
-
create: [Permissions.IsAny],
|
|
294
|
-
delete: [Permissions.IsAny],
|
|
295
|
-
list: [Permissions.IsAny],
|
|
296
|
-
read: [Permissions.IsAny],
|
|
297
|
-
update: [Permissions.IsAny],
|
|
298
|
-
},
|
|
299
|
-
preUpdate: () => {
|
|
300
|
-
throw new APIError({
|
|
301
|
-
disableExternalErrorTracking: true,
|
|
302
|
-
status: 400,
|
|
303
|
-
title: "Custom preUpdate error",
|
|
304
|
-
});
|
|
305
|
-
},
|
|
306
|
-
})
|
|
307
|
-
);
|
|
308
|
-
server = supertest(app);
|
|
309
|
-
|
|
110
|
+
it("updates with populate", async () => {
|
|
310
111
|
const res = await server
|
|
311
112
|
.patch(`/food/${spinach._id}`)
|
|
312
113
|
.send({
|
|
313
|
-
name: "
|
|
114
|
+
name: "NotSpinach",
|
|
314
115
|
})
|
|
315
|
-
.expect(
|
|
316
|
-
|
|
317
|
-
expect(res.body.
|
|
318
|
-
expect(res.body.
|
|
116
|
+
.expect(200);
|
|
117
|
+
expect(res.body.data.ownerId._id).toBe(admin._id.toString());
|
|
118
|
+
expect(res.body.data.ownerId.email).toBe(admin.email);
|
|
119
|
+
expect(res.body.data.ownerId.name).toBeUndefined();
|
|
319
120
|
});
|
|
121
|
+
});
|
|
320
122
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const spinach = await FoodModel.create({
|
|
326
|
-
calories: 1,
|
|
327
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
328
|
-
hidden: false,
|
|
329
|
-
name: "Spinach",
|
|
330
|
-
ownerId: (notAdmin as any)._id,
|
|
331
|
-
source: {
|
|
332
|
-
name: "Brand",
|
|
333
|
-
},
|
|
334
|
-
});
|
|
123
|
+
describe("responseHandler", () => {
|
|
124
|
+
let admin: any;
|
|
125
|
+
let agent: TestAgent;
|
|
126
|
+
let spinach: Food;
|
|
335
127
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
modelRouter(FoodModel, {
|
|
339
|
-
allowAnonymous: true,
|
|
340
|
-
permissions: {
|
|
341
|
-
create: [Permissions.IsAny],
|
|
342
|
-
delete: [Permissions.IsAny],
|
|
343
|
-
list: [Permissions.IsAny],
|
|
344
|
-
read: [Permissions.IsAny],
|
|
345
|
-
update: [Permissions.IsAny],
|
|
346
|
-
},
|
|
347
|
-
preUpdate: () => {
|
|
348
|
-
const error: any = new Error("Some custom error");
|
|
349
|
-
error.disableExternalErrorTracking = true;
|
|
350
|
-
throw error;
|
|
351
|
-
},
|
|
352
|
-
})
|
|
353
|
-
);
|
|
354
|
-
server = supertest(app);
|
|
128
|
+
beforeEach(async () => {
|
|
129
|
+
[admin] = await setupDb();
|
|
355
130
|
|
|
356
|
-
|
|
357
|
-
.patch(`/food/${spinach._id}`)
|
|
358
|
-
.send({
|
|
359
|
-
name: "Broccoli",
|
|
360
|
-
})
|
|
361
|
-
.expect(400);
|
|
362
|
-
|
|
363
|
-
expect(res.body.title).toContain("preUpdate hook error");
|
|
364
|
-
expect(res.body.disableExternalErrorTracking).toBe(true);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it("preDelete hook preserves disableExternalErrorTracking on non-APIError", async () => {
|
|
368
|
-
const notAdmin = await UserModel.findOne({
|
|
369
|
-
email: "notAdmin@example.com",
|
|
370
|
-
});
|
|
371
|
-
const spinach = await FoodModel.create({
|
|
372
|
-
calories: 1,
|
|
373
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
374
|
-
hidden: false,
|
|
375
|
-
name: "Spinach",
|
|
376
|
-
ownerId: (notAdmin as any)._id,
|
|
377
|
-
source: {
|
|
378
|
-
name: "Brand",
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
app.use(
|
|
383
|
-
"/food",
|
|
384
|
-
modelRouter(FoodModel, {
|
|
385
|
-
allowAnonymous: true,
|
|
386
|
-
permissions: {
|
|
387
|
-
create: [Permissions.IsAny],
|
|
388
|
-
delete: [Permissions.IsAny],
|
|
389
|
-
list: [Permissions.IsAny],
|
|
390
|
-
read: [Permissions.IsAny],
|
|
391
|
-
update: [Permissions.IsAny],
|
|
392
|
-
},
|
|
393
|
-
preDelete: () => {
|
|
394
|
-
const error: any = new Error("Some custom error");
|
|
395
|
-
error.disableExternalErrorTracking = true;
|
|
396
|
-
throw error;
|
|
397
|
-
},
|
|
398
|
-
})
|
|
399
|
-
);
|
|
400
|
-
server = supertest(app);
|
|
401
|
-
|
|
402
|
-
const res = await agent.delete(`/food/${spinach._id}`).expect(403);
|
|
403
|
-
|
|
404
|
-
expect(res.body.title).toContain("preDelete hook error");
|
|
405
|
-
expect(res.body.disableExternalErrorTracking).toBe(true);
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
describe("model array operations", () => {
|
|
410
|
-
let admin: any;
|
|
411
|
-
let spinach: Food;
|
|
412
|
-
let apple: Food;
|
|
413
|
-
let agent: TestAgent;
|
|
414
|
-
|
|
415
|
-
beforeEach(async () => {
|
|
416
|
-
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
417
|
-
|
|
418
|
-
[admin] = await setupDb();
|
|
419
|
-
|
|
420
|
-
[spinach, apple] = await Promise.all([
|
|
131
|
+
[spinach] = await Promise.all([
|
|
421
132
|
FoodModel.create({
|
|
422
133
|
calories: 1,
|
|
423
134
|
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
@@ -430,1478 +141,15 @@ describe("@terreno/api", () => {
|
|
|
430
141
|
}),
|
|
431
142
|
FoodModel.create({
|
|
432
143
|
calories: 100,
|
|
433
|
-
|
|
434
|
-
{
|
|
435
|
-
name: "Fruit",
|
|
436
|
-
show: true,
|
|
437
|
-
},
|
|
438
|
-
{
|
|
439
|
-
name: "Popular",
|
|
440
|
-
show: false,
|
|
441
|
-
},
|
|
442
|
-
],
|
|
443
|
-
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
444
|
-
hidden: false,
|
|
445
|
-
name: "Apple",
|
|
446
|
-
ownerId: admin._id,
|
|
447
|
-
tags: ["healthy", "cheap"],
|
|
448
|
-
}),
|
|
449
|
-
]);
|
|
450
|
-
|
|
451
|
-
app = getBaseServer();
|
|
452
|
-
setupAuth(app, UserModel as any);
|
|
453
|
-
addAuthRoutes(app, UserModel as any);
|
|
454
|
-
app.use(
|
|
455
|
-
"/food",
|
|
456
|
-
modelRouter(FoodModel, {
|
|
457
|
-
allowAnonymous: true,
|
|
458
|
-
permissions: {
|
|
459
|
-
create: [Permissions.IsAdmin],
|
|
460
|
-
delete: [Permissions.IsAdmin],
|
|
461
|
-
list: [Permissions.IsAdmin],
|
|
462
|
-
read: [Permissions.IsAdmin],
|
|
463
|
-
update: [Permissions.IsAdmin],
|
|
464
|
-
},
|
|
465
|
-
queryFields: ["hidden", "calories", "created", "source.name"],
|
|
466
|
-
sort: {created: "descending"},
|
|
467
|
-
})
|
|
468
|
-
);
|
|
469
|
-
server = supertest(app);
|
|
470
|
-
agent = await authAsUser(app, "admin");
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it("add array sub-schema item", async () => {
|
|
474
|
-
// Incorrect way, should have "categories" as a top level key.
|
|
475
|
-
let res = await agent
|
|
476
|
-
.post(`/food/${apple._id}/categories`)
|
|
477
|
-
.send({name: "Good Seller", show: false})
|
|
478
|
-
.expect(400);
|
|
479
|
-
expect(res.body.title).toBe(
|
|
480
|
-
"Malformed body, array operations should have a single, top level key, got: name,show"
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
res = await agent
|
|
484
|
-
.post(`/food/${apple._id}/categories`)
|
|
485
|
-
.send({categories: {name: "Good Seller", show: false}})
|
|
486
|
-
.expect(200);
|
|
487
|
-
expect(res.body.data.categories).toHaveLength(3);
|
|
488
|
-
expect(res.body.data.categories[2].name).toBe("Good Seller");
|
|
489
|
-
|
|
490
|
-
res = await agent
|
|
491
|
-
.post(`/food/${spinach._id}/categories`)
|
|
492
|
-
.send({categories: {name: "Good Seller", show: false}})
|
|
493
|
-
.expect(200);
|
|
494
|
-
expect(res.body.data.categories).toHaveLength(1);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
it("update array sub-schema item", async () => {
|
|
498
|
-
let res = await agent
|
|
499
|
-
.patch(`/food/${apple._id}/categories/xyz`)
|
|
500
|
-
.send({categories: {name: "Good Seller", show: false}})
|
|
501
|
-
.expect(404);
|
|
502
|
-
expect(res.body.title).toBe("Could not find categories/xyz");
|
|
503
|
-
res = await agent
|
|
504
|
-
.patch(`/food/${apple._id}/categories/${apple.categories[1]._id}`)
|
|
505
|
-
.send({categories: {name: "Good Seller", show: false}})
|
|
506
|
-
.expect(200);
|
|
507
|
-
expect(res.body.data.categories).toHaveLength(2);
|
|
508
|
-
expect(res.body.data.categories[1].name).toBe("Good Seller");
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
it("delete array sub-schema item", async () => {
|
|
512
|
-
let res = await agent.delete(`/food/${apple._id}/categories/xyz`).expect(404);
|
|
513
|
-
expect(res.body.title).toBe("Could not find categories/xyz");
|
|
514
|
-
res = await agent
|
|
515
|
-
.delete(`/food/${apple._id}/categories/${apple.categories[0]._id}`)
|
|
516
|
-
.expect(200);
|
|
517
|
-
expect(res.body.data.categories).toHaveLength(1);
|
|
518
|
-
expect(res.body.data.categories[0].name).toBe("Popular");
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
it("add array item", async () => {
|
|
522
|
-
let res = await agent.post(`/food/${apple._id}/tags`).send({tags: "popular"}).expect(200);
|
|
523
|
-
expect(res.body.data.tags).toHaveLength(3);
|
|
524
|
-
expect(res.body.data.tags).toEqual(["healthy", "cheap", "popular"]);
|
|
525
|
-
|
|
526
|
-
res = await agent.post(`/food/${spinach._id}/tags`).send({tags: "popular"}).expect(200);
|
|
527
|
-
expect(res.body.data.tags).toEqual(["popular"]);
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it("update array item", async () => {
|
|
531
|
-
let res = await agent
|
|
532
|
-
.patch(`/food/${apple._id}/tags/xyz`)
|
|
533
|
-
.send({tags: "unhealthy"})
|
|
534
|
-
.expect(404);
|
|
535
|
-
expect(res.body.title).toBe("Could not find tags/xyz");
|
|
536
|
-
res = await agent
|
|
537
|
-
.patch(`/food/${apple._id}/tags/healthy`)
|
|
538
|
-
.send({tags: "unhealthy"})
|
|
539
|
-
.expect(200);
|
|
540
|
-
expect(res.body.data.tags).toEqual(["unhealthy", "cheap"]);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
it("delete array item", async () => {
|
|
544
|
-
let res = await agent.delete(`/food/${apple._id}/tags/xyz`).expect(404);
|
|
545
|
-
expect(res.body.title).toBe("Could not find tags/xyz");
|
|
546
|
-
res = await agent.delete(`/food/${apple._id}/tags/healthy`).expect(200);
|
|
547
|
-
expect(res.body.data.tags).toEqual(["cheap"]);
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
it("updates timestamps on array subdocuments", async () => {
|
|
551
|
-
// Create a food with categories that have timestamps
|
|
552
|
-
const foodWithTimestamps = await FoodModel.create({
|
|
553
|
-
calories: 100,
|
|
554
|
-
categories: [
|
|
555
|
-
{
|
|
556
|
-
name: "Category 1",
|
|
557
|
-
show: true,
|
|
558
|
-
updated: new Date("2024-01-01T00:00:00.000Z"),
|
|
559
|
-
},
|
|
560
|
-
{
|
|
561
|
-
name: "Category 2",
|
|
562
|
-
show: true,
|
|
563
|
-
updated: new Date("2024-01-01T00:00:00.000Z"),
|
|
564
|
-
},
|
|
565
|
-
],
|
|
566
|
-
created: new Date(),
|
|
567
|
-
name: "Food with Timestamps",
|
|
568
|
-
ownerId: admin._id,
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
const firstCategoryId = foodWithTimestamps.categories?.[0]?._id?.toString();
|
|
572
|
-
const secondCategoryId = foodWithTimestamps.categories?.[1]?._id?.toString();
|
|
573
|
-
|
|
574
|
-
if (!firstCategoryId || !secondCategoryId) {
|
|
575
|
-
throw new Error("Failed to create food with categories");
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Wait a moment to ensure timestamp difference
|
|
579
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
580
|
-
|
|
581
|
-
// Update one of the categories
|
|
582
|
-
const res = await agent
|
|
583
|
-
.patch(`/food/${foodWithTimestamps._id}/categories/${firstCategoryId}`)
|
|
584
|
-
.send({categories: {name: "Updated Category"}})
|
|
585
|
-
.expect(200);
|
|
586
|
-
|
|
587
|
-
// Verify the updated category has a newer timestamp
|
|
588
|
-
const updatedCategory = res.body.data.categories.find((c: any) => c._id === firstCategoryId);
|
|
589
|
-
const unchangedCategory = res.body.data.categories.find(
|
|
590
|
-
(c: any) => c._id === secondCategoryId
|
|
591
|
-
);
|
|
592
|
-
|
|
593
|
-
if (!updatedCategory || !unchangedCategory) {
|
|
594
|
-
throw new Error("Failed to find categories in response");
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
expect(updatedCategory.updated).not.toBe(updatedCategory.created);
|
|
598
|
-
expect(unchangedCategory.updated).toBe(unchangedCategory.created);
|
|
599
|
-
expect(updatedCategory.name).toBe("Updated Category");
|
|
600
|
-
// Unchanged.
|
|
601
|
-
expect(updatedCategory.show).toBe(true);
|
|
602
|
-
expect(unchangedCategory.show).toBe(true);
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
it("array operations call postUpdate with different copy of document", async () => {
|
|
606
|
-
let postUpdateDoc: any;
|
|
607
|
-
let postUpdatePrevDoc: any;
|
|
608
|
-
let postUpdateCalled = false;
|
|
609
|
-
|
|
610
|
-
app = getBaseServer();
|
|
611
|
-
setupAuth(app, UserModel as any);
|
|
612
|
-
addAuthRoutes(app, UserModel as any);
|
|
613
|
-
app.use(
|
|
614
|
-
"/food",
|
|
615
|
-
modelRouter(FoodModel, {
|
|
616
|
-
allowAnonymous: true,
|
|
617
|
-
permissions: {
|
|
618
|
-
create: [Permissions.IsAdmin],
|
|
619
|
-
delete: [Permissions.IsAdmin],
|
|
620
|
-
list: [Permissions.IsAdmin],
|
|
621
|
-
read: [Permissions.IsAdmin],
|
|
622
|
-
update: [Permissions.IsAdmin],
|
|
623
|
-
},
|
|
624
|
-
postUpdate: async (doc: any, _cleanedBody: any, _request: any, prevValue: any) => {
|
|
625
|
-
postUpdateDoc = doc;
|
|
626
|
-
postUpdatePrevDoc = prevValue;
|
|
627
|
-
postUpdateCalled = true;
|
|
628
|
-
},
|
|
629
|
-
})
|
|
630
|
-
);
|
|
631
|
-
server = supertest(app);
|
|
632
|
-
agent = await authAsUser(app, "admin");
|
|
633
|
-
|
|
634
|
-
// Test POST operation (add to array)
|
|
635
|
-
await agent
|
|
636
|
-
.post(`/food/${apple._id}/categories`)
|
|
637
|
-
.send({categories: {name: "New Category", show: true}})
|
|
638
|
-
.expect(200);
|
|
639
|
-
|
|
640
|
-
expect(postUpdateCalled).toBe(true);
|
|
641
|
-
expect(postUpdateDoc).toBeDefined();
|
|
642
|
-
expect(postUpdatePrevDoc).toBeDefined();
|
|
643
|
-
|
|
644
|
-
// Verify they are different object references
|
|
645
|
-
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
646
|
-
|
|
647
|
-
// Verify the content is different (new category added)
|
|
648
|
-
expect(postUpdateDoc.categories).toHaveLength(3);
|
|
649
|
-
expect(postUpdatePrevDoc.categories).toHaveLength(2);
|
|
650
|
-
|
|
651
|
-
// Reset for next test
|
|
652
|
-
postUpdateCalled = false;
|
|
653
|
-
postUpdateDoc = undefined;
|
|
654
|
-
postUpdatePrevDoc = undefined;
|
|
655
|
-
|
|
656
|
-
// Test PATCH operation (update array item)
|
|
657
|
-
const categoryId = apple.categories[0]._id;
|
|
658
|
-
if (!categoryId) {
|
|
659
|
-
throw new Error("Category ID is undefined");
|
|
660
|
-
}
|
|
661
|
-
await agent
|
|
662
|
-
.patch(`/food/${apple._id}/categories/${categoryId}`)
|
|
663
|
-
.send({categories: {name: "Updated Category", show: false}})
|
|
664
|
-
.expect(200);
|
|
665
|
-
|
|
666
|
-
expect(postUpdateCalled).toBe(true);
|
|
667
|
-
expect(postUpdateDoc).toBeDefined();
|
|
668
|
-
expect(postUpdatePrevDoc).toBeDefined();
|
|
669
|
-
|
|
670
|
-
// Verify they are different object references
|
|
671
|
-
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
672
|
-
|
|
673
|
-
// Verify the content is different (category updated)
|
|
674
|
-
const updatedCategory = postUpdateDoc.categories.find(
|
|
675
|
-
(c: any) => c._id.toString() === categoryId.toString()
|
|
676
|
-
);
|
|
677
|
-
const prevCategory = postUpdatePrevDoc.categories.find(
|
|
678
|
-
(c: any) => c._id.toString() === categoryId.toString()
|
|
679
|
-
);
|
|
680
|
-
|
|
681
|
-
expect(updatedCategory.name).toBe("Updated Category");
|
|
682
|
-
expect(prevCategory.name).toBe("Fruit");
|
|
683
|
-
|
|
684
|
-
// Reset for next test
|
|
685
|
-
postUpdateCalled = false;
|
|
686
|
-
postUpdateDoc = undefined;
|
|
687
|
-
postUpdatePrevDoc = undefined;
|
|
688
|
-
|
|
689
|
-
// Test DELETE operation (remove from array)
|
|
690
|
-
await agent.delete(`/food/${apple._id}/categories/${categoryId}`).expect(200);
|
|
691
|
-
|
|
692
|
-
expect(postUpdateCalled).toBe(true);
|
|
693
|
-
expect(postUpdateDoc).toBeDefined();
|
|
694
|
-
expect(postUpdatePrevDoc).toBeDefined();
|
|
695
|
-
|
|
696
|
-
// Verify they are different object references
|
|
697
|
-
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
698
|
-
|
|
699
|
-
// Verify the content is different (category removed)
|
|
700
|
-
const remainingCategories = postUpdateDoc.categories.filter(
|
|
701
|
-
(c: any) => c._id.toString() === categoryId.toString()
|
|
702
|
-
);
|
|
703
|
-
const prevCategories = postUpdatePrevDoc.categories.filter(
|
|
704
|
-
(c: any) => c._id.toString() === categoryId.toString()
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
expect(remainingCategories).toHaveLength(0);
|
|
708
|
-
expect(prevCategories).toHaveLength(1);
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
it("array operations with string arrays call postUpdate with different copy", async () => {
|
|
712
|
-
let postUpdateDoc: any;
|
|
713
|
-
let postUpdatePrevDoc: any;
|
|
714
|
-
let postUpdateCalled = false;
|
|
715
|
-
|
|
716
|
-
app = getBaseServer();
|
|
717
|
-
setupAuth(app, UserModel as any);
|
|
718
|
-
addAuthRoutes(app, UserModel as any);
|
|
719
|
-
app.use(
|
|
720
|
-
"/food",
|
|
721
|
-
modelRouter(FoodModel, {
|
|
722
|
-
allowAnonymous: true,
|
|
723
|
-
permissions: {
|
|
724
|
-
create: [Permissions.IsAdmin],
|
|
725
|
-
delete: [Permissions.IsAdmin],
|
|
726
|
-
list: [Permissions.IsAdmin],
|
|
727
|
-
read: [Permissions.IsAdmin],
|
|
728
|
-
update: [Permissions.IsAdmin],
|
|
729
|
-
},
|
|
730
|
-
postUpdate: async (doc: any, _cleanedBody: any, _request: any, prevValue: any) => {
|
|
731
|
-
postUpdateDoc = doc;
|
|
732
|
-
postUpdatePrevDoc = prevValue;
|
|
733
|
-
postUpdateCalled = true;
|
|
734
|
-
},
|
|
735
|
-
})
|
|
736
|
-
);
|
|
737
|
-
server = supertest(app);
|
|
738
|
-
agent = await authAsUser(app, "admin");
|
|
739
|
-
|
|
740
|
-
// Test POST operation with string array (add tag)
|
|
741
|
-
await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(200);
|
|
742
|
-
|
|
743
|
-
expect(postUpdateCalled).toBe(true);
|
|
744
|
-
expect(postUpdateDoc).toBeDefined();
|
|
745
|
-
expect(postUpdatePrevDoc).toBeDefined();
|
|
746
|
-
|
|
747
|
-
// Verify they are different object references
|
|
748
|
-
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
749
|
-
|
|
750
|
-
// Verify the content is different (new tag added)
|
|
751
|
-
expect(postUpdateDoc.tags).toHaveLength(3);
|
|
752
|
-
expect(postUpdatePrevDoc.tags).toHaveLength(2);
|
|
753
|
-
expect(postUpdateDoc.tags).toContain("organic");
|
|
754
|
-
expect(postUpdatePrevDoc.tags).not.toContain("organic");
|
|
755
|
-
|
|
756
|
-
// Reset for next test
|
|
757
|
-
postUpdateCalled = false;
|
|
758
|
-
postUpdateDoc = undefined;
|
|
759
|
-
postUpdatePrevDoc = undefined;
|
|
760
|
-
|
|
761
|
-
// Test PATCH operation with string array (update tag)
|
|
762
|
-
await agent
|
|
763
|
-
.patch(`/food/${apple._id}/tags/healthy`)
|
|
764
|
-
.send({tags: "super-healthy"})
|
|
765
|
-
.expect(200);
|
|
766
|
-
|
|
767
|
-
expect(postUpdateCalled).toBe(true);
|
|
768
|
-
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
769
|
-
|
|
770
|
-
// Verify the content is different (tag updated)
|
|
771
|
-
expect(postUpdateDoc.tags).toContain("super-healthy");
|
|
772
|
-
expect(postUpdatePrevDoc.tags).toContain("healthy");
|
|
773
|
-
expect(postUpdateDoc.tags).not.toContain("healthy");
|
|
774
|
-
expect(postUpdatePrevDoc.tags).not.toContain("super-healthy");
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
describe("standard methods", () => {
|
|
779
|
-
let notAdmin: any;
|
|
780
|
-
let admin: any;
|
|
781
|
-
let adminOther: any;
|
|
782
|
-
let agent: TestAgent;
|
|
783
|
-
|
|
784
|
-
let spinach: Food;
|
|
785
|
-
let apple: Food;
|
|
786
|
-
let carrots: Food;
|
|
787
|
-
let pizza: Food;
|
|
788
|
-
|
|
789
|
-
beforeEach(async () => {
|
|
790
|
-
[admin, notAdmin, adminOther] = await setupDb();
|
|
791
|
-
|
|
792
|
-
const results = (await Promise.all([
|
|
793
|
-
FoodModel.create({
|
|
794
|
-
calories: 1,
|
|
795
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
796
|
-
eatenBy: [admin._id],
|
|
797
|
-
hidden: false,
|
|
798
|
-
lastEatenWith: {
|
|
799
|
-
dressing: new Date("2021-12-03T19:00:30.000Z"),
|
|
800
|
-
},
|
|
801
|
-
name: "Spinach",
|
|
802
|
-
ownerId: notAdmin._id,
|
|
803
|
-
source: {
|
|
804
|
-
dateAdded: "2023-12-13T12:30:00.000Z",
|
|
805
|
-
href: "https://www.google.com",
|
|
806
|
-
name: "Brand",
|
|
807
|
-
},
|
|
808
|
-
}),
|
|
809
|
-
FoodModel.create({
|
|
810
|
-
calories: 100,
|
|
811
|
-
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
144
|
+
created: Date.now() - 10,
|
|
812
145
|
hidden: true,
|
|
813
146
|
name: "Apple",
|
|
814
|
-
ownerId: admin
|
|
815
|
-
tags: ["healthy"],
|
|
816
|
-
}),
|
|
817
|
-
FoodModel.create({
|
|
818
|
-
calories: 100,
|
|
819
|
-
created: new Date("2021-12-03T00:00:00.000Z"),
|
|
820
|
-
eatenBy: [admin._id, notAdmin._id],
|
|
821
|
-
hidden: false,
|
|
822
|
-
name: "Carrots",
|
|
823
|
-
ownerId: admin._id,
|
|
824
|
-
source: {
|
|
825
|
-
name: "USDA",
|
|
826
|
-
},
|
|
827
|
-
tags: ["healthy", "cheap"],
|
|
828
|
-
}),
|
|
829
|
-
FoodModel.create({
|
|
830
|
-
calories: 400,
|
|
831
|
-
created: new Date("2021-12-03T00:00:10.000Z"),
|
|
832
|
-
eatenBy: [adminOther._id],
|
|
833
|
-
hidden: false,
|
|
834
|
-
name: "Pizza",
|
|
835
|
-
ownerId: admin._id,
|
|
836
|
-
tags: ["cheap"],
|
|
147
|
+
ownerId: admin?._id,
|
|
837
148
|
}),
|
|
838
|
-
])
|
|
839
|
-
[spinach, apple, carrots, pizza] = results;
|
|
149
|
+
]);
|
|
840
150
|
app = getBaseServer();
|
|
841
151
|
setupAuth(app, UserModel as any);
|
|
842
|
-
addAuthRoutes(app, UserModel as any);
|
|
843
|
-
app.use(logRequests);
|
|
844
|
-
app.use(
|
|
845
|
-
"/food",
|
|
846
|
-
modelRouter(FoodModel, {
|
|
847
|
-
allowAnonymous: true,
|
|
848
|
-
defaultLimit: 2,
|
|
849
|
-
defaultQueryParams: {hidden: false},
|
|
850
|
-
maxLimit: 3,
|
|
851
|
-
permissions: {
|
|
852
|
-
create: [Permissions.IsAuthenticated],
|
|
853
|
-
delete: [Permissions.IsAdmin],
|
|
854
|
-
list: [Permissions.IsAny],
|
|
855
|
-
read: [Permissions.IsAny],
|
|
856
|
-
update: [Permissions.IsOwner],
|
|
857
|
-
},
|
|
858
|
-
populatePaths: [{path: "ownerId"}],
|
|
859
|
-
queryFields: ["hidden", "name", "calories", "created", "source.name", "tags", "eatenBy"],
|
|
860
|
-
sort: {created: "descending"},
|
|
861
|
-
})
|
|
862
|
-
);
|
|
863
|
-
server = supertest(app);
|
|
864
|
-
agent = await authAsUser(app, "notAdmin");
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
it("read default", async () => {
|
|
868
|
-
const res = await agent.get(`/food/${spinach._id}`).expect(200);
|
|
869
|
-
expect(res.body.data._id).toBe(spinach._id.toString());
|
|
870
|
-
// Ensure populate works
|
|
871
|
-
expect(res.body.data.ownerId._id).toBe(notAdmin.id);
|
|
872
|
-
// Ensure maps are properly transformed
|
|
873
|
-
expect(res.body.data.lastEatenWith).toEqual({
|
|
874
|
-
dressing: "2021-12-03T19:00:30.000Z",
|
|
875
|
-
});
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
it("list default", async () => {
|
|
879
|
-
const res = await agent.get("/food").expect(200);
|
|
880
|
-
expect(res.body.data).toHaveLength(2);
|
|
881
|
-
expect(res.body.data[0].id).toBe((spinach as any).id);
|
|
882
|
-
expect(res.body.data[0].ownerId._id).toBe(notAdmin.id);
|
|
883
|
-
expect(res.body.data[1].id).toBe((pizza as any).id);
|
|
884
|
-
expect(res.body.data[1].ownerId._id).toBe(admin.id);
|
|
885
|
-
// Check that mongoose Map is handled correctly.
|
|
886
|
-
expect(res.body.data[0].lastEatenWith).toEqual({
|
|
887
|
-
dressing: "2021-12-03T19:00:30.000Z",
|
|
888
|
-
});
|
|
889
|
-
expect(res.body.data[1].lastEatenWith).toEqual(undefined);
|
|
890
|
-
|
|
891
|
-
expect(res.body.more).toBe(true);
|
|
892
|
-
expect(res.body.total).toBe(3);
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
it("list limit", async () => {
|
|
896
|
-
const res = await agent.get("/food?limit=1").expect(200);
|
|
897
|
-
expect(res.body.data).toHaveLength(1);
|
|
898
|
-
expect(res.body.data[0].id).toBe((spinach as any).id);
|
|
899
|
-
expect(res.body.data[0].ownerId._id).toBe(notAdmin.id);
|
|
900
|
-
expect(res.body.more).toBe(true);
|
|
901
|
-
expect(res.body.total).toBe(3);
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
it("list limit over", async () => {
|
|
905
|
-
// This shouldn't be seen, it's the end of the list.
|
|
906
|
-
await FoodModel.create({
|
|
907
|
-
calories: 400,
|
|
908
|
-
created: new Date("2021-12-02T00:00:10.000Z"),
|
|
909
|
-
hidden: false,
|
|
910
|
-
name: "Pizza",
|
|
911
|
-
ownerId: admin._id,
|
|
912
|
-
});
|
|
913
|
-
const res = await agent.get("/food?limit=4").expect(200);
|
|
914
|
-
expect(res.body.data).toHaveLength(3);
|
|
915
|
-
expect(res.body.more).toBe(true);
|
|
916
|
-
expect(res.body.total).toBe(4);
|
|
917
|
-
expect(res.body.data[0].id).toBe((spinach as any).id);
|
|
918
|
-
expect(res.body.data[1].id).toBe((pizza as any).id);
|
|
919
|
-
expect(res.body.data[2].id).toBe((carrots as any).id);
|
|
920
|
-
|
|
921
|
-
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
|
922
|
-
'More than 3 results returned for foods without pagination, data may be silently truncated. req.query: {"limit":"4"}'
|
|
923
|
-
);
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
it("list page", async () => {
|
|
927
|
-
// Should skip to carrots since apples are hidden
|
|
928
|
-
const res = await agent.get("/food?limit=1&page=2").expect(200);
|
|
929
|
-
expect(res.body.data).toHaveLength(1);
|
|
930
|
-
expect(res.body.more).toBe(true);
|
|
931
|
-
expect(res.body.total).toBe(3);
|
|
932
|
-
expect(res.body.data[0].id).toBe((pizza as any).id);
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
it("list page 0 ", async () => {
|
|
936
|
-
const res = await agent.get("/food?limit=1&page=0").expect(400);
|
|
937
|
-
expect(res.body.title).toBe("Invalid page: 0");
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
it("list page with garbage ", async () => {
|
|
941
|
-
const res = await agent.get("/food?limit=1&page=abc").expect(400);
|
|
942
|
-
expect(res.body.title).toBe("Invalid page: abc");
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
it("list page over", async () => {
|
|
946
|
-
// Should skip to carrots since apples are hidden
|
|
947
|
-
const res = await agent.get("/food?limit=1&page=5").expect(200);
|
|
948
|
-
expect(res.body.data).toHaveLength(0);
|
|
949
|
-
expect(res.body.more).toBe(false);
|
|
950
|
-
expect(res.body.total).toBe(3);
|
|
951
|
-
});
|
|
952
|
-
|
|
953
|
-
it("list query params", async () => {
|
|
954
|
-
// Should skip to carrots since apples are hidden
|
|
955
|
-
const res = await agent.get("/food?hidden=true").expect(200);
|
|
956
|
-
expect(res.body.data).toHaveLength(1);
|
|
957
|
-
expect(res.body.more).toBe(false);
|
|
958
|
-
expect(res.body.total).toBe(1);
|
|
959
|
-
expect(res.body.data[0].id).toBe((apple as any).id);
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
it("list query params not in list", async () => {
|
|
963
|
-
// Should skip to carrots since apples are hidden
|
|
964
|
-
const res = await agent.get(`/food?ownerId=${admin._id}`).expect(400);
|
|
965
|
-
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
it("list query by nested param", async () => {
|
|
969
|
-
// Should skip to carrots since apples are hidden
|
|
970
|
-
const res = await agent.get("/food?source.name=USDA").expect(200);
|
|
971
|
-
expect(res.body.data).toHaveLength(1);
|
|
972
|
-
expect(res.body.total).toBe(1);
|
|
973
|
-
expect(res.body.data[0].id).toBe((carrots as any).id);
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
it("query by date", async () => {
|
|
977
|
-
const authRes = await server
|
|
978
|
-
.post("/auth/login")
|
|
979
|
-
.send({email: "admin@example.com", password: "securePassword"})
|
|
980
|
-
.expect(200);
|
|
981
|
-
const token = authRes.body.data.token;
|
|
982
|
-
|
|
983
|
-
// Inclusive
|
|
984
|
-
let res = await server
|
|
985
|
-
.get(
|
|
986
|
-
`/food?limit=3&${qs.stringify({
|
|
987
|
-
created: {
|
|
988
|
-
$gte: "2021-12-03T00:00:00.000Z",
|
|
989
|
-
$lte: "2021-12-03T00:00:20.000Z",
|
|
990
|
-
},
|
|
991
|
-
})}`
|
|
992
|
-
)
|
|
993
|
-
.set("authorization", `Bearer ${token}`)
|
|
994
|
-
.expect(200);
|
|
995
|
-
expect(res.body.data.map((d: any) => d.created)).toEqual(
|
|
996
|
-
expect.arrayContaining([
|
|
997
|
-
"2021-12-03T00:00:20.000Z",
|
|
998
|
-
"2021-12-03T00:00:10.000Z",
|
|
999
|
-
"2021-12-03T00:00:00.000Z",
|
|
1000
|
-
])
|
|
1001
|
-
);
|
|
1002
|
-
expect(res.body.data.map((d: any) => d.created)).toHaveLength(3);
|
|
1003
|
-
|
|
1004
|
-
// Inclusive one side
|
|
1005
|
-
res = await server
|
|
1006
|
-
.get(
|
|
1007
|
-
`/food?limit=3&${qs.stringify({
|
|
1008
|
-
created: {
|
|
1009
|
-
$gte: "2021-12-03T00:00:00.000Z",
|
|
1010
|
-
$lt: "2021-12-03T00:00:20.000Z",
|
|
1011
|
-
},
|
|
1012
|
-
})}`
|
|
1013
|
-
)
|
|
1014
|
-
.set("authorization", `Bearer ${token}`)
|
|
1015
|
-
.expect(200);
|
|
1016
|
-
expect(res.body.data.map((d: any) => d.created)).toEqual(
|
|
1017
|
-
expect.arrayContaining(["2021-12-03T00:00:10.000Z", "2021-12-03T00:00:00.000Z"])
|
|
1018
|
-
);
|
|
1019
|
-
expect(res.body.data.map((d: any) => d.created)).toHaveLength(2);
|
|
1020
|
-
|
|
1021
|
-
// Inclusive both sides
|
|
1022
|
-
res = await server
|
|
1023
|
-
.get(
|
|
1024
|
-
`/food?limit=3&${qs.stringify({
|
|
1025
|
-
created: {
|
|
1026
|
-
$gt: "2021-12-03T00:00:00.000Z",
|
|
1027
|
-
$lt: "2021-12-03T00:00:20.000Z",
|
|
1028
|
-
},
|
|
1029
|
-
})}`
|
|
1030
|
-
)
|
|
1031
|
-
.set("authorization", `Bearer ${token}`)
|
|
1032
|
-
.expect(200);
|
|
1033
|
-
const createdDates = res.body.data.map((d: any) => d.created);
|
|
1034
|
-
expect(createdDates).toEqual(expect.arrayContaining(["2021-12-03T00:00:10.000Z"]));
|
|
1035
|
-
expect(createdDates).toHaveLength(1);
|
|
1036
|
-
});
|
|
1037
|
-
|
|
1038
|
-
it("query with a space", async () => {
|
|
1039
|
-
const greenBeans = await FoodModel.create({
|
|
1040
|
-
calories: 102,
|
|
1041
|
-
created: Date.now() - 10,
|
|
1042
|
-
name: "Green Beans",
|
|
1043
|
-
ownerId: admin?._id,
|
|
1044
|
-
});
|
|
1045
|
-
const res = await agent.get(`/food?${qs.stringify({name: "Green Beans"})}`).expect(200);
|
|
1046
|
-
expect(res.body.data).toHaveLength(1);
|
|
1047
|
-
expect(res.body.data[0].id).toBe(greenBeans?.id);
|
|
1048
|
-
expect(res.body.data[0].name).toBe("Green Beans");
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
it("query with a regex", async () => {
|
|
1052
|
-
const greenBeans = await FoodModel.create({
|
|
1053
|
-
calories: 102,
|
|
1054
|
-
created: Date.now() - 10,
|
|
1055
|
-
name: "Green Beans",
|
|
1056
|
-
ownerId: admin?._id,
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
// Case sensitive does match correct casing
|
|
1060
|
-
let res = await agent.get(`/food?${qs.stringify({name: {$regex: "Green"}})}`).expect(200);
|
|
1061
|
-
expect(res.body.data).toHaveLength(1);
|
|
1062
|
-
expect(res.body.data[0].id).toBe(greenBeans?.id);
|
|
1063
|
-
expect(res.body.data[0].name).toBe("Green Beans");
|
|
1064
|
-
|
|
1065
|
-
// Fails with different casing and sensitive
|
|
1066
|
-
res = await agent.get(`/food?${qs.stringify({name: {$regex: "green"}})}`).expect(200);
|
|
1067
|
-
expect(res.body.data).toHaveLength(0);
|
|
1068
|
-
|
|
1069
|
-
// Case insensitive does match different casing
|
|
1070
|
-
res = await agent
|
|
1071
|
-
.get(`/food?${qs.stringify({name: {$options: "i", $regex: "green"}})}`)
|
|
1072
|
-
.expect(200);
|
|
1073
|
-
expect(res.body.data).toHaveLength(1);
|
|
1074
|
-
expect(res.body.data[0].id).toBe(greenBeans?.id);
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
it("query with an $in operator", async () => {
|
|
1078
|
-
// Query including a hidden food
|
|
1079
|
-
let res = await server
|
|
1080
|
-
.get(
|
|
1081
|
-
`/food?${qs.stringify({
|
|
1082
|
-
name: {
|
|
1083
|
-
$in: ["Apple", "Spinach"],
|
|
1084
|
-
},
|
|
1085
|
-
})}`
|
|
1086
|
-
)
|
|
1087
|
-
.expect(200);
|
|
1088
|
-
const names1 = res.body.data.map((d: any) => d.name);
|
|
1089
|
-
expect(names1).toEqual(expect.arrayContaining(["Spinach"]));
|
|
1090
|
-
expect(names1).toHaveLength(1);
|
|
1091
|
-
|
|
1092
|
-
// Query without hidden food.
|
|
1093
|
-
res = await server
|
|
1094
|
-
.get(
|
|
1095
|
-
`/food?${qs.stringify({
|
|
1096
|
-
name: {
|
|
1097
|
-
$in: ["Carrots", "Spinach"],
|
|
1098
|
-
},
|
|
1099
|
-
})}`
|
|
1100
|
-
)
|
|
1101
|
-
.expect(200);
|
|
1102
|
-
const names2 = res.body.data.map((d: any) => d.name);
|
|
1103
|
-
expect(names2).toEqual(expect.arrayContaining(["Spinach", "Carrots"]));
|
|
1104
|
-
expect(names2).toHaveLength(2);
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
it("query with an $in for _ids in nested object", async () => {
|
|
1108
|
-
// Query including a hidden food
|
|
1109
|
-
const res = await server
|
|
1110
|
-
.get(
|
|
1111
|
-
`/food?${qs.stringify({
|
|
1112
|
-
eatenBy: {
|
|
1113
|
-
$in: [notAdmin._id.toString(), adminOther._id.toString()],
|
|
1114
|
-
},
|
|
1115
|
-
})}`
|
|
1116
|
-
)
|
|
1117
|
-
.expect(200);
|
|
1118
|
-
expect(res.body.more).toBe(false);
|
|
1119
|
-
expect(res.body.total).toBe(2);
|
|
1120
|
-
expect(res.body.data).toHaveLength(2);
|
|
1121
|
-
const names3 = res.body.data.map((d: any) => d.name);
|
|
1122
|
-
expect(names3).toEqual(expect.arrayContaining(["Carrots", "Pizza"]));
|
|
1123
|
-
expect(names3).toHaveLength(2);
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
it("query $and operator on same field", async () => {
|
|
1127
|
-
const res = await agent
|
|
1128
|
-
.get(`/food?${qs.stringify({$and: [{tags: "healthy"}, {tags: "cheap"}]})}`)
|
|
1129
|
-
.expect(200);
|
|
1130
|
-
expect(res.body.data).toHaveLength(1);
|
|
1131
|
-
expect(res.body.data[0].id).toBe(carrots?._id.toString());
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
it("query $and operator on same field, nested objects", async () => {
|
|
1135
|
-
const res = await agent
|
|
1136
|
-
.get(
|
|
1137
|
-
`/food?${qs.stringify({
|
|
1138
|
-
$and: [{eatenBy: admin.id}, {eatenBy: notAdmin.id}],
|
|
1139
|
-
})}`
|
|
1140
|
-
)
|
|
1141
|
-
.expect(200);
|
|
1142
|
-
expect(res.body.data).toHaveLength(1);
|
|
1143
|
-
expect(res.body.data[0].id).toBe(carrots?._id.toString());
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
it("query $or operator on same field", async () => {
|
|
1147
|
-
const res = await agent
|
|
1148
|
-
.get(`/food?${qs.stringify({$or: [{name: "Carrots"}, {name: "Pizza"}]})}`)
|
|
1149
|
-
.expect(200);
|
|
1150
|
-
expect(res.body.data).toHaveLength(2);
|
|
1151
|
-
// Only carrots matches both
|
|
1152
|
-
const ids1 = res.body.data.map((d) => d.id);
|
|
1153
|
-
expect(ids1).toEqual(
|
|
1154
|
-
expect.arrayContaining([carrots?._id.toString(), pizza?._id.toString()])
|
|
1155
|
-
);
|
|
1156
|
-
expect(ids1).toHaveLength(2);
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
it("query $and operator on same field, nested objects", async () => {
|
|
1160
|
-
const res = await agent
|
|
1161
|
-
.get(
|
|
1162
|
-
`/food?${qs.stringify({
|
|
1163
|
-
$or: [{eatenBy: admin.id}, {eatenBy: notAdmin.id}],
|
|
1164
|
-
limit: 3,
|
|
1165
|
-
})}`
|
|
1166
|
-
)
|
|
1167
|
-
.expect(200);
|
|
1168
|
-
expect(res.body.data).toHaveLength(2);
|
|
1169
|
-
const ids2 = res.body.data.map((d) => d.id);
|
|
1170
|
-
expect(ids2).toEqual(
|
|
1171
|
-
expect.arrayContaining([carrots?._id.toString(), spinach?._id.toString()])
|
|
1172
|
-
);
|
|
1173
|
-
expect(ids2).toHaveLength(2);
|
|
1174
|
-
});
|
|
1175
|
-
|
|
1176
|
-
it("query $and and $or are rejected if field is not in queryFields", async () => {
|
|
1177
|
-
let res = await agent
|
|
1178
|
-
.get(`/food?${qs.stringify({$and: [{ownerId: "healthy"}, {tags: "cheap"}]})}`)
|
|
1179
|
-
.expect(400);
|
|
1180
|
-
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
1181
|
-
// Check in the other order
|
|
1182
|
-
res = await agent
|
|
1183
|
-
.get(`/food?${qs.stringify({$and: [{tags: "cheap"}, {ownerId: "healthy"}]})}`)
|
|
1184
|
-
.expect(400);
|
|
1185
|
-
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
1186
|
-
|
|
1187
|
-
res = await agent
|
|
1188
|
-
.get(`/food?${qs.stringify({$or: [{tags: "cheap"}, {ownerId: "healthy"}]})}`)
|
|
1189
|
-
.expect(400);
|
|
1190
|
-
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
|
-
it("query with a number", async () => {
|
|
1194
|
-
const res = await agent.get("/food?calories=100").expect(200);
|
|
1195
|
-
expect(res.body.data).toHaveLength(1);
|
|
1196
|
-
expect(res.body.data[0].id).toBe(carrots?._id.toString());
|
|
1197
|
-
});
|
|
1198
|
-
|
|
1199
|
-
it("update", async () => {
|
|
1200
|
-
let res = await agent.patch(`/food/${spinach._id}`).send({name: "Kale"}).expect(200);
|
|
1201
|
-
expect(res.body.data.name).toBe("Kale");
|
|
1202
|
-
expect(res.body.data.calories).toBe(1);
|
|
1203
|
-
expect(res.body.data.hidden).toBe(false);
|
|
1204
|
-
|
|
1205
|
-
// Update a Map field.
|
|
1206
|
-
res = await agent
|
|
1207
|
-
.patch(`/food/${spinach._id}`)
|
|
1208
|
-
.send({lastEatenWith: {dressing: "2023-12-03T00:00:20.000Z"}})
|
|
1209
|
-
.expect(200);
|
|
1210
|
-
expect(res.body.data.name).toBe("Kale");
|
|
1211
|
-
expect(res.body.data.calories).toBe(1);
|
|
1212
|
-
expect(res.body.data.hidden).toBe(false);
|
|
1213
|
-
expect(res.body.data.lastEatenWith).toEqual({
|
|
1214
|
-
dressing: "2023-12-03T00:00:20.000Z",
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
// Update a Map field.
|
|
1218
|
-
res = await agent
|
|
1219
|
-
.patch(`/food/${spinach._id}`)
|
|
1220
|
-
.send({
|
|
1221
|
-
lastEatenWith: {
|
|
1222
|
-
cucumber: "2023-12-04T12:00:20.000Z",
|
|
1223
|
-
dressing: "2023-12-03T00:00:20.000Z",
|
|
1224
|
-
},
|
|
1225
|
-
})
|
|
1226
|
-
.expect(200);
|
|
1227
|
-
expect(res.body.data.lastEatenWith).toEqual({
|
|
1228
|
-
cucumber: "2023-12-04T12:00:20.000Z",
|
|
1229
|
-
dressing: "2023-12-03T00:00:20.000Z",
|
|
1230
|
-
});
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
it("update using dot notation", async () => {
|
|
1234
|
-
// Allows updating a single field in a nested object
|
|
1235
|
-
const res = await agent
|
|
1236
|
-
.patch(`/food/${spinach._id}`)
|
|
1237
|
-
.send({"source.href": "https://food.com"})
|
|
1238
|
-
.expect(200);
|
|
1239
|
-
// Assert the field was updated with dot notation.
|
|
1240
|
-
expect(res.body.data.source.href).toBe("https://food.com");
|
|
1241
|
-
// Assert these fields haven't changed.
|
|
1242
|
-
expect(res.body.data.source.name).toBe("Brand");
|
|
1243
|
-
expect(res.body.data.source.dateAdded).toBe("2023-12-13T12:30:00.000Z");
|
|
1244
|
-
|
|
1245
|
-
const dbSpinach = await FoodModel.findById(spinach._id);
|
|
1246
|
-
expect(dbSpinach?.source.href).toBe("https://food.com");
|
|
1247
|
-
expect(dbSpinach?.source.name).toBe("Brand");
|
|
1248
|
-
expect(dbSpinach?.source.dateAdded).toBe("2023-12-13T12:30:00.000Z");
|
|
1249
|
-
});
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
describe("populate", () => {
|
|
1253
|
-
let admin: any;
|
|
1254
|
-
let notAdmin: any;
|
|
1255
|
-
let agent: TestAgent;
|
|
1256
|
-
|
|
1257
|
-
let spinach: Food;
|
|
1258
|
-
|
|
1259
|
-
beforeEach(async () => {
|
|
1260
|
-
[admin, notAdmin] = await setupDb();
|
|
1261
|
-
|
|
1262
|
-
[spinach] = await Promise.all([
|
|
1263
|
-
FoodModel.create({
|
|
1264
|
-
calories: 1,
|
|
1265
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
1266
|
-
hidden: false,
|
|
1267
|
-
name: "Spinach",
|
|
1268
|
-
ownerId: admin._id,
|
|
1269
|
-
source: {
|
|
1270
|
-
name: "Brand",
|
|
1271
|
-
},
|
|
1272
|
-
}),
|
|
1273
|
-
FoodModel.create({
|
|
1274
|
-
calories: 1,
|
|
1275
|
-
created: new Date("2022-12-03T00:00:20.000Z"),
|
|
1276
|
-
hidden: false,
|
|
1277
|
-
name: "Carrots",
|
|
1278
|
-
ownerId: notAdmin._id,
|
|
1279
|
-
source: {
|
|
1280
|
-
name: "User",
|
|
1281
|
-
},
|
|
1282
|
-
}),
|
|
1283
|
-
]);
|
|
1284
|
-
app = getBaseServer();
|
|
1285
|
-
setupAuth(app, UserModel as any);
|
|
1286
|
-
addAuthRoutes(app, UserModel as any);
|
|
1287
|
-
app.use(
|
|
1288
|
-
"/food",
|
|
1289
|
-
modelRouter(FoodModel, {
|
|
1290
|
-
allowAnonymous: true,
|
|
1291
|
-
permissions: {
|
|
1292
|
-
create: [Permissions.IsAny],
|
|
1293
|
-
delete: [Permissions.IsAny],
|
|
1294
|
-
list: [Permissions.IsAny],
|
|
1295
|
-
read: [Permissions.IsAny],
|
|
1296
|
-
update: [Permissions.IsAny],
|
|
1297
|
-
},
|
|
1298
|
-
populatePaths: [{fields: ["email"], path: "ownerId"}],
|
|
1299
|
-
sort: "-created",
|
|
1300
|
-
})
|
|
1301
|
-
);
|
|
1302
|
-
server = supertest(app);
|
|
1303
|
-
agent = await authAsUser(app, "notAdmin");
|
|
1304
|
-
});
|
|
1305
|
-
|
|
1306
|
-
it("lists with populate", async () => {
|
|
1307
|
-
const res = await agent.get("/food").expect(200);
|
|
1308
|
-
expect(res.body.data).toHaveLength(2);
|
|
1309
|
-
const [carrots, spin] = res.body.data;
|
|
1310
|
-
expect(carrots.ownerId._id).toBe(notAdmin._id.toString());
|
|
1311
|
-
expect(carrots.ownerId.email).toBe(notAdmin.email);
|
|
1312
|
-
expect(carrots.ownerId.name).toBeUndefined();
|
|
1313
|
-
expect(spin.ownerId._id).toBe(admin._id.toString());
|
|
1314
|
-
expect(spin.ownerId.email).toBe(admin.email);
|
|
1315
|
-
expect(spin.ownerId.name).toBeUndefined();
|
|
1316
|
-
});
|
|
1317
|
-
|
|
1318
|
-
it("reads with populate", async () => {
|
|
1319
|
-
const res = await agent.get(`/food/${spinach._id}`).expect(200);
|
|
1320
|
-
expect(res.body.data.ownerId._id).toBe(admin._id.toString());
|
|
1321
|
-
expect(res.body.data.ownerId.email).toBe(admin.email);
|
|
1322
|
-
expect(res.body.data.ownerId.name).toBeUndefined();
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
it("creates with populate", async () => {
|
|
1326
|
-
const res = await server
|
|
1327
|
-
.post("/food")
|
|
1328
|
-
.send({
|
|
1329
|
-
calories: 15,
|
|
1330
|
-
name: "Broccoli",
|
|
1331
|
-
ownerId: admin._id,
|
|
1332
|
-
})
|
|
1333
|
-
.expect(201);
|
|
1334
|
-
expect(res.body.data.ownerId._id).toBe(admin._id.toString());
|
|
1335
|
-
expect(res.body.data.ownerId.email).toBe(admin.email);
|
|
1336
|
-
expect(res.body.data.ownerId.name).toBeUndefined();
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
it("updates with populate", async () => {
|
|
1340
|
-
const res = await server
|
|
1341
|
-
.patch(`/food/${spinach._id}`)
|
|
1342
|
-
.send({
|
|
1343
|
-
name: "NotSpinach",
|
|
1344
|
-
})
|
|
1345
|
-
.expect(200);
|
|
1346
|
-
expect(res.body.data.ownerId._id).toBe(admin._id.toString());
|
|
1347
|
-
expect(res.body.data.ownerId.email).toBe(admin.email);
|
|
1348
|
-
expect(res.body.data.ownerId.name).toBeUndefined();
|
|
1349
|
-
});
|
|
1350
|
-
});
|
|
1351
|
-
|
|
1352
|
-
describe("responseHandler", () => {
|
|
1353
|
-
let admin: any;
|
|
1354
|
-
let agent: TestAgent;
|
|
1355
|
-
|
|
1356
|
-
let spinach: Food;
|
|
1357
|
-
|
|
1358
|
-
beforeEach(async () => {
|
|
1359
|
-
[admin] = await setupDb();
|
|
1360
|
-
|
|
1361
|
-
[spinach] = await Promise.all([
|
|
1362
|
-
FoodModel.create({
|
|
1363
|
-
calories: 1,
|
|
1364
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
1365
|
-
hidden: false,
|
|
1366
|
-
name: "Spinach",
|
|
1367
|
-
ownerId: admin._id,
|
|
1368
|
-
source: {
|
|
1369
|
-
name: "Brand",
|
|
1370
|
-
},
|
|
1371
|
-
}),
|
|
1372
|
-
FoodModel.create({
|
|
1373
|
-
calories: 100,
|
|
1374
|
-
created: Date.now() - 10,
|
|
1375
|
-
hidden: true,
|
|
1376
|
-
name: "Apple",
|
|
1377
|
-
ownerId: admin?._id,
|
|
1378
|
-
}),
|
|
1379
|
-
]);
|
|
1380
|
-
app = getBaseServer();
|
|
1381
|
-
setupAuth(app, UserModel as any);
|
|
1382
|
-
addAuthRoutes(app, UserModel as any);
|
|
1383
|
-
app.use(
|
|
1384
|
-
"/food",
|
|
1385
|
-
modelRouter(FoodModel, {
|
|
1386
|
-
allowAnonymous: true,
|
|
1387
|
-
permissions: {
|
|
1388
|
-
create: [Permissions.IsAny],
|
|
1389
|
-
delete: [Permissions.IsAny],
|
|
1390
|
-
list: [Permissions.IsAny],
|
|
1391
|
-
read: [Permissions.IsAny],
|
|
1392
|
-
update: [Permissions.IsAny],
|
|
1393
|
-
},
|
|
1394
|
-
responseHandler: (data, method) => {
|
|
1395
|
-
if (method === "list") {
|
|
1396
|
-
return (data as any).map((d: any) => ({
|
|
1397
|
-
foo: "bar",
|
|
1398
|
-
id: (d as any)._id,
|
|
1399
|
-
}));
|
|
1400
|
-
}
|
|
1401
|
-
return {
|
|
1402
|
-
foo: "bar",
|
|
1403
|
-
id: (data as any)._id,
|
|
1404
|
-
};
|
|
1405
|
-
},
|
|
1406
|
-
})
|
|
1407
|
-
);
|
|
1408
|
-
server = supertest(app);
|
|
1409
|
-
agent = await authAsUser(app, "notAdmin");
|
|
1410
|
-
});
|
|
1411
|
-
|
|
1412
|
-
it("reads with serialize", async () => {
|
|
1413
|
-
const res = await agent.get(`/food/${spinach._id}`).expect(200);
|
|
1414
|
-
expect(res.body.data.ownerId).toBeUndefined();
|
|
1415
|
-
expect(res.body.data.id).toBe(spinach._id.toString());
|
|
1416
|
-
expect(res.body.data.foo).toBe("bar");
|
|
1417
|
-
});
|
|
1418
|
-
|
|
1419
|
-
it("list with serialize", async () => {
|
|
1420
|
-
const res = await agent.get("/food").expect(200);
|
|
1421
|
-
expect(res.body.data[0].ownerId).toBeUndefined();
|
|
1422
|
-
expect(res.body.data[1].ownerId).toBeUndefined();
|
|
1423
|
-
|
|
1424
|
-
expect(res.body.data[0].id).toBeDefined();
|
|
1425
|
-
expect(res.body.data[0].foo).toBe("bar");
|
|
1426
|
-
expect(res.body.data[1].id).toBeDefined();
|
|
1427
|
-
expect(res.body.data[1].foo).toBe("bar");
|
|
1428
|
-
});
|
|
1429
|
-
});
|
|
1430
|
-
|
|
1431
|
-
describe("plugins", () => {
|
|
1432
|
-
let agent: TestAgent;
|
|
1433
|
-
|
|
1434
|
-
beforeEach(async () => {
|
|
1435
|
-
await setupDb();
|
|
1436
|
-
app = getBaseServer();
|
|
1437
|
-
setupAuth(app, UserModel as any);
|
|
1438
|
-
addAuthRoutes(app, UserModel as any);
|
|
1439
|
-
app.use(
|
|
1440
|
-
"/users",
|
|
1441
|
-
modelRouter(UserModel, {
|
|
1442
|
-
allowAnonymous: true,
|
|
1443
|
-
permissions: {
|
|
1444
|
-
create: [Permissions.IsAny],
|
|
1445
|
-
delete: [Permissions.IsAny],
|
|
1446
|
-
list: [Permissions.IsAny],
|
|
1447
|
-
read: [Permissions.IsAny],
|
|
1448
|
-
update: [Permissions.IsAny],
|
|
1449
|
-
},
|
|
1450
|
-
})
|
|
1451
|
-
);
|
|
1452
|
-
server = supertest(app);
|
|
1453
|
-
agent = await authAsUser(app, "notAdmin");
|
|
1454
|
-
});
|
|
1455
|
-
|
|
1456
|
-
it("check that security fields are filtered", async () => {
|
|
1457
|
-
const res = await agent.get("/users").expect(200);
|
|
1458
|
-
expect(res.body.data[0].email).toBeDefined();
|
|
1459
|
-
expect(res.body.data[0].token).toBeUndefined();
|
|
1460
|
-
expect(res.body.data[0].hash).toBeUndefined();
|
|
1461
|
-
expect(res.body.data[0].salt).toBeUndefined();
|
|
1462
|
-
});
|
|
1463
|
-
});
|
|
1464
|
-
|
|
1465
|
-
describe("error handling", () => {
|
|
1466
|
-
let admin: any;
|
|
1467
|
-
let agent: TestAgent;
|
|
1468
|
-
let spinach: Food;
|
|
1469
|
-
|
|
1470
|
-
beforeEach(async () => {
|
|
1471
|
-
[admin] = await setupDb();
|
|
1472
|
-
|
|
1473
|
-
spinach = await FoodModel.create({
|
|
1474
|
-
calories: 1,
|
|
1475
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
1476
|
-
hidden: false,
|
|
1477
|
-
name: "Spinach",
|
|
1478
|
-
ownerId: admin._id,
|
|
1479
|
-
source: {
|
|
1480
|
-
name: "Brand",
|
|
1481
|
-
},
|
|
1482
|
-
});
|
|
1483
|
-
|
|
1484
|
-
app = getBaseServer();
|
|
1485
|
-
setupAuth(app, UserModel as any);
|
|
1486
|
-
addAuthRoutes(app, UserModel as any);
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
it("PUT returns 500 not supported", async () => {
|
|
1490
|
-
app.use(
|
|
1491
|
-
"/food",
|
|
1492
|
-
modelRouter(FoodModel, {
|
|
1493
|
-
allowAnonymous: true,
|
|
1494
|
-
permissions: {
|
|
1495
|
-
create: [Permissions.IsAny],
|
|
1496
|
-
delete: [Permissions.IsAny],
|
|
1497
|
-
list: [Permissions.IsAny],
|
|
1498
|
-
read: [Permissions.IsAny],
|
|
1499
|
-
update: [Permissions.IsAny],
|
|
1500
|
-
},
|
|
1501
|
-
})
|
|
1502
|
-
);
|
|
1503
|
-
server = supertest(app);
|
|
1504
|
-
|
|
1505
|
-
const res = await server.put(`/food/${spinach._id}`).send({name: "Kale"}).expect(500);
|
|
1506
|
-
expect(res.body.title).toBe("PUT is not supported.");
|
|
1507
|
-
});
|
|
1508
|
-
|
|
1509
|
-
it("preCreate returning undefined throws error", async () => {
|
|
1510
|
-
app.use(
|
|
1511
|
-
"/food",
|
|
1512
|
-
modelRouter(FoodModel, {
|
|
1513
|
-
allowAnonymous: true,
|
|
1514
|
-
permissions: {
|
|
1515
|
-
create: [Permissions.IsAny],
|
|
1516
|
-
delete: [Permissions.IsAny],
|
|
1517
|
-
list: [Permissions.IsAny],
|
|
1518
|
-
read: [Permissions.IsAny],
|
|
1519
|
-
update: [Permissions.IsAny],
|
|
1520
|
-
},
|
|
1521
|
-
preCreate: () => undefined as any,
|
|
1522
|
-
})
|
|
1523
|
-
);
|
|
1524
|
-
server = supertest(app);
|
|
1525
|
-
|
|
1526
|
-
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(403);
|
|
1527
|
-
expect(res.body.title).toBe("Create not allowed");
|
|
1528
|
-
expect(res.body.detail).toBe("A body must be returned from preCreate");
|
|
1529
|
-
});
|
|
1530
|
-
|
|
1531
|
-
it("preUpdate returning undefined throws error", async () => {
|
|
1532
|
-
app.use(
|
|
1533
|
-
"/food",
|
|
1534
|
-
modelRouter(FoodModel, {
|
|
1535
|
-
allowAnonymous: true,
|
|
1536
|
-
permissions: {
|
|
1537
|
-
create: [Permissions.IsAny],
|
|
1538
|
-
delete: [Permissions.IsAny],
|
|
1539
|
-
list: [Permissions.IsAny],
|
|
1540
|
-
read: [Permissions.IsAny],
|
|
1541
|
-
update: [Permissions.IsAny],
|
|
1542
|
-
},
|
|
1543
|
-
preUpdate: () => undefined as any,
|
|
1544
|
-
})
|
|
1545
|
-
);
|
|
1546
|
-
server = supertest(app);
|
|
1547
|
-
|
|
1548
|
-
const res = await server.patch(`/food/${spinach._id}`).send({name: "Kale"}).expect(403);
|
|
1549
|
-
expect(res.body.title).toBe("Update not allowed");
|
|
1550
|
-
expect(res.body.detail).toBe("A body must be returned from preUpdate");
|
|
1551
|
-
});
|
|
1552
|
-
|
|
1553
|
-
it("preDelete returning undefined throws error", async () => {
|
|
1554
|
-
app.use(
|
|
1555
|
-
"/food",
|
|
1556
|
-
modelRouter(FoodModel, {
|
|
1557
|
-
allowAnonymous: true,
|
|
1558
|
-
permissions: {
|
|
1559
|
-
create: [Permissions.IsAny],
|
|
1560
|
-
delete: [Permissions.IsAny],
|
|
1561
|
-
list: [Permissions.IsAny],
|
|
1562
|
-
read: [Permissions.IsAny],
|
|
1563
|
-
update: [Permissions.IsAny],
|
|
1564
|
-
},
|
|
1565
|
-
preDelete: () => undefined as any,
|
|
1566
|
-
})
|
|
1567
|
-
);
|
|
1568
|
-
server = supertest(app);
|
|
1569
|
-
agent = await authAsUser(app, "notAdmin");
|
|
1570
|
-
|
|
1571
|
-
const res = await agent.delete(`/food/${spinach._id}`).expect(403);
|
|
1572
|
-
expect(res.body.title).toBe("Delete not allowed");
|
|
1573
|
-
expect(res.body.detail).toBe("A body must be returned from preDelete");
|
|
1574
|
-
});
|
|
1575
|
-
|
|
1576
|
-
it("postCreate hook error is handled", async () => {
|
|
1577
|
-
app.use(
|
|
1578
|
-
"/food",
|
|
1579
|
-
modelRouter(FoodModel, {
|
|
1580
|
-
allowAnonymous: true,
|
|
1581
|
-
permissions: {
|
|
1582
|
-
create: [Permissions.IsAny],
|
|
1583
|
-
delete: [Permissions.IsAny],
|
|
1584
|
-
list: [Permissions.IsAny],
|
|
1585
|
-
read: [Permissions.IsAny],
|
|
1586
|
-
update: [Permissions.IsAny],
|
|
1587
|
-
},
|
|
1588
|
-
postCreate: () => {
|
|
1589
|
-
throw new Error("postCreate failed");
|
|
1590
|
-
},
|
|
1591
|
-
})
|
|
1592
|
-
);
|
|
1593
|
-
server = supertest(app);
|
|
1594
|
-
|
|
1595
|
-
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(400);
|
|
1596
|
-
expect(res.body.title).toContain("postCreate hook error");
|
|
1597
|
-
});
|
|
1598
|
-
|
|
1599
|
-
it("postUpdate hook error is handled", async () => {
|
|
1600
|
-
app.use(
|
|
1601
|
-
"/food",
|
|
1602
|
-
modelRouter(FoodModel, {
|
|
1603
|
-
allowAnonymous: true,
|
|
1604
|
-
permissions: {
|
|
1605
|
-
create: [Permissions.IsAny],
|
|
1606
|
-
delete: [Permissions.IsAny],
|
|
1607
|
-
list: [Permissions.IsAny],
|
|
1608
|
-
read: [Permissions.IsAny],
|
|
1609
|
-
update: [Permissions.IsAny],
|
|
1610
|
-
},
|
|
1611
|
-
postUpdate: () => {
|
|
1612
|
-
throw new Error("postUpdate failed");
|
|
1613
|
-
},
|
|
1614
|
-
})
|
|
1615
|
-
);
|
|
1616
|
-
server = supertest(app);
|
|
1617
|
-
|
|
1618
|
-
const res = await server.patch(`/food/${spinach._id}`).send({name: "Kale"}).expect(400);
|
|
1619
|
-
expect(res.body.title).toContain("postUpdate hook error");
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
it("postDelete hook error is handled", async () => {
|
|
1623
|
-
app.use(
|
|
1624
|
-
"/food",
|
|
1625
|
-
modelRouter(FoodModel, {
|
|
1626
|
-
allowAnonymous: true,
|
|
1627
|
-
permissions: {
|
|
1628
|
-
create: [Permissions.IsAny],
|
|
1629
|
-
delete: [Permissions.IsAny],
|
|
1630
|
-
list: [Permissions.IsAny],
|
|
1631
|
-
read: [Permissions.IsAny],
|
|
1632
|
-
update: [Permissions.IsAny],
|
|
1633
|
-
},
|
|
1634
|
-
postDelete: () => {
|
|
1635
|
-
throw new Error("postDelete failed");
|
|
1636
|
-
},
|
|
1637
|
-
})
|
|
1638
|
-
);
|
|
1639
|
-
server = supertest(app);
|
|
1640
|
-
agent = await authAsUser(app, "notAdmin");
|
|
1641
|
-
|
|
1642
|
-
const res = await agent.delete(`/food/${spinach._id}`).expect(400);
|
|
1643
|
-
expect(res.body.title).toContain("postDelete hook error");
|
|
1644
|
-
});
|
|
1645
|
-
|
|
1646
|
-
it("responseHandler error in read is handled", async () => {
|
|
1647
|
-
app.use(
|
|
1648
|
-
"/food",
|
|
1649
|
-
modelRouter(FoodModel, {
|
|
1650
|
-
allowAnonymous: true,
|
|
1651
|
-
permissions: {
|
|
1652
|
-
create: [Permissions.IsAny],
|
|
1653
|
-
delete: [Permissions.IsAny],
|
|
1654
|
-
list: [Permissions.IsAny],
|
|
1655
|
-
read: [Permissions.IsAny],
|
|
1656
|
-
update: [Permissions.IsAny],
|
|
1657
|
-
},
|
|
1658
|
-
responseHandler: (_data, method) => {
|
|
1659
|
-
if (method === "read") {
|
|
1660
|
-
throw new Error("responseHandler read failed");
|
|
1661
|
-
}
|
|
1662
|
-
return {} as any;
|
|
1663
|
-
},
|
|
1664
|
-
})
|
|
1665
|
-
);
|
|
1666
|
-
server = supertest(app);
|
|
1667
|
-
|
|
1668
|
-
const res = await server.get(`/food/${spinach._id}`).expect(500);
|
|
1669
|
-
expect(res.body.title).toContain("responseHandler error");
|
|
1670
|
-
});
|
|
1671
|
-
|
|
1672
|
-
it("responseHandler error in create is handled", async () => {
|
|
1673
|
-
app.use(
|
|
1674
|
-
"/food",
|
|
1675
|
-
modelRouter(FoodModel, {
|
|
1676
|
-
allowAnonymous: true,
|
|
1677
|
-
permissions: {
|
|
1678
|
-
create: [Permissions.IsAny],
|
|
1679
|
-
delete: [Permissions.IsAny],
|
|
1680
|
-
list: [Permissions.IsAny],
|
|
1681
|
-
read: [Permissions.IsAny],
|
|
1682
|
-
update: [Permissions.IsAny],
|
|
1683
|
-
},
|
|
1684
|
-
responseHandler: (_data, method) => {
|
|
1685
|
-
if (method === "create") {
|
|
1686
|
-
throw new Error("responseHandler create failed");
|
|
1687
|
-
}
|
|
1688
|
-
return {} as any;
|
|
1689
|
-
},
|
|
1690
|
-
})
|
|
1691
|
-
);
|
|
1692
|
-
server = supertest(app);
|
|
1693
|
-
|
|
1694
|
-
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(500);
|
|
1695
|
-
expect(res.body.title).toContain("responseHandler error");
|
|
1696
|
-
});
|
|
1697
|
-
|
|
1698
|
-
it("responseHandler error in update is handled", async () => {
|
|
1699
|
-
app.use(
|
|
1700
|
-
"/food",
|
|
1701
|
-
modelRouter(FoodModel, {
|
|
1702
|
-
allowAnonymous: true,
|
|
1703
|
-
permissions: {
|
|
1704
|
-
create: [Permissions.IsAny],
|
|
1705
|
-
delete: [Permissions.IsAny],
|
|
1706
|
-
list: [Permissions.IsAny],
|
|
1707
|
-
read: [Permissions.IsAny],
|
|
1708
|
-
update: [Permissions.IsAny],
|
|
1709
|
-
},
|
|
1710
|
-
responseHandler: (_data, method) => {
|
|
1711
|
-
if (method === "update") {
|
|
1712
|
-
throw new Error("responseHandler update failed");
|
|
1713
|
-
}
|
|
1714
|
-
return {} as any;
|
|
1715
|
-
},
|
|
1716
|
-
})
|
|
1717
|
-
);
|
|
1718
|
-
server = supertest(app);
|
|
1719
|
-
|
|
1720
|
-
const res = await server.patch(`/food/${spinach._id}`).send({name: "Kale"}).expect(500);
|
|
1721
|
-
expect(res.body.title).toContain("responseHandler error");
|
|
1722
|
-
});
|
|
1723
|
-
|
|
1724
|
-
it("responseHandler error in list is handled", async () => {
|
|
1725
|
-
app.use(
|
|
1726
|
-
"/food",
|
|
1727
|
-
modelRouter(FoodModel, {
|
|
1728
|
-
allowAnonymous: true,
|
|
1729
|
-
permissions: {
|
|
1730
|
-
create: [Permissions.IsAny],
|
|
1731
|
-
delete: [Permissions.IsAny],
|
|
1732
|
-
list: [Permissions.IsAny],
|
|
1733
|
-
read: [Permissions.IsAny],
|
|
1734
|
-
update: [Permissions.IsAny],
|
|
1735
|
-
},
|
|
1736
|
-
responseHandler: (_data, method) => {
|
|
1737
|
-
if (method === "list") {
|
|
1738
|
-
throw new Error("responseHandler list failed");
|
|
1739
|
-
}
|
|
1740
|
-
return {} as any;
|
|
1741
|
-
},
|
|
1742
|
-
})
|
|
1743
|
-
);
|
|
1744
|
-
server = supertest(app);
|
|
1745
|
-
|
|
1746
|
-
const res = await server.get("/food").expect(500);
|
|
1747
|
-
expect(res.body.title).toContain("responseHandler error");
|
|
1748
|
-
});
|
|
1749
|
-
|
|
1750
|
-
it("list with non-array responseHandler returns data directly", async () => {
|
|
1751
|
-
app.use(
|
|
1752
|
-
"/food",
|
|
1753
|
-
modelRouter(FoodModel, {
|
|
1754
|
-
allowAnonymous: true,
|
|
1755
|
-
permissions: {
|
|
1756
|
-
create: [Permissions.IsAny],
|
|
1757
|
-
delete: [Permissions.IsAny],
|
|
1758
|
-
list: [Permissions.IsAny],
|
|
1759
|
-
read: [Permissions.IsAny],
|
|
1760
|
-
update: [Permissions.IsAny],
|
|
1761
|
-
},
|
|
1762
|
-
responseHandler: (_data, method) => {
|
|
1763
|
-
if (method === "list") {
|
|
1764
|
-
return {custom: "response"} as any;
|
|
1765
|
-
}
|
|
1766
|
-
return {} as any;
|
|
1767
|
-
},
|
|
1768
|
-
})
|
|
1769
|
-
);
|
|
1770
|
-
server = supertest(app);
|
|
1771
|
-
|
|
1772
|
-
const res = await server.get("/food").expect(200);
|
|
1773
|
-
expect(res.body.data).toEqual({custom: "response"});
|
|
1774
|
-
expect(res.body.more).toBeUndefined();
|
|
1775
|
-
expect(res.body.total).toBeUndefined();
|
|
1776
|
-
});
|
|
1777
|
-
|
|
1778
|
-
it("list with query sort param", async () => {
|
|
1779
|
-
await FoodModel.create({
|
|
1780
|
-
calories: 200,
|
|
1781
|
-
created: new Date("2021-12-04T00:00:20.000Z"),
|
|
1782
|
-
hidden: false,
|
|
1783
|
-
name: "Apple",
|
|
1784
|
-
ownerId: admin._id,
|
|
1785
|
-
});
|
|
1786
|
-
|
|
1787
|
-
app.use(
|
|
1788
|
-
"/food",
|
|
1789
|
-
modelRouter(FoodModel, {
|
|
1790
|
-
allowAnonymous: true,
|
|
1791
|
-
permissions: {
|
|
1792
|
-
create: [Permissions.IsAny],
|
|
1793
|
-
delete: [Permissions.IsAny],
|
|
1794
|
-
list: [Permissions.IsAny],
|
|
1795
|
-
read: [Permissions.IsAny],
|
|
1796
|
-
update: [Permissions.IsAny],
|
|
1797
|
-
},
|
|
1798
|
-
queryFields: ["name"],
|
|
1799
|
-
})
|
|
1800
|
-
);
|
|
1801
|
-
server = supertest(app);
|
|
1802
|
-
|
|
1803
|
-
// Sort by name ascending
|
|
1804
|
-
let res = await server.get("/food?sort=name").expect(200);
|
|
1805
|
-
expect(res.body.data[0].name).toBe("Apple");
|
|
1806
|
-
expect(res.body.data[1].name).toBe("Spinach");
|
|
1807
|
-
|
|
1808
|
-
// Sort by name descending
|
|
1809
|
-
res = await server.get("/food?sort=-name").expect(200);
|
|
1810
|
-
expect(res.body.data[0].name).toBe("Spinach");
|
|
1811
|
-
expect(res.body.data[1].name).toBe("Apple");
|
|
1812
|
-
});
|
|
1813
|
-
|
|
1814
|
-
it("queryFilter error is handled", async () => {
|
|
1815
|
-
app.use(
|
|
1816
|
-
"/food",
|
|
1817
|
-
modelRouter(FoodModel, {
|
|
1818
|
-
allowAnonymous: true,
|
|
1819
|
-
permissions: {
|
|
1820
|
-
create: [Permissions.IsAny],
|
|
1821
|
-
delete: [Permissions.IsAny],
|
|
1822
|
-
list: [Permissions.IsAny],
|
|
1823
|
-
read: [Permissions.IsAny],
|
|
1824
|
-
update: [Permissions.IsAny],
|
|
1825
|
-
},
|
|
1826
|
-
queryFilter: () => {
|
|
1827
|
-
throw new Error("queryFilter failed");
|
|
1828
|
-
},
|
|
1829
|
-
})
|
|
1830
|
-
);
|
|
1831
|
-
server = supertest(app);
|
|
1832
|
-
|
|
1833
|
-
const res = await server.get("/food").expect(400);
|
|
1834
|
-
expect(res.body.title).toContain("Query filter error");
|
|
1835
|
-
});
|
|
1836
|
-
|
|
1837
|
-
it("custom endpoints take priority", async () => {
|
|
1838
|
-
app.use(
|
|
1839
|
-
"/food",
|
|
1840
|
-
modelRouter(FoodModel, {
|
|
1841
|
-
allowAnonymous: true,
|
|
1842
|
-
endpoints: (router: any) => {
|
|
1843
|
-
router.get("/custom", (_req: any, res: any) => {
|
|
1844
|
-
res.json({custom: true});
|
|
1845
|
-
});
|
|
1846
|
-
},
|
|
1847
|
-
permissions: {
|
|
1848
|
-
create: [Permissions.IsAny],
|
|
1849
|
-
delete: [Permissions.IsAny],
|
|
1850
|
-
list: [Permissions.IsAny],
|
|
1851
|
-
read: [Permissions.IsAny],
|
|
1852
|
-
update: [Permissions.IsAny],
|
|
1853
|
-
},
|
|
1854
|
-
})
|
|
1855
|
-
);
|
|
1856
|
-
server = supertest(app);
|
|
1857
|
-
|
|
1858
|
-
const res = await server.get("/food/custom").expect(200);
|
|
1859
|
-
expect(res.body.custom).toBe(true);
|
|
1860
|
-
});
|
|
1861
|
-
|
|
1862
|
-
it("disallowed query param returns 400", async () => {
|
|
1863
|
-
app.use(
|
|
1864
|
-
"/food",
|
|
1865
|
-
modelRouter(FoodModel, {
|
|
1866
|
-
allowAnonymous: true,
|
|
1867
|
-
permissions: {
|
|
1868
|
-
create: [Permissions.IsAny],
|
|
1869
|
-
delete: [Permissions.IsAny],
|
|
1870
|
-
list: [Permissions.IsAny],
|
|
1871
|
-
read: [Permissions.IsAny],
|
|
1872
|
-
update: [Permissions.IsAny],
|
|
1873
|
-
},
|
|
1874
|
-
queryFields: ["name"],
|
|
1875
|
-
})
|
|
1876
|
-
);
|
|
1877
|
-
server = supertest(app);
|
|
1878
|
-
|
|
1879
|
-
const res = await server.get("/food?calories=100").expect(400);
|
|
1880
|
-
expect(res.body.title).toContain("calories is not allowed as a query param");
|
|
1881
|
-
});
|
|
1882
|
-
|
|
1883
|
-
it("queryFilter returning null returns empty array", async () => {
|
|
1884
|
-
app.use(
|
|
1885
|
-
"/food",
|
|
1886
|
-
modelRouter(FoodModel, {
|
|
1887
|
-
allowAnonymous: true,
|
|
1888
|
-
permissions: {
|
|
1889
|
-
create: [Permissions.IsAny],
|
|
1890
|
-
delete: [Permissions.IsAny],
|
|
1891
|
-
list: [Permissions.IsAny],
|
|
1892
|
-
read: [Permissions.IsAny],
|
|
1893
|
-
update: [Permissions.IsAny],
|
|
1894
|
-
},
|
|
1895
|
-
queryFilter: () => null,
|
|
1896
|
-
})
|
|
1897
|
-
);
|
|
1898
|
-
server = supertest(app);
|
|
1899
|
-
|
|
1900
|
-
const res = await server.get("/food").expect(200);
|
|
1901
|
-
expect(res.body.data).toEqual([]);
|
|
1902
|
-
});
|
|
1903
|
-
|
|
1904
|
-
it("preUpdate returning null throws error", async () => {
|
|
152
|
+
addAuthRoutes(app, UserModel as any);
|
|
1905
153
|
app.use(
|
|
1906
154
|
"/food",
|
|
1907
155
|
modelRouter(FoodModel, {
|
|
@@ -1913,85 +161,54 @@ describe("@terreno/api", () => {
|
|
|
1913
161
|
read: [Permissions.IsAny],
|
|
1914
162
|
update: [Permissions.IsAny],
|
|
1915
163
|
},
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
"/food",
|
|
1928
|
-
modelRouter(FoodModel, {
|
|
1929
|
-
allowAnonymous: true,
|
|
1930
|
-
permissions: {
|
|
1931
|
-
create: [Permissions.IsAny],
|
|
1932
|
-
delete: [Permissions.IsAny],
|
|
1933
|
-
list: [Permissions.IsAny],
|
|
1934
|
-
read: [Permissions.IsAny],
|
|
1935
|
-
update: [Permissions.IsAny],
|
|
164
|
+
responseHandler: (data, method) => {
|
|
165
|
+
if (method === "list") {
|
|
166
|
+
return (data as any).map((d: any) => ({
|
|
167
|
+
foo: "bar",
|
|
168
|
+
id: (d as any)._id,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
foo: "bar",
|
|
173
|
+
id: (data as any)._id,
|
|
174
|
+
};
|
|
1936
175
|
},
|
|
1937
|
-
preDelete: () => null,
|
|
1938
176
|
})
|
|
1939
177
|
);
|
|
1940
178
|
server = supertest(app);
|
|
1941
179
|
agent = await authAsUser(app, "notAdmin");
|
|
1942
|
-
|
|
1943
|
-
const res = await agent.delete(`/food/${spinach._id}`).expect(403);
|
|
1944
|
-
expect(res.body.title).toBe("Delete not allowed");
|
|
1945
180
|
});
|
|
1946
181
|
|
|
1947
|
-
it("
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
permissions: {
|
|
1953
|
-
create: [Permissions.IsAny],
|
|
1954
|
-
delete: [Permissions.IsAny],
|
|
1955
|
-
list: [Permissions.IsAny],
|
|
1956
|
-
read: [Permissions.IsAny],
|
|
1957
|
-
update: [Permissions.IsAny],
|
|
1958
|
-
},
|
|
1959
|
-
preCreate: () => null,
|
|
1960
|
-
})
|
|
1961
|
-
);
|
|
1962
|
-
server = supertest(app);
|
|
1963
|
-
|
|
1964
|
-
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(403);
|
|
1965
|
-
expect(res.body.title).toBe("Create not allowed");
|
|
182
|
+
it("reads with serialize", async () => {
|
|
183
|
+
const res = await agent.get(`/food/${spinach._id}`).expect(200);
|
|
184
|
+
expect(res.body.data.ownerId).toBeUndefined();
|
|
185
|
+
expect(res.body.data.id).toBe(spinach._id.toString());
|
|
186
|
+
expect(res.body.data.foo).toBe("bar");
|
|
1966
187
|
});
|
|
1967
188
|
|
|
1968
|
-
it("
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
allowAnonymous: true,
|
|
1973
|
-
permissions: {
|
|
1974
|
-
create: [Permissions.IsAny],
|
|
1975
|
-
delete: [Permissions.IsAny],
|
|
1976
|
-
list: [Permissions.IsAny],
|
|
1977
|
-
read: [Permissions.IsAny],
|
|
1978
|
-
update: [Permissions.IsAny],
|
|
1979
|
-
},
|
|
1980
|
-
preCreate: () => {
|
|
1981
|
-
throw new Error("preCreate failed");
|
|
1982
|
-
},
|
|
1983
|
-
})
|
|
1984
|
-
);
|
|
1985
|
-
server = supertest(app);
|
|
189
|
+
it("list with serialize", async () => {
|
|
190
|
+
const res = await agent.get("/food").expect(200);
|
|
191
|
+
expect(res.body.data[0].ownerId).toBeUndefined();
|
|
192
|
+
expect(res.body.data[1].ownerId).toBeUndefined();
|
|
1986
193
|
|
|
1987
|
-
|
|
1988
|
-
expect(res.body.
|
|
194
|
+
expect(res.body.data[0].id).toBeDefined();
|
|
195
|
+
expect(res.body.data[0].foo).toBe("bar");
|
|
196
|
+
expect(res.body.data[1].id).toBeDefined();
|
|
197
|
+
expect(res.body.data[1].foo).toBe("bar");
|
|
1989
198
|
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("plugins", () => {
|
|
202
|
+
let agent: TestAgent;
|
|
1990
203
|
|
|
1991
|
-
|
|
204
|
+
beforeEach(async () => {
|
|
205
|
+
await setupDb();
|
|
206
|
+
app = getBaseServer();
|
|
207
|
+
setupAuth(app, UserModel as any);
|
|
208
|
+
addAuthRoutes(app, UserModel as any);
|
|
1992
209
|
app.use(
|
|
1993
|
-
"/
|
|
1994
|
-
modelRouter(
|
|
210
|
+
"/users",
|
|
211
|
+
modelRouter(UserModel, {
|
|
1995
212
|
allowAnonymous: true,
|
|
1996
213
|
permissions: {
|
|
1997
214
|
create: [Permissions.IsAny],
|
|
@@ -2000,44 +217,37 @@ describe("@terreno/api", () => {
|
|
|
2000
217
|
read: [Permissions.IsAny],
|
|
2001
218
|
update: [Permissions.IsAny],
|
|
2002
219
|
},
|
|
2003
|
-
preUpdate: () => {
|
|
2004
|
-
throw new Error("preUpdate failed");
|
|
2005
|
-
},
|
|
2006
220
|
})
|
|
2007
221
|
);
|
|
2008
222
|
server = supertest(app);
|
|
2009
|
-
|
|
2010
|
-
const res = await server.patch(`/food/${spinach._id}`).send({name: "Kale"}).expect(400);
|
|
2011
|
-
expect(res.body.title).toContain("preUpdate hook error");
|
|
223
|
+
agent = await authAsUser(app, "notAdmin");
|
|
2012
224
|
});
|
|
2013
225
|
|
|
2014
|
-
it("
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
226
|
+
it("check that security fields are filtered", async () => {
|
|
227
|
+
const res = await agent.get("/users").expect(200);
|
|
228
|
+
expect(res.body.data[0].email).toBeDefined();
|
|
229
|
+
expect(res.body.data[0].token).toBeUndefined();
|
|
230
|
+
expect(res.body.data[0].hash).toBeUndefined();
|
|
231
|
+
expect(res.body.data[0].salt).toBeUndefined();
|
|
2019
232
|
});
|
|
2020
233
|
});
|
|
2021
234
|
|
|
2022
|
-
describe("
|
|
235
|
+
describe("error handling", () => {
|
|
2023
236
|
let admin: any;
|
|
2024
|
-
let
|
|
2025
|
-
let agent: TestAgent;
|
|
237
|
+
let spinach: Food;
|
|
2026
238
|
|
|
2027
239
|
beforeEach(async () => {
|
|
2028
240
|
[admin] = await setupDb();
|
|
2029
241
|
|
|
2030
|
-
|
|
2031
|
-
calories:
|
|
2032
|
-
|
|
2033
|
-
{name: "Fruit", show: true},
|
|
2034
|
-
{name: "Popular", show: false},
|
|
2035
|
-
],
|
|
2036
|
-
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
242
|
+
spinach = await FoodModel.create({
|
|
243
|
+
calories: 1,
|
|
244
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
2037
245
|
hidden: false,
|
|
2038
|
-
name: "
|
|
246
|
+
name: "Spinach",
|
|
2039
247
|
ownerId: admin._id,
|
|
2040
|
-
|
|
248
|
+
source: {
|
|
249
|
+
name: "Brand",
|
|
250
|
+
},
|
|
2041
251
|
});
|
|
2042
252
|
|
|
2043
253
|
app = getBaseServer();
|
|
@@ -2045,216 +255,193 @@ describe("@terreno/api", () => {
|
|
|
2045
255
|
addAuthRoutes(app, UserModel as any);
|
|
2046
256
|
});
|
|
2047
257
|
|
|
2048
|
-
it("
|
|
258
|
+
it("PUT returns 500 not supported", async () => {
|
|
2049
259
|
app.use(
|
|
2050
260
|
"/food",
|
|
2051
261
|
modelRouter(FoodModel, {
|
|
2052
262
|
allowAnonymous: true,
|
|
2053
263
|
permissions: {
|
|
2054
|
-
create: [Permissions.
|
|
2055
|
-
delete: [Permissions.
|
|
2056
|
-
list: [Permissions.
|
|
2057
|
-
read: [Permissions.
|
|
2058
|
-
update: [Permissions.
|
|
264
|
+
create: [Permissions.IsAny],
|
|
265
|
+
delete: [Permissions.IsAny],
|
|
266
|
+
list: [Permissions.IsAny],
|
|
267
|
+
read: [Permissions.IsAny],
|
|
268
|
+
update: [Permissions.IsAny],
|
|
2059
269
|
},
|
|
2060
|
-
preUpdate: () => undefined as any,
|
|
2061
270
|
})
|
|
2062
271
|
);
|
|
2063
272
|
server = supertest(app);
|
|
2064
|
-
agent = await authAsUser(app, "admin");
|
|
2065
273
|
|
|
2066
|
-
const res = await
|
|
2067
|
-
expect(res.body.title).toBe("
|
|
2068
|
-
expect(res.body.detail).toBe("A body must be returned from preUpdate");
|
|
274
|
+
const res = await server.put(`/food/${spinach._id}`).send({name: "Kale"}).expect(500);
|
|
275
|
+
expect(res.body.title).toBe("PUT is not supported.");
|
|
2069
276
|
});
|
|
2070
277
|
|
|
2071
|
-
it("
|
|
278
|
+
it("responseHandler error in read is handled", async () => {
|
|
2072
279
|
app.use(
|
|
2073
280
|
"/food",
|
|
2074
281
|
modelRouter(FoodModel, {
|
|
2075
282
|
allowAnonymous: true,
|
|
2076
283
|
permissions: {
|
|
2077
|
-
create: [Permissions.
|
|
2078
|
-
delete: [Permissions.
|
|
2079
|
-
list: [Permissions.
|
|
2080
|
-
read: [Permissions.
|
|
2081
|
-
update: [Permissions.
|
|
284
|
+
create: [Permissions.IsAny],
|
|
285
|
+
delete: [Permissions.IsAny],
|
|
286
|
+
list: [Permissions.IsAny],
|
|
287
|
+
read: [Permissions.IsAny],
|
|
288
|
+
update: [Permissions.IsAny],
|
|
289
|
+
},
|
|
290
|
+
responseHandler: (_data, method) => {
|
|
291
|
+
if (method === "read") {
|
|
292
|
+
throw new Error("responseHandler read failed");
|
|
293
|
+
}
|
|
294
|
+
return {} as any;
|
|
2082
295
|
},
|
|
2083
|
-
preUpdate: () => null,
|
|
2084
296
|
})
|
|
2085
297
|
);
|
|
2086
298
|
server = supertest(app);
|
|
2087
|
-
agent = await authAsUser(app, "admin");
|
|
2088
299
|
|
|
2089
|
-
const res = await
|
|
2090
|
-
expect(res.body.title).
|
|
300
|
+
const res = await server.get(`/food/${spinach._id}`).expect(500);
|
|
301
|
+
expect(res.body.title).toContain("responseHandler error");
|
|
2091
302
|
});
|
|
2092
303
|
|
|
2093
|
-
it("
|
|
304
|
+
it("responseHandler error in create is handled", async () => {
|
|
2094
305
|
app.use(
|
|
2095
306
|
"/food",
|
|
2096
307
|
modelRouter(FoodModel, {
|
|
2097
308
|
allowAnonymous: true,
|
|
2098
309
|
permissions: {
|
|
2099
|
-
create: [Permissions.
|
|
2100
|
-
delete: [Permissions.
|
|
2101
|
-
list: [Permissions.
|
|
2102
|
-
read: [Permissions.
|
|
2103
|
-
update: [Permissions.
|
|
310
|
+
create: [Permissions.IsAny],
|
|
311
|
+
delete: [Permissions.IsAny],
|
|
312
|
+
list: [Permissions.IsAny],
|
|
313
|
+
read: [Permissions.IsAny],
|
|
314
|
+
update: [Permissions.IsAny],
|
|
2104
315
|
},
|
|
2105
|
-
|
|
2106
|
-
|
|
316
|
+
responseHandler: (_data, method) => {
|
|
317
|
+
if (method === "create") {
|
|
318
|
+
throw new Error("responseHandler create failed");
|
|
319
|
+
}
|
|
320
|
+
return {} as any;
|
|
2107
321
|
},
|
|
2108
322
|
})
|
|
2109
323
|
);
|
|
2110
324
|
server = supertest(app);
|
|
2111
|
-
agent = await authAsUser(app, "admin");
|
|
2112
325
|
|
|
2113
|
-
const res = await
|
|
2114
|
-
expect(res.body.title).toContain("
|
|
326
|
+
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(500);
|
|
327
|
+
expect(res.body.title).toContain("responseHandler error");
|
|
2115
328
|
});
|
|
2116
329
|
|
|
2117
|
-
it("
|
|
330
|
+
it("responseHandler error in update is handled", async () => {
|
|
2118
331
|
app.use(
|
|
2119
332
|
"/food",
|
|
2120
333
|
modelRouter(FoodModel, {
|
|
2121
334
|
allowAnonymous: true,
|
|
2122
335
|
permissions: {
|
|
2123
|
-
create: [Permissions.
|
|
2124
|
-
delete: [Permissions.
|
|
2125
|
-
list: [Permissions.
|
|
2126
|
-
read: [Permissions.
|
|
2127
|
-
update: [Permissions.
|
|
336
|
+
create: [Permissions.IsAny],
|
|
337
|
+
delete: [Permissions.IsAny],
|
|
338
|
+
list: [Permissions.IsAny],
|
|
339
|
+
read: [Permissions.IsAny],
|
|
340
|
+
update: [Permissions.IsAny],
|
|
2128
341
|
},
|
|
2129
|
-
|
|
2130
|
-
|
|
342
|
+
responseHandler: (_data, method) => {
|
|
343
|
+
if (method === "update") {
|
|
344
|
+
throw new Error("responseHandler update failed");
|
|
345
|
+
}
|
|
346
|
+
return {} as any;
|
|
2131
347
|
},
|
|
2132
348
|
})
|
|
2133
349
|
);
|
|
2134
350
|
server = supertest(app);
|
|
2135
|
-
agent = await authAsUser(app, "admin");
|
|
2136
351
|
|
|
2137
|
-
const res = await
|
|
2138
|
-
expect(res.body.title).toContain("
|
|
352
|
+
const res = await server.patch(`/food/${spinach._id}`).send({name: "Kale"}).expect(500);
|
|
353
|
+
expect(res.body.title).toContain("responseHandler error");
|
|
2139
354
|
});
|
|
2140
355
|
|
|
2141
|
-
it("
|
|
356
|
+
it("responseHandler error in list is handled", async () => {
|
|
2142
357
|
app.use(
|
|
2143
358
|
"/food",
|
|
2144
359
|
modelRouter(FoodModel, {
|
|
2145
360
|
allowAnonymous: true,
|
|
2146
361
|
permissions: {
|
|
2147
|
-
create: [Permissions.
|
|
2148
|
-
delete: [Permissions.
|
|
362
|
+
create: [Permissions.IsAny],
|
|
363
|
+
delete: [Permissions.IsAny],
|
|
2149
364
|
list: [Permissions.IsAny],
|
|
2150
365
|
read: [Permissions.IsAny],
|
|
2151
|
-
update: [Permissions.
|
|
366
|
+
update: [Permissions.IsAny],
|
|
2152
367
|
},
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(405);
|
|
2159
|
-
expect(res.body.title).toContain("Access to PATCH");
|
|
2160
|
-
});
|
|
2161
|
-
|
|
2162
|
-
it("array operation on non-existent document returns 404", async () => {
|
|
2163
|
-
app.use(
|
|
2164
|
-
"/food",
|
|
2165
|
-
modelRouter(FoodModel, {
|
|
2166
|
-
allowAnonymous: true,
|
|
2167
|
-
permissions: {
|
|
2168
|
-
create: [Permissions.IsAdmin],
|
|
2169
|
-
delete: [Permissions.IsAdmin],
|
|
2170
|
-
list: [Permissions.IsAdmin],
|
|
2171
|
-
read: [Permissions.IsAdmin],
|
|
2172
|
-
update: [Permissions.IsAdmin],
|
|
368
|
+
responseHandler: (_data, method) => {
|
|
369
|
+
if (method === "list") {
|
|
370
|
+
throw new Error("responseHandler list failed");
|
|
371
|
+
}
|
|
372
|
+
return {} as any;
|
|
2173
373
|
},
|
|
2174
374
|
})
|
|
2175
375
|
);
|
|
2176
376
|
server = supertest(app);
|
|
2177
|
-
agent = await authAsUser(app, "admin");
|
|
2178
377
|
|
|
2179
|
-
const
|
|
2180
|
-
|
|
2181
|
-
expect(res.body.title).toContain("Could not find document to PATCH");
|
|
378
|
+
const res = await server.get("/food").expect(500);
|
|
379
|
+
expect(res.body.title).toContain("responseHandler error");
|
|
2182
380
|
});
|
|
2183
381
|
|
|
2184
|
-
it("
|
|
2185
|
-
// Create food owned by admin, then try to update as notAdmin
|
|
382
|
+
it("list with non-array responseHandler returns data directly", async () => {
|
|
2186
383
|
app.use(
|
|
2187
384
|
"/food",
|
|
2188
385
|
modelRouter(FoodModel, {
|
|
2189
386
|
allowAnonymous: true,
|
|
2190
387
|
permissions: {
|
|
2191
|
-
create: [Permissions.
|
|
2192
|
-
delete: [Permissions.
|
|
2193
|
-
list: [Permissions.
|
|
2194
|
-
read: [Permissions.
|
|
2195
|
-
update: [Permissions.
|
|
388
|
+
create: [Permissions.IsAny],
|
|
389
|
+
delete: [Permissions.IsAny],
|
|
390
|
+
list: [Permissions.IsAny],
|
|
391
|
+
read: [Permissions.IsAny],
|
|
392
|
+
update: [Permissions.IsAny],
|
|
393
|
+
},
|
|
394
|
+
responseHandler: (_data, method) => {
|
|
395
|
+
if (method === "list") {
|
|
396
|
+
return {custom: "response"} as any;
|
|
397
|
+
}
|
|
398
|
+
return {} as any;
|
|
2196
399
|
},
|
|
2197
400
|
})
|
|
2198
401
|
);
|
|
2199
402
|
server = supertest(app);
|
|
2200
|
-
// Login as notAdmin and try to update admin's food (apple)
|
|
2201
|
-
agent = await authAsUser(app, "notAdmin");
|
|
2202
403
|
|
|
2203
|
-
const res = await
|
|
2204
|
-
expect(res.body.
|
|
404
|
+
const res = await server.get("/food").expect(200);
|
|
405
|
+
expect(res.body.data).toEqual({custom: "response"});
|
|
406
|
+
expect(res.body.more).toBeUndefined();
|
|
407
|
+
expect(res.body.total).toBeUndefined();
|
|
2205
408
|
});
|
|
2206
409
|
|
|
2207
|
-
it("
|
|
410
|
+
it("list with query sort param", async () => {
|
|
411
|
+
await FoodModel.create({
|
|
412
|
+
calories: 200,
|
|
413
|
+
created: new Date("2021-12-04T00:00:20.000Z"),
|
|
414
|
+
hidden: false,
|
|
415
|
+
name: "Apple",
|
|
416
|
+
ownerId: admin._id,
|
|
417
|
+
});
|
|
418
|
+
|
|
2208
419
|
app.use(
|
|
2209
420
|
"/food",
|
|
2210
421
|
modelRouter(FoodModel, {
|
|
2211
422
|
allowAnonymous: true,
|
|
2212
423
|
permissions: {
|
|
2213
|
-
create: [Permissions.
|
|
2214
|
-
delete: [Permissions.
|
|
2215
|
-
list: [Permissions.
|
|
2216
|
-
read: [Permissions.
|
|
2217
|
-
update: [Permissions.
|
|
424
|
+
create: [Permissions.IsAny],
|
|
425
|
+
delete: [Permissions.IsAny],
|
|
426
|
+
list: [Permissions.IsAny],
|
|
427
|
+
read: [Permissions.IsAny],
|
|
428
|
+
update: [Permissions.IsAny],
|
|
2218
429
|
},
|
|
2219
|
-
|
|
2220
|
-
adminWriteFields: ["name"],
|
|
2221
|
-
}),
|
|
430
|
+
queryFields: ["name"],
|
|
2222
431
|
})
|
|
2223
432
|
);
|
|
2224
433
|
server = supertest(app);
|
|
2225
|
-
agent = await authAsUser(app, "admin");
|
|
2226
|
-
|
|
2227
|
-
// Try to update tags field, which is not in the allowed write fields
|
|
2228
|
-
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
|
|
2229
|
-
expect(res.body.title).toContain("cannot write fields");
|
|
2230
|
-
});
|
|
2231
|
-
});
|
|
2232
|
-
|
|
2233
|
-
describe("transformer errors", () => {
|
|
2234
|
-
let admin: any;
|
|
2235
|
-
let spinach: Food;
|
|
2236
|
-
let agent: TestAgent;
|
|
2237
434
|
|
|
2238
|
-
|
|
2239
|
-
[
|
|
2240
|
-
|
|
2241
|
-
spinach = await FoodModel.create({
|
|
2242
|
-
calories: 1,
|
|
2243
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
2244
|
-
hidden: false,
|
|
2245
|
-
name: "Spinach",
|
|
2246
|
-
ownerId: admin._id,
|
|
2247
|
-
source: {
|
|
2248
|
-
name: "Brand",
|
|
2249
|
-
},
|
|
2250
|
-
});
|
|
435
|
+
let res = await server.get("/food?sort=name").expect(200);
|
|
436
|
+
expect(res.body.data[0].name).toBe("Apple");
|
|
437
|
+
expect(res.body.data[1].name).toBe("Spinach");
|
|
2251
438
|
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
439
|
+
res = await server.get("/food?sort=-name").expect(200);
|
|
440
|
+
expect(res.body.data[0].name).toBe("Spinach");
|
|
441
|
+
expect(res.body.data[1].name).toBe("Apple");
|
|
2255
442
|
});
|
|
2256
443
|
|
|
2257
|
-
it("
|
|
444
|
+
it("queryFilter error is handled", async () => {
|
|
2258
445
|
app.use(
|
|
2259
446
|
"/food",
|
|
2260
447
|
modelRouter(FoodModel, {
|
|
@@ -2266,23 +453,27 @@ describe("@terreno/api", () => {
|
|
|
2266
453
|
read: [Permissions.IsAny],
|
|
2267
454
|
update: [Permissions.IsAny],
|
|
2268
455
|
},
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
}),
|
|
456
|
+
queryFilter: () => {
|
|
457
|
+
throw new Error("queryFilter failed");
|
|
458
|
+
},
|
|
2273
459
|
})
|
|
2274
460
|
);
|
|
2275
461
|
server = supertest(app);
|
|
2276
462
|
|
|
2277
|
-
const res = await server.
|
|
2278
|
-
expect(res.body.title).toContain("
|
|
463
|
+
const res = await server.get("/food").expect(400);
|
|
464
|
+
expect(res.body.title).toContain("Query filter error");
|
|
2279
465
|
});
|
|
2280
466
|
|
|
2281
|
-
it("
|
|
467
|
+
it("custom endpoints take priority", async () => {
|
|
2282
468
|
app.use(
|
|
2283
469
|
"/food",
|
|
2284
470
|
modelRouter(FoodModel, {
|
|
2285
471
|
allowAnonymous: true,
|
|
472
|
+
endpoints: (router: any) => {
|
|
473
|
+
router.get("/custom", (_req: any, res: any) => {
|
|
474
|
+
res.json({custom: true});
|
|
475
|
+
});
|
|
476
|
+
},
|
|
2286
477
|
permissions: {
|
|
2287
478
|
create: [Permissions.IsAny],
|
|
2288
479
|
delete: [Permissions.IsAny],
|
|
@@ -2290,25 +481,18 @@ describe("@terreno/api", () => {
|
|
|
2290
481
|
read: [Permissions.IsAny],
|
|
2291
482
|
update: [Permissions.IsAny],
|
|
2292
483
|
},
|
|
2293
|
-
transformer: AdminOwnerTransformer({
|
|
2294
|
-
// Only allow 'name' to be written, so 'calories' will throw
|
|
2295
|
-
anonWriteFields: ["name"],
|
|
2296
|
-
}),
|
|
2297
484
|
})
|
|
2298
485
|
);
|
|
2299
486
|
server = supertest(app);
|
|
2300
487
|
|
|
2301
|
-
const res = await server.
|
|
2302
|
-
expect(res.body.
|
|
488
|
+
const res = await server.get("/food/custom").expect(200);
|
|
489
|
+
expect(res.body.custom).toBe(true);
|
|
2303
490
|
});
|
|
2304
491
|
|
|
2305
|
-
it("
|
|
2306
|
-
// Use a model that has required fields
|
|
2307
|
-
const {RequiredModel} = await import("./tests");
|
|
2308
|
-
|
|
492
|
+
it("disallowed query param returns 400", async () => {
|
|
2309
493
|
app.use(
|
|
2310
|
-
"/
|
|
2311
|
-
modelRouter(
|
|
494
|
+
"/food",
|
|
495
|
+
modelRouter(FoodModel, {
|
|
2312
496
|
allowAnonymous: true,
|
|
2313
497
|
permissions: {
|
|
2314
498
|
create: [Permissions.IsAny],
|
|
@@ -2317,16 +501,16 @@ describe("@terreno/api", () => {
|
|
|
2317
501
|
read: [Permissions.IsAny],
|
|
2318
502
|
update: [Permissions.IsAny],
|
|
2319
503
|
},
|
|
504
|
+
queryFields: ["name"],
|
|
2320
505
|
})
|
|
2321
506
|
);
|
|
2322
507
|
server = supertest(app);
|
|
2323
508
|
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
expect(res.body.title).toContain("Required");
|
|
509
|
+
const res = await server.get("/food?calories=100").expect(400);
|
|
510
|
+
expect(res.body.title).toContain("calories is not allowed as a query param");
|
|
2327
511
|
});
|
|
2328
512
|
|
|
2329
|
-
it("
|
|
513
|
+
it("queryFilter returning null returns empty array", async () => {
|
|
2330
514
|
app.use(
|
|
2331
515
|
"/food",
|
|
2332
516
|
modelRouter(FoodModel, {
|
|
@@ -2338,36 +522,32 @@ describe("@terreno/api", () => {
|
|
|
2338
522
|
read: [Permissions.IsAny],
|
|
2339
523
|
update: [Permissions.IsAny],
|
|
2340
524
|
},
|
|
2341
|
-
|
|
2342
|
-
throw new APIError({
|
|
2343
|
-
disableExternalErrorTracking: true,
|
|
2344
|
-
status: 400,
|
|
2345
|
-
title: "Custom preDelete APIError",
|
|
2346
|
-
});
|
|
2347
|
-
},
|
|
525
|
+
queryFilter: () => null,
|
|
2348
526
|
})
|
|
2349
527
|
);
|
|
2350
528
|
server = supertest(app);
|
|
2351
|
-
agent = await authAsUser(app, "notAdmin");
|
|
2352
529
|
|
|
2353
|
-
const res = await
|
|
2354
|
-
expect(res.body.
|
|
2355
|
-
expect(res.body.disableExternalErrorTracking).toBe(true);
|
|
530
|
+
const res = await server.get("/food").expect(200);
|
|
531
|
+
expect(res.body.data).toEqual([]);
|
|
2356
532
|
});
|
|
2357
533
|
});
|
|
2358
534
|
|
|
2359
|
-
describe("
|
|
535
|
+
describe("transformer errors", () => {
|
|
2360
536
|
let admin: any;
|
|
537
|
+
let spinach: Food;
|
|
2361
538
|
|
|
2362
539
|
beforeEach(async () => {
|
|
2363
540
|
[admin] = await setupDb();
|
|
2364
541
|
|
|
2365
|
-
await FoodModel.create({
|
|
542
|
+
spinach = await FoodModel.create({
|
|
2366
543
|
calories: 1,
|
|
2367
544
|
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
2368
545
|
hidden: false,
|
|
2369
546
|
name: "Spinach",
|
|
2370
547
|
ownerId: admin._id,
|
|
548
|
+
source: {
|
|
549
|
+
name: "Brand",
|
|
550
|
+
},
|
|
2371
551
|
});
|
|
2372
552
|
|
|
2373
553
|
app = getBaseServer();
|
|
@@ -2375,46 +555,7 @@ describe("@terreno/api", () => {
|
|
|
2375
555
|
addAuthRoutes(app, UserModel as any);
|
|
2376
556
|
});
|
|
2377
557
|
|
|
2378
|
-
it("
|
|
2379
|
-
app.use(
|
|
2380
|
-
"/food",
|
|
2381
|
-
modelRouter(FoodModel, {
|
|
2382
|
-
allowAnonymous: true,
|
|
2383
|
-
permissions: {
|
|
2384
|
-
create: [Permissions.IsAny],
|
|
2385
|
-
delete: [Permissions.IsAny],
|
|
2386
|
-
list: [Permissions.IsAny],
|
|
2387
|
-
read: [Permissions.IsAny],
|
|
2388
|
-
update: [Permissions.IsAny],
|
|
2389
|
-
},
|
|
2390
|
-
queryFields: ["name", "period"],
|
|
2391
|
-
queryFilter: (_user, query) => {
|
|
2392
|
-
// Simulate a queryFilter that accepts and processes period
|
|
2393
|
-
if (query?.period) {
|
|
2394
|
-
// Period is processed but shouldn't be passed to mongo
|
|
2395
|
-
return query;
|
|
2396
|
-
}
|
|
2397
|
-
return query ?? {};
|
|
2398
|
-
},
|
|
2399
|
-
})
|
|
2400
|
-
);
|
|
2401
|
-
server = supertest(app);
|
|
2402
|
-
|
|
2403
|
-
// period should be accepted and processed without error
|
|
2404
|
-
const res = await server.get("/food?period=weekly").expect(200);
|
|
2405
|
-
expect(res.body.data).toBeDefined();
|
|
2406
|
-
});
|
|
2407
|
-
|
|
2408
|
-
it("query with false value", async () => {
|
|
2409
|
-
// Create a food that is hidden
|
|
2410
|
-
await FoodModel.create({
|
|
2411
|
-
calories: 50,
|
|
2412
|
-
created: new Date("2021-12-04T00:00:20.000Z"),
|
|
2413
|
-
hidden: true,
|
|
2414
|
-
name: "HiddenFood",
|
|
2415
|
-
ownerId: admin._id,
|
|
2416
|
-
});
|
|
2417
|
-
|
|
558
|
+
it("transform error in create is handled", async () => {
|
|
2418
559
|
app.use(
|
|
2419
560
|
"/food",
|
|
2420
561
|
modelRouter(FoodModel, {
|
|
@@ -2426,19 +567,18 @@ describe("@terreno/api", () => {
|
|
|
2426
567
|
read: [Permissions.IsAny],
|
|
2427
568
|
update: [Permissions.IsAny],
|
|
2428
569
|
},
|
|
2429
|
-
|
|
570
|
+
transformer: AdminOwnerTransformer({
|
|
571
|
+
anonWriteFields: ["name"],
|
|
572
|
+
}),
|
|
2430
573
|
})
|
|
2431
574
|
);
|
|
2432
575
|
server = supertest(app);
|
|
2433
576
|
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
expect(res.body.data.every((f: any) => f.hidden === false)).toBe(true);
|
|
577
|
+
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(400);
|
|
578
|
+
expect(res.body.title).toContain("cannot write fields");
|
|
2437
579
|
});
|
|
2438
580
|
|
|
2439
|
-
it("
|
|
2440
|
-
// The $search code path just accesses the collection but doesn't do anything with it
|
|
2441
|
-
// This test verifies the code path is exercised
|
|
581
|
+
it("transform error in patch is handled", async () => {
|
|
2442
582
|
app.use(
|
|
2443
583
|
"/food",
|
|
2444
584
|
modelRouter(FoodModel, {
|
|
@@ -2450,24 +590,21 @@ describe("@terreno/api", () => {
|
|
|
2450
590
|
read: [Permissions.IsAny],
|
|
2451
591
|
update: [Permissions.IsAny],
|
|
2452
592
|
},
|
|
2453
|
-
|
|
2454
|
-
|
|
593
|
+
transformer: AdminOwnerTransformer({
|
|
594
|
+
anonWriteFields: ["name"],
|
|
595
|
+
}),
|
|
2455
596
|
})
|
|
2456
597
|
);
|
|
2457
598
|
server = supertest(app);
|
|
2458
599
|
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
const res = await server.get("/food?$search=test");
|
|
2462
|
-
// May return 500 because $search is passed to Mongo which doesn't support it without Atlas
|
|
2463
|
-
// The important thing is we've exercised the code path
|
|
2464
|
-
expect(res.status === 200 || res.status === 500).toBe(true);
|
|
600
|
+
const res = await server.patch(`/food/${spinach._id}`).send({calories: 100}).expect(403);
|
|
601
|
+
expect(res.body.title).toContain("cannot write fields");
|
|
2465
602
|
});
|
|
2466
603
|
|
|
2467
|
-
it("
|
|
604
|
+
it("model.create validation error is handled", async () => {
|
|
2468
605
|
app.use(
|
|
2469
|
-
"/
|
|
2470
|
-
modelRouter(
|
|
606
|
+
"/required",
|
|
607
|
+
modelRouter(RequiredModel, {
|
|
2471
608
|
allowAnonymous: true,
|
|
2472
609
|
permissions: {
|
|
2473
610
|
create: [Permissions.IsAny],
|
|
@@ -2476,13 +613,12 @@ describe("@terreno/api", () => {
|
|
|
2476
613
|
read: [Permissions.IsAny],
|
|
2477
614
|
update: [Permissions.IsAny],
|
|
2478
615
|
},
|
|
2479
|
-
queryFields: ["name", "$autocomplete"],
|
|
2480
616
|
})
|
|
2481
617
|
);
|
|
2482
618
|
server = supertest(app);
|
|
2483
619
|
|
|
2484
|
-
const res = await server.
|
|
2485
|
-
expect(res.
|
|
620
|
+
const res = await server.post("/required").send({about: "test"}).expect(400);
|
|
621
|
+
expect(res.body.title).toContain("Required");
|
|
2486
622
|
});
|
|
2487
623
|
});
|
|
2488
624
|
|
|
@@ -2508,7 +644,6 @@ describe("@terreno/api", () => {
|
|
|
2508
644
|
{fields: ["email"], path: "ownerId"},
|
|
2509
645
|
{fields: ["name"], path: "eatenBy"},
|
|
2510
646
|
]);
|
|
2511
|
-
// The result should be a query with populate applied
|
|
2512
647
|
expect(result).toBeDefined();
|
|
2513
648
|
});
|
|
2514
649
|
});
|
|
@@ -2526,9 +661,6 @@ describe("@terreno/api", () => {
|
|
|
2526
661
|
});
|
|
2527
662
|
|
|
2528
663
|
it("soft deletes user with deleted field", async () => {
|
|
2529
|
-
// UserModel has the isDisabledPlugin which adds a 'disabled' field,
|
|
2530
|
-
// but we need to test the 'deleted' field check.
|
|
2531
|
-
// Let's use a model that has the deleted field.
|
|
2532
664
|
app.use(
|
|
2533
665
|
"/users",
|
|
2534
666
|
modelRouter(UserModel, {
|
|
@@ -2545,11 +677,9 @@ describe("@terreno/api", () => {
|
|
|
2545
677
|
server = supertest(app);
|
|
2546
678
|
agent = await authAsUser(app, "notAdmin");
|
|
2547
679
|
|
|
2548
|
-
// Delete a user - this should use deleteOne since User doesn't have deleted field
|
|
2549
680
|
const res = await agent.delete(`/users/${admin._id}`).expect(204);
|
|
2550
681
|
expect(res.body).toEqual({});
|
|
2551
682
|
|
|
2552
|
-
// Verify user was deleted
|
|
2553
683
|
const deletedUser = await UserModel.findById(admin._id);
|
|
2554
684
|
expect(deletedUser).toBeNull();
|
|
2555
685
|
});
|
|
@@ -2575,7 +705,6 @@ describe("@terreno/api", () => {
|
|
|
2575
705
|
});
|
|
2576
706
|
|
|
2577
707
|
it("handles populate with valid path in create", async () => {
|
|
2578
|
-
// Test that valid populate works in create flow
|
|
2579
708
|
app.use(
|
|
2580
709
|
"/food",
|
|
2581
710
|
modelRouter(FoodModel, {
|
|
@@ -2597,7 +726,6 @@ describe("@terreno/api", () => {
|
|
|
2597
726
|
.send({calories: 15, name: "Broccoli", ownerId: admin._id})
|
|
2598
727
|
.expect(201);
|
|
2599
728
|
expect(res.body.data.name).toBe("Broccoli");
|
|
2600
|
-
// Verify populate worked - ownerId should be an object with email
|
|
2601
729
|
expect(res.body.data.ownerId.email).toBe(admin.email);
|
|
2602
730
|
});
|
|
2603
731
|
});
|
|
@@ -2626,7 +754,6 @@ describe("@terreno/api", () => {
|
|
|
2626
754
|
});
|
|
2627
755
|
|
|
2628
756
|
it("handles patch save error with validation failure", async () => {
|
|
2629
|
-
// The FoodModel has strict: "throw" which will cause validation errors for unknown fields
|
|
2630
757
|
app.use(
|
|
2631
758
|
"/food",
|
|
2632
759
|
modelRouter(FoodModel, {
|
|
@@ -2642,7 +769,6 @@ describe("@terreno/api", () => {
|
|
|
2642
769
|
);
|
|
2643
770
|
server = supertest(app);
|
|
2644
771
|
|
|
2645
|
-
// Try to patch with an invalid field (will be caught by strict: "throw")
|
|
2646
772
|
const res = await server
|
|
2647
773
|
.patch(`/food/${spinach._id}`)
|
|
2648
774
|
.send({invalidField: "value"})
|
|
@@ -2661,7 +787,6 @@ describe("@terreno/api", () => {
|
|
|
2661
787
|
});
|
|
2662
788
|
|
|
2663
789
|
it("handles undefined body after transform when no preCreate", async () => {
|
|
2664
|
-
// Create a transformer that returns undefined
|
|
2665
790
|
app.use(
|
|
2666
791
|
"/food",
|
|
2667
792
|
modelRouter(FoodModel, {
|
|
@@ -2699,18 +824,13 @@ describe("@terreno/api", () => {
|
|
|
2699
824
|
});
|
|
2700
825
|
|
|
2701
826
|
it("soft deletes document with deleted field using isDeletedPlugin", async () => {
|
|
2702
|
-
// Create a test schema with the isDeletedPlugin
|
|
2703
827
|
const mongoose = await import("mongoose");
|
|
2704
828
|
|
|
2705
|
-
// Create a temporary model with the deleted field
|
|
2706
829
|
const softDeleteSchema = new mongoose.Schema({
|
|
2707
830
|
deleted: {default: false, type: Boolean},
|
|
2708
831
|
name: String,
|
|
2709
832
|
});
|
|
2710
|
-
// Manually add the deleted field (simulating what isDeletedPlugin does)
|
|
2711
|
-
// The schema already has the deleted field, so it should use soft delete
|
|
2712
833
|
|
|
2713
|
-
// Check if the model already exists to avoid OverwriteModelError
|
|
2714
834
|
let SoftDeleteModel;
|
|
2715
835
|
try {
|
|
2716
836
|
SoftDeleteModel = mongoose.model("SoftDeleteTest");
|
|
@@ -2718,10 +838,8 @@ describe("@terreno/api", () => {
|
|
|
2718
838
|
SoftDeleteModel = mongoose.model("SoftDeleteTest", softDeleteSchema);
|
|
2719
839
|
}
|
|
2720
840
|
|
|
2721
|
-
// Clean up any existing documents
|
|
2722
841
|
await SoftDeleteModel.deleteMany({});
|
|
2723
842
|
|
|
2724
|
-
// Create a test document
|
|
2725
843
|
const testDoc = await SoftDeleteModel.create({name: "TestItem"});
|
|
2726
844
|
|
|
2727
845
|
app.use(
|
|
@@ -2740,516 +858,13 @@ describe("@terreno/api", () => {
|
|
|
2740
858
|
server = supertest(app);
|
|
2741
859
|
agent = await authAsUser(app, "notAdmin");
|
|
2742
860
|
|
|
2743
|
-
// Delete should soft delete (set deleted: true) instead of hard delete
|
|
2744
861
|
await agent.delete(`/softdelete/${testDoc._id}`).expect(204);
|
|
2745
862
|
|
|
2746
|
-
// Verify document was soft deleted (not hard deleted)
|
|
2747
863
|
const softDeleted = await SoftDeleteModel.findById(testDoc._id);
|
|
2748
864
|
expect(softDeleted).not.toBeNull();
|
|
2749
865
|
expect(softDeleted?.deleted).toBe(true);
|
|
2750
866
|
|
|
2751
|
-
// Clean up
|
|
2752
867
|
await SoftDeleteModel.deleteMany({});
|
|
2753
868
|
});
|
|
2754
869
|
});
|
|
2755
|
-
|
|
2756
|
-
describe("array operation with undefined preUpdate return", () => {
|
|
2757
|
-
let admin: any;
|
|
2758
|
-
let apple: Food;
|
|
2759
|
-
let agent: TestAgent;
|
|
2760
|
-
|
|
2761
|
-
beforeEach(async () => {
|
|
2762
|
-
[admin] = await setupDb();
|
|
2763
|
-
|
|
2764
|
-
apple = await FoodModel.create({
|
|
2765
|
-
calories: 100,
|
|
2766
|
-
categories: [
|
|
2767
|
-
{name: "Fruit", show: true},
|
|
2768
|
-
{name: "Popular", show: false},
|
|
2769
|
-
],
|
|
2770
|
-
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
2771
|
-
hidden: false,
|
|
2772
|
-
name: "Apple",
|
|
2773
|
-
ownerId: admin._id,
|
|
2774
|
-
tags: ["healthy", "cheap"],
|
|
2775
|
-
});
|
|
2776
|
-
|
|
2777
|
-
app = getBaseServer();
|
|
2778
|
-
setupAuth(app, UserModel as any);
|
|
2779
|
-
addAuthRoutes(app, UserModel as any);
|
|
2780
|
-
});
|
|
2781
|
-
|
|
2782
|
-
it("array operation preUpdate returning undefined for array POST throws error", async () => {
|
|
2783
|
-
app.use(
|
|
2784
|
-
"/food",
|
|
2785
|
-
modelRouter(FoodModel, {
|
|
2786
|
-
allowAnonymous: true,
|
|
2787
|
-
permissions: {
|
|
2788
|
-
create: [Permissions.IsAdmin],
|
|
2789
|
-
delete: [Permissions.IsAdmin],
|
|
2790
|
-
list: [Permissions.IsAdmin],
|
|
2791
|
-
read: [Permissions.IsAdmin],
|
|
2792
|
-
update: [Permissions.IsAdmin],
|
|
2793
|
-
},
|
|
2794
|
-
preUpdate: () => undefined as any,
|
|
2795
|
-
})
|
|
2796
|
-
);
|
|
2797
|
-
server = supertest(app);
|
|
2798
|
-
agent = await authAsUser(app, "admin");
|
|
2799
|
-
|
|
2800
|
-
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
|
|
2801
|
-
expect(res.body.title).toBe("Update not allowed");
|
|
2802
|
-
expect(res.body.detail).toBe("A body must be returned from preUpdate");
|
|
2803
|
-
});
|
|
2804
|
-
|
|
2805
|
-
it("array operation preUpdate returning null for array PATCH throws error", async () => {
|
|
2806
|
-
app.use(
|
|
2807
|
-
"/food",
|
|
2808
|
-
modelRouter(FoodModel, {
|
|
2809
|
-
allowAnonymous: true,
|
|
2810
|
-
permissions: {
|
|
2811
|
-
create: [Permissions.IsAdmin],
|
|
2812
|
-
delete: [Permissions.IsAdmin],
|
|
2813
|
-
list: [Permissions.IsAdmin],
|
|
2814
|
-
read: [Permissions.IsAdmin],
|
|
2815
|
-
update: [Permissions.IsAdmin],
|
|
2816
|
-
},
|
|
2817
|
-
preUpdate: () => null,
|
|
2818
|
-
})
|
|
2819
|
-
);
|
|
2820
|
-
server = supertest(app);
|
|
2821
|
-
agent = await authAsUser(app, "admin");
|
|
2822
|
-
|
|
2823
|
-
const res = await agent
|
|
2824
|
-
.patch(`/food/${apple._id}/tags/healthy`)
|
|
2825
|
-
.send({tags: "unhealthy"})
|
|
2826
|
-
.expect(403);
|
|
2827
|
-
expect(res.body.title).toBe("Update not allowed");
|
|
2828
|
-
});
|
|
2829
|
-
|
|
2830
|
-
it("array operation preUpdate error for array DELETE is handled", async () => {
|
|
2831
|
-
app.use(
|
|
2832
|
-
"/food",
|
|
2833
|
-
modelRouter(FoodModel, {
|
|
2834
|
-
allowAnonymous: true,
|
|
2835
|
-
permissions: {
|
|
2836
|
-
create: [Permissions.IsAdmin],
|
|
2837
|
-
delete: [Permissions.IsAdmin],
|
|
2838
|
-
list: [Permissions.IsAdmin],
|
|
2839
|
-
read: [Permissions.IsAdmin],
|
|
2840
|
-
update: [Permissions.IsAdmin],
|
|
2841
|
-
},
|
|
2842
|
-
preUpdate: () => {
|
|
2843
|
-
throw new Error("preUpdate error during delete");
|
|
2844
|
-
},
|
|
2845
|
-
})
|
|
2846
|
-
);
|
|
2847
|
-
server = supertest(app);
|
|
2848
|
-
agent = await authAsUser(app, "admin");
|
|
2849
|
-
|
|
2850
|
-
const res = await agent.delete(`/food/${apple._id}/tags/healthy`).expect(400);
|
|
2851
|
-
expect(res.body.title).toContain("preUpdate hook error");
|
|
2852
|
-
});
|
|
2853
|
-
});
|
|
2854
|
-
});
|
|
2855
|
-
|
|
2856
|
-
describe("errors module", () => {
|
|
2857
|
-
describe("APIError", () => {
|
|
2858
|
-
it("sets default status to 500 when not provided", () => {
|
|
2859
|
-
const error = new APIError({title: "Test error"});
|
|
2860
|
-
expect(error.status).toBe(500);
|
|
2861
|
-
});
|
|
2862
|
-
|
|
2863
|
-
it("sets status to 500 for invalid status codes below 400", () => {
|
|
2864
|
-
const error = new APIError({status: 200, title: "Test error"});
|
|
2865
|
-
expect(error.status).toBe(500);
|
|
2866
|
-
});
|
|
2867
|
-
|
|
2868
|
-
it("sets status to 500 for invalid status codes above 599", () => {
|
|
2869
|
-
const error = new APIError({status: 600, title: "Test error"});
|
|
2870
|
-
expect(error.status).toBe(500);
|
|
2871
|
-
});
|
|
2872
|
-
|
|
2873
|
-
it("includes error stack in message when error is provided", () => {
|
|
2874
|
-
const originalError = new Error("Original error");
|
|
2875
|
-
const apiError = new APIError({
|
|
2876
|
-
error: originalError,
|
|
2877
|
-
title: "Wrapped error",
|
|
2878
|
-
});
|
|
2879
|
-
expect(apiError.message).toContain("Wrapped error");
|
|
2880
|
-
expect(originalError.stack).toBeDefined();
|
|
2881
|
-
expect(apiError.message).toContain(originalError.stack as string);
|
|
2882
|
-
});
|
|
2883
|
-
|
|
2884
|
-
it("includes detail in message when provided", () => {
|
|
2885
|
-
const error = new APIError({
|
|
2886
|
-
detail: "More details here",
|
|
2887
|
-
title: "Test error",
|
|
2888
|
-
});
|
|
2889
|
-
expect(error.message).toContain("Test error");
|
|
2890
|
-
expect(error.message).toContain("More details here");
|
|
2891
|
-
});
|
|
2892
|
-
|
|
2893
|
-
it("sets fields in meta when provided", () => {
|
|
2894
|
-
const error = new APIError({
|
|
2895
|
-
fields: {email: "Invalid email format"},
|
|
2896
|
-
title: "Validation error",
|
|
2897
|
-
});
|
|
2898
|
-
expect(error.meta?.fields).toEqual({email: "Invalid email format"});
|
|
2899
|
-
});
|
|
2900
|
-
});
|
|
2901
|
-
|
|
2902
|
-
describe("errorsPlugin", () => {
|
|
2903
|
-
it("adds apiErrors field to schema", async () => {
|
|
2904
|
-
const mongoose = await import("mongoose");
|
|
2905
|
-
const {errorsPlugin} = await import("./errors");
|
|
2906
|
-
|
|
2907
|
-
const testSchema = new mongoose.Schema({name: String});
|
|
2908
|
-
errorsPlugin(testSchema);
|
|
2909
|
-
|
|
2910
|
-
expect(testSchema.path("apiErrors")).toBeDefined();
|
|
2911
|
-
});
|
|
2912
|
-
});
|
|
2913
|
-
|
|
2914
|
-
describe("isAPIError", () => {
|
|
2915
|
-
it("returns true for APIError instances", () => {
|
|
2916
|
-
const {isAPIError} = require("./errors");
|
|
2917
|
-
const error = new APIError({title: "Test"});
|
|
2918
|
-
expect(isAPIError(error)).toBe(true);
|
|
2919
|
-
});
|
|
2920
|
-
|
|
2921
|
-
it("returns false for regular Error instances", () => {
|
|
2922
|
-
const {isAPIError} = require("./errors");
|
|
2923
|
-
const error = new Error("Test");
|
|
2924
|
-
expect(isAPIError(error)).toBe(false);
|
|
2925
|
-
});
|
|
2926
|
-
});
|
|
2927
|
-
|
|
2928
|
-
describe("getDisableExternalErrorTracking", () => {
|
|
2929
|
-
it("returns undefined for non-objects", () => {
|
|
2930
|
-
const {getDisableExternalErrorTracking} = require("./errors");
|
|
2931
|
-
expect(getDisableExternalErrorTracking(null)).toBeUndefined();
|
|
2932
|
-
expect(getDisableExternalErrorTracking("string")).toBeUndefined();
|
|
2933
|
-
});
|
|
2934
|
-
|
|
2935
|
-
it("returns value from APIError", () => {
|
|
2936
|
-
const {getDisableExternalErrorTracking} = require("./errors");
|
|
2937
|
-
const error = new APIError({disableExternalErrorTracking: true, title: "Test"});
|
|
2938
|
-
expect(getDisableExternalErrorTracking(error)).toBe(true);
|
|
2939
|
-
});
|
|
2940
|
-
|
|
2941
|
-
it("returns value from plain object with property", () => {
|
|
2942
|
-
const {getDisableExternalErrorTracking} = require("./errors");
|
|
2943
|
-
const obj = {disableExternalErrorTracking: true};
|
|
2944
|
-
expect(getDisableExternalErrorTracking(obj)).toBe(true);
|
|
2945
|
-
});
|
|
2946
|
-
});
|
|
2947
|
-
|
|
2948
|
-
describe("getAPIErrorBody", () => {
|
|
2949
|
-
it("includes all non-undefined fields", () => {
|
|
2950
|
-
const {getAPIErrorBody} = require("./errors");
|
|
2951
|
-
const error = new APIError({
|
|
2952
|
-
code: "TEST_CODE",
|
|
2953
|
-
detail: "Test detail",
|
|
2954
|
-
id: "error-123",
|
|
2955
|
-
links: {about: "http://example.com"},
|
|
2956
|
-
meta: {extra: "data"},
|
|
2957
|
-
source: {parameter: "id"},
|
|
2958
|
-
status: 400,
|
|
2959
|
-
title: "Test error",
|
|
2960
|
-
});
|
|
2961
|
-
const body = getAPIErrorBody(error);
|
|
2962
|
-
|
|
2963
|
-
expect(body.title).toBe("Test error");
|
|
2964
|
-
expect(body.status).toBe(400);
|
|
2965
|
-
expect(body.code).toBe("TEST_CODE");
|
|
2966
|
-
expect(body.detail).toBe("Test detail");
|
|
2967
|
-
expect(body.id).toBe("error-123");
|
|
2968
|
-
expect(body.links).toEqual({about: "http://example.com"});
|
|
2969
|
-
expect(body.source).toEqual({parameter: "id"});
|
|
2970
|
-
expect(body.meta).toEqual({extra: "data"});
|
|
2971
|
-
});
|
|
2972
|
-
});
|
|
2973
|
-
|
|
2974
|
-
describe("apiUnauthorizedMiddleware", () => {
|
|
2975
|
-
it("returns 401 for Unauthorized errors", () => {
|
|
2976
|
-
const {apiUnauthorizedMiddleware} = require("./errors");
|
|
2977
|
-
const err = new Error("Unauthorized");
|
|
2978
|
-
const res = {
|
|
2979
|
-
json: function (data: any) {
|
|
2980
|
-
(this as any).body = data;
|
|
2981
|
-
return this;
|
|
2982
|
-
},
|
|
2983
|
-
send: function () {
|
|
2984
|
-
return this;
|
|
2985
|
-
},
|
|
2986
|
-
status: function (code: number) {
|
|
2987
|
-
(this as any).statusCode = code;
|
|
2988
|
-
return this;
|
|
2989
|
-
},
|
|
2990
|
-
};
|
|
2991
|
-
const next = () => {};
|
|
2992
|
-
|
|
2993
|
-
apiUnauthorizedMiddleware(err, {}, res, next);
|
|
2994
|
-
expect((res as any).statusCode).toBe(401);
|
|
2995
|
-
expect((res as any).body.title).toBe("Unauthorized");
|
|
2996
|
-
});
|
|
2997
|
-
|
|
2998
|
-
it("calls next for non-Unauthorized errors", () => {
|
|
2999
|
-
const {apiUnauthorizedMiddleware} = require("./errors");
|
|
3000
|
-
const err = new Error("Some other error");
|
|
3001
|
-
let nextCalled = false;
|
|
3002
|
-
const next = () => {
|
|
3003
|
-
nextCalled = true;
|
|
3004
|
-
};
|
|
3005
|
-
|
|
3006
|
-
apiUnauthorizedMiddleware(err, {}, {}, next);
|
|
3007
|
-
expect(nextCalled).toBe(true);
|
|
3008
|
-
});
|
|
3009
|
-
});
|
|
3010
|
-
});
|
|
3011
|
-
|
|
3012
|
-
describe("permissions module", () => {
|
|
3013
|
-
describe("OwnerQueryFilter", () => {
|
|
3014
|
-
it("returns ownerId filter when user is provided", () => {
|
|
3015
|
-
const {OwnerQueryFilter} = require("./permissions");
|
|
3016
|
-
const user = {id: "user-123"};
|
|
3017
|
-
const filter = OwnerQueryFilter(user);
|
|
3018
|
-
expect(filter).toEqual({ownerId: "user-123"});
|
|
3019
|
-
});
|
|
3020
|
-
|
|
3021
|
-
it("returns null when user is undefined", () => {
|
|
3022
|
-
const {OwnerQueryFilter} = require("./permissions");
|
|
3023
|
-
const filter = OwnerQueryFilter(undefined);
|
|
3024
|
-
expect(filter).toBeNull();
|
|
3025
|
-
});
|
|
3026
|
-
});
|
|
3027
|
-
|
|
3028
|
-
describe("Permissions.IsAuthenticatedOrReadOnly", () => {
|
|
3029
|
-
it("returns true for authenticated non-anonymous users", () => {
|
|
3030
|
-
const {Permissions} = require("./permissions");
|
|
3031
|
-
const user = {id: "user-123", isAnonymous: false};
|
|
3032
|
-
expect(Permissions.IsAuthenticatedOrReadOnly("create", user)).toBe(true);
|
|
3033
|
-
});
|
|
3034
|
-
|
|
3035
|
-
it("returns true for read methods when user is anonymous", () => {
|
|
3036
|
-
const {Permissions} = require("./permissions");
|
|
3037
|
-
const user = {id: "user-123", isAnonymous: true};
|
|
3038
|
-
expect(Permissions.IsAuthenticatedOrReadOnly("list", user)).toBe(true);
|
|
3039
|
-
expect(Permissions.IsAuthenticatedOrReadOnly("read", user)).toBe(true);
|
|
3040
|
-
});
|
|
3041
|
-
|
|
3042
|
-
it("returns false for write methods when user is anonymous", () => {
|
|
3043
|
-
const {Permissions} = require("./permissions");
|
|
3044
|
-
const user = {id: "user-123", isAnonymous: true};
|
|
3045
|
-
expect(Permissions.IsAuthenticatedOrReadOnly("create", user)).toBe(false);
|
|
3046
|
-
expect(Permissions.IsAuthenticatedOrReadOnly("update", user)).toBe(false);
|
|
3047
|
-
expect(Permissions.IsAuthenticatedOrReadOnly("delete", user)).toBe(false);
|
|
3048
|
-
});
|
|
3049
|
-
});
|
|
3050
|
-
|
|
3051
|
-
describe("Permissions.IsOwnerOrReadOnly", () => {
|
|
3052
|
-
it("returns true when no object is provided", () => {
|
|
3053
|
-
const {Permissions} = require("./permissions");
|
|
3054
|
-
expect(Permissions.IsOwnerOrReadOnly("update", {id: "user-123"}, undefined)).toBe(true);
|
|
3055
|
-
});
|
|
3056
|
-
|
|
3057
|
-
it("returns true for admin users", () => {
|
|
3058
|
-
const {Permissions} = require("./permissions");
|
|
3059
|
-
const user = {admin: true, id: "admin-123"};
|
|
3060
|
-
const obj = {ownerId: "other-user"};
|
|
3061
|
-
expect(Permissions.IsOwnerOrReadOnly("update", user, obj)).toBe(true);
|
|
3062
|
-
});
|
|
3063
|
-
|
|
3064
|
-
it("returns true when user is owner", () => {
|
|
3065
|
-
const {Permissions} = require("./permissions");
|
|
3066
|
-
const user = {id: "user-123"};
|
|
3067
|
-
const obj = {ownerId: "user-123"};
|
|
3068
|
-
expect(Permissions.IsOwnerOrReadOnly("update", user, obj)).toBe(true);
|
|
3069
|
-
});
|
|
3070
|
-
|
|
3071
|
-
it("returns true for read methods when not owner", () => {
|
|
3072
|
-
const {Permissions} = require("./permissions");
|
|
3073
|
-
const user = {id: "user-123"};
|
|
3074
|
-
const obj = {ownerId: "other-user"};
|
|
3075
|
-
expect(Permissions.IsOwnerOrReadOnly("list", user, obj)).toBe(true);
|
|
3076
|
-
expect(Permissions.IsOwnerOrReadOnly("read", user, obj)).toBe(true);
|
|
3077
|
-
});
|
|
3078
|
-
|
|
3079
|
-
it("returns false for write methods when not owner", () => {
|
|
3080
|
-
const {Permissions} = require("./permissions");
|
|
3081
|
-
const user = {id: "user-123"};
|
|
3082
|
-
const obj = {ownerId: "other-user"};
|
|
3083
|
-
expect(Permissions.IsOwnerOrReadOnly("update", user, obj)).toBe(false);
|
|
3084
|
-
expect(Permissions.IsOwnerOrReadOnly("delete", user, obj)).toBe(false);
|
|
3085
|
-
});
|
|
3086
|
-
});
|
|
3087
|
-
});
|
|
3088
|
-
|
|
3089
|
-
describe("utils module", () => {
|
|
3090
|
-
describe("isValidObjectId", () => {
|
|
3091
|
-
it("returns true for valid ObjectId strings", () => {
|
|
3092
|
-
const {isValidObjectId} = require("./utils");
|
|
3093
|
-
expect(isValidObjectId("507f1f77bcf86cd799439011")).toBe(true);
|
|
3094
|
-
});
|
|
3095
|
-
|
|
3096
|
-
it("returns false for invalid ObjectId strings", () => {
|
|
3097
|
-
const {isValidObjectId} = require("./utils");
|
|
3098
|
-
expect(isValidObjectId("invalid-id")).toBe(false);
|
|
3099
|
-
expect(isValidObjectId("12345")).toBe(false);
|
|
3100
|
-
expect(isValidObjectId("")).toBe(false);
|
|
3101
|
-
});
|
|
3102
|
-
|
|
3103
|
-
it("returns false for 12-character strings that are not valid ObjectIds", () => {
|
|
3104
|
-
const {isValidObjectId} = require("./utils");
|
|
3105
|
-
// mongoose's native isValid returns true for any 12-char string
|
|
3106
|
-
// but our implementation should return false since toString won't match
|
|
3107
|
-
expect(isValidObjectId("123456789012")).toBe(false);
|
|
3108
|
-
});
|
|
3109
|
-
});
|
|
3110
|
-
|
|
3111
|
-
describe("timeout", () => {
|
|
3112
|
-
it("resolves after specified time", async () => {
|
|
3113
|
-
const {timeout} = require("./utils");
|
|
3114
|
-
const start = Date.now();
|
|
3115
|
-
await timeout(50);
|
|
3116
|
-
const elapsed = Date.now() - start;
|
|
3117
|
-
expect(elapsed).toBeGreaterThanOrEqual(40);
|
|
3118
|
-
});
|
|
3119
|
-
});
|
|
3120
|
-
|
|
3121
|
-
// Note: Comprehensive checkModelsStrict tests are in utils.test.ts with mocked mongoose
|
|
3122
|
-
});
|
|
3123
|
-
|
|
3124
|
-
describe("populate module", () => {
|
|
3125
|
-
describe("unpopulate", () => {
|
|
3126
|
-
it("throws error when path is empty", async () => {
|
|
3127
|
-
const {unpopulate} = await import("./populate");
|
|
3128
|
-
const doc = {name: "test"};
|
|
3129
|
-
expect(() => unpopulate(doc as any, "")).toThrow("path is required");
|
|
3130
|
-
});
|
|
3131
|
-
|
|
3132
|
-
it("unpopulates single populated field", async () => {
|
|
3133
|
-
const {unpopulate} = await import("./populate");
|
|
3134
|
-
const doc = {
|
|
3135
|
-
name: "test",
|
|
3136
|
-
ownerId: {_id: "owner-123", email: "owner@test.com"},
|
|
3137
|
-
};
|
|
3138
|
-
const result = unpopulate(doc as any, "ownerId") as any;
|
|
3139
|
-
expect(result.ownerId).toBe("owner-123");
|
|
3140
|
-
});
|
|
3141
|
-
|
|
3142
|
-
it("unpopulates array of populated fields", async () => {
|
|
3143
|
-
const {unpopulate} = await import("./populate");
|
|
3144
|
-
const doc = {
|
|
3145
|
-
items: [{_id: "item-1", name: "Item 1"}, {_id: "item-2", name: "Item 2"}, "item-3"],
|
|
3146
|
-
name: "test",
|
|
3147
|
-
};
|
|
3148
|
-
const result = unpopulate(doc as any, "items") as any;
|
|
3149
|
-
expect(result.items).toEqual(["item-1", "item-2", "item-3"]);
|
|
3150
|
-
});
|
|
3151
|
-
|
|
3152
|
-
it("handles nested paths", async () => {
|
|
3153
|
-
const {unpopulate} = await import("./populate");
|
|
3154
|
-
const doc = {
|
|
3155
|
-
name: "test",
|
|
3156
|
-
nested: {
|
|
3157
|
-
items: [
|
|
3158
|
-
{_id: "item-1", name: "Item 1"},
|
|
3159
|
-
{_id: "item-2", name: "Item 2"},
|
|
3160
|
-
],
|
|
3161
|
-
},
|
|
3162
|
-
};
|
|
3163
|
-
const result = unpopulate(doc as any, "nested.items") as any;
|
|
3164
|
-
expect(result.nested.items).toEqual(["item-1", "item-2"]);
|
|
3165
|
-
});
|
|
3166
|
-
|
|
3167
|
-
it("returns original doc when path does not exist", async () => {
|
|
3168
|
-
const {unpopulate} = await import("./populate");
|
|
3169
|
-
const doc = {name: "test"};
|
|
3170
|
-
const result = unpopulate(doc as any, "nonexistent") as any;
|
|
3171
|
-
expect(result).toEqual(doc);
|
|
3172
|
-
});
|
|
3173
|
-
|
|
3174
|
-
it("handles nested array paths", async () => {
|
|
3175
|
-
const {unpopulate} = await import("./populate");
|
|
3176
|
-
const doc = {
|
|
3177
|
-
containers: [
|
|
3178
|
-
{items: [{_id: "item-1"}, {_id: "item-2"}]},
|
|
3179
|
-
{items: [{_id: "item-3"}, {_id: "item-4"}]},
|
|
3180
|
-
],
|
|
3181
|
-
name: "test",
|
|
3182
|
-
};
|
|
3183
|
-
const result = unpopulate(doc as any, "containers.items") as any;
|
|
3184
|
-
expect(result.containers[0].items).toEqual(["item-1", "item-2"]);
|
|
3185
|
-
expect(result.containers[1].items).toEqual(["item-3", "item-4"]);
|
|
3186
|
-
});
|
|
3187
|
-
});
|
|
3188
|
-
});
|
|
3189
|
-
|
|
3190
|
-
describe("auth module edge cases", () => {
|
|
3191
|
-
describe("generateTokens", () => {
|
|
3192
|
-
it("returns null tokens when user is missing", async () => {
|
|
3193
|
-
const {generateTokens} = await import("./auth");
|
|
3194
|
-
const result = await generateTokens(null);
|
|
3195
|
-
expect(result.token).toBeNull();
|
|
3196
|
-
expect(result.refreshToken).toBeNull();
|
|
3197
|
-
});
|
|
3198
|
-
|
|
3199
|
-
it("returns null tokens when user has no _id", async () => {
|
|
3200
|
-
const {generateTokens} = await import("./auth");
|
|
3201
|
-
const result = await generateTokens({email: "test@test.com"});
|
|
3202
|
-
expect(result.token).toBeNull();
|
|
3203
|
-
expect(result.refreshToken).toBeNull();
|
|
3204
|
-
});
|
|
3205
|
-
|
|
3206
|
-
it("includes custom payload from generateJWTPayload option", async () => {
|
|
3207
|
-
const {generateTokens} = await import("./auth");
|
|
3208
|
-
const jwt = await import("jsonwebtoken");
|
|
3209
|
-
|
|
3210
|
-
const user = {_id: "user-123"};
|
|
3211
|
-
const result = await generateTokens(user, {
|
|
3212
|
-
generateJWTPayload: (u) => ({customField: "customValue", userId: u._id}),
|
|
3213
|
-
});
|
|
3214
|
-
|
|
3215
|
-
expect(result.token).toBeDefined();
|
|
3216
|
-
const decoded = jwt.decode(result.token as string) as any;
|
|
3217
|
-
expect(decoded.customField).toBe("customValue");
|
|
3218
|
-
expect(decoded.id).toBe("user-123");
|
|
3219
|
-
});
|
|
3220
|
-
|
|
3221
|
-
it("uses custom token expiration from generateTokenExpiration option", async () => {
|
|
3222
|
-
const {generateTokens} = await import("./auth");
|
|
3223
|
-
const jwt = await import("jsonwebtoken");
|
|
3224
|
-
|
|
3225
|
-
const user = {_id: "user-123"};
|
|
3226
|
-
const result = await generateTokens(user, {
|
|
3227
|
-
generateTokenExpiration: () => "1h",
|
|
3228
|
-
});
|
|
3229
|
-
|
|
3230
|
-
expect(result.token).toBeDefined();
|
|
3231
|
-
const decoded = jwt.decode(result.token as string) as any;
|
|
3232
|
-
// Check that exp is roughly 1 hour from now (within 5 seconds tolerance)
|
|
3233
|
-
const expectedExp = Math.floor(Date.now() / 1000) + 3600;
|
|
3234
|
-
expect(decoded.exp).toBeGreaterThan(expectedExp - 5);
|
|
3235
|
-
expect(decoded.exp).toBeLessThan(expectedExp + 5);
|
|
3236
|
-
});
|
|
3237
|
-
|
|
3238
|
-
it("uses custom refresh token expiration from generateRefreshTokenExpiration option", async () => {
|
|
3239
|
-
const {generateTokens} = await import("./auth");
|
|
3240
|
-
const jwt = await import("jsonwebtoken");
|
|
3241
|
-
|
|
3242
|
-
const user = {_id: "user-123"};
|
|
3243
|
-
const result = await generateTokens(user, {
|
|
3244
|
-
generateRefreshTokenExpiration: () => "7d",
|
|
3245
|
-
});
|
|
3246
|
-
|
|
3247
|
-
expect(result.refreshToken).toBeDefined();
|
|
3248
|
-
const decoded = jwt.decode(result.refreshToken as string) as any;
|
|
3249
|
-
// Check that exp is roughly 7 days from now
|
|
3250
|
-
const expectedExp = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
|
|
3251
|
-
expect(decoded.exp).toBeGreaterThan(expectedExp - 10);
|
|
3252
|
-
expect(decoded.exp).toBeLessThan(expectedExp + 10);
|
|
3253
|
-
});
|
|
3254
|
-
});
|
|
3255
870
|
});
|