@terreno/api 0.0.11 → 0.0.13
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
package/src/api.test.ts
CHANGED
|
@@ -1,47 +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 sortBy from "lodash/sortBy";
|
|
5
|
-
import type mongoose from "mongoose";
|
|
6
|
-
import qs from "qs";
|
|
7
3
|
import supertest from "supertest";
|
|
8
4
|
import type TestAgent from "supertest/lib/agent";
|
|
9
5
|
|
|
10
|
-
import {modelRouter} from "./api";
|
|
6
|
+
import {addPopulateToQuery, modelRouter} from "./api";
|
|
11
7
|
import {addAuthRoutes, setupAuth} from "./auth";
|
|
12
|
-
import {APIError} from "./errors";
|
|
13
|
-
import {logRequests} from "./expressServer";
|
|
14
8
|
import {Permissions} from "./permissions";
|
|
15
9
|
import {
|
|
16
10
|
authAsUser,
|
|
17
11
|
type Food,
|
|
18
12
|
FoodModel,
|
|
19
13
|
getBaseServer,
|
|
20
|
-
|
|
21
|
-
StaffUserModel,
|
|
22
|
-
type SuperUser,
|
|
23
|
-
SuperUserModel,
|
|
14
|
+
RequiredModel,
|
|
24
15
|
setupDb,
|
|
25
16
|
UserModel,
|
|
26
17
|
} from "./tests";
|
|
18
|
+
import {AdminOwnerTransformer} from "./transformers";
|
|
27
19
|
|
|
28
20
|
describe("@terreno/api", () => {
|
|
29
21
|
let server: TestAgent;
|
|
30
22
|
let app: express.Application;
|
|
31
23
|
|
|
32
|
-
describe("
|
|
24
|
+
describe("populate", () => {
|
|
25
|
+
let admin: any;
|
|
26
|
+
let notAdmin: any;
|
|
33
27
|
let agent: TestAgent;
|
|
28
|
+
let spinach: Food;
|
|
34
29
|
|
|
35
30
|
beforeEach(async () => {
|
|
36
|
-
await setupDb();
|
|
31
|
+
[admin, notAdmin] = await setupDb();
|
|
32
|
+
|
|
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",
|
|
42
|
+
},
|
|
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",
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
37
55
|
app = getBaseServer();
|
|
38
56
|
setupAuth(app, UserModel as any);
|
|
39
57
|
addAuthRoutes(app, UserModel as any);
|
|
40
|
-
agent = await authAsUser(app, "notAdmin");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("pre hooks change data", async () => {
|
|
44
|
-
let deleteCalled = false;
|
|
45
58
|
app.use(
|
|
46
59
|
"/food",
|
|
47
60
|
modelRouter(FoodModel, {
|
|
@@ -53,66 +66,90 @@ describe("@terreno/api", () => {
|
|
|
53
66
|
read: [Permissions.IsAny],
|
|
54
67
|
update: [Permissions.IsAny],
|
|
55
68
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return data;
|
|
59
|
-
},
|
|
60
|
-
preDelete: (data: any) => {
|
|
61
|
-
deleteCalled = true;
|
|
62
|
-
return data;
|
|
63
|
-
},
|
|
64
|
-
preUpdate: (data: any) => {
|
|
65
|
-
data.calories = 15;
|
|
66
|
-
return data;
|
|
67
|
-
},
|
|
69
|
+
populatePaths: [{fields: ["email"], path: "ownerId"}],
|
|
70
|
+
sort: "-created",
|
|
68
71
|
})
|
|
69
72
|
);
|
|
70
73
|
server = supertest(app);
|
|
74
|
+
agent = await authAsUser(app, "notAdmin");
|
|
75
|
+
});
|
|
76
|
+
|
|
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();
|
|
87
|
+
});
|
|
88
|
+
|
|
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();
|
|
94
|
+
});
|
|
71
95
|
|
|
72
|
-
|
|
96
|
+
it("creates with populate", async () => {
|
|
97
|
+
const res = await server
|
|
73
98
|
.post("/food")
|
|
74
99
|
.send({
|
|
75
100
|
calories: 15,
|
|
76
101
|
name: "Broccoli",
|
|
102
|
+
ownerId: admin._id,
|
|
77
103
|
})
|
|
78
104
|
.expect(201);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
expect(broccoli.name).toBe("Broccoli");
|
|
84
|
-
// Overwritten by the pre create hook
|
|
85
|
-
expect(broccoli.calories).toBe(14);
|
|
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();
|
|
108
|
+
});
|
|
86
109
|
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
it("updates with populate", async () => {
|
|
111
|
+
const res = await server
|
|
112
|
+
.patch(`/food/${spinach._id}`)
|
|
89
113
|
.send({
|
|
90
|
-
name: "
|
|
114
|
+
name: "NotSpinach",
|
|
91
115
|
})
|
|
92
116
|
.expect(200);
|
|
93
|
-
expect(res.body.data.
|
|
94
|
-
|
|
95
|
-
expect(res.body.data.
|
|
96
|
-
|
|
97
|
-
await agent.delete(`/food/${broccoli._id}`).expect(204);
|
|
98
|
-
expect(deleteCalled).toBe(true);
|
|
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();
|
|
99
120
|
});
|
|
121
|
+
});
|
|
100
122
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const spinach = await FoodModel.create({
|
|
106
|
-
calories: 1,
|
|
107
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
108
|
-
hidden: false,
|
|
109
|
-
name: "Spinach",
|
|
110
|
-
ownerId: (notAdmin as any)._id,
|
|
111
|
-
source: {
|
|
112
|
-
name: "Brand",
|
|
113
|
-
},
|
|
114
|
-
});
|
|
123
|
+
describe("responseHandler", () => {
|
|
124
|
+
let admin: any;
|
|
125
|
+
let agent: TestAgent;
|
|
126
|
+
let spinach: Food;
|
|
115
127
|
|
|
128
|
+
beforeEach(async () => {
|
|
129
|
+
[admin] = await setupDb();
|
|
130
|
+
|
|
131
|
+
[spinach] = await Promise.all([
|
|
132
|
+
FoodModel.create({
|
|
133
|
+
calories: 1,
|
|
134
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
135
|
+
hidden: false,
|
|
136
|
+
name: "Spinach",
|
|
137
|
+
ownerId: admin._id,
|
|
138
|
+
source: {
|
|
139
|
+
name: "Brand",
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
FoodModel.create({
|
|
143
|
+
calories: 100,
|
|
144
|
+
created: Date.now() - 10,
|
|
145
|
+
hidden: true,
|
|
146
|
+
name: "Apple",
|
|
147
|
+
ownerId: admin?._id,
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
app = getBaseServer();
|
|
151
|
+
setupAuth(app, UserModel as any);
|
|
152
|
+
addAuthRoutes(app, UserModel as any);
|
|
116
153
|
app.use(
|
|
117
154
|
"/food",
|
|
118
155
|
modelRouter(FoodModel, {
|
|
@@ -124,37 +161,54 @@ describe("@terreno/api", () => {
|
|
|
124
161
|
read: [Permissions.IsAny],
|
|
125
162
|
update: [Permissions.IsAny],
|
|
126
163
|
},
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
};
|
|
175
|
+
},
|
|
130
176
|
})
|
|
131
177
|
);
|
|
132
178
|
server = supertest(app);
|
|
179
|
+
agent = await authAsUser(app, "notAdmin");
|
|
180
|
+
});
|
|
133
181
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.expect(403);
|
|
141
|
-
const broccoli = await FoodModel.findById(res.body._id);
|
|
142
|
-
expect(broccoli).toBeNull();
|
|
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");
|
|
187
|
+
});
|
|
143
188
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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();
|
|
193
|
+
|
|
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");
|
|
151
198
|
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("plugins", () => {
|
|
202
|
+
let agent: TestAgent;
|
|
152
203
|
|
|
153
|
-
|
|
154
|
-
|
|
204
|
+
beforeEach(async () => {
|
|
205
|
+
await setupDb();
|
|
206
|
+
app = getBaseServer();
|
|
207
|
+
setupAuth(app, UserModel as any);
|
|
208
|
+
addAuthRoutes(app, UserModel as any);
|
|
155
209
|
app.use(
|
|
156
|
-
"/
|
|
157
|
-
modelRouter(
|
|
210
|
+
"/users",
|
|
211
|
+
modelRouter(UserModel, {
|
|
158
212
|
allowAnonymous: true,
|
|
159
213
|
permissions: {
|
|
160
214
|
create: [Permissions.IsAny],
|
|
@@ -163,58 +217,45 @@ describe("@terreno/api", () => {
|
|
|
163
217
|
read: [Permissions.IsAny],
|
|
164
218
|
update: [Permissions.IsAny],
|
|
165
219
|
},
|
|
166
|
-
postCreate: async (data: any) => {
|
|
167
|
-
data.calories = 14;
|
|
168
|
-
await data.save();
|
|
169
|
-
return data;
|
|
170
|
-
},
|
|
171
|
-
postDelete: (data: any) => {
|
|
172
|
-
deleteCalled = true;
|
|
173
|
-
return data;
|
|
174
|
-
},
|
|
175
|
-
postUpdate: async (data: any) => {
|
|
176
|
-
data.calories = 15;
|
|
177
|
-
await data.save();
|
|
178
|
-
return data;
|
|
179
|
-
},
|
|
180
220
|
})
|
|
181
221
|
);
|
|
182
222
|
server = supertest(app);
|
|
223
|
+
agent = await authAsUser(app, "notAdmin");
|
|
224
|
+
});
|
|
183
225
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!broccoli) {
|
|
193
|
-
throw new Error("Broccoli was not created");
|
|
194
|
-
}
|
|
195
|
-
expect(broccoli.name).toBe("Broccoli");
|
|
196
|
-
// Overwritten by the pre create hook
|
|
197
|
-
expect(broccoli.calories).toBe(14);
|
|
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();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
198
234
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
235
|
+
describe("error handling", () => {
|
|
236
|
+
let admin: any;
|
|
237
|
+
let spinach: Food;
|
|
238
|
+
|
|
239
|
+
beforeEach(async () => {
|
|
240
|
+
[admin] = await setupDb();
|
|
241
|
+
|
|
242
|
+
spinach = await FoodModel.create({
|
|
243
|
+
calories: 1,
|
|
244
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
245
|
+
hidden: false,
|
|
246
|
+
name: "Spinach",
|
|
247
|
+
ownerId: admin._id,
|
|
248
|
+
source: {
|
|
249
|
+
name: "Brand",
|
|
250
|
+
},
|
|
251
|
+
});
|
|
212
252
|
|
|
213
|
-
|
|
214
|
-
|
|
253
|
+
app = getBaseServer();
|
|
254
|
+
setupAuth(app, UserModel as any);
|
|
255
|
+
addAuthRoutes(app, UserModel as any);
|
|
215
256
|
});
|
|
216
257
|
|
|
217
|
-
it("
|
|
258
|
+
it("PUT returns 500 not supported", async () => {
|
|
218
259
|
app.use(
|
|
219
260
|
"/food",
|
|
220
261
|
modelRouter(FoodModel, {
|
|
@@ -226,30 +267,15 @@ describe("@terreno/api", () => {
|
|
|
226
267
|
read: [Permissions.IsAny],
|
|
227
268
|
update: [Permissions.IsAny],
|
|
228
269
|
},
|
|
229
|
-
preCreate: () => {
|
|
230
|
-
throw new APIError({
|
|
231
|
-
disableExternalErrorTracking: true,
|
|
232
|
-
status: 400,
|
|
233
|
-
title: "Custom preCreate error",
|
|
234
|
-
});
|
|
235
|
-
},
|
|
236
270
|
})
|
|
237
271
|
);
|
|
238
272
|
server = supertest(app);
|
|
239
273
|
|
|
240
|
-
const res = await server
|
|
241
|
-
|
|
242
|
-
.send({
|
|
243
|
-
calories: 15,
|
|
244
|
-
name: "Broccoli",
|
|
245
|
-
})
|
|
246
|
-
.expect(400);
|
|
247
|
-
|
|
248
|
-
expect(res.body.title).toBe("Custom preCreate error");
|
|
249
|
-
expect(res.body.disableExternalErrorTracking).toBe(true);
|
|
274
|
+
const res = await server.put(`/food/${spinach._id}`).send({name: "Kale"}).expect(500);
|
|
275
|
+
expect(res.body.title).toBe("PUT is not supported.");
|
|
250
276
|
});
|
|
251
277
|
|
|
252
|
-
it("
|
|
278
|
+
it("responseHandler error in read is handled", async () => {
|
|
253
279
|
app.use(
|
|
254
280
|
"/food",
|
|
255
281
|
modelRouter(FoodModel, {
|
|
@@ -261,42 +287,21 @@ describe("@terreno/api", () => {
|
|
|
261
287
|
read: [Permissions.IsAny],
|
|
262
288
|
update: [Permissions.IsAny],
|
|
263
289
|
},
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
290
|
+
responseHandler: (_data, method) => {
|
|
291
|
+
if (method === "read") {
|
|
292
|
+
throw new Error("responseHandler read failed");
|
|
293
|
+
}
|
|
294
|
+
return {} as any;
|
|
268
295
|
},
|
|
269
296
|
})
|
|
270
297
|
);
|
|
271
298
|
server = supertest(app);
|
|
272
299
|
|
|
273
|
-
const res = await server
|
|
274
|
-
|
|
275
|
-
.send({
|
|
276
|
-
calories: 15,
|
|
277
|
-
name: "Broccoli",
|
|
278
|
-
})
|
|
279
|
-
.expect(400);
|
|
280
|
-
|
|
281
|
-
expect(res.body.title).toContain("preCreate hook error");
|
|
282
|
-
expect(res.body.disableExternalErrorTracking).toBe(true);
|
|
300
|
+
const res = await server.get(`/food/${spinach._id}`).expect(500);
|
|
301
|
+
expect(res.body.title).toContain("responseHandler error");
|
|
283
302
|
});
|
|
284
303
|
|
|
285
|
-
it("
|
|
286
|
-
const notAdmin = await UserModel.findOne({
|
|
287
|
-
email: "notAdmin@example.com",
|
|
288
|
-
});
|
|
289
|
-
const spinach = await FoodModel.create({
|
|
290
|
-
calories: 1,
|
|
291
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
292
|
-
hidden: false,
|
|
293
|
-
name: "Spinach",
|
|
294
|
-
ownerId: (notAdmin as any)._id,
|
|
295
|
-
source: {
|
|
296
|
-
name: "Brand",
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
|
|
304
|
+
it("responseHandler error in create is handled", async () => {
|
|
300
305
|
app.use(
|
|
301
306
|
"/food",
|
|
302
307
|
modelRouter(FoodModel, {
|
|
@@ -308,43 +313,21 @@ describe("@terreno/api", () => {
|
|
|
308
313
|
read: [Permissions.IsAny],
|
|
309
314
|
update: [Permissions.IsAny],
|
|
310
315
|
},
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
});
|
|
316
|
+
responseHandler: (_data, method) => {
|
|
317
|
+
if (method === "create") {
|
|
318
|
+
throw new Error("responseHandler create failed");
|
|
319
|
+
}
|
|
320
|
+
return {} as any;
|
|
317
321
|
},
|
|
318
322
|
})
|
|
319
323
|
);
|
|
320
324
|
server = supertest(app);
|
|
321
325
|
|
|
322
|
-
const res = await server
|
|
323
|
-
|
|
324
|
-
.send({
|
|
325
|
-
name: "Broccoli",
|
|
326
|
-
})
|
|
327
|
-
.expect(400);
|
|
328
|
-
|
|
329
|
-
expect(res.body.title).toBe("Custom preUpdate error");
|
|
330
|
-
expect(res.body.disableExternalErrorTracking).toBe(true);
|
|
326
|
+
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(500);
|
|
327
|
+
expect(res.body.title).toContain("responseHandler error");
|
|
331
328
|
});
|
|
332
329
|
|
|
333
|
-
it("
|
|
334
|
-
const notAdmin = await UserModel.findOne({
|
|
335
|
-
email: "notAdmin@example.com",
|
|
336
|
-
});
|
|
337
|
-
const spinach = await FoodModel.create({
|
|
338
|
-
calories: 1,
|
|
339
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
340
|
-
hidden: false,
|
|
341
|
-
name: "Spinach",
|
|
342
|
-
ownerId: (notAdmin as any)._id,
|
|
343
|
-
source: {
|
|
344
|
-
name: "Brand",
|
|
345
|
-
},
|
|
346
|
-
});
|
|
347
|
-
|
|
330
|
+
it("responseHandler error in update is handled", async () => {
|
|
348
331
|
app.use(
|
|
349
332
|
"/food",
|
|
350
333
|
modelRouter(FoodModel, {
|
|
@@ -356,41 +339,21 @@ describe("@terreno/api", () => {
|
|
|
356
339
|
read: [Permissions.IsAny],
|
|
357
340
|
update: [Permissions.IsAny],
|
|
358
341
|
},
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
342
|
+
responseHandler: (_data, method) => {
|
|
343
|
+
if (method === "update") {
|
|
344
|
+
throw new Error("responseHandler update failed");
|
|
345
|
+
}
|
|
346
|
+
return {} as any;
|
|
363
347
|
},
|
|
364
348
|
})
|
|
365
349
|
);
|
|
366
350
|
server = supertest(app);
|
|
367
351
|
|
|
368
|
-
const res = await server
|
|
369
|
-
|
|
370
|
-
.send({
|
|
371
|
-
name: "Broccoli",
|
|
372
|
-
})
|
|
373
|
-
.expect(400);
|
|
374
|
-
|
|
375
|
-
expect(res.body.title).toContain("preUpdate hook error");
|
|
376
|
-
expect(res.body.disableExternalErrorTracking).toBe(true);
|
|
352
|
+
const res = await server.patch(`/food/${spinach._id}`).send({name: "Kale"}).expect(500);
|
|
353
|
+
expect(res.body.title).toContain("responseHandler error");
|
|
377
354
|
});
|
|
378
355
|
|
|
379
|
-
it("
|
|
380
|
-
const notAdmin = await UserModel.findOne({
|
|
381
|
-
email: "notAdmin@example.com",
|
|
382
|
-
});
|
|
383
|
-
const spinach = await FoodModel.create({
|
|
384
|
-
calories: 1,
|
|
385
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
386
|
-
hidden: false,
|
|
387
|
-
name: "Spinach",
|
|
388
|
-
ownerId: (notAdmin as any)._id,
|
|
389
|
-
source: {
|
|
390
|
-
name: "Brand",
|
|
391
|
-
},
|
|
392
|
-
});
|
|
393
|
-
|
|
356
|
+
it("responseHandler error in list is handled", async () => {
|
|
394
357
|
app.use(
|
|
395
358
|
"/food",
|
|
396
359
|
modelRouter(FoodModel, {
|
|
@@ -402,900 +365,220 @@ describe("@terreno/api", () => {
|
|
|
402
365
|
read: [Permissions.IsAny],
|
|
403
366
|
update: [Permissions.IsAny],
|
|
404
367
|
},
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
368
|
+
responseHandler: (_data, method) => {
|
|
369
|
+
if (method === "list") {
|
|
370
|
+
throw new Error("responseHandler list failed");
|
|
371
|
+
}
|
|
372
|
+
return {} as any;
|
|
409
373
|
},
|
|
410
374
|
})
|
|
411
375
|
);
|
|
412
376
|
server = supertest(app);
|
|
413
377
|
|
|
414
|
-
const res = await
|
|
415
|
-
|
|
416
|
-
expect(res.body.title).toContain("preDelete hook error");
|
|
417
|
-
expect(res.body.disableExternalErrorTracking).toBe(true);
|
|
378
|
+
const res = await server.get("/food").expect(500);
|
|
379
|
+
expect(res.body.title).toContain("responseHandler error");
|
|
418
380
|
});
|
|
419
|
-
});
|
|
420
381
|
|
|
421
|
-
|
|
422
|
-
let admin: any;
|
|
423
|
-
let spinach: Food;
|
|
424
|
-
let apple: Food;
|
|
425
|
-
let agent: TestAgent;
|
|
426
|
-
|
|
427
|
-
beforeEach(async () => {
|
|
428
|
-
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
429
|
-
|
|
430
|
-
[admin] = await setupDb();
|
|
431
|
-
|
|
432
|
-
[spinach, apple] = await Promise.all([
|
|
433
|
-
FoodModel.create({
|
|
434
|
-
calories: 1,
|
|
435
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
436
|
-
hidden: false,
|
|
437
|
-
name: "Spinach",
|
|
438
|
-
ownerId: admin._id,
|
|
439
|
-
source: {
|
|
440
|
-
name: "Brand",
|
|
441
|
-
},
|
|
442
|
-
}),
|
|
443
|
-
FoodModel.create({
|
|
444
|
-
calories: 100,
|
|
445
|
-
categories: [
|
|
446
|
-
{
|
|
447
|
-
name: "Fruit",
|
|
448
|
-
show: true,
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
name: "Popular",
|
|
452
|
-
show: false,
|
|
453
|
-
},
|
|
454
|
-
],
|
|
455
|
-
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
456
|
-
hidden: false,
|
|
457
|
-
name: "Apple",
|
|
458
|
-
ownerId: admin._id,
|
|
459
|
-
tags: ["healthy", "cheap"],
|
|
460
|
-
}),
|
|
461
|
-
]);
|
|
462
|
-
|
|
463
|
-
app = getBaseServer();
|
|
464
|
-
setupAuth(app, UserModel as any);
|
|
465
|
-
addAuthRoutes(app, UserModel as any);
|
|
382
|
+
it("list with non-array responseHandler returns data directly", async () => {
|
|
466
383
|
app.use(
|
|
467
384
|
"/food",
|
|
468
385
|
modelRouter(FoodModel, {
|
|
469
386
|
allowAnonymous: true,
|
|
470
387
|
permissions: {
|
|
471
|
-
create: [Permissions.
|
|
472
|
-
delete: [Permissions.
|
|
473
|
-
list: [Permissions.
|
|
474
|
-
read: [Permissions.
|
|
475
|
-
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;
|
|
476
399
|
},
|
|
477
|
-
queryFields: ["hidden", "calories", "created", "source.name"],
|
|
478
|
-
sort: {created: "descending"},
|
|
479
400
|
})
|
|
480
401
|
);
|
|
481
402
|
server = supertest(app);
|
|
482
|
-
agent = await authAsUser(app, "admin");
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("add array sub-schema item", async () => {
|
|
486
|
-
// Incorrect way, should have "categories" as a top level key.
|
|
487
|
-
let res = await agent
|
|
488
|
-
.post(`/food/${apple._id}/categories`)
|
|
489
|
-
.send({name: "Good Seller", show: false})
|
|
490
|
-
.expect(400);
|
|
491
|
-
expect(res.body.title).toBe(
|
|
492
|
-
"Malformed body, array operations should have a single, top level key, got: name,show"
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
res = await agent
|
|
496
|
-
.post(`/food/${apple._id}/categories`)
|
|
497
|
-
.send({categories: {name: "Good Seller", show: false}})
|
|
498
|
-
.expect(200);
|
|
499
|
-
expect(res.body.data.categories).toHaveLength(3);
|
|
500
|
-
expect(res.body.data.categories[2].name).toBe("Good Seller");
|
|
501
|
-
|
|
502
|
-
res = await agent
|
|
503
|
-
.post(`/food/${spinach._id}/categories`)
|
|
504
|
-
.send({categories: {name: "Good Seller", show: false}})
|
|
505
|
-
.expect(200);
|
|
506
|
-
expect(res.body.data.categories).toHaveLength(1);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it("update array sub-schema item", async () => {
|
|
510
|
-
let res = await agent
|
|
511
|
-
.patch(`/food/${apple._id}/categories/xyz`)
|
|
512
|
-
.send({categories: {name: "Good Seller", show: false}})
|
|
513
|
-
.expect(404);
|
|
514
|
-
expect(res.body.title).toBe("Could not find categories/xyz");
|
|
515
|
-
res = await agent
|
|
516
|
-
.patch(`/food/${apple._id}/categories/${apple.categories[1]._id}`)
|
|
517
|
-
.send({categories: {name: "Good Seller", show: false}})
|
|
518
|
-
.expect(200);
|
|
519
|
-
expect(res.body.data.categories).toHaveLength(2);
|
|
520
|
-
expect(res.body.data.categories[1].name).toBe("Good Seller");
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
it("delete array sub-schema item", async () => {
|
|
524
|
-
let res = await agent.delete(`/food/${apple._id}/categories/xyz`).expect(404);
|
|
525
|
-
expect(res.body.title).toBe("Could not find categories/xyz");
|
|
526
|
-
res = await agent
|
|
527
|
-
.delete(`/food/${apple._id}/categories/${apple.categories[0]._id}`)
|
|
528
|
-
.expect(200);
|
|
529
|
-
expect(res.body.data.categories).toHaveLength(1);
|
|
530
|
-
expect(res.body.data.categories[0].name).toBe("Popular");
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it("add array item", async () => {
|
|
534
|
-
let res = await agent.post(`/food/${apple._id}/tags`).send({tags: "popular"}).expect(200);
|
|
535
|
-
expect(res.body.data.tags).toHaveLength(3);
|
|
536
|
-
expect(res.body.data.tags).toEqual(["healthy", "cheap", "popular"]);
|
|
537
|
-
|
|
538
|
-
res = await agent.post(`/food/${spinach._id}/tags`).send({tags: "popular"}).expect(200);
|
|
539
|
-
expect(res.body.data.tags).toEqual(["popular"]);
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
it("update array item", async () => {
|
|
543
|
-
let res = await agent
|
|
544
|
-
.patch(`/food/${apple._id}/tags/xyz`)
|
|
545
|
-
.send({tags: "unhealthy"})
|
|
546
|
-
.expect(404);
|
|
547
|
-
expect(res.body.title).toBe("Could not find tags/xyz");
|
|
548
|
-
res = await agent
|
|
549
|
-
.patch(`/food/${apple._id}/tags/healthy`)
|
|
550
|
-
.send({tags: "unhealthy"})
|
|
551
|
-
.expect(200);
|
|
552
|
-
expect(res.body.data.tags).toEqual(["unhealthy", "cheap"]);
|
|
553
|
-
});
|
|
554
403
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
expect(res.body.
|
|
558
|
-
res
|
|
559
|
-
expect(res.body.data.tags).toEqual(["cheap"]);
|
|
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();
|
|
560
408
|
});
|
|
561
409
|
|
|
562
|
-
it("
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
name: "Category 1",
|
|
569
|
-
show: true,
|
|
570
|
-
updated: new Date("2024-01-01T00:00:00.000Z"),
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
name: "Category 2",
|
|
574
|
-
show: true,
|
|
575
|
-
updated: new Date("2024-01-01T00:00:00.000Z"),
|
|
576
|
-
},
|
|
577
|
-
],
|
|
578
|
-
created: new Date(),
|
|
579
|
-
name: "Food with Timestamps",
|
|
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",
|
|
580
416
|
ownerId: admin._id,
|
|
581
417
|
});
|
|
582
418
|
|
|
583
|
-
const firstCategoryId = foodWithTimestamps.categories?.[0]?._id?.toString();
|
|
584
|
-
const secondCategoryId = foodWithTimestamps.categories?.[1]?._id?.toString();
|
|
585
|
-
|
|
586
|
-
if (!firstCategoryId || !secondCategoryId) {
|
|
587
|
-
throw new Error("Failed to create food with categories");
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Wait a moment to ensure timestamp difference
|
|
591
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
592
|
-
|
|
593
|
-
// Update one of the categories
|
|
594
|
-
const res = await agent
|
|
595
|
-
.patch(`/food/${foodWithTimestamps._id}/categories/${firstCategoryId}`)
|
|
596
|
-
.send({categories: {name: "Updated Category"}})
|
|
597
|
-
.expect(200);
|
|
598
|
-
|
|
599
|
-
// Verify the updated category has a newer timestamp
|
|
600
|
-
const updatedCategory = res.body.data.categories.find((c: any) => c._id === firstCategoryId);
|
|
601
|
-
const unchangedCategory = res.body.data.categories.find(
|
|
602
|
-
(c: any) => c._id === secondCategoryId
|
|
603
|
-
);
|
|
604
|
-
|
|
605
|
-
if (!updatedCategory || !unchangedCategory) {
|
|
606
|
-
throw new Error("Failed to find categories in response");
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
expect(updatedCategory.updated).not.toBe(updatedCategory.created);
|
|
610
|
-
expect(unchangedCategory.updated).toBe(unchangedCategory.created);
|
|
611
|
-
expect(updatedCategory.name).toBe("Updated Category");
|
|
612
|
-
// Unchanged.
|
|
613
|
-
expect(updatedCategory.show).toBe(true);
|
|
614
|
-
expect(unchangedCategory.show).toBe(true);
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
it("array operations call postUpdate with different copy of document", async () => {
|
|
618
|
-
let postUpdateDoc: any;
|
|
619
|
-
let postUpdatePrevDoc: any;
|
|
620
|
-
let postUpdateCalled = false;
|
|
621
|
-
|
|
622
|
-
app = getBaseServer();
|
|
623
|
-
setupAuth(app, UserModel as any);
|
|
624
|
-
addAuthRoutes(app, UserModel as any);
|
|
625
419
|
app.use(
|
|
626
420
|
"/food",
|
|
627
421
|
modelRouter(FoodModel, {
|
|
628
422
|
allowAnonymous: true,
|
|
629
423
|
permissions: {
|
|
630
|
-
create: [Permissions.
|
|
631
|
-
delete: [Permissions.
|
|
632
|
-
list: [Permissions.
|
|
633
|
-
read: [Permissions.
|
|
634
|
-
update: [Permissions.
|
|
635
|
-
},
|
|
636
|
-
postUpdate: async (doc: any, _cleanedBody: any, _request: any, prevValue: any) => {
|
|
637
|
-
postUpdateDoc = doc;
|
|
638
|
-
postUpdatePrevDoc = prevValue;
|
|
639
|
-
postUpdateCalled = true;
|
|
424
|
+
create: [Permissions.IsAny],
|
|
425
|
+
delete: [Permissions.IsAny],
|
|
426
|
+
list: [Permissions.IsAny],
|
|
427
|
+
read: [Permissions.IsAny],
|
|
428
|
+
update: [Permissions.IsAny],
|
|
640
429
|
},
|
|
430
|
+
queryFields: ["name"],
|
|
641
431
|
})
|
|
642
432
|
);
|
|
643
433
|
server = supertest(app);
|
|
644
|
-
agent = await authAsUser(app, "admin");
|
|
645
|
-
|
|
646
|
-
// Test POST operation (add to array)
|
|
647
|
-
await agent
|
|
648
|
-
.post(`/food/${apple._id}/categories`)
|
|
649
|
-
.send({categories: {name: "New Category", show: true}})
|
|
650
|
-
.expect(200);
|
|
651
|
-
|
|
652
|
-
expect(postUpdateCalled).toBe(true);
|
|
653
|
-
expect(postUpdateDoc).toBeDefined();
|
|
654
|
-
expect(postUpdatePrevDoc).toBeDefined();
|
|
655
|
-
|
|
656
|
-
// Verify they are different object references
|
|
657
|
-
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
658
|
-
|
|
659
|
-
// Verify the content is different (new category added)
|
|
660
|
-
expect(postUpdateDoc.categories).toHaveLength(3);
|
|
661
|
-
expect(postUpdatePrevDoc.categories).toHaveLength(2);
|
|
662
|
-
|
|
663
|
-
// Reset for next test
|
|
664
|
-
postUpdateCalled = false;
|
|
665
|
-
postUpdateDoc = undefined;
|
|
666
|
-
postUpdatePrevDoc = undefined;
|
|
667
|
-
|
|
668
|
-
// Test PATCH operation (update array item)
|
|
669
|
-
const categoryId = apple.categories[0]._id;
|
|
670
|
-
if (!categoryId) {
|
|
671
|
-
throw new Error("Category ID is undefined");
|
|
672
|
-
}
|
|
673
|
-
await agent
|
|
674
|
-
.patch(`/food/${apple._id}/categories/${categoryId}`)
|
|
675
|
-
.send({categories: {name: "Updated Category", show: false}})
|
|
676
|
-
.expect(200);
|
|
677
|
-
|
|
678
|
-
expect(postUpdateCalled).toBe(true);
|
|
679
|
-
expect(postUpdateDoc).toBeDefined();
|
|
680
|
-
expect(postUpdatePrevDoc).toBeDefined();
|
|
681
|
-
|
|
682
|
-
// Verify they are different object references
|
|
683
|
-
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
684
|
-
|
|
685
|
-
// Verify the content is different (category updated)
|
|
686
|
-
const updatedCategory = postUpdateDoc.categories.find(
|
|
687
|
-
(c: any) => c._id.toString() === categoryId.toString()
|
|
688
|
-
);
|
|
689
|
-
const prevCategory = postUpdatePrevDoc.categories.find(
|
|
690
|
-
(c: any) => c._id.toString() === categoryId.toString()
|
|
691
|
-
);
|
|
692
|
-
|
|
693
|
-
expect(updatedCategory.name).toBe("Updated Category");
|
|
694
|
-
expect(prevCategory.name).toBe("Fruit");
|
|
695
|
-
|
|
696
|
-
// Reset for next test
|
|
697
|
-
postUpdateCalled = false;
|
|
698
|
-
postUpdateDoc = undefined;
|
|
699
|
-
postUpdatePrevDoc = undefined;
|
|
700
|
-
|
|
701
|
-
// Test DELETE operation (remove from array)
|
|
702
|
-
await agent.delete(`/food/${apple._id}/categories/${categoryId}`).expect(200);
|
|
703
|
-
|
|
704
|
-
expect(postUpdateCalled).toBe(true);
|
|
705
|
-
expect(postUpdateDoc).toBeDefined();
|
|
706
|
-
expect(postUpdatePrevDoc).toBeDefined();
|
|
707
434
|
|
|
708
|
-
|
|
709
|
-
expect(
|
|
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");
|
|
710
438
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
);
|
|
715
|
-
const prevCategories = postUpdatePrevDoc.categories.filter(
|
|
716
|
-
(c: any) => c._id.toString() === categoryId.toString()
|
|
717
|
-
);
|
|
718
|
-
|
|
719
|
-
expect(remainingCategories).toHaveLength(0);
|
|
720
|
-
expect(prevCategories).toHaveLength(1);
|
|
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");
|
|
721
442
|
});
|
|
722
443
|
|
|
723
|
-
it("
|
|
724
|
-
let postUpdateDoc: any;
|
|
725
|
-
let postUpdatePrevDoc: any;
|
|
726
|
-
let postUpdateCalled = false;
|
|
727
|
-
|
|
728
|
-
app = getBaseServer();
|
|
729
|
-
setupAuth(app, UserModel as any);
|
|
730
|
-
addAuthRoutes(app, UserModel as any);
|
|
444
|
+
it("queryFilter error is handled", async () => {
|
|
731
445
|
app.use(
|
|
732
446
|
"/food",
|
|
733
447
|
modelRouter(FoodModel, {
|
|
734
448
|
allowAnonymous: true,
|
|
735
449
|
permissions: {
|
|
736
|
-
create: [Permissions.
|
|
737
|
-
delete: [Permissions.
|
|
738
|
-
list: [Permissions.
|
|
739
|
-
read: [Permissions.
|
|
740
|
-
update: [Permissions.
|
|
450
|
+
create: [Permissions.IsAny],
|
|
451
|
+
delete: [Permissions.IsAny],
|
|
452
|
+
list: [Permissions.IsAny],
|
|
453
|
+
read: [Permissions.IsAny],
|
|
454
|
+
update: [Permissions.IsAny],
|
|
741
455
|
},
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
postUpdatePrevDoc = prevValue;
|
|
745
|
-
postUpdateCalled = true;
|
|
456
|
+
queryFilter: () => {
|
|
457
|
+
throw new Error("queryFilter failed");
|
|
746
458
|
},
|
|
747
459
|
})
|
|
748
460
|
);
|
|
749
461
|
server = supertest(app);
|
|
750
|
-
agent = await authAsUser(app, "admin");
|
|
751
|
-
|
|
752
|
-
// Test POST operation with string array (add tag)
|
|
753
|
-
await agent.post(`/food/${apple._id}/tags`).send({tags: "organic"}).expect(200);
|
|
754
|
-
|
|
755
|
-
expect(postUpdateCalled).toBe(true);
|
|
756
|
-
expect(postUpdateDoc).toBeDefined();
|
|
757
|
-
expect(postUpdatePrevDoc).toBeDefined();
|
|
758
|
-
|
|
759
|
-
// Verify they are different object references
|
|
760
|
-
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
761
|
-
|
|
762
|
-
// Verify the content is different (new tag added)
|
|
763
|
-
expect(postUpdateDoc.tags).toHaveLength(3);
|
|
764
|
-
expect(postUpdatePrevDoc.tags).toHaveLength(2);
|
|
765
|
-
expect(postUpdateDoc.tags).toContain("organic");
|
|
766
|
-
expect(postUpdatePrevDoc.tags).not.toContain("organic");
|
|
767
|
-
|
|
768
|
-
// Reset for next test
|
|
769
|
-
postUpdateCalled = false;
|
|
770
|
-
postUpdateDoc = undefined;
|
|
771
|
-
postUpdatePrevDoc = undefined;
|
|
772
462
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
.patch(`/food/${apple._id}/tags/healthy`)
|
|
776
|
-
.send({tags: "super-healthy"})
|
|
777
|
-
.expect(200);
|
|
778
|
-
|
|
779
|
-
expect(postUpdateCalled).toBe(true);
|
|
780
|
-
expect(postUpdateDoc).not.toBe(postUpdatePrevDoc);
|
|
781
|
-
|
|
782
|
-
// Verify the content is different (tag updated)
|
|
783
|
-
expect(postUpdateDoc.tags).toContain("super-healthy");
|
|
784
|
-
expect(postUpdatePrevDoc.tags).toContain("healthy");
|
|
785
|
-
expect(postUpdateDoc.tags).not.toContain("healthy");
|
|
786
|
-
expect(postUpdatePrevDoc.tags).not.toContain("super-healthy");
|
|
463
|
+
const res = await server.get("/food").expect(400);
|
|
464
|
+
expect(res.body.title).toContain("Query filter error");
|
|
787
465
|
});
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
describe("standard methods", () => {
|
|
791
|
-
let notAdmin: any;
|
|
792
|
-
let admin: any;
|
|
793
|
-
let adminOther: any;
|
|
794
|
-
let agent: TestAgent;
|
|
795
466
|
|
|
796
|
-
|
|
797
|
-
let apple: Food;
|
|
798
|
-
let carrots: Food;
|
|
799
|
-
let pizza: Food;
|
|
800
|
-
|
|
801
|
-
beforeEach(async () => {
|
|
802
|
-
[admin, notAdmin, adminOther] = await setupDb();
|
|
803
|
-
|
|
804
|
-
const results = (await Promise.all([
|
|
805
|
-
FoodModel.create({
|
|
806
|
-
calories: 1,
|
|
807
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
808
|
-
eatenBy: [admin._id],
|
|
809
|
-
hidden: false,
|
|
810
|
-
lastEatenWith: {
|
|
811
|
-
dressing: new Date("2021-12-03T19:00:30.000Z"),
|
|
812
|
-
},
|
|
813
|
-
name: "Spinach",
|
|
814
|
-
ownerId: notAdmin._id,
|
|
815
|
-
source: {
|
|
816
|
-
dateAdded: "2023-12-13T12:30:00.000Z",
|
|
817
|
-
href: "https://www.google.com",
|
|
818
|
-
name: "Brand",
|
|
819
|
-
},
|
|
820
|
-
}),
|
|
821
|
-
FoodModel.create({
|
|
822
|
-
calories: 100,
|
|
823
|
-
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
824
|
-
hidden: true,
|
|
825
|
-
name: "Apple",
|
|
826
|
-
ownerId: admin._id,
|
|
827
|
-
tags: ["healthy"],
|
|
828
|
-
}),
|
|
829
|
-
FoodModel.create({
|
|
830
|
-
calories: 100,
|
|
831
|
-
created: new Date("2021-12-03T00:00:00.000Z"),
|
|
832
|
-
eatenBy: [admin._id, notAdmin._id],
|
|
833
|
-
hidden: false,
|
|
834
|
-
name: "Carrots",
|
|
835
|
-
ownerId: admin._id,
|
|
836
|
-
source: {
|
|
837
|
-
name: "USDA",
|
|
838
|
-
},
|
|
839
|
-
tags: ["healthy", "cheap"],
|
|
840
|
-
}),
|
|
841
|
-
FoodModel.create({
|
|
842
|
-
calories: 400,
|
|
843
|
-
created: new Date("2021-12-03T00:00:10.000Z"),
|
|
844
|
-
eatenBy: [adminOther._id],
|
|
845
|
-
hidden: false,
|
|
846
|
-
name: "Pizza",
|
|
847
|
-
ownerId: admin._id,
|
|
848
|
-
tags: ["cheap"],
|
|
849
|
-
}),
|
|
850
|
-
])) as [Food, Food, Food, Food];
|
|
851
|
-
[spinach, apple, carrots, pizza] = results;
|
|
852
|
-
app = getBaseServer();
|
|
853
|
-
setupAuth(app, UserModel as any);
|
|
854
|
-
addAuthRoutes(app, UserModel as any);
|
|
855
|
-
app.use(logRequests);
|
|
467
|
+
it("custom endpoints take priority", async () => {
|
|
856
468
|
app.use(
|
|
857
469
|
"/food",
|
|
858
470
|
modelRouter(FoodModel, {
|
|
859
471
|
allowAnonymous: true,
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
472
|
+
endpoints: (router: any) => {
|
|
473
|
+
router.get("/custom", (_req: any, res: any) => {
|
|
474
|
+
res.json({custom: true});
|
|
475
|
+
});
|
|
476
|
+
},
|
|
863
477
|
permissions: {
|
|
864
|
-
create: [Permissions.
|
|
865
|
-
delete: [Permissions.
|
|
478
|
+
create: [Permissions.IsAny],
|
|
479
|
+
delete: [Permissions.IsAny],
|
|
866
480
|
list: [Permissions.IsAny],
|
|
867
481
|
read: [Permissions.IsAny],
|
|
868
|
-
update: [Permissions.
|
|
482
|
+
update: [Permissions.IsAny],
|
|
869
483
|
},
|
|
870
|
-
populatePaths: [{path: "ownerId"}],
|
|
871
|
-
queryFields: ["hidden", "name", "calories", "created", "source.name", "tags", "eatenBy"],
|
|
872
|
-
sort: {created: "descending"},
|
|
873
484
|
})
|
|
874
485
|
);
|
|
875
486
|
server = supertest(app);
|
|
876
|
-
agent = await authAsUser(app, "notAdmin");
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
it("read default", async () => {
|
|
880
|
-
const res = await agent.get(`/food/${spinach._id}`).expect(200);
|
|
881
|
-
expect(res.body.data._id).toBe(spinach._id.toString());
|
|
882
|
-
// Ensure populate works
|
|
883
|
-
expect(res.body.data.ownerId._id).toBe(notAdmin.id);
|
|
884
|
-
// Ensure maps are properly transformed
|
|
885
|
-
expect(res.body.data.lastEatenWith).toEqual({
|
|
886
|
-
dressing: "2021-12-03T19:00:30.000Z",
|
|
887
|
-
});
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
it("list default", async () => {
|
|
891
|
-
const res = await agent.get("/food").expect(200);
|
|
892
|
-
expect(res.body.data).toHaveLength(2);
|
|
893
|
-
expect(res.body.data[0].id).toBe((spinach as any).id);
|
|
894
|
-
expect(res.body.data[0].ownerId._id).toBe(notAdmin.id);
|
|
895
|
-
expect(res.body.data[1].id).toBe((pizza as any).id);
|
|
896
|
-
expect(res.body.data[1].ownerId._id).toBe(admin.id);
|
|
897
|
-
// Check that mongoose Map is handled correctly.
|
|
898
|
-
expect(res.body.data[0].lastEatenWith).toEqual({
|
|
899
|
-
dressing: "2021-12-03T19:00:30.000Z",
|
|
900
|
-
});
|
|
901
|
-
expect(res.body.data[1].lastEatenWith).toEqual(undefined);
|
|
902
|
-
|
|
903
|
-
expect(res.body.more).toBe(true);
|
|
904
|
-
expect(res.body.total).toBe(3);
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
it("list limit", async () => {
|
|
908
|
-
const res = await agent.get("/food?limit=1").expect(200);
|
|
909
|
-
expect(res.body.data).toHaveLength(1);
|
|
910
|
-
expect(res.body.data[0].id).toBe((spinach as any).id);
|
|
911
|
-
expect(res.body.data[0].ownerId._id).toBe(notAdmin.id);
|
|
912
|
-
expect(res.body.more).toBe(true);
|
|
913
|
-
expect(res.body.total).toBe(3);
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
it("list limit over", async () => {
|
|
917
|
-
// This shouldn't be seen, it's the end of the list.
|
|
918
|
-
await FoodModel.create({
|
|
919
|
-
calories: 400,
|
|
920
|
-
created: new Date("2021-12-02T00:00:10.000Z"),
|
|
921
|
-
hidden: false,
|
|
922
|
-
name: "Pizza",
|
|
923
|
-
ownerId: admin._id,
|
|
924
|
-
});
|
|
925
|
-
const res = await agent.get("/food?limit=4").expect(200);
|
|
926
|
-
expect(res.body.data).toHaveLength(3);
|
|
927
|
-
expect(res.body.more).toBe(true);
|
|
928
|
-
expect(res.body.total).toBe(4);
|
|
929
|
-
expect(res.body.data[0].id).toBe((spinach as any).id);
|
|
930
|
-
expect(res.body.data[1].id).toBe((pizza as any).id);
|
|
931
|
-
expect(res.body.data[2].id).toBe((carrots as any).id);
|
|
932
|
-
|
|
933
|
-
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
|
934
|
-
'More than 3 results returned for foods without pagination, data may be silently truncated. req.query: {"limit":"4"}'
|
|
935
|
-
);
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
it("list page", async () => {
|
|
939
|
-
// Should skip to carrots since apples are hidden
|
|
940
|
-
const res = await agent.get("/food?limit=1&page=2").expect(200);
|
|
941
|
-
expect(res.body.data).toHaveLength(1);
|
|
942
|
-
expect(res.body.more).toBe(true);
|
|
943
|
-
expect(res.body.total).toBe(3);
|
|
944
|
-
expect(res.body.data[0].id).toBe((pizza as any).id);
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
it("list page 0 ", async () => {
|
|
948
|
-
const res = await agent.get("/food?limit=1&page=0").expect(400);
|
|
949
|
-
expect(res.body.title).toBe("Invalid page: 0");
|
|
950
|
-
});
|
|
951
|
-
|
|
952
|
-
it("list page with garbage ", async () => {
|
|
953
|
-
const res = await agent.get("/food?limit=1&page=abc").expect(400);
|
|
954
|
-
expect(res.body.title).toBe("Invalid page: abc");
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
it("list page over", async () => {
|
|
958
|
-
// Should skip to carrots since apples are hidden
|
|
959
|
-
const res = await agent.get("/food?limit=1&page=5").expect(200);
|
|
960
|
-
expect(res.body.data).toHaveLength(0);
|
|
961
|
-
expect(res.body.more).toBe(false);
|
|
962
|
-
expect(res.body.total).toBe(3);
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
it("list query params", async () => {
|
|
966
|
-
// Should skip to carrots since apples are hidden
|
|
967
|
-
const res = await agent.get("/food?hidden=true").expect(200);
|
|
968
|
-
expect(res.body.data).toHaveLength(1);
|
|
969
|
-
expect(res.body.more).toBe(false);
|
|
970
|
-
expect(res.body.total).toBe(1);
|
|
971
|
-
expect(res.body.data[0].id).toBe((apple as any).id);
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
it("list query params not in list", async () => {
|
|
975
|
-
// Should skip to carrots since apples are hidden
|
|
976
|
-
const res = await agent.get(`/food?ownerId=${admin._id}`).expect(400);
|
|
977
|
-
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
978
|
-
});
|
|
979
487
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const res = await agent.get("/food?source.name=USDA").expect(200);
|
|
983
|
-
expect(res.body.data).toHaveLength(1);
|
|
984
|
-
expect(res.body.total).toBe(1);
|
|
985
|
-
expect(res.body.data[0].id).toBe((carrots as any).id);
|
|
488
|
+
const res = await server.get("/food/custom").expect(200);
|
|
489
|
+
expect(res.body.custom).toBe(true);
|
|
986
490
|
});
|
|
987
491
|
|
|
988
|
-
it("query
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
},
|
|
1003
|
-
})}`
|
|
1004
|
-
)
|
|
1005
|
-
.set("authorization", `Bearer ${token}`)
|
|
1006
|
-
.expect(200);
|
|
1007
|
-
expect(res.body.data.map((d: any) => d.created)).toEqual(
|
|
1008
|
-
expect.arrayContaining([
|
|
1009
|
-
"2021-12-03T00:00:20.000Z",
|
|
1010
|
-
"2021-12-03T00:00:10.000Z",
|
|
1011
|
-
"2021-12-03T00:00:00.000Z",
|
|
1012
|
-
])
|
|
1013
|
-
);
|
|
1014
|
-
expect(res.body.data.map((d: any) => d.created)).toHaveLength(3);
|
|
1015
|
-
|
|
1016
|
-
// Inclusive one side
|
|
1017
|
-
res = await server
|
|
1018
|
-
.get(
|
|
1019
|
-
`/food?limit=3&${qs.stringify({
|
|
1020
|
-
created: {
|
|
1021
|
-
$gte: "2021-12-03T00:00:00.000Z",
|
|
1022
|
-
$lt: "2021-12-03T00:00:20.000Z",
|
|
1023
|
-
},
|
|
1024
|
-
})}`
|
|
1025
|
-
)
|
|
1026
|
-
.set("authorization", `Bearer ${token}`)
|
|
1027
|
-
.expect(200);
|
|
1028
|
-
expect(res.body.data.map((d: any) => d.created)).toEqual(
|
|
1029
|
-
expect.arrayContaining(["2021-12-03T00:00:10.000Z", "2021-12-03T00:00:00.000Z"])
|
|
1030
|
-
);
|
|
1031
|
-
expect(res.body.data.map((d: any) => d.created)).toHaveLength(2);
|
|
1032
|
-
|
|
1033
|
-
// Inclusive both sides
|
|
1034
|
-
res = await server
|
|
1035
|
-
.get(
|
|
1036
|
-
`/food?limit=3&${qs.stringify({
|
|
1037
|
-
created: {
|
|
1038
|
-
$gt: "2021-12-03T00:00:00.000Z",
|
|
1039
|
-
$lt: "2021-12-03T00:00:20.000Z",
|
|
1040
|
-
},
|
|
1041
|
-
})}`
|
|
1042
|
-
)
|
|
1043
|
-
.set("authorization", `Bearer ${token}`)
|
|
1044
|
-
.expect(200);
|
|
1045
|
-
const createdDates = res.body.data.map((d: any) => d.created);
|
|
1046
|
-
expect(createdDates).toEqual(expect.arrayContaining(["2021-12-03T00:00:10.000Z"]));
|
|
1047
|
-
expect(createdDates).toHaveLength(1);
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
it("query with a space", async () => {
|
|
1051
|
-
const greenBeans = await FoodModel.create({
|
|
1052
|
-
calories: 102,
|
|
1053
|
-
created: Date.now() - 10,
|
|
1054
|
-
name: "Green Beans",
|
|
1055
|
-
ownerId: admin?._id,
|
|
1056
|
-
});
|
|
1057
|
-
const res = await agent.get(`/food?${qs.stringify({name: "Green Beans"})}`).expect(200);
|
|
1058
|
-
expect(res.body.data).toHaveLength(1);
|
|
1059
|
-
expect(res.body.data[0].id).toBe(greenBeans?.id);
|
|
1060
|
-
expect(res.body.data[0].name).toBe("Green Beans");
|
|
1061
|
-
});
|
|
1062
|
-
|
|
1063
|
-
it("query with a regex", async () => {
|
|
1064
|
-
const greenBeans = await FoodModel.create({
|
|
1065
|
-
calories: 102,
|
|
1066
|
-
created: Date.now() - 10,
|
|
1067
|
-
name: "Green Beans",
|
|
1068
|
-
ownerId: admin?._id,
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
// Case sensitive does match correct casing
|
|
1072
|
-
let res = await agent.get(`/food?${qs.stringify({name: {$regex: "Green"}})}`).expect(200);
|
|
1073
|
-
expect(res.body.data).toHaveLength(1);
|
|
1074
|
-
expect(res.body.data[0].id).toBe(greenBeans?.id);
|
|
1075
|
-
expect(res.body.data[0].name).toBe("Green Beans");
|
|
1076
|
-
|
|
1077
|
-
// Fails with different casing and sensitive
|
|
1078
|
-
res = await agent.get(`/food?${qs.stringify({name: {$regex: "green"}})}`).expect(200);
|
|
1079
|
-
expect(res.body.data).toHaveLength(0);
|
|
1080
|
-
|
|
1081
|
-
// Case insensitive does match different casing
|
|
1082
|
-
res = await agent
|
|
1083
|
-
.get(`/food?${qs.stringify({name: {$options: "i", $regex: "green"}})}`)
|
|
1084
|
-
.expect(200);
|
|
1085
|
-
expect(res.body.data).toHaveLength(1);
|
|
1086
|
-
expect(res.body.data[0].id).toBe(greenBeans?.id);
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
it("query with an $in operator", async () => {
|
|
1090
|
-
// Query including a hidden food
|
|
1091
|
-
let res = await server
|
|
1092
|
-
.get(
|
|
1093
|
-
`/food?${qs.stringify({
|
|
1094
|
-
name: {
|
|
1095
|
-
$in: ["Apple", "Spinach"],
|
|
1096
|
-
},
|
|
1097
|
-
})}`
|
|
1098
|
-
)
|
|
1099
|
-
.expect(200);
|
|
1100
|
-
const names1 = res.body.data.map((d: any) => d.name);
|
|
1101
|
-
expect(names1).toEqual(expect.arrayContaining(["Spinach"]));
|
|
1102
|
-
expect(names1).toHaveLength(1);
|
|
1103
|
-
|
|
1104
|
-
// Query without hidden food.
|
|
1105
|
-
res = await server
|
|
1106
|
-
.get(
|
|
1107
|
-
`/food?${qs.stringify({
|
|
1108
|
-
name: {
|
|
1109
|
-
$in: ["Carrots", "Spinach"],
|
|
1110
|
-
},
|
|
1111
|
-
})}`
|
|
1112
|
-
)
|
|
1113
|
-
.expect(200);
|
|
1114
|
-
const names2 = res.body.data.map((d: any) => d.name);
|
|
1115
|
-
expect(names2).toEqual(expect.arrayContaining(["Spinach", "Carrots"]));
|
|
1116
|
-
expect(names2).toHaveLength(2);
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
it("query with an $in for _ids in nested object", async () => {
|
|
1120
|
-
// Query including a hidden food
|
|
1121
|
-
const res = await server
|
|
1122
|
-
.get(
|
|
1123
|
-
`/food?${qs.stringify({
|
|
1124
|
-
eatenBy: {
|
|
1125
|
-
$in: [notAdmin._id.toString(), adminOther._id.toString()],
|
|
1126
|
-
},
|
|
1127
|
-
})}`
|
|
1128
|
-
)
|
|
1129
|
-
.expect(200);
|
|
1130
|
-
expect(res.body.more).toBe(false);
|
|
1131
|
-
expect(res.body.total).toBe(2);
|
|
1132
|
-
expect(res.body.data).toHaveLength(2);
|
|
1133
|
-
const names3 = res.body.data.map((d: any) => d.name);
|
|
1134
|
-
expect(names3).toEqual(expect.arrayContaining(["Carrots", "Pizza"]));
|
|
1135
|
-
expect(names3).toHaveLength(2);
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
it("query $and operator on same field", async () => {
|
|
1139
|
-
const res = await agent
|
|
1140
|
-
.get(`/food?${qs.stringify({$and: [{tags: "healthy"}, {tags: "cheap"}]})}`)
|
|
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 $and operator on same field, nested objects", async () => {
|
|
1147
|
-
const res = await agent
|
|
1148
|
-
.get(
|
|
1149
|
-
`/food?${qs.stringify({
|
|
1150
|
-
$and: [{eatenBy: admin.id}, {eatenBy: notAdmin.id}],
|
|
1151
|
-
})}`
|
|
1152
|
-
)
|
|
1153
|
-
.expect(200);
|
|
1154
|
-
expect(res.body.data).toHaveLength(1);
|
|
1155
|
-
expect(res.body.data[0].id).toBe(carrots?._id.toString());
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
it("query $or operator on same field", async () => {
|
|
1159
|
-
const res = await agent
|
|
1160
|
-
.get(`/food?${qs.stringify({$or: [{name: "Carrots"}, {name: "Pizza"}]})}`)
|
|
1161
|
-
.expect(200);
|
|
1162
|
-
expect(res.body.data).toHaveLength(2);
|
|
1163
|
-
// Only carrots matches both
|
|
1164
|
-
const ids1 = res.body.data.map((d) => d.id);
|
|
1165
|
-
expect(ids1).toEqual(
|
|
1166
|
-
expect.arrayContaining([carrots?._id.toString(), pizza?._id.toString()])
|
|
1167
|
-
);
|
|
1168
|
-
expect(ids1).toHaveLength(2);
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
it("query $and operator on same field, nested objects", async () => {
|
|
1172
|
-
const res = await agent
|
|
1173
|
-
.get(
|
|
1174
|
-
`/food?${qs.stringify({
|
|
1175
|
-
$or: [{eatenBy: admin.id}, {eatenBy: notAdmin.id}],
|
|
1176
|
-
limit: 3,
|
|
1177
|
-
})}`
|
|
1178
|
-
)
|
|
1179
|
-
.expect(200);
|
|
1180
|
-
expect(res.body.data).toHaveLength(2);
|
|
1181
|
-
const ids2 = res.body.data.map((d) => d.id);
|
|
1182
|
-
expect(ids2).toEqual(
|
|
1183
|
-
expect.arrayContaining([carrots?._id.toString(), spinach?._id.toString()])
|
|
492
|
+
it("disallowed query param returns 400", async () => {
|
|
493
|
+
app.use(
|
|
494
|
+
"/food",
|
|
495
|
+
modelRouter(FoodModel, {
|
|
496
|
+
allowAnonymous: true,
|
|
497
|
+
permissions: {
|
|
498
|
+
create: [Permissions.IsAny],
|
|
499
|
+
delete: [Permissions.IsAny],
|
|
500
|
+
list: [Permissions.IsAny],
|
|
501
|
+
read: [Permissions.IsAny],
|
|
502
|
+
update: [Permissions.IsAny],
|
|
503
|
+
},
|
|
504
|
+
queryFields: ["name"],
|
|
505
|
+
})
|
|
1184
506
|
);
|
|
1185
|
-
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
it("query $and and $or are rejected if field is not in queryFields", async () => {
|
|
1189
|
-
let res = await agent
|
|
1190
|
-
.get(`/food?${qs.stringify({$and: [{ownerId: "healthy"}, {tags: "cheap"}]})}`)
|
|
1191
|
-
.expect(400);
|
|
1192
|
-
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
1193
|
-
// Check in the other order
|
|
1194
|
-
res = await agent
|
|
1195
|
-
.get(`/food?${qs.stringify({$and: [{tags: "cheap"}, {ownerId: "healthy"}]})}`)
|
|
1196
|
-
.expect(400);
|
|
1197
|
-
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
1198
|
-
|
|
1199
|
-
res = await agent
|
|
1200
|
-
.get(`/food?${qs.stringify({$or: [{tags: "cheap"}, {ownerId: "healthy"}]})}`)
|
|
1201
|
-
.expect(400);
|
|
1202
|
-
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
1203
|
-
});
|
|
507
|
+
server = supertest(app);
|
|
1204
508
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
expect(res.body.data).toHaveLength(1);
|
|
1208
|
-
expect(res.body.data[0].id).toBe(carrots?._id.toString());
|
|
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");
|
|
1209
511
|
});
|
|
1210
512
|
|
|
1211
|
-
it("
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
expect(res.body.data.name).toBe("Kale");
|
|
1223
|
-
expect(res.body.data.calories).toBe(1);
|
|
1224
|
-
expect(res.body.data.hidden).toBe(false);
|
|
1225
|
-
expect(res.body.data.lastEatenWith).toEqual({
|
|
1226
|
-
dressing: "2023-12-03T00:00:20.000Z",
|
|
1227
|
-
});
|
|
1228
|
-
|
|
1229
|
-
// Update a Map field.
|
|
1230
|
-
res = await agent
|
|
1231
|
-
.patch(`/food/${spinach._id}`)
|
|
1232
|
-
.send({
|
|
1233
|
-
lastEatenWith: {
|
|
1234
|
-
cucumber: "2023-12-04T12:00:20.000Z",
|
|
1235
|
-
dressing: "2023-12-03T00:00:20.000Z",
|
|
513
|
+
it("queryFilter returning null returns empty array", async () => {
|
|
514
|
+
app.use(
|
|
515
|
+
"/food",
|
|
516
|
+
modelRouter(FoodModel, {
|
|
517
|
+
allowAnonymous: true,
|
|
518
|
+
permissions: {
|
|
519
|
+
create: [Permissions.IsAny],
|
|
520
|
+
delete: [Permissions.IsAny],
|
|
521
|
+
list: [Permissions.IsAny],
|
|
522
|
+
read: [Permissions.IsAny],
|
|
523
|
+
update: [Permissions.IsAny],
|
|
1236
524
|
},
|
|
525
|
+
queryFilter: () => null,
|
|
1237
526
|
})
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
cucumber: "2023-12-04T12:00:20.000Z",
|
|
1241
|
-
dressing: "2023-12-03T00:00:20.000Z",
|
|
1242
|
-
});
|
|
1243
|
-
});
|
|
527
|
+
);
|
|
528
|
+
server = supertest(app);
|
|
1244
529
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
const res = await agent
|
|
1248
|
-
.patch(`/food/${spinach._id}`)
|
|
1249
|
-
.send({"source.href": "https://food.com"})
|
|
1250
|
-
.expect(200);
|
|
1251
|
-
// Assert the field was updated with dot notation.
|
|
1252
|
-
expect(res.body.data.source.href).toBe("https://food.com");
|
|
1253
|
-
// Assert these fields haven't changed.
|
|
1254
|
-
expect(res.body.data.source.name).toBe("Brand");
|
|
1255
|
-
expect(res.body.data.source.dateAdded).toBe("2023-12-13T12:30:00.000Z");
|
|
1256
|
-
|
|
1257
|
-
const dbSpinach = await FoodModel.findById(spinach._id);
|
|
1258
|
-
expect(dbSpinach?.source.href).toBe("https://food.com");
|
|
1259
|
-
expect(dbSpinach?.source.name).toBe("Brand");
|
|
1260
|
-
expect(dbSpinach?.source.dateAdded).toBe("2023-12-13T12:30:00.000Z");
|
|
530
|
+
const res = await server.get("/food").expect(200);
|
|
531
|
+
expect(res.body.data).toEqual([]);
|
|
1261
532
|
});
|
|
1262
533
|
});
|
|
1263
534
|
|
|
1264
|
-
describe("
|
|
535
|
+
describe("transformer errors", () => {
|
|
1265
536
|
let admin: any;
|
|
1266
|
-
let notAdmin: any;
|
|
1267
|
-
let agent: TestAgent;
|
|
1268
|
-
|
|
1269
537
|
let spinach: Food;
|
|
1270
538
|
|
|
1271
539
|
beforeEach(async () => {
|
|
1272
|
-
[admin
|
|
540
|
+
[admin] = await setupDb();
|
|
1273
541
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
542
|
+
spinach = await FoodModel.create({
|
|
543
|
+
calories: 1,
|
|
544
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
545
|
+
hidden: false,
|
|
546
|
+
name: "Spinach",
|
|
547
|
+
ownerId: admin._id,
|
|
548
|
+
source: {
|
|
549
|
+
name: "Brand",
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
app = getBaseServer();
|
|
554
|
+
setupAuth(app, UserModel as any);
|
|
555
|
+
addAuthRoutes(app, UserModel as any);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("transform error in create is handled", async () => {
|
|
559
|
+
app.use(
|
|
560
|
+
"/food",
|
|
561
|
+
modelRouter(FoodModel, {
|
|
562
|
+
allowAnonymous: true,
|
|
563
|
+
permissions: {
|
|
564
|
+
create: [Permissions.IsAny],
|
|
565
|
+
delete: [Permissions.IsAny],
|
|
566
|
+
list: [Permissions.IsAny],
|
|
567
|
+
read: [Permissions.IsAny],
|
|
568
|
+
update: [Permissions.IsAny],
|
|
1293
569
|
},
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
570
|
+
transformer: AdminOwnerTransformer({
|
|
571
|
+
anonWriteFields: ["name"],
|
|
572
|
+
}),
|
|
573
|
+
})
|
|
574
|
+
);
|
|
575
|
+
server = supertest(app);
|
|
576
|
+
|
|
577
|
+
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(400);
|
|
578
|
+
expect(res.body.title).toContain("cannot write fields");
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("transform error in patch is handled", async () => {
|
|
1299
582
|
app.use(
|
|
1300
583
|
"/food",
|
|
1301
584
|
modelRouter(FoodModel, {
|
|
@@ -1307,94 +590,80 @@ describe("@terreno/api", () => {
|
|
|
1307
590
|
read: [Permissions.IsAny],
|
|
1308
591
|
update: [Permissions.IsAny],
|
|
1309
592
|
},
|
|
1310
|
-
|
|
1311
|
-
|
|
593
|
+
transformer: AdminOwnerTransformer({
|
|
594
|
+
anonWriteFields: ["name"],
|
|
595
|
+
}),
|
|
1312
596
|
})
|
|
1313
597
|
);
|
|
1314
598
|
server = supertest(app);
|
|
1315
|
-
|
|
599
|
+
|
|
600
|
+
const res = await server.patch(`/food/${spinach._id}`).send({calories: 100}).expect(403);
|
|
601
|
+
expect(res.body.title).toContain("cannot write fields");
|
|
1316
602
|
});
|
|
1317
603
|
|
|
1318
|
-
it("
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
604
|
+
it("model.create validation error is handled", async () => {
|
|
605
|
+
app.use(
|
|
606
|
+
"/required",
|
|
607
|
+
modelRouter(RequiredModel, {
|
|
608
|
+
allowAnonymous: true,
|
|
609
|
+
permissions: {
|
|
610
|
+
create: [Permissions.IsAny],
|
|
611
|
+
delete: [Permissions.IsAny],
|
|
612
|
+
list: [Permissions.IsAny],
|
|
613
|
+
read: [Permissions.IsAny],
|
|
614
|
+
update: [Permissions.IsAny],
|
|
615
|
+
},
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
server = supertest(app);
|
|
619
|
+
|
|
620
|
+
const res = await server.post("/required").send({about: "test"}).expect(400);
|
|
621
|
+
expect(res.body.title).toContain("Required");
|
|
1328
622
|
});
|
|
623
|
+
});
|
|
1329
624
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
625
|
+
describe("addPopulateToQuery", () => {
|
|
626
|
+
it("returns query unchanged with no populate paths", async () => {
|
|
627
|
+
await setupDb();
|
|
628
|
+
const query = FoodModel.find({});
|
|
629
|
+
const result = addPopulateToQuery(query, undefined);
|
|
630
|
+
expect(result).toBe(query);
|
|
1335
631
|
});
|
|
1336
632
|
|
|
1337
|
-
it("
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
name: "Broccoli",
|
|
1343
|
-
ownerId: admin._id,
|
|
1344
|
-
})
|
|
1345
|
-
.expect(201);
|
|
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();
|
|
633
|
+
it("returns query unchanged with empty populate paths", async () => {
|
|
634
|
+
await setupDb();
|
|
635
|
+
const query = FoodModel.find({});
|
|
636
|
+
const result = addPopulateToQuery(query, []);
|
|
637
|
+
expect(result).toBe(query);
|
|
1349
638
|
});
|
|
1350
639
|
|
|
1351
|
-
it("
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
expect(
|
|
1359
|
-
expect(res.body.data.ownerId.email).toBe(admin.email);
|
|
1360
|
-
expect(res.body.data.ownerId.name).toBeUndefined();
|
|
640
|
+
it("applies multiple populate paths", async () => {
|
|
641
|
+
await setupDb();
|
|
642
|
+
const query = FoodModel.find({});
|
|
643
|
+
const result = addPopulateToQuery(query, [
|
|
644
|
+
{fields: ["email"], path: "ownerId"},
|
|
645
|
+
{fields: ["name"], path: "eatenBy"},
|
|
646
|
+
]);
|
|
647
|
+
expect(result).toBeDefined();
|
|
1361
648
|
});
|
|
1362
649
|
});
|
|
1363
650
|
|
|
1364
|
-
describe("
|
|
651
|
+
describe("soft delete with isDeleted plugin", () => {
|
|
1365
652
|
let admin: any;
|
|
1366
653
|
let agent: TestAgent;
|
|
1367
654
|
|
|
1368
|
-
let spinach: Food;
|
|
1369
|
-
|
|
1370
655
|
beforeEach(async () => {
|
|
1371
656
|
[admin] = await setupDb();
|
|
1372
657
|
|
|
1373
|
-
[spinach] = await Promise.all([
|
|
1374
|
-
FoodModel.create({
|
|
1375
|
-
calories: 1,
|
|
1376
|
-
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
1377
|
-
hidden: false,
|
|
1378
|
-
name: "Spinach",
|
|
1379
|
-
ownerId: admin._id,
|
|
1380
|
-
source: {
|
|
1381
|
-
name: "Brand",
|
|
1382
|
-
},
|
|
1383
|
-
}),
|
|
1384
|
-
FoodModel.create({
|
|
1385
|
-
calories: 100,
|
|
1386
|
-
created: Date.now() - 10,
|
|
1387
|
-
hidden: true,
|
|
1388
|
-
name: "Apple",
|
|
1389
|
-
ownerId: admin?._id,
|
|
1390
|
-
}),
|
|
1391
|
-
]);
|
|
1392
658
|
app = getBaseServer();
|
|
1393
659
|
setupAuth(app, UserModel as any);
|
|
1394
660
|
addAuthRoutes(app, UserModel as any);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("soft deletes user with deleted field", async () => {
|
|
1395
664
|
app.use(
|
|
1396
|
-
"/
|
|
1397
|
-
modelRouter(
|
|
665
|
+
"/users",
|
|
666
|
+
modelRouter(UserModel, {
|
|
1398
667
|
allowAnonymous: true,
|
|
1399
668
|
permissions: {
|
|
1400
669
|
create: [Permissions.IsAny],
|
|
@@ -1403,54 +672,42 @@ describe("@terreno/api", () => {
|
|
|
1403
672
|
read: [Permissions.IsAny],
|
|
1404
673
|
update: [Permissions.IsAny],
|
|
1405
674
|
},
|
|
1406
|
-
responseHandler: (data, method) => {
|
|
1407
|
-
if (method === "list") {
|
|
1408
|
-
return (data as any).map((d: any) => ({
|
|
1409
|
-
foo: "bar",
|
|
1410
|
-
id: (d as any)._id,
|
|
1411
|
-
}));
|
|
1412
|
-
}
|
|
1413
|
-
return {
|
|
1414
|
-
foo: "bar",
|
|
1415
|
-
id: (data as any)._id,
|
|
1416
|
-
};
|
|
1417
|
-
},
|
|
1418
675
|
})
|
|
1419
676
|
);
|
|
1420
677
|
server = supertest(app);
|
|
1421
678
|
agent = await authAsUser(app, "notAdmin");
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
it("reads with serialize", async () => {
|
|
1425
|
-
const res = await agent.get(`/food/${spinach._id}`).expect(200);
|
|
1426
|
-
expect(res.body.data.ownerId).toBeUndefined();
|
|
1427
|
-
expect(res.body.data.id).toBe(spinach._id.toString());
|
|
1428
|
-
expect(res.body.data.foo).toBe("bar");
|
|
1429
|
-
});
|
|
1430
679
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
expect(res.body.data[0].ownerId).toBeUndefined();
|
|
1434
|
-
expect(res.body.data[1].ownerId).toBeUndefined();
|
|
680
|
+
const res = await agent.delete(`/users/${admin._id}`).expect(204);
|
|
681
|
+
expect(res.body).toEqual({});
|
|
1435
682
|
|
|
1436
|
-
|
|
1437
|
-
expect(
|
|
1438
|
-
expect(res.body.data[1].id).toBeDefined();
|
|
1439
|
-
expect(res.body.data[1].foo).toBe("bar");
|
|
683
|
+
const deletedUser = await UserModel.findById(admin._id);
|
|
684
|
+
expect(deletedUser).toBeNull();
|
|
1440
685
|
});
|
|
1441
686
|
});
|
|
1442
687
|
|
|
1443
|
-
describe("
|
|
1444
|
-
let
|
|
688
|
+
describe("populate in create", () => {
|
|
689
|
+
let admin: any;
|
|
1445
690
|
|
|
1446
691
|
beforeEach(async () => {
|
|
1447
|
-
await setupDb();
|
|
692
|
+
[admin] = await setupDb();
|
|
693
|
+
|
|
694
|
+
await FoodModel.create({
|
|
695
|
+
calories: 1,
|
|
696
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
697
|
+
hidden: false,
|
|
698
|
+
name: "Spinach",
|
|
699
|
+
ownerId: admin._id,
|
|
700
|
+
});
|
|
701
|
+
|
|
1448
702
|
app = getBaseServer();
|
|
1449
703
|
setupAuth(app, UserModel as any);
|
|
1450
704
|
addAuthRoutes(app, UserModel as any);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("handles populate with valid path in create", async () => {
|
|
1451
708
|
app.use(
|
|
1452
|
-
"/
|
|
1453
|
-
modelRouter(
|
|
709
|
+
"/food",
|
|
710
|
+
modelRouter(FoodModel, {
|
|
1454
711
|
allowAnonymous: true,
|
|
1455
712
|
permissions: {
|
|
1456
713
|
create: [Permissions.IsAny],
|
|
@@ -1459,203 +716,155 @@ describe("@terreno/api", () => {
|
|
|
1459
716
|
read: [Permissions.IsAny],
|
|
1460
717
|
update: [Permissions.IsAny],
|
|
1461
718
|
},
|
|
719
|
+
populatePaths: [{fields: ["email"], path: "ownerId"}],
|
|
1462
720
|
})
|
|
1463
721
|
);
|
|
1464
722
|
server = supertest(app);
|
|
1465
|
-
agent = await authAsUser(app, "notAdmin");
|
|
1466
|
-
});
|
|
1467
723
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
expect(res.body.data
|
|
1473
|
-
expect(res.body.data
|
|
724
|
+
const res = await server
|
|
725
|
+
.post("/food")
|
|
726
|
+
.send({calories: 15, name: "Broccoli", ownerId: admin._id})
|
|
727
|
+
.expect(201);
|
|
728
|
+
expect(res.body.data.name).toBe("Broccoli");
|
|
729
|
+
expect(res.body.data.ownerId.email).toBe(admin.email);
|
|
1474
730
|
});
|
|
1475
731
|
});
|
|
1476
732
|
|
|
1477
|
-
describe("
|
|
1478
|
-
let
|
|
1479
|
-
let
|
|
1480
|
-
let notAdmin: mongoose.Document;
|
|
1481
|
-
let agent: TestAgent;
|
|
733
|
+
describe("save error handling", () => {
|
|
734
|
+
let admin: any;
|
|
735
|
+
let spinach: Food;
|
|
1482
736
|
|
|
1483
737
|
beforeEach(async () => {
|
|
1484
|
-
[
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
superUser = (await UserModel.findById(superUserId)) as any;
|
|
738
|
+
[admin] = await setupDb();
|
|
739
|
+
|
|
740
|
+
spinach = await FoodModel.create({
|
|
741
|
+
calories: 1,
|
|
742
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
743
|
+
hidden: false,
|
|
744
|
+
name: "Spinach",
|
|
745
|
+
ownerId: admin._id,
|
|
746
|
+
source: {
|
|
747
|
+
name: "Brand",
|
|
748
|
+
},
|
|
749
|
+
});
|
|
1497
750
|
|
|
1498
751
|
app = getBaseServer();
|
|
1499
752
|
setupAuth(app, UserModel as any);
|
|
1500
753
|
addAuthRoutes(app, UserModel as any);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("handles patch save error with validation failure", async () => {
|
|
1501
757
|
app.use(
|
|
1502
|
-
"/
|
|
1503
|
-
modelRouter(
|
|
758
|
+
"/food",
|
|
759
|
+
modelRouter(FoodModel, {
|
|
1504
760
|
allowAnonymous: true,
|
|
1505
|
-
discriminatorKey: "__t",
|
|
1506
761
|
permissions: {
|
|
1507
|
-
create: [Permissions.
|
|
1508
|
-
delete: [Permissions.
|
|
1509
|
-
list: [Permissions.
|
|
1510
|
-
read: [Permissions.
|
|
1511
|
-
update: [Permissions.
|
|
762
|
+
create: [Permissions.IsAny],
|
|
763
|
+
delete: [Permissions.IsAny],
|
|
764
|
+
list: [Permissions.IsAny],
|
|
765
|
+
read: [Permissions.IsAny],
|
|
766
|
+
update: [Permissions.IsAny],
|
|
1512
767
|
},
|
|
1513
768
|
})
|
|
1514
769
|
);
|
|
1515
|
-
|
|
1516
770
|
server = supertest(app);
|
|
1517
771
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
expect(res.body.data).toHaveLength(5);
|
|
1524
|
-
|
|
1525
|
-
const data = sortBy(res.body.data, ["email"]);
|
|
1526
|
-
|
|
1527
|
-
expect(data[0].email).toBe("admin+other@example.com");
|
|
1528
|
-
expect(data[0].department).toBeUndefined();
|
|
1529
|
-
expect(data[0].supertitle).toBeUndefined();
|
|
1530
|
-
expect(data[0].__t).toBeUndefined();
|
|
1531
|
-
|
|
1532
|
-
expect(data[1].email).toBe("admin@example.com");
|
|
1533
|
-
expect(data[1].department).toBeUndefined();
|
|
1534
|
-
expect(data[1].supertitle).toBeUndefined();
|
|
1535
|
-
expect(data[1].__t).toBeUndefined();
|
|
1536
|
-
|
|
1537
|
-
expect(data[2].email).toBe("notAdmin@example.com");
|
|
1538
|
-
expect(data[2].department).toBeUndefined();
|
|
1539
|
-
expect(data[2].supertitle).toBeUndefined();
|
|
1540
|
-
expect(data[2].__t).toBeUndefined();
|
|
1541
|
-
|
|
1542
|
-
expect(data[3].email).toBe("staff@example.com");
|
|
1543
|
-
expect(data[3].department).toBe("Accounting");
|
|
1544
|
-
expect(data[3].supertitle).toBeUndefined();
|
|
1545
|
-
expect(data[3].__t).toBe("Staff");
|
|
1546
|
-
|
|
1547
|
-
expect(data[4].email).toBe("superuser@example.com");
|
|
1548
|
-
expect(data[4].department).toBeUndefined();
|
|
1549
|
-
expect(data[4].superTitle).toBe("Super Man");
|
|
1550
|
-
expect(data[4].__t).toBe("SuperUser");
|
|
772
|
+
const res = await server
|
|
773
|
+
.patch(`/food/${spinach._id}`)
|
|
774
|
+
.send({invalidField: "value"})
|
|
775
|
+
.expect(400);
|
|
776
|
+
expect(res.body.title).toContain("preUpdate hook save error");
|
|
1551
777
|
});
|
|
778
|
+
});
|
|
1552
779
|
|
|
1553
|
-
|
|
1554
|
-
|
|
780
|
+
describe("body undefined after transform without preCreate", () => {
|
|
781
|
+
beforeEach(async () => {
|
|
782
|
+
await setupDb();
|
|
1555
783
|
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
784
|
+
app = getBaseServer();
|
|
785
|
+
setupAuth(app, UserModel as any);
|
|
786
|
+
addAuthRoutes(app, UserModel as any);
|
|
1559
787
|
});
|
|
1560
788
|
|
|
1561
|
-
it("
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
789
|
+
it("handles undefined body after transform when no preCreate", async () => {
|
|
790
|
+
app.use(
|
|
791
|
+
"/food",
|
|
792
|
+
modelRouter(FoodModel, {
|
|
793
|
+
allowAnonymous: true,
|
|
794
|
+
permissions: {
|
|
795
|
+
create: [Permissions.IsAny],
|
|
796
|
+
delete: [Permissions.IsAny],
|
|
797
|
+
list: [Permissions.IsAny],
|
|
798
|
+
read: [Permissions.IsAny],
|
|
799
|
+
update: [Permissions.IsAny],
|
|
800
|
+
},
|
|
801
|
+
transformer: {
|
|
802
|
+
transform: () => undefined,
|
|
803
|
+
},
|
|
804
|
+
})
|
|
805
|
+
);
|
|
806
|
+
server = supertest(app);
|
|
1573
807
|
|
|
1574
|
-
const
|
|
1575
|
-
expect(
|
|
808
|
+
const res = await server.post("/food").send({calories: 15, name: "Broccoli"}).expect(400);
|
|
809
|
+
expect(res.body.title).toBe("Invalid request body");
|
|
810
|
+
expect(res.body.detail).toBe("Body is undefined");
|
|
1576
811
|
});
|
|
812
|
+
});
|
|
1577
813
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
.send({email: "newemail@example.com", superTitle: "The Boss"})
|
|
1582
|
-
.expect(200);
|
|
1583
|
-
|
|
1584
|
-
expect(res.body.data.email).toBe("newemail@example.com");
|
|
1585
|
-
expect(res.body.data.superTitle).toBeUndefined();
|
|
1586
|
-
|
|
1587
|
-
const user = await SuperUserModel.findById(notAdmin._id);
|
|
1588
|
-
expect(user?.superTitle).toBeUndefined();
|
|
1589
|
-
});
|
|
814
|
+
describe("soft delete with deleted field", () => {
|
|
815
|
+
let _admin: any;
|
|
816
|
+
let agent: TestAgent;
|
|
1590
817
|
|
|
1591
|
-
|
|
1592
|
-
await
|
|
1593
|
-
.patch(`/users/${notAdmin._id}`)
|
|
1594
|
-
.send({__t: "Staff", superTitle: "Batman"})
|
|
1595
|
-
.expect(404);
|
|
818
|
+
beforeEach(async () => {
|
|
819
|
+
[_admin] = await setupDb();
|
|
1596
820
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
.expect(404);
|
|
821
|
+
app = getBaseServer();
|
|
822
|
+
setupAuth(app, UserModel as any);
|
|
823
|
+
addAuthRoutes(app, UserModel as any);
|
|
1601
824
|
});
|
|
1602
825
|
|
|
1603
|
-
it("
|
|
1604
|
-
const
|
|
1605
|
-
.patch(`/users/${superUser._id}`)
|
|
1606
|
-
.send({__t: "SuperUser", department: "Journalism"})
|
|
1607
|
-
.expect(200);
|
|
1608
|
-
|
|
1609
|
-
expect(res.body.data.department).toBeUndefined();
|
|
1610
|
-
|
|
1611
|
-
const user = await SuperUserModel.findById(superUser._id);
|
|
1612
|
-
expect((user as any)?.department).toBeUndefined();
|
|
1613
|
-
});
|
|
826
|
+
it("soft deletes document with deleted field using isDeletedPlugin", async () => {
|
|
827
|
+
const mongoose = await import("mongoose");
|
|
1614
828
|
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
__t: "SuperUser",
|
|
1620
|
-
department: "R&D",
|
|
1621
|
-
email: "brucewayne@example.com",
|
|
1622
|
-
superTitle: "Batman",
|
|
1623
|
-
})
|
|
1624
|
-
.expect(201);
|
|
829
|
+
const softDeleteSchema = new mongoose.Schema({
|
|
830
|
+
deleted: {default: false, type: Boolean},
|
|
831
|
+
name: String,
|
|
832
|
+
});
|
|
1625
833
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
834
|
+
let SoftDeleteModel;
|
|
835
|
+
try {
|
|
836
|
+
SoftDeleteModel = mongoose.model("SoftDeleteTest");
|
|
837
|
+
} catch {
|
|
838
|
+
SoftDeleteModel = mongoose.model("SoftDeleteTest", softDeleteSchema);
|
|
839
|
+
}
|
|
1631
840
|
|
|
1632
|
-
|
|
1633
|
-
expect(user?.superTitle).toBe("Batman");
|
|
1634
|
-
});
|
|
841
|
+
await SoftDeleteModel.deleteMany({});
|
|
1635
842
|
|
|
1636
|
-
|
|
1637
|
-
// Fails without __t.
|
|
1638
|
-
await agent.delete(`/users/${superUser._id}`).expect(404);
|
|
843
|
+
const testDoc = await SoftDeleteModel.create({name: "TestItem"});
|
|
1639
844
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
845
|
+
app.use(
|
|
846
|
+
"/softdelete",
|
|
847
|
+
modelRouter(SoftDeleteModel, {
|
|
848
|
+
allowAnonymous: true,
|
|
849
|
+
permissions: {
|
|
850
|
+
create: [Permissions.IsAny],
|
|
851
|
+
delete: [Permissions.IsAny],
|
|
852
|
+
list: [Permissions.IsAny],
|
|
853
|
+
read: [Permissions.IsAny],
|
|
854
|
+
update: [Permissions.IsAny],
|
|
855
|
+
},
|
|
1644
856
|
})
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
expect(user).toBeNull();
|
|
1649
|
-
});
|
|
857
|
+
);
|
|
858
|
+
server = supertest(app);
|
|
859
|
+
agent = await authAsUser(app, "notAdmin");
|
|
1650
860
|
|
|
1651
|
-
|
|
1652
|
-
// Fails for base user with __t
|
|
1653
|
-
await agent.delete(`/users/${notAdmin._id}`).send({__t: "SuperUser"}).expect(404);
|
|
861
|
+
await agent.delete(`/softdelete/${testDoc._id}`).expect(204);
|
|
1654
862
|
|
|
1655
|
-
await
|
|
863
|
+
const softDeleted = await SoftDeleteModel.findById(testDoc._id);
|
|
864
|
+
expect(softDeleted).not.toBeNull();
|
|
865
|
+
expect(softDeleted?.deleted).toBe(true);
|
|
1656
866
|
|
|
1657
|
-
|
|
1658
|
-
expect(user).toBeNull();
|
|
867
|
+
await SoftDeleteModel.deleteMany({});
|
|
1659
868
|
});
|
|
1660
869
|
});
|
|
1661
870
|
});
|