@terreno/api 0.0.11 → 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/CLAUDE.md +107 -0
- package/biome.jsonc +1 -1
- package/bunfig.toml +3 -2
- package/dist/api.arrayOperations.test.d.ts +1 -0
- package/dist/api.arrayOperations.test.js +868 -0
- package/dist/api.d.ts +3 -14
- 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.js +44 -68
- package/dist/api.query.test.d.ts +1 -0
- package/dist/api.query.test.js +805 -0
- package/dist/api.test.js +691 -1678
- 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.d.ts +1 -1
- package/dist/permissions.js +17 -25
- 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 +235 -7
- package/package.json +3 -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 +510 -1301
- package/src/api.ts +19 -61
- 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/permissions.ts +4 -14
- package/src/populate.test.ts +58 -0
- package/src/utils.test.ts +214 -9
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
+
import type express from "express";
|
|
3
|
+
import supertest from "supertest";
|
|
4
|
+
import type TestAgent from "supertest/lib/agent";
|
|
5
|
+
|
|
6
|
+
import {modelRouter} from "./api";
|
|
7
|
+
import {addAuthRoutes, setupAuth} from "./auth";
|
|
8
|
+
import {Permissions} from "./permissions";
|
|
9
|
+
import {authAsUser, type Food, FoodModel, getBaseServer, setupDb, UserModel} from "./tests";
|
|
10
|
+
import {AdminOwnerTransformer} from "./transformers";
|
|
11
|
+
|
|
12
|
+
describe("model array operations", () => {
|
|
13
|
+
let _server: TestAgent;
|
|
14
|
+
let app: express.Application;
|
|
15
|
+
let admin: any;
|
|
16
|
+
let spinach: Food;
|
|
17
|
+
let apple: Food;
|
|
18
|
+
let agent: TestAgent;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
22
|
+
|
|
23
|
+
[admin] = await setupDb();
|
|
24
|
+
|
|
25
|
+
[spinach, apple] = await Promise.all([
|
|
26
|
+
FoodModel.create({
|
|
27
|
+
calories: 1,
|
|
28
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
29
|
+
hidden: false,
|
|
30
|
+
name: "Spinach",
|
|
31
|
+
ownerId: admin._id,
|
|
32
|
+
source: {
|
|
33
|
+
name: "Brand",
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
FoodModel.create({
|
|
37
|
+
calories: 100,
|
|
38
|
+
categories: [
|
|
39
|
+
{
|
|
40
|
+
name: "Fruit",
|
|
41
|
+
show: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "Popular",
|
|
45
|
+
show: false,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
49
|
+
hidden: false,
|
|
50
|
+
name: "Apple",
|
|
51
|
+
ownerId: admin._id,
|
|
52
|
+
tags: ["healthy", "cheap"],
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
app = getBaseServer();
|
|
57
|
+
setupAuth(app, UserModel as any);
|
|
58
|
+
addAuthRoutes(app, UserModel as any);
|
|
59
|
+
app.use(
|
|
60
|
+
"/food",
|
|
61
|
+
modelRouter(FoodModel, {
|
|
62
|
+
allowAnonymous: true,
|
|
63
|
+
permissions: {
|
|
64
|
+
create: [Permissions.IsAdmin],
|
|
65
|
+
delete: [Permissions.IsAdmin],
|
|
66
|
+
list: [Permissions.IsAdmin],
|
|
67
|
+
read: [Permissions.IsAdmin],
|
|
68
|
+
update: [Permissions.IsAdmin],
|
|
69
|
+
},
|
|
70
|
+
queryFields: ["hidden", "calories", "created", "source.name"],
|
|
71
|
+
sort: {created: "descending"},
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
_server = supertest(app);
|
|
75
|
+
agent = await authAsUser(app, "admin");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("add array sub-schema item", async () => {
|
|
79
|
+
// Incorrect way, should have "categories" as a top level key.
|
|
80
|
+
let res = await agent
|
|
81
|
+
.post(`/food/${apple._id}/categories`)
|
|
82
|
+
.send({name: "Good Seller", show: false})
|
|
83
|
+
.expect(400);
|
|
84
|
+
expect(res.body.title).toBe(
|
|
85
|
+
"Malformed body, array operations should have a single, top level key, got: name,show"
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
res = await agent
|
|
89
|
+
.post(`/food/${apple._id}/categories`)
|
|
90
|
+
.send({categories: {name: "Good Seller", show: false}})
|
|
91
|
+
.expect(200);
|
|
92
|
+
expect(res.body.data.categories).toHaveLength(3);
|
|
93
|
+
expect(res.body.data.categories[2].name).toBe("Good Seller");
|
|
94
|
+
|
|
95
|
+
res = await agent
|
|
96
|
+
.post(`/food/${spinach._id}/categories`)
|
|
97
|
+
.send({categories: {name: "Good Seller", show: false}})
|
|
98
|
+
.expect(200);
|
|
99
|
+
expect(res.body.data.categories).toHaveLength(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("update array sub-schema item", async () => {
|
|
103
|
+
let res = await agent
|
|
104
|
+
.patch(`/food/${apple._id}/categories/xyz`)
|
|
105
|
+
.send({categories: {name: "Good Seller", show: false}})
|
|
106
|
+
.expect(404);
|
|
107
|
+
expect(res.body.title).toBe("Could not find categories/xyz");
|
|
108
|
+
res = await agent
|
|
109
|
+
.patch(`/food/${apple._id}/categories/${apple.categories[1]._id}`)
|
|
110
|
+
.send({categories: {name: "Good Seller", show: false}})
|
|
111
|
+
.expect(200);
|
|
112
|
+
expect(res.body.data.categories).toHaveLength(2);
|
|
113
|
+
expect(res.body.data.categories[1].name).toBe("Good Seller");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("delete array sub-schema item", async () => {
|
|
117
|
+
let res = await agent.delete(`/food/${apple._id}/categories/xyz`).expect(404);
|
|
118
|
+
expect(res.body.title).toBe("Could not find categories/xyz");
|
|
119
|
+
res = await agent
|
|
120
|
+
.delete(`/food/${apple._id}/categories/${apple.categories[0]._id}`)
|
|
121
|
+
.expect(200);
|
|
122
|
+
expect(res.body.data.categories).toHaveLength(1);
|
|
123
|
+
expect(res.body.data.categories[0].name).toBe("Popular");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("add array item", async () => {
|
|
127
|
+
let res = await agent.post(`/food/${apple._id}/tags`).send({tags: "popular"}).expect(200);
|
|
128
|
+
expect(res.body.data.tags).toHaveLength(3);
|
|
129
|
+
expect(res.body.data.tags).toEqual(["healthy", "cheap", "popular"]);
|
|
130
|
+
|
|
131
|
+
res = await agent.post(`/food/${spinach._id}/tags`).send({tags: "popular"}).expect(200);
|
|
132
|
+
expect(res.body.data.tags).toEqual(["popular"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("update array item", async () => {
|
|
136
|
+
let res = await agent
|
|
137
|
+
.patch(`/food/${apple._id}/tags/xyz`)
|
|
138
|
+
.send({tags: "unhealthy"})
|
|
139
|
+
.expect(404);
|
|
140
|
+
expect(res.body.title).toBe("Could not find tags/xyz");
|
|
141
|
+
res = await agent
|
|
142
|
+
.patch(`/food/${apple._id}/tags/healthy`)
|
|
143
|
+
.send({tags: "unhealthy"})
|
|
144
|
+
.expect(200);
|
|
145
|
+
expect(res.body.data.tags).toEqual(["unhealthy", "cheap"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("delete array item", async () => {
|
|
149
|
+
let res = await agent.delete(`/food/${apple._id}/tags/xyz`).expect(404);
|
|
150
|
+
expect(res.body.title).toBe("Could not find tags/xyz");
|
|
151
|
+
res = await agent.delete(`/food/${apple._id}/tags/healthy`).expect(200);
|
|
152
|
+
expect(res.body.data.tags).toEqual(["cheap"]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("updates timestamps on array subdocuments", async () => {
|
|
156
|
+
// Create a food with categories that have timestamps
|
|
157
|
+
const foodWithTimestamps = await FoodModel.create({
|
|
158
|
+
calories: 100,
|
|
159
|
+
categories: [
|
|
160
|
+
{
|
|
161
|
+
name: "Category 1",
|
|
162
|
+
show: true,
|
|
163
|
+
updated: new Date("2024-01-01T00:00:00.000Z"),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "Category 2",
|
|
167
|
+
show: true,
|
|
168
|
+
updated: new Date("2024-01-01T00:00:00.000Z"),
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
created: new Date(),
|
|
172
|
+
name: "Food with Timestamps",
|
|
173
|
+
ownerId: admin._id,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const firstCategoryId = foodWithTimestamps.categories?.[0]?._id?.toString();
|
|
177
|
+
const secondCategoryId = foodWithTimestamps.categories?.[1]?._id?.toString();
|
|
178
|
+
|
|
179
|
+
if (!firstCategoryId || !secondCategoryId) {
|
|
180
|
+
throw new Error("Failed to create food with categories");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Wait a moment to ensure timestamp difference
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
185
|
+
|
|
186
|
+
// Update one of the categories
|
|
187
|
+
const res = await agent
|
|
188
|
+
.patch(`/food/${foodWithTimestamps._id}/categories/${firstCategoryId}`)
|
|
189
|
+
.send({categories: {name: "Updated Category"}})
|
|
190
|
+
.expect(200);
|
|
191
|
+
|
|
192
|
+
// Verify the updated category has a newer timestamp
|
|
193
|
+
const updatedCategory = res.body.data.categories.find((c: any) => c._id === firstCategoryId);
|
|
194
|
+
const unchangedCategory = res.body.data.categories.find((c: any) => c._id === secondCategoryId);
|
|
195
|
+
|
|
196
|
+
if (!updatedCategory || !unchangedCategory) {
|
|
197
|
+
throw new Error("Failed to find categories in response");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
expect(updatedCategory.updated).not.toBe(updatedCategory.created);
|
|
201
|
+
expect(unchangedCategory.updated).toBe(unchangedCategory.created);
|
|
202
|
+
expect(updatedCategory.name).toBe("Updated Category");
|
|
203
|
+
// Unchanged.
|
|
204
|
+
expect(updatedCategory.show).toBe(true);
|
|
205
|
+
expect(unchangedCategory.show).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("array operations call postUpdate with different copy of document", async () => {
|
|
209
|
+
let postUpdateDoc: any;
|
|
210
|
+
let postUpdatePrevDoc: any;
|
|
211
|
+
let postUpdateCalled = false;
|
|
212
|
+
|
|
213
|
+
app = getBaseServer();
|
|
214
|
+
setupAuth(app, UserModel as any);
|
|
215
|
+
addAuthRoutes(app, UserModel as any);
|
|
216
|
+
app.use(
|
|
217
|
+
"/food",
|
|
218
|
+
modelRouter(FoodModel, {
|
|
219
|
+
allowAnonymous: true,
|
|
220
|
+
permissions: {
|
|
221
|
+
create: [Permissions.IsAdmin],
|
|
222
|
+
delete: [Permissions.IsAdmin],
|
|
223
|
+
list: [Permissions.IsAdmin],
|
|
224
|
+
read: [Permissions.IsAdmin],
|
|
225
|
+
update: [Permissions.IsAdmin],
|
|
226
|
+
},
|
|
227
|
+
postUpdate: async (doc: any, _cleanedBody: any, _request: any, prevValue: any) => {
|
|
228
|
+
postUpdateDoc = doc;
|
|
229
|
+
postUpdatePrevDoc = prevValue;
|
|
230
|
+
postUpdateCalled = true;
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
_server = supertest(app);
|
|
235
|
+
agent = await authAsUser(app, "admin");
|
|
236
|
+
|
|
237
|
+
// Test POST operation (add to array)
|
|
238
|
+
await agent
|
|
239
|
+
.post(`/food/${apple._id}/categories`)
|
|
240
|
+
.send({categories: {name: "New Category", show: true}})
|
|
241
|
+
.expect(200);
|
|
242
|
+
|
|
243
|
+
expect(postUpdateCalled).toBe(true);
|
|
244
|
+
expect(postUpdateDoc).toBeDefined();
|
|
245
|
+
expect(postUpdatePrevDoc).toBeDefined();
|
|
246
|
+
|
|
247
|
+
// Verify they are different object references
|
|
248
|
+
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
249
|
+
|
|
250
|
+
// Verify the content is different (new category added)
|
|
251
|
+
expect(postUpdateDoc.categories).toHaveLength(3);
|
|
252
|
+
expect(postUpdatePrevDoc.categories).toHaveLength(2);
|
|
253
|
+
|
|
254
|
+
// Reset for next test
|
|
255
|
+
postUpdateCalled = false;
|
|
256
|
+
postUpdateDoc = undefined;
|
|
257
|
+
postUpdatePrevDoc = undefined;
|
|
258
|
+
|
|
259
|
+
// Test PATCH operation (update array item)
|
|
260
|
+
const categoryId = apple.categories[0]._id;
|
|
261
|
+
if (!categoryId) {
|
|
262
|
+
throw new Error("Category ID is undefined");
|
|
263
|
+
}
|
|
264
|
+
await agent
|
|
265
|
+
.patch(`/food/${apple._id}/categories/${categoryId}`)
|
|
266
|
+
.send({categories: {name: "Updated Category", show: false}})
|
|
267
|
+
.expect(200);
|
|
268
|
+
|
|
269
|
+
expect(postUpdateCalled).toBe(true);
|
|
270
|
+
expect(postUpdateDoc).toBeDefined();
|
|
271
|
+
expect(postUpdatePrevDoc).toBeDefined();
|
|
272
|
+
|
|
273
|
+
// Verify they are different object references
|
|
274
|
+
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
275
|
+
|
|
276
|
+
// Verify the content is different (category updated)
|
|
277
|
+
const updatedCategory = postUpdateDoc.categories.find(
|
|
278
|
+
(c: any) => c._id.toString() === categoryId.toString()
|
|
279
|
+
);
|
|
280
|
+
const prevCategory = postUpdatePrevDoc.categories.find(
|
|
281
|
+
(c: any) => c._id.toString() === categoryId.toString()
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(updatedCategory.name).toBe("Updated Category");
|
|
285
|
+
expect(prevCategory.name).toBe("Fruit");
|
|
286
|
+
|
|
287
|
+
// Reset for next test
|
|
288
|
+
postUpdateCalled = false;
|
|
289
|
+
postUpdateDoc = undefined;
|
|
290
|
+
postUpdatePrevDoc = undefined;
|
|
291
|
+
|
|
292
|
+
// Test DELETE operation (remove from array)
|
|
293
|
+
await agent.delete(`/food/${apple._id}/categories/${categoryId}`).expect(200);
|
|
294
|
+
|
|
295
|
+
expect(postUpdateCalled).toBe(true);
|
|
296
|
+
expect(postUpdateDoc).toBeDefined();
|
|
297
|
+
expect(postUpdatePrevDoc).toBeDefined();
|
|
298
|
+
|
|
299
|
+
// Verify they are different object references
|
|
300
|
+
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
301
|
+
|
|
302
|
+
// Verify the content is different (category removed)
|
|
303
|
+
const remainingCategories = postUpdateDoc.categories.filter(
|
|
304
|
+
(c: any) => c._id.toString() === categoryId.toString()
|
|
305
|
+
);
|
|
306
|
+
const prevCategories = postUpdatePrevDoc.categories.filter(
|
|
307
|
+
(c: any) => c._id.toString() === categoryId.toString()
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
expect(remainingCategories).toHaveLength(0);
|
|
311
|
+
expect(prevCategories).toHaveLength(1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("array operations with string arrays call postUpdate with different copy", async () => {
|
|
315
|
+
let postUpdateDoc: any;
|
|
316
|
+
let postUpdatePrevDoc: any;
|
|
317
|
+
let postUpdateCalled = false;
|
|
318
|
+
|
|
319
|
+
app = getBaseServer();
|
|
320
|
+
setupAuth(app, UserModel as any);
|
|
321
|
+
addAuthRoutes(app, UserModel as any);
|
|
322
|
+
app.use(
|
|
323
|
+
"/food",
|
|
324
|
+
modelRouter(FoodModel, {
|
|
325
|
+
allowAnonymous: true,
|
|
326
|
+
permissions: {
|
|
327
|
+
create: [Permissions.IsAdmin],
|
|
328
|
+
delete: [Permissions.IsAdmin],
|
|
329
|
+
list: [Permissions.IsAdmin],
|
|
330
|
+
read: [Permissions.IsAdmin],
|
|
331
|
+
update: [Permissions.IsAdmin],
|
|
332
|
+
},
|
|
333
|
+
postUpdate: async (doc: any, _cleanedBody: any, _request: any, prevValue: any) => {
|
|
334
|
+
postUpdateDoc = doc;
|
|
335
|
+
postUpdatePrevDoc = prevValue;
|
|
336
|
+
postUpdateCalled = true;
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
_server = supertest(app);
|
|
341
|
+
agent = await authAsUser(app, "admin");
|
|
342
|
+
|
|
343
|
+
// Test POST operation with string array (add tag)
|
|
344
|
+
await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(200);
|
|
345
|
+
|
|
346
|
+
expect(postUpdateCalled).toBe(true);
|
|
347
|
+
expect(postUpdateDoc).toBeDefined();
|
|
348
|
+
expect(postUpdatePrevDoc).toBeDefined();
|
|
349
|
+
|
|
350
|
+
// Verify they are different object references
|
|
351
|
+
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
352
|
+
|
|
353
|
+
// Verify the content is different (new tag added)
|
|
354
|
+
expect(postUpdateDoc.tags).toHaveLength(3);
|
|
355
|
+
expect(postUpdatePrevDoc.tags).toHaveLength(2);
|
|
356
|
+
expect(postUpdateDoc.tags).toContain("organic");
|
|
357
|
+
expect(postUpdatePrevDoc.tags).not.toContain("organic");
|
|
358
|
+
|
|
359
|
+
// Reset for next test
|
|
360
|
+
postUpdateCalled = false;
|
|
361
|
+
postUpdateDoc = undefined;
|
|
362
|
+
postUpdatePrevDoc = undefined;
|
|
363
|
+
|
|
364
|
+
// Test PATCH operation with string array (update tag)
|
|
365
|
+
await agent.patch(`/food/${apple._id}/tags/healthy`).send({tags: "super-healthy"}).expect(200);
|
|
366
|
+
|
|
367
|
+
expect(postUpdateCalled).toBe(true);
|
|
368
|
+
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
369
|
+
|
|
370
|
+
// Verify the content is different (tag updated)
|
|
371
|
+
expect(postUpdateDoc.tags).toContain("super-healthy");
|
|
372
|
+
expect(postUpdatePrevDoc.tags).toContain("healthy");
|
|
373
|
+
expect(postUpdateDoc.tags).not.toContain("healthy");
|
|
374
|
+
expect(postUpdatePrevDoc.tags).not.toContain("super-healthy");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("array operation errors", () => {
|
|
379
|
+
let _server: TestAgent;
|
|
380
|
+
let app: express.Application;
|
|
381
|
+
let admin: any;
|
|
382
|
+
let apple: Food;
|
|
383
|
+
let agent: TestAgent;
|
|
384
|
+
|
|
385
|
+
beforeEach(async () => {
|
|
386
|
+
[admin] = await setupDb();
|
|
387
|
+
|
|
388
|
+
apple = await FoodModel.create({
|
|
389
|
+
calories: 100,
|
|
390
|
+
categories: [
|
|
391
|
+
{name: "Fruit", show: true},
|
|
392
|
+
{name: "Popular", show: false},
|
|
393
|
+
],
|
|
394
|
+
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
395
|
+
hidden: false,
|
|
396
|
+
name: "Apple",
|
|
397
|
+
ownerId: admin._id,
|
|
398
|
+
tags: ["healthy", "cheap"],
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
app = getBaseServer();
|
|
402
|
+
setupAuth(app, UserModel as any);
|
|
403
|
+
addAuthRoutes(app, UserModel as any);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("array operation preUpdate returning undefined throws error", async () => {
|
|
407
|
+
app.use(
|
|
408
|
+
"/food",
|
|
409
|
+
modelRouter(FoodModel, {
|
|
410
|
+
allowAnonymous: true,
|
|
411
|
+
permissions: {
|
|
412
|
+
create: [Permissions.IsAdmin],
|
|
413
|
+
delete: [Permissions.IsAdmin],
|
|
414
|
+
list: [Permissions.IsAdmin],
|
|
415
|
+
read: [Permissions.IsAdmin],
|
|
416
|
+
update: [Permissions.IsAdmin],
|
|
417
|
+
},
|
|
418
|
+
preUpdate: () => undefined as any,
|
|
419
|
+
})
|
|
420
|
+
);
|
|
421
|
+
_server = supertest(app);
|
|
422
|
+
agent = await authAsUser(app, "admin");
|
|
423
|
+
|
|
424
|
+
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
|
|
425
|
+
expect(res.body.title).toBe("Update not allowed");
|
|
426
|
+
expect(res.body.detail).toBe("A body must be returned from preUpdate");
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("array operation preUpdate returning null throws error", async () => {
|
|
430
|
+
app.use(
|
|
431
|
+
"/food",
|
|
432
|
+
modelRouter(FoodModel, {
|
|
433
|
+
allowAnonymous: true,
|
|
434
|
+
permissions: {
|
|
435
|
+
create: [Permissions.IsAdmin],
|
|
436
|
+
delete: [Permissions.IsAdmin],
|
|
437
|
+
list: [Permissions.IsAdmin],
|
|
438
|
+
read: [Permissions.IsAdmin],
|
|
439
|
+
update: [Permissions.IsAdmin],
|
|
440
|
+
},
|
|
441
|
+
preUpdate: () => null,
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
_server = supertest(app);
|
|
445
|
+
agent = await authAsUser(app, "admin");
|
|
446
|
+
|
|
447
|
+
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
|
|
448
|
+
expect(res.body.title).toBe("Update not allowed");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("array operation preUpdate error is handled", async () => {
|
|
452
|
+
app.use(
|
|
453
|
+
"/food",
|
|
454
|
+
modelRouter(FoodModel, {
|
|
455
|
+
allowAnonymous: true,
|
|
456
|
+
permissions: {
|
|
457
|
+
create: [Permissions.IsAdmin],
|
|
458
|
+
delete: [Permissions.IsAdmin],
|
|
459
|
+
list: [Permissions.IsAdmin],
|
|
460
|
+
read: [Permissions.IsAdmin],
|
|
461
|
+
update: [Permissions.IsAdmin],
|
|
462
|
+
},
|
|
463
|
+
preUpdate: () => {
|
|
464
|
+
throw new Error("preUpdate array failed");
|
|
465
|
+
},
|
|
466
|
+
})
|
|
467
|
+
);
|
|
468
|
+
_server = supertest(app);
|
|
469
|
+
agent = await authAsUser(app, "admin");
|
|
470
|
+
|
|
471
|
+
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(400);
|
|
472
|
+
expect(res.body.title).toContain("preUpdate hook error");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("array operation postUpdate error is handled", async () => {
|
|
476
|
+
app.use(
|
|
477
|
+
"/food",
|
|
478
|
+
modelRouter(FoodModel, {
|
|
479
|
+
allowAnonymous: true,
|
|
480
|
+
permissions: {
|
|
481
|
+
create: [Permissions.IsAdmin],
|
|
482
|
+
delete: [Permissions.IsAdmin],
|
|
483
|
+
list: [Permissions.IsAdmin],
|
|
484
|
+
read: [Permissions.IsAdmin],
|
|
485
|
+
update: [Permissions.IsAdmin],
|
|
486
|
+
},
|
|
487
|
+
postUpdate: () => {
|
|
488
|
+
throw new Error("postUpdate array failed");
|
|
489
|
+
},
|
|
490
|
+
})
|
|
491
|
+
);
|
|
492
|
+
_server = supertest(app);
|
|
493
|
+
agent = await authAsUser(app, "admin");
|
|
494
|
+
|
|
495
|
+
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(400);
|
|
496
|
+
expect(res.body.title).toContain("PATCH Post Update error");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("array operation denied without update permission", async () => {
|
|
500
|
+
app.use(
|
|
501
|
+
"/food",
|
|
502
|
+
modelRouter(FoodModel, {
|
|
503
|
+
allowAnonymous: true,
|
|
504
|
+
permissions: {
|
|
505
|
+
create: [Permissions.IsAdmin],
|
|
506
|
+
delete: [Permissions.IsAdmin],
|
|
507
|
+
list: [Permissions.IsAny],
|
|
508
|
+
read: [Permissions.IsAny],
|
|
509
|
+
update: [Permissions.IsAdmin],
|
|
510
|
+
},
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
_server = supertest(app);
|
|
514
|
+
agent = await authAsUser(app, "notAdmin");
|
|
515
|
+
|
|
516
|
+
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(405);
|
|
517
|
+
expect(res.body.title).toContain("Access to PATCH");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("array operation on non-existent document returns 404", async () => {
|
|
521
|
+
app.use(
|
|
522
|
+
"/food",
|
|
523
|
+
modelRouter(FoodModel, {
|
|
524
|
+
allowAnonymous: true,
|
|
525
|
+
permissions: {
|
|
526
|
+
create: [Permissions.IsAdmin],
|
|
527
|
+
delete: [Permissions.IsAdmin],
|
|
528
|
+
list: [Permissions.IsAdmin],
|
|
529
|
+
read: [Permissions.IsAdmin],
|
|
530
|
+
update: [Permissions.IsAdmin],
|
|
531
|
+
},
|
|
532
|
+
})
|
|
533
|
+
);
|
|
534
|
+
_server = supertest(app);
|
|
535
|
+
agent = await authAsUser(app, "admin");
|
|
536
|
+
|
|
537
|
+
const fakeId = "000000000000000000000000";
|
|
538
|
+
const res = await agent.post(`/food/${fakeId}/tags`).send({tags: "organic"}).expect(404);
|
|
539
|
+
expect(res.body.title).toContain("Could not find document to PATCH");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("array operation denied when user cannot update specific doc", async () => {
|
|
543
|
+
// Create food owned by admin, then try to update as notAdmin
|
|
544
|
+
app.use(
|
|
545
|
+
"/food",
|
|
546
|
+
modelRouter(FoodModel, {
|
|
547
|
+
allowAnonymous: true,
|
|
548
|
+
permissions: {
|
|
549
|
+
create: [Permissions.IsAuthenticated],
|
|
550
|
+
delete: [Permissions.IsAuthenticated],
|
|
551
|
+
list: [Permissions.IsAuthenticated],
|
|
552
|
+
read: [Permissions.IsAuthenticated],
|
|
553
|
+
update: [Permissions.IsOwner],
|
|
554
|
+
},
|
|
555
|
+
})
|
|
556
|
+
);
|
|
557
|
+
_server = supertest(app);
|
|
558
|
+
// Login as notAdmin and try to update admin's food (apple)
|
|
559
|
+
agent = await authAsUser(app, "notAdmin");
|
|
560
|
+
|
|
561
|
+
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
|
|
562
|
+
expect(res.body.title).toContain("Patch not allowed");
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("array operation transform error is handled", async () => {
|
|
566
|
+
app.use(
|
|
567
|
+
"/food",
|
|
568
|
+
modelRouter(FoodModel, {
|
|
569
|
+
allowAnonymous: true,
|
|
570
|
+
permissions: {
|
|
571
|
+
create: [Permissions.IsAdmin],
|
|
572
|
+
delete: [Permissions.IsAdmin],
|
|
573
|
+
list: [Permissions.IsAdmin],
|
|
574
|
+
read: [Permissions.IsAdmin],
|
|
575
|
+
update: [Permissions.IsAdmin],
|
|
576
|
+
},
|
|
577
|
+
transformer: AdminOwnerTransformer({
|
|
578
|
+
adminWriteFields: ["name"],
|
|
579
|
+
}),
|
|
580
|
+
})
|
|
581
|
+
);
|
|
582
|
+
_server = supertest(app);
|
|
583
|
+
agent = await authAsUser(app, "admin");
|
|
584
|
+
|
|
585
|
+
// Try to update tags field, which is not in the allowed write fields
|
|
586
|
+
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
|
|
587
|
+
expect(res.body.title).toContain("cannot write fields");
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe("array operation with undefined preUpdate return", () => {
|
|
592
|
+
let _server: TestAgent;
|
|
593
|
+
let app: express.Application;
|
|
594
|
+
let admin: any;
|
|
595
|
+
let apple: Food;
|
|
596
|
+
let agent: TestAgent;
|
|
597
|
+
|
|
598
|
+
beforeEach(async () => {
|
|
599
|
+
[admin] = await setupDb();
|
|
600
|
+
|
|
601
|
+
apple = await FoodModel.create({
|
|
602
|
+
calories: 100,
|
|
603
|
+
categories: [
|
|
604
|
+
{name: "Fruit", show: true},
|
|
605
|
+
{name: "Popular", show: false},
|
|
606
|
+
],
|
|
607
|
+
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
608
|
+
hidden: false,
|
|
609
|
+
name: "Apple",
|
|
610
|
+
ownerId: admin._id,
|
|
611
|
+
tags: ["healthy", "cheap"],
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
app = getBaseServer();
|
|
615
|
+
setupAuth(app, UserModel as any);
|
|
616
|
+
addAuthRoutes(app, UserModel as any);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("array operation preUpdate returning undefined for array POST throws error", async () => {
|
|
620
|
+
app.use(
|
|
621
|
+
"/food",
|
|
622
|
+
modelRouter(FoodModel, {
|
|
623
|
+
allowAnonymous: true,
|
|
624
|
+
permissions: {
|
|
625
|
+
create: [Permissions.IsAdmin],
|
|
626
|
+
delete: [Permissions.IsAdmin],
|
|
627
|
+
list: [Permissions.IsAdmin],
|
|
628
|
+
read: [Permissions.IsAdmin],
|
|
629
|
+
update: [Permissions.IsAdmin],
|
|
630
|
+
},
|
|
631
|
+
preUpdate: () => undefined as any,
|
|
632
|
+
})
|
|
633
|
+
);
|
|
634
|
+
_server = supertest(app);
|
|
635
|
+
agent = await authAsUser(app, "admin");
|
|
636
|
+
|
|
637
|
+
const res = await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(403);
|
|
638
|
+
expect(res.body.title).toBe("Update not allowed");
|
|
639
|
+
expect(res.body.detail).toBe("A body must be returned from preUpdate");
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("array operation preUpdate returning null for array PATCH throws error", async () => {
|
|
643
|
+
app.use(
|
|
644
|
+
"/food",
|
|
645
|
+
modelRouter(FoodModel, {
|
|
646
|
+
allowAnonymous: true,
|
|
647
|
+
permissions: {
|
|
648
|
+
create: [Permissions.IsAdmin],
|
|
649
|
+
delete: [Permissions.IsAdmin],
|
|
650
|
+
list: [Permissions.IsAdmin],
|
|
651
|
+
read: [Permissions.IsAdmin],
|
|
652
|
+
update: [Permissions.IsAdmin],
|
|
653
|
+
},
|
|
654
|
+
preUpdate: () => null,
|
|
655
|
+
})
|
|
656
|
+
);
|
|
657
|
+
_server = supertest(app);
|
|
658
|
+
agent = await authAsUser(app, "admin");
|
|
659
|
+
|
|
660
|
+
const res = await agent
|
|
661
|
+
.patch(`/food/${apple._id}/tags/healthy`)
|
|
662
|
+
.send({tags: "unhealthy"})
|
|
663
|
+
.expect(403);
|
|
664
|
+
expect(res.body.title).toBe("Update not allowed");
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it("array operation preUpdate error for array DELETE is handled", async () => {
|
|
668
|
+
app.use(
|
|
669
|
+
"/food",
|
|
670
|
+
modelRouter(FoodModel, {
|
|
671
|
+
allowAnonymous: true,
|
|
672
|
+
permissions: {
|
|
673
|
+
create: [Permissions.IsAdmin],
|
|
674
|
+
delete: [Permissions.IsAdmin],
|
|
675
|
+
list: [Permissions.IsAdmin],
|
|
676
|
+
read: [Permissions.IsAdmin],
|
|
677
|
+
update: [Permissions.IsAdmin],
|
|
678
|
+
},
|
|
679
|
+
preUpdate: () => {
|
|
680
|
+
throw new Error("preUpdate error during delete");
|
|
681
|
+
},
|
|
682
|
+
})
|
|
683
|
+
);
|
|
684
|
+
_server = supertest(app);
|
|
685
|
+
agent = await authAsUser(app, "admin");
|
|
686
|
+
|
|
687
|
+
const res = await agent.delete(`/food/${apple._id}/tags/healthy`).expect(400);
|
|
688
|
+
expect(res.body.title).toContain("preUpdate hook error");
|
|
689
|
+
});
|
|
690
|
+
});
|