@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/permissions.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type TestAgent from "supertest/lib/agent";
|
|
|
5
5
|
|
|
6
6
|
import {modelRouter} from "./api";
|
|
7
7
|
import {addAuthRoutes, setupAuth} from "./auth";
|
|
8
|
-
import {Permissions} from "./permissions";
|
|
8
|
+
import {OwnerQueryFilter, Permissions} from "./permissions";
|
|
9
9
|
import {
|
|
10
10
|
authAsUser,
|
|
11
11
|
type Food,
|
|
@@ -217,3 +217,72 @@ describe("permissions", () => {
|
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
219
|
});
|
|
220
|
+
|
|
221
|
+
describe("permissions module", () => {
|
|
222
|
+
describe("OwnerQueryFilter", () => {
|
|
223
|
+
it("returns ownerId filter when user is provided", () => {
|
|
224
|
+
const user = {id: "user-123"} as any;
|
|
225
|
+
const filter = OwnerQueryFilter(user);
|
|
226
|
+
expect(filter).toEqual({ownerId: "user-123"});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("returns null when user is undefined", () => {
|
|
230
|
+
const filter = OwnerQueryFilter(undefined);
|
|
231
|
+
expect(filter).toBeNull();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("Permissions.IsAuthenticatedOrReadOnly", () => {
|
|
236
|
+
it("returns true for authenticated non-anonymous users", () => {
|
|
237
|
+
const user = {id: "user-123", isAnonymous: false} as any;
|
|
238
|
+
expect(Permissions.IsAuthenticatedOrReadOnly("create", user)).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("returns true for read methods when user is anonymous", () => {
|
|
242
|
+
const user = {id: "user-123", isAnonymous: true} as any;
|
|
243
|
+
expect(Permissions.IsAuthenticatedOrReadOnly("list", user)).toBe(true);
|
|
244
|
+
expect(Permissions.IsAuthenticatedOrReadOnly("read", user)).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("returns false for write methods when user is anonymous", () => {
|
|
248
|
+
const user = {id: "user-123", isAnonymous: true} as any;
|
|
249
|
+
expect(Permissions.IsAuthenticatedOrReadOnly("create", user)).toBe(false);
|
|
250
|
+
expect(Permissions.IsAuthenticatedOrReadOnly("update", user)).toBe(false);
|
|
251
|
+
expect(Permissions.IsAuthenticatedOrReadOnly("delete", user)).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("Permissions.IsOwnerOrReadOnly", () => {
|
|
256
|
+
it("returns true when no object is provided", () => {
|
|
257
|
+
expect(Permissions.IsOwnerOrReadOnly("update", {id: "user-123"} as any, undefined)).toBe(
|
|
258
|
+
true
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("returns true for admin users", () => {
|
|
263
|
+
const user = {admin: true, id: "admin-123"} as any;
|
|
264
|
+
const obj = {ownerId: "other-user"};
|
|
265
|
+
expect(Permissions.IsOwnerOrReadOnly("update", user, obj)).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("returns true when user is owner", () => {
|
|
269
|
+
const user = {id: "user-123"} as any;
|
|
270
|
+
const obj = {ownerId: "user-123"};
|
|
271
|
+
expect(Permissions.IsOwnerOrReadOnly("update", user, obj)).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("returns true for read methods when not owner", () => {
|
|
275
|
+
const user = {id: "user-123"} as any;
|
|
276
|
+
const obj = {ownerId: "other-user"};
|
|
277
|
+
expect(Permissions.IsOwnerOrReadOnly("list", user, obj)).toBe(true);
|
|
278
|
+
expect(Permissions.IsOwnerOrReadOnly("read", user, obj)).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("returns false for write methods when not owner", () => {
|
|
282
|
+
const user = {id: "user-123"} as any;
|
|
283
|
+
const obj = {ownerId: "other-user"};
|
|
284
|
+
expect(Permissions.IsOwnerOrReadOnly("update", user, obj)).toBe(false);
|
|
285
|
+
expect(Permissions.IsOwnerOrReadOnly("delete", user, obj)).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
package/src/permissions.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type express from "express";
|
|
|
4
4
|
import type {NextFunction} from "express";
|
|
5
5
|
import mongoose, {type Model} from "mongoose";
|
|
6
6
|
|
|
7
|
-
import {addPopulateToQuery,
|
|
7
|
+
import {addPopulateToQuery, type ModelRouterOptions, type RESTMethod} from "./api";
|
|
8
8
|
import type {User} from "./auth";
|
|
9
9
|
import {APIError} from "./errors";
|
|
10
10
|
import {logger} from "./logger";
|
|
@@ -101,8 +101,8 @@ export async function checkPermissions<T>(
|
|
|
101
101
|
// finds the relevant object, checks the permissions, and attaches the object to the request as
|
|
102
102
|
// req.obj.
|
|
103
103
|
export function permissionMiddleware<T>(
|
|
104
|
-
|
|
105
|
-
options: Pick<ModelRouterOptions<T>, "permissions" | "populatePaths"
|
|
104
|
+
model: Model<T>,
|
|
105
|
+
options: Pick<ModelRouterOptions<T>, "permissions" | "populatePaths">
|
|
106
106
|
) {
|
|
107
107
|
return async (req: express.Request, _res: express.Response, next: NextFunction) => {
|
|
108
108
|
if (req.method === "OPTIONS") {
|
|
@@ -131,8 +131,6 @@ export function permissionMiddleware<T>(
|
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
const model = getModel(baseModel, req.body, options);
|
|
135
|
-
|
|
136
134
|
// All methods check for permissions.
|
|
137
135
|
if (!(await checkPermissions(method, options.permissions[method], req.user))) {
|
|
138
136
|
throw new APIError({
|
|
@@ -159,15 +157,7 @@ export function permissionMiddleware<T>(
|
|
|
159
157
|
title: `GET failed on ${req.params.id}`,
|
|
160
158
|
});
|
|
161
159
|
}
|
|
162
|
-
if (!data
|
|
163
|
-
// For discriminated models, return 404 without checking hidden state
|
|
164
|
-
if (["update", "delete"].includes(method) && data?.__t && !req.body?.__t) {
|
|
165
|
-
throw new APIError({
|
|
166
|
-
status: 404,
|
|
167
|
-
title: `Document ${req.params.id} not found for model ${model.modelName}`,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
160
|
+
if (!data) {
|
|
171
161
|
// Check if document exists but is hidden. Completely skip plugins.
|
|
172
162
|
const hiddenDoc = await model.collection.findOne({
|
|
173
163
|
_id: new mongoose.Types.ObjectId(req.params.id),
|
package/src/populate.test.ts
CHANGED
|
@@ -63,3 +63,61 @@ describe("populate functions", () => {
|
|
|
63
63
|
expect(populated.likesIds[1].userId.name).toBeUndefined();
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
describe("unpopulate edge cases", () => {
|
|
68
|
+
it("throws error when path is empty", () => {
|
|
69
|
+
const doc = {name: "test"};
|
|
70
|
+
expect(() => unpopulate(doc as any, "")).toThrow("path is required");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("unpopulates single populated field", () => {
|
|
74
|
+
const doc = {
|
|
75
|
+
name: "test",
|
|
76
|
+
ownerId: {_id: "owner-123", email: "owner@test.com"},
|
|
77
|
+
};
|
|
78
|
+
const result = unpopulate(doc as any, "ownerId") as any;
|
|
79
|
+
expect(result.ownerId).toBe("owner-123");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("unpopulates array of populated fields", () => {
|
|
83
|
+
const doc = {
|
|
84
|
+
items: [{_id: "item-1", name: "Item 1"}, {_id: "item-2", name: "Item 2"}, "item-3"],
|
|
85
|
+
name: "test",
|
|
86
|
+
};
|
|
87
|
+
const result = unpopulate(doc as any, "items") as any;
|
|
88
|
+
expect(result.items).toEqual(["item-1", "item-2", "item-3"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("handles nested paths", () => {
|
|
92
|
+
const doc = {
|
|
93
|
+
name: "test",
|
|
94
|
+
nested: {
|
|
95
|
+
items: [
|
|
96
|
+
{_id: "item-1", name: "Item 1"},
|
|
97
|
+
{_id: "item-2", name: "Item 2"},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const result = unpopulate(doc as any, "nested.items") as any;
|
|
102
|
+
expect(result.nested.items).toEqual(["item-1", "item-2"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns original doc when path does not exist", () => {
|
|
106
|
+
const doc = {name: "test"};
|
|
107
|
+
const result = unpopulate(doc as any, "nonexistent") as any;
|
|
108
|
+
expect(result).toEqual(doc);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("handles nested array paths", () => {
|
|
112
|
+
const doc = {
|
|
113
|
+
containers: [
|
|
114
|
+
{items: [{_id: "item-1"}, {_id: "item-2"}]},
|
|
115
|
+
{items: [{_id: "item-3"}, {_id: "item-4"}]},
|
|
116
|
+
],
|
|
117
|
+
name: "test",
|
|
118
|
+
};
|
|
119
|
+
const result = unpopulate(doc as any, "containers.items") as any;
|
|
120
|
+
expect(result.containers[0].items).toEqual(["item-1", "item-2"]);
|
|
121
|
+
expect(result.containers[1].items).toEqual(["item-3", "item-4"]);
|
|
122
|
+
});
|
|
123
|
+
});
|
package/src/utils.test.ts
CHANGED
|
@@ -1,14 +1,219 @@
|
|
|
1
|
-
import {describe, expect, it} from "bun:test";
|
|
1
|
+
import {describe, expect, it, spyOn} from "bun:test";
|
|
2
|
+
import mongoose from "mongoose";
|
|
2
3
|
|
|
3
|
-
import {isValidObjectId} from "./utils";
|
|
4
|
+
import {checkModelsStrict, isValidObjectId, timeout} from "./utils";
|
|
4
5
|
|
|
5
6
|
describe("utils", () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
describe("isValidObjectId", () => {
|
|
8
|
+
it("checks valid ObjectIds", () => {
|
|
9
|
+
expect(isValidObjectId("62c44da0003d9f8ee8cc925c")).toBe(true);
|
|
10
|
+
expect(isValidObjectId("620000000000000000000000")).toBe(true);
|
|
11
|
+
// Mongoose's builtin "ObjectId.isValid" will falsely say this is an ObjectId.
|
|
12
|
+
expect(isValidObjectId("1234567890ab")).toBe(false);
|
|
13
|
+
expect(isValidObjectId("microsoft123")).toBe(false);
|
|
14
|
+
expect(isValidObjectId("62c44da0003d9f8ee8cc925x")).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("checkModelsStrict", () => {
|
|
19
|
+
it("throws error when toObject.virtuals is not true", () => {
|
|
20
|
+
// Create a schema without toObject.virtuals
|
|
21
|
+
const testSchema = new mongoose.Schema({name: String});
|
|
22
|
+
testSchema.set("strict", "throw");
|
|
23
|
+
// Not setting toObject.virtuals
|
|
24
|
+
|
|
25
|
+
if (mongoose.models.ToObjectTestModel) {
|
|
26
|
+
delete mongoose.models.ToObjectTestModel;
|
|
27
|
+
}
|
|
28
|
+
mongoose.model("ToObjectTestModel", testSchema);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// This should throw because ToObjectTestModel doesn't have toObject.virtuals
|
|
32
|
+
expect(() => checkModelsStrict()).toThrow("toObject.virtuals not set to true");
|
|
33
|
+
} finally {
|
|
34
|
+
delete mongoose.models.ToObjectTestModel;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("throws error when toJSON.virtuals is not true", () => {
|
|
39
|
+
// Create a schema with toObject.virtuals but without toJSON.virtuals
|
|
40
|
+
const testSchema = new mongoose.Schema({name: String});
|
|
41
|
+
testSchema.set("toObject", {virtuals: true});
|
|
42
|
+
testSchema.set("strict", "throw");
|
|
43
|
+
// Not setting toJSON.virtuals
|
|
44
|
+
|
|
45
|
+
if (mongoose.models.ToJsonTestModel) {
|
|
46
|
+
delete mongoose.models.ToJsonTestModel;
|
|
47
|
+
}
|
|
48
|
+
mongoose.model("ToJsonTestModel", testSchema);
|
|
49
|
+
|
|
50
|
+
// Use spyOn to intercept modelNames and return only our test model
|
|
51
|
+
const spy = spyOn(mongoose, "modelNames").mockReturnValue(["ToJsonTestModel"]);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
expect(() => checkModelsStrict()).toThrow("toJSON.virtuals not set to true");
|
|
55
|
+
} finally {
|
|
56
|
+
spy.mockRestore();
|
|
57
|
+
delete mongoose.models.ToJsonTestModel;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("throws error when strict mode is not set to throw", () => {
|
|
62
|
+
// Create a schema with virtuals but without strict mode
|
|
63
|
+
const testSchema = new mongoose.Schema({name: String});
|
|
64
|
+
testSchema.set("toObject", {virtuals: true});
|
|
65
|
+
testSchema.set("toJSON", {virtuals: true});
|
|
66
|
+
// Not setting strict to "throw"
|
|
67
|
+
|
|
68
|
+
if (mongoose.models.StrictTestModel) {
|
|
69
|
+
delete mongoose.models.StrictTestModel;
|
|
70
|
+
}
|
|
71
|
+
mongoose.model("StrictTestModel", testSchema);
|
|
72
|
+
|
|
73
|
+
const spy = spyOn(mongoose, "modelNames").mockReturnValue(["StrictTestModel"]);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
expect(() => checkModelsStrict()).toThrow("is not set to strict mode");
|
|
77
|
+
} finally {
|
|
78
|
+
spy.mockRestore();
|
|
79
|
+
delete mongoose.models.StrictTestModel;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("passes when all checks pass", () => {
|
|
84
|
+
// Create a properly configured schema
|
|
85
|
+
const testSchema = new mongoose.Schema({name: String});
|
|
86
|
+
testSchema.set("toObject", {virtuals: true});
|
|
87
|
+
testSchema.set("toJSON", {virtuals: true});
|
|
88
|
+
testSchema.set("strict", "throw");
|
|
89
|
+
|
|
90
|
+
if (mongoose.models.GoodTestModel) {
|
|
91
|
+
delete mongoose.models.GoodTestModel;
|
|
92
|
+
}
|
|
93
|
+
mongoose.model("GoodTestModel", testSchema);
|
|
94
|
+
|
|
95
|
+
const spy = spyOn(mongoose, "modelNames").mockReturnValue(["GoodTestModel"]);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
expect(() => checkModelsStrict()).not.toThrow();
|
|
99
|
+
} finally {
|
|
100
|
+
spy.mockRestore();
|
|
101
|
+
delete mongoose.models.GoodTestModel;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("skips strict mode check for ignored models", () => {
|
|
106
|
+
// Create a properly configured model
|
|
107
|
+
const goodSchema = new mongoose.Schema({name: String});
|
|
108
|
+
goodSchema.set("toObject", {virtuals: true});
|
|
109
|
+
goodSchema.set("toJSON", {virtuals: true});
|
|
110
|
+
goodSchema.set("strict", "throw");
|
|
111
|
+
|
|
112
|
+
if (mongoose.models.GoodModel) {
|
|
113
|
+
delete mongoose.models.GoodModel;
|
|
114
|
+
}
|
|
115
|
+
mongoose.model("GoodModel", goodSchema);
|
|
116
|
+
|
|
117
|
+
// Create a model without strict mode that we'll ignore
|
|
118
|
+
const badSchema = new mongoose.Schema({name: String});
|
|
119
|
+
badSchema.set("toObject", {virtuals: true});
|
|
120
|
+
badSchema.set("toJSON", {virtuals: true});
|
|
121
|
+
// Not setting strict - should fail unless ignored
|
|
122
|
+
|
|
123
|
+
if (mongoose.models.IgnoredModel) {
|
|
124
|
+
delete mongoose.models.IgnoredModel;
|
|
125
|
+
}
|
|
126
|
+
mongoose.model("IgnoredModel", badSchema);
|
|
127
|
+
|
|
128
|
+
const spy = spyOn(mongoose, "modelNames").mockReturnValue(["GoodModel", "IgnoredModel"]);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Without ignoring, should throw for IgnoredModel
|
|
132
|
+
expect(() => checkModelsStrict()).toThrow("is not set to strict mode");
|
|
133
|
+
|
|
134
|
+
// With ignoring IgnoredModel, should pass
|
|
135
|
+
expect(() => checkModelsStrict(["IgnoredModel"])).not.toThrow();
|
|
136
|
+
} finally {
|
|
137
|
+
spy.mockRestore();
|
|
138
|
+
delete mongoose.models.GoodModel;
|
|
139
|
+
delete mongoose.models.IgnoredModel;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("handles multiple models and validates all", () => {
|
|
144
|
+
// Create three properly configured models
|
|
145
|
+
const schema1 = new mongoose.Schema({name: String});
|
|
146
|
+
schema1.set("toObject", {virtuals: true});
|
|
147
|
+
schema1.set("toJSON", {virtuals: true});
|
|
148
|
+
schema1.set("strict", "throw");
|
|
149
|
+
|
|
150
|
+
const schema2 = new mongoose.Schema({value: Number});
|
|
151
|
+
schema2.set("toObject", {virtuals: true});
|
|
152
|
+
schema2.set("toJSON", {virtuals: true});
|
|
153
|
+
schema2.set("strict", "throw");
|
|
154
|
+
|
|
155
|
+
const schema3 = new mongoose.Schema({active: Boolean});
|
|
156
|
+
schema3.set("toObject", {virtuals: true});
|
|
157
|
+
schema3.set("toJSON", {virtuals: true});
|
|
158
|
+
schema3.set("strict", "throw");
|
|
159
|
+
|
|
160
|
+
if (mongoose.models.MultiModel1) delete mongoose.models.MultiModel1;
|
|
161
|
+
if (mongoose.models.MultiModel2) delete mongoose.models.MultiModel2;
|
|
162
|
+
if (mongoose.models.MultiModel3) delete mongoose.models.MultiModel3;
|
|
163
|
+
|
|
164
|
+
mongoose.model("MultiModel1", schema1);
|
|
165
|
+
mongoose.model("MultiModel2", schema2);
|
|
166
|
+
mongoose.model("MultiModel3", schema3);
|
|
167
|
+
|
|
168
|
+
const spy = spyOn(mongoose, "modelNames").mockReturnValue([
|
|
169
|
+
"MultiModel1",
|
|
170
|
+
"MultiModel2",
|
|
171
|
+
"MultiModel3",
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
expect(() => checkModelsStrict()).not.toThrow();
|
|
176
|
+
} finally {
|
|
177
|
+
spy.mockRestore();
|
|
178
|
+
delete mongoose.models.MultiModel1;
|
|
179
|
+
delete mongoose.models.MultiModel2;
|
|
180
|
+
delete mongoose.models.MultiModel3;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("handles empty model list", () => {
|
|
185
|
+
const spy = spyOn(mongoose, "modelNames").mockReturnValue([]);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
expect(() => checkModelsStrict()).not.toThrow();
|
|
189
|
+
} finally {
|
|
190
|
+
spy.mockRestore();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("timeout", () => {
|
|
196
|
+
it("resolves after specified time", async () => {
|
|
197
|
+
const start = Date.now();
|
|
198
|
+
await timeout(50);
|
|
199
|
+
const elapsed = Date.now() - start;
|
|
200
|
+
expect(elapsed).toBeGreaterThanOrEqual(40);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("isValidObjectId additional cases", () => {
|
|
205
|
+
it("returns true for valid ObjectId strings", () => {
|
|
206
|
+
expect(isValidObjectId("507f1f77bcf86cd799439011")).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("returns false for invalid ObjectId strings", () => {
|
|
210
|
+
expect(isValidObjectId("invalid-id")).toBe(false);
|
|
211
|
+
expect(isValidObjectId("12345")).toBe(false);
|
|
212
|
+
expect(isValidObjectId("")).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("returns false for 12-character strings that are not valid ObjectIds", () => {
|
|
216
|
+
expect(isValidObjectId("123456789012")).toBe(false);
|
|
217
|
+
});
|
|
13
218
|
});
|
|
14
219
|
});
|