@terreno/api 0.12.2 → 0.13.1
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/dist/configurationPlugin.js +7 -1
- package/dist/consentApp.d.ts +2 -2
- package/dist/consentApp.js +2 -1
- package/dist/githubAuth.test.js +409 -0
- package/dist/models/consentForm.js +9 -1
- package/dist/notifiers/slackNotifier.d.ts +2 -2
- package/dist/notifiers/slackNotifier.js +38 -7
- package/dist/openApiEtag.js +0 -7
- package/dist/plugins.d.ts +3 -3
- package/dist/plugins.js +8 -4
- package/dist/populate.test.js +5 -1
- package/dist/scriptRunner.js +2 -2
- package/dist/secretProviders.js +4 -1
- package/dist/tests.js +1 -0
- package/dist/utils.js +13 -3
- package/package.json +1 -1
- package/src/configurationPlugin.ts +7 -1
- package/src/consentApp.ts +3 -3
- package/src/githubAuth.test.ts +327 -0
- package/src/models/consentForm.ts +10 -1
- package/src/notifiers/slackNotifier.ts +7 -6
- package/src/openApiEtag.ts +0 -7
- package/src/plugins.ts +13 -8
- package/src/populate.test.ts +45 -20
- package/src/scriptRunner.ts +2 -2
- package/src/secretProviders.ts +4 -3
- package/src/tests.ts +18 -14
- package/src/utils.ts +13 -3
package/src/populate.test.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
-
import mongoose, {Schema} from "mongoose";
|
|
2
|
+
import mongoose, {type Document, type HydratedDocument, Schema} from "mongoose";
|
|
3
3
|
|
|
4
4
|
import {fixMixedFields, getOpenApiSpecForModel, unpopulate} from "./populate";
|
|
5
|
-
import {FoodModel, setupDb, UserModel} from "./tests";
|
|
5
|
+
import {FoodModel, setupDb, type User, UserModel} from "./tests";
|
|
6
6
|
|
|
7
7
|
describe("populate functions", () => {
|
|
8
|
-
let admin:
|
|
9
|
-
let notAdmin:
|
|
8
|
+
let admin: HydratedDocument<User>;
|
|
9
|
+
let notAdmin: HydratedDocument<User>;
|
|
10
10
|
|
|
11
|
+
// noExplicitAny: typing as HydratedDocument<Food> causes cascading errors on populated field access patterns (e.g. populated.ownerId.name)
|
|
11
12
|
let spinach: any;
|
|
12
13
|
|
|
13
14
|
beforeEach(async () => {
|
|
@@ -44,6 +45,7 @@ describe("populate functions", () => {
|
|
|
44
45
|
expect(populated.likesIds[1].userId.id).toBe(notAdmin.id);
|
|
45
46
|
expect(populated.likesIds[1].userId.name).toBe("Not Admin");
|
|
46
47
|
|
|
48
|
+
// noExplicitAny: unpopulate returns Document<T> which doesn't expose model properties; would require refactoring the return type
|
|
47
49
|
let unpopulated: any = unpopulate(populated, "ownerId");
|
|
48
50
|
expect(spinach.ownerId.name).toBeUndefined();
|
|
49
51
|
expect(unpopulated.ownerId.toString()).toBe(admin.id);
|
|
@@ -68,7 +70,7 @@ describe("populate functions", () => {
|
|
|
68
70
|
describe("unpopulate edge cases", () => {
|
|
69
71
|
it("throws error when path is empty", () => {
|
|
70
72
|
const doc = {name: "test"};
|
|
71
|
-
expect(() => unpopulate(doc as
|
|
73
|
+
expect(() => unpopulate(doc as unknown as Document<unknown>, "")).toThrow("path is required");
|
|
72
74
|
});
|
|
73
75
|
|
|
74
76
|
it("unpopulates single populated field", () => {
|
|
@@ -76,7 +78,9 @@ describe("unpopulate edge cases", () => {
|
|
|
76
78
|
name: "test",
|
|
77
79
|
ownerId: {_id: "owner-123", email: "owner@test.com"},
|
|
78
80
|
};
|
|
79
|
-
const result = unpopulate(doc as
|
|
81
|
+
const result = unpopulate(doc as unknown as Document<unknown>, "ownerId") as unknown as {
|
|
82
|
+
ownerId: string;
|
|
83
|
+
};
|
|
80
84
|
expect(result.ownerId).toBe("owner-123");
|
|
81
85
|
});
|
|
82
86
|
|
|
@@ -85,7 +89,9 @@ describe("unpopulate edge cases", () => {
|
|
|
85
89
|
items: [{_id: "item-1", name: "Item 1"}, {_id: "item-2", name: "Item 2"}, "item-3"],
|
|
86
90
|
name: "test",
|
|
87
91
|
};
|
|
88
|
-
const result = unpopulate(doc as
|
|
92
|
+
const result = unpopulate(doc as unknown as Document<unknown>, "items") as unknown as {
|
|
93
|
+
items: string[];
|
|
94
|
+
};
|
|
89
95
|
expect(result.items).toEqual(["item-1", "item-2", "item-3"]);
|
|
90
96
|
});
|
|
91
97
|
|
|
@@ -99,13 +105,17 @@ describe("unpopulate edge cases", () => {
|
|
|
99
105
|
],
|
|
100
106
|
},
|
|
101
107
|
};
|
|
102
|
-
const result = unpopulate(doc as
|
|
108
|
+
const result = unpopulate(doc as unknown as Document<unknown>, "nested.items") as unknown as {
|
|
109
|
+
nested: {items: string[]};
|
|
110
|
+
};
|
|
103
111
|
expect(result.nested.items).toEqual(["item-1", "item-2"]);
|
|
104
112
|
});
|
|
105
113
|
|
|
106
114
|
it("returns original doc when path does not exist", () => {
|
|
107
115
|
const doc = {name: "test"};
|
|
108
|
-
const result = unpopulate(doc as
|
|
116
|
+
const result = unpopulate(doc as unknown as Document<unknown>, "nonexistent") as unknown as {
|
|
117
|
+
name: string;
|
|
118
|
+
};
|
|
109
119
|
expect(result).toEqual(doc);
|
|
110
120
|
});
|
|
111
121
|
|
|
@@ -117,7 +127,10 @@ describe("unpopulate edge cases", () => {
|
|
|
117
127
|
],
|
|
118
128
|
name: "test",
|
|
119
129
|
};
|
|
120
|
-
const result = unpopulate(
|
|
130
|
+
const result = unpopulate(
|
|
131
|
+
doc as unknown as Document<unknown>,
|
|
132
|
+
"containers.items"
|
|
133
|
+
) as unknown as {containers: {items: string[]}[]};
|
|
121
134
|
expect(result.containers[0].items).toEqual(["item-1", "item-2"]);
|
|
122
135
|
expect(result.containers[1].items).toEqual(["item-3", "item-4"]);
|
|
123
136
|
});
|
|
@@ -131,12 +144,14 @@ describe("fixMixedFields", () => {
|
|
|
131
144
|
|
|
132
145
|
it("returns early when properties is missing", () => {
|
|
133
146
|
const schema = new Schema({});
|
|
134
|
-
expect(() => fixMixedFields(schema, null as
|
|
147
|
+
expect(() => fixMixedFields(schema, null as unknown as Record<string, unknown>)).not.toThrow();
|
|
135
148
|
});
|
|
136
149
|
|
|
137
150
|
it("replaces Mixed fields with only description", () => {
|
|
138
151
|
const schema = new Schema({data: {description: "any data", type: Schema.Types.Mixed}});
|
|
139
|
-
const properties:
|
|
152
|
+
const properties: Record<string, Record<string, unknown>> = {
|
|
153
|
+
data: {description: "any data", type: "object"},
|
|
154
|
+
};
|
|
140
155
|
fixMixedFields(schema, properties);
|
|
141
156
|
expect(properties.data).toEqual({description: "any data"});
|
|
142
157
|
});
|
|
@@ -144,14 +159,14 @@ describe("fixMixedFields", () => {
|
|
|
144
159
|
it("recurses into arrays of sub-documents", () => {
|
|
145
160
|
const subSchema = new Schema({meta: {type: Schema.Types.Mixed}});
|
|
146
161
|
const schema = new Schema({items: [subSchema]});
|
|
147
|
-
const properties
|
|
162
|
+
const properties = {
|
|
148
163
|
items: {
|
|
149
164
|
items: {
|
|
150
165
|
properties: {
|
|
151
|
-
meta: {type: "object"},
|
|
166
|
+
meta: {type: "object"} as Record<string, unknown>,
|
|
152
167
|
},
|
|
153
168
|
},
|
|
154
|
-
type: "array",
|
|
169
|
+
type: "array" as const,
|
|
155
170
|
},
|
|
156
171
|
};
|
|
157
172
|
fixMixedFields(schema, properties);
|
|
@@ -211,7 +226,7 @@ describe("getOpenApiSpecForModel edge cases", () => {
|
|
|
211
226
|
populatePaths: [{path: "eatenBy"}],
|
|
212
227
|
});
|
|
213
228
|
expect(result.properties.eatenBy).toBeDefined();
|
|
214
|
-
expect((result.properties.eatenBy as
|
|
229
|
+
expect((result.properties.eatenBy as Record<string, unknown>).items).toBeDefined();
|
|
215
230
|
});
|
|
216
231
|
|
|
217
232
|
it("populates nested ref in sub-schema (likesIds.userId)", () => {
|
|
@@ -224,7 +239,7 @@ describe("getOpenApiSpecForModel edge cases", () => {
|
|
|
224
239
|
it("includes virtuals from model schema", () => {
|
|
225
240
|
const result = getOpenApiSpecForModel(FoodModel);
|
|
226
241
|
expect(result.properties.description).toBeDefined();
|
|
227
|
-
expect((result.properties.description as
|
|
242
|
+
expect((result.properties.description as Record<string, unknown>).type).toBe("any");
|
|
228
243
|
});
|
|
229
244
|
|
|
230
245
|
it("includes virtuals from child schemas", () => {
|
|
@@ -240,7 +255,10 @@ describe("getOpenApiSpecForModel edge cases", () => {
|
|
|
240
255
|
mongoose.models.ParentWithChildVirtual ||
|
|
241
256
|
mongoose.model("ParentWithChildVirtual", parentSchema);
|
|
242
257
|
const result = getOpenApiSpecForModel(ParentModel);
|
|
243
|
-
const detail = result.properties.detail as
|
|
258
|
+
const detail = result.properties.detail as Record<
|
|
259
|
+
string,
|
|
260
|
+
Record<string, Record<string, unknown>>
|
|
261
|
+
>;
|
|
244
262
|
expect(detail.properties.displayAmount).toBeDefined();
|
|
245
263
|
expect(detail.properties.displayAmount.type).toBe("any");
|
|
246
264
|
});
|
|
@@ -251,7 +269,10 @@ describe("filterKeys (via getOpenApiSpecForModel populatePaths)", () => {
|
|
|
251
269
|
const result = getOpenApiSpecForModel(FoodModel, {
|
|
252
270
|
populatePaths: [{fields: ["name.nested"], path: "ownerId"}],
|
|
253
271
|
});
|
|
254
|
-
const ownerProps = (result.properties.ownerId as
|
|
272
|
+
const ownerProps = (result.properties.ownerId as Record<string, unknown>).properties as Record<
|
|
273
|
+
string,
|
|
274
|
+
unknown
|
|
275
|
+
>;
|
|
255
276
|
expect(ownerProps.name).toBeDefined();
|
|
256
277
|
expect(typeof ownerProps.name).toBe("object");
|
|
257
278
|
});
|
|
@@ -261,8 +282,12 @@ describe("filterKeys (via getOpenApiSpecForModel populatePaths)", () => {
|
|
|
261
282
|
populatePaths: [{fields: ["__proto__.polluted"], path: "ownerId"}],
|
|
262
283
|
});
|
|
263
284
|
expect(result.properties).toBeDefined();
|
|
285
|
+
// noExplicitAny: testing that prototype pollution did not add a 'polluted' property to Object.prototype
|
|
264
286
|
expect((Object.prototype as any).polluted).toBeUndefined();
|
|
265
|
-
const ownerProps = (result.properties.ownerId as
|
|
287
|
+
const ownerProps = (result.properties.ownerId as Record<string, unknown>).properties as Record<
|
|
288
|
+
string,
|
|
289
|
+
unknown
|
|
290
|
+
>;
|
|
266
291
|
expect(ownerProps).toBeDefined();
|
|
267
292
|
expect(Object.keys(ownerProps)).not.toContain("__proto__");
|
|
268
293
|
});
|
package/src/scriptRunner.ts
CHANGED
|
@@ -86,7 +86,7 @@ const progressSchema = new Schema(
|
|
|
86
86
|
percentage: {description: "Progress percentage from 0 to 100", max: 100, min: 0, type: Number},
|
|
87
87
|
stage: {description: "Current stage of the task", type: String},
|
|
88
88
|
},
|
|
89
|
-
{_id: false}
|
|
89
|
+
{_id: false, strict: "throw"}
|
|
90
90
|
);
|
|
91
91
|
|
|
92
92
|
const logSchema = new Schema(
|
|
@@ -100,7 +100,7 @@ const logSchema = new Schema(
|
|
|
100
100
|
message: {description: "Log message", required: true, type: String},
|
|
101
101
|
timestamp: {description: "When this log entry was created", required: true, type: Date},
|
|
102
102
|
},
|
|
103
|
-
{_id: false}
|
|
103
|
+
{_id: false, strict: "throw"}
|
|
104
104
|
);
|
|
105
105
|
|
|
106
106
|
const backgroundTaskSchema = new Schema<
|
package/src/secretProviders.ts
CHANGED
|
@@ -86,9 +86,10 @@ export class GcpSecretProvider implements SecretProvider {
|
|
|
86
86
|
const SecretManagerServiceClient =
|
|
87
87
|
mod.SecretManagerServiceClient ?? mod.default?.SecretManagerServiceClient;
|
|
88
88
|
if (!SecretManagerServiceClient) {
|
|
89
|
-
throw new
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
throw new APIError({
|
|
90
|
+
status: 500,
|
|
91
|
+
title: "SecretManagerServiceClient not found in @google-cloud/secret-manager module",
|
|
92
|
+
});
|
|
92
93
|
}
|
|
93
94
|
this.client = new SecretManagerServiceClient();
|
|
94
95
|
}
|
package/src/tests.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express, {type Express} from "express";
|
|
2
2
|
import mongoose, {type Model, model, Schema} from "mongoose";
|
|
3
|
-
import passportLocalMongoose from "passport-local-mongoose";
|
|
3
|
+
import passportLocalMongoose, {type PassportLocalMongooseDocument} from "passport-local-mongoose";
|
|
4
4
|
import qs from "qs";
|
|
5
5
|
import supertest from "supertest";
|
|
6
6
|
import type TestAgent from "supertest/lib/agent";
|
|
@@ -62,19 +62,22 @@ const userSchema = new Schema<User>({
|
|
|
62
62
|
username: {description: "The user's username", type: String},
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
userSchema.plugin(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
userSchema.plugin(
|
|
66
|
+
passportLocalMongoose as unknown as (schema: Schema, options?: Record<string, unknown>) => void,
|
|
67
|
+
{
|
|
68
|
+
attemptsField: "attempts",
|
|
69
|
+
interval: process.env.NODE_ENV === "test" ? 1 : 100,
|
|
70
|
+
limitAttempts: true,
|
|
71
|
+
maxAttempts: 3,
|
|
72
|
+
maxInterval: process.env.NODE_ENV === "test" ? 1 : 300000,
|
|
73
|
+
usernameCaseInsensitive: true,
|
|
74
|
+
usernameField: "email",
|
|
75
|
+
}
|
|
76
|
+
);
|
|
74
77
|
// userSchema.plugin(tokenPlugin);
|
|
75
78
|
userSchema.plugin(createdUpdatedPlugin);
|
|
76
79
|
userSchema.plugin(isDisabledPlugin);
|
|
77
|
-
userSchema.methods.postCreate = async function (body:
|
|
80
|
+
userSchema.methods.postCreate = async function (body: {age?: number}) {
|
|
78
81
|
this.age = body.age;
|
|
79
82
|
return this.save();
|
|
80
83
|
};
|
|
@@ -121,6 +124,7 @@ const foodSchema = new Schema<Food>(
|
|
|
121
124
|
type: Schema.Types.ObjectId,
|
|
122
125
|
},
|
|
123
126
|
],
|
|
127
|
+
// noExplicitAny: DateOnly is a custom SchemaType not recognized by Mongoose's built-in type definitions
|
|
124
128
|
expiration: {description: "Expiration date of the food", type: DateOnly as any},
|
|
125
129
|
hidden: {
|
|
126
130
|
default: false,
|
|
@@ -221,13 +225,13 @@ export const setupDb = async () => {
|
|
|
221
225
|
UserModel.create({admin: true, email: "admin@example.com", name: "Admin"}),
|
|
222
226
|
UserModel.create({admin: true, email: "admin+other@example.com", name: "Admin Other"}),
|
|
223
227
|
]);
|
|
224
|
-
await (notAdmin as
|
|
228
|
+
await (notAdmin as unknown as PassportLocalMongooseDocument).setPassword("password");
|
|
225
229
|
await notAdmin.save();
|
|
226
230
|
|
|
227
|
-
await (admin as
|
|
231
|
+
await (admin as unknown as PassportLocalMongooseDocument).setPassword("securePassword");
|
|
228
232
|
await admin.save();
|
|
229
233
|
|
|
230
|
-
await (adminOther as
|
|
234
|
+
await (adminOther as unknown as PassportLocalMongooseDocument).setPassword("otherPassword");
|
|
231
235
|
|
|
232
236
|
await adminOther.save();
|
|
233
237
|
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import mongoose, {Types} from "mongoose";
|
|
2
2
|
|
|
3
|
+
import {APIError} from "./errors";
|
|
3
4
|
import {logger} from "./logger";
|
|
4
5
|
|
|
5
6
|
// A better version of mongoose's ObjectId.isValid,
|
|
@@ -31,17 +32,26 @@ export const checkModelsStrict = (ignoredModels: string[] = []): void => {
|
|
|
31
32
|
const schema = mongoose.model(model).schema;
|
|
32
33
|
|
|
33
34
|
if (schema.get("toObject")?.virtuals !== true) {
|
|
34
|
-
throw new
|
|
35
|
+
throw new APIError({
|
|
36
|
+
status: 500,
|
|
37
|
+
title: `Model ${model} toObject.virtuals not set to true`,
|
|
38
|
+
});
|
|
35
39
|
}
|
|
36
40
|
if (schema.get("toJSON")?.virtuals !== true) {
|
|
37
|
-
throw new
|
|
41
|
+
throw new APIError({
|
|
42
|
+
status: 500,
|
|
43
|
+
title: `Model ${model} toJSON.virtuals not set to true`,
|
|
44
|
+
});
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
if (ignoredModels.includes(model)) {
|
|
41
48
|
continue;
|
|
42
49
|
}
|
|
43
50
|
if (schema.get("strict") !== "throw") {
|
|
44
|
-
throw new
|
|
51
|
+
throw new APIError({
|
|
52
|
+
status: 500,
|
|
53
|
+
title: `Model ${model} is not set to strict mode.`,
|
|
54
|
+
});
|
|
45
55
|
}
|
|
46
56
|
}
|
|
47
57
|
};
|