@terreno/api 0.0.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/LICENSE +202 -0
- package/README.md +170 -0
- package/biome.jsonc +22 -0
- package/bunfig.toml +4 -0
- package/dist/api.d.ts +227 -0
- package/dist/api.js +1024 -0
- package/dist/api.test.d.ts +1 -0
- package/dist/api.test.js +2143 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +512 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +778 -0
- package/dist/errors.d.ts +75 -0
- package/dist/errors.js +216 -0
- package/dist/example.d.ts +1 -0
- package/dist/example.js +118 -0
- package/dist/expressServer.d.ts +35 -0
- package/dist/expressServer.js +436 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +30 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.js +249 -0
- package/dist/middleware.d.ts +10 -0
- package/dist/middleware.js +52 -0
- package/dist/notifiers/googleChatNotifier.d.ts +5 -0
- package/dist/notifiers/googleChatNotifier.js +130 -0
- package/dist/notifiers/googleChatNotifier.test.d.ts +1 -0
- package/dist/notifiers/googleChatNotifier.test.js +260 -0
- package/dist/notifiers/index.d.ts +3 -0
- package/dist/notifiers/index.js +19 -0
- package/dist/notifiers/slackNotifier.d.ts +5 -0
- package/dist/notifiers/slackNotifier.js +130 -0
- package/dist/notifiers/slackNotifier.test.d.ts +1 -0
- package/dist/notifiers/slackNotifier.test.js +259 -0
- package/dist/notifiers/zoomNotifier.d.ts +34 -0
- package/dist/notifiers/zoomNotifier.js +181 -0
- package/dist/notifiers/zoomNotifier.test.d.ts +1 -0
- package/dist/notifiers/zoomNotifier.test.js +370 -0
- package/dist/openApi.d.ts +60 -0
- package/dist/openApi.js +441 -0
- package/dist/openApi.test.d.ts +1 -0
- package/dist/openApi.test.js +445 -0
- package/dist/openApiBuilder.d.ts +419 -0
- package/dist/openApiBuilder.js +424 -0
- package/dist/openApiBuilder.test.d.ts +1 -0
- package/dist/openApiBuilder.test.js +509 -0
- package/dist/openApiEtag.d.ts +7 -0
- package/dist/openApiEtag.js +38 -0
- package/dist/permissions.d.ts +26 -0
- package/dist/permissions.js +331 -0
- package/dist/permissions.test.d.ts +1 -0
- package/dist/permissions.test.js +413 -0
- package/dist/plugins.d.ts +67 -0
- package/dist/plugins.js +315 -0
- package/dist/plugins.test.d.ts +1 -0
- package/dist/plugins.test.js +639 -0
- package/dist/populate.d.ts +14 -0
- package/dist/populate.js +315 -0
- package/dist/populate.test.d.ts +1 -0
- package/dist/populate.test.js +133 -0
- package/dist/response.d.ts +0 -0
- package/dist/response.js +1 -0
- package/dist/tests/bunSetup.d.ts +1 -0
- package/dist/tests/bunSetup.js +297 -0
- package/dist/tests/index.d.ts +1 -0
- package/dist/tests/index.js +17 -0
- package/dist/tests.d.ts +99 -0
- package/dist/tests.js +273 -0
- package/dist/transformers.d.ts +25 -0
- package/dist/transformers.js +217 -0
- package/dist/transformers.test.d.ts +1 -0
- package/dist/transformers.test.js +370 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +143 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +14 -0
- package/index.ts +1 -0
- package/package.json +88 -0
- package/src/__snapshots__/openApi.test.ts.snap +4814 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1485 -0
- package/src/api.test.ts +1661 -0
- package/src/api.ts +1036 -0
- package/src/auth.test.ts +550 -0
- package/src/auth.ts +408 -0
- package/src/errors.ts +225 -0
- package/src/example.ts +99 -0
- package/src/express.d.ts +5 -0
- package/src/expressServer.ts +387 -0
- package/src/index.ts +14 -0
- package/src/logger.ts +190 -0
- package/src/middleware.ts +18 -0
- package/src/notifiers/googleChatNotifier.test.ts +114 -0
- package/src/notifiers/googleChatNotifier.ts +47 -0
- package/src/notifiers/index.ts +3 -0
- package/src/notifiers/slackNotifier.test.ts +113 -0
- package/src/notifiers/slackNotifier.ts +55 -0
- package/src/notifiers/zoomNotifier.test.ts +207 -0
- package/src/notifiers/zoomNotifier.ts +111 -0
- package/src/openApi.test.ts +331 -0
- package/src/openApi.ts +494 -0
- package/src/openApiBuilder.test.ts +442 -0
- package/src/openApiBuilder.ts +636 -0
- package/src/openApiEtag.ts +40 -0
- package/src/permissions.test.ts +219 -0
- package/src/permissions.ts +228 -0
- package/src/plugins.test.ts +390 -0
- package/src/plugins.ts +289 -0
- package/src/populate.test.ts +65 -0
- package/src/populate.ts +258 -0
- package/src/response.ts +0 -0
- package/src/tests/bunSetup.ts +234 -0
- package/src/tests/index.ts +1 -0
- package/src/tests.ts +218 -0
- package/src/transformers.test.ts +202 -0
- package/src/transformers.ts +170 -0
- package/src/utils.test.ts +14 -0
- package/src/utils.ts +47 -0
- package/tsconfig.json +60 -0
- package/types.d.ts +17 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {unpopulate} from "./populate";
|
|
4
|
+
import {FoodModel, setupDb} from "./tests";
|
|
5
|
+
|
|
6
|
+
describe("populate functions", () => {
|
|
7
|
+
let admin: any;
|
|
8
|
+
let notAdmin: any;
|
|
9
|
+
|
|
10
|
+
let spinach: any;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
[admin, notAdmin] = await setupDb();
|
|
14
|
+
|
|
15
|
+
[spinach] = await Promise.all([
|
|
16
|
+
FoodModel.create({
|
|
17
|
+
calories: 1,
|
|
18
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
19
|
+
eatenBy: [admin._id],
|
|
20
|
+
hidden: false,
|
|
21
|
+
likesIds: [
|
|
22
|
+
{likes: true, userId: admin._id},
|
|
23
|
+
{likes: false, userId: notAdmin._id},
|
|
24
|
+
],
|
|
25
|
+
name: "Spinach",
|
|
26
|
+
ownerId: admin._id,
|
|
27
|
+
source: {
|
|
28
|
+
name: "Brand",
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("unpopulate", async () => {
|
|
35
|
+
let populated = await spinach.populate("ownerId");
|
|
36
|
+
populated = await populated.populate("eatenBy");
|
|
37
|
+
populated = await populated.populate("likesIds.userId");
|
|
38
|
+
expect(populated.ownerId.name).toBe("Admin");
|
|
39
|
+
expect(populated.eatenBy[0].id).toBe(admin.id);
|
|
40
|
+
expect(populated.eatenBy[0].name).toBe("Admin");
|
|
41
|
+
expect(populated.likesIds[0].userId.id).toBe(admin.id);
|
|
42
|
+
expect(populated.likesIds[0].userId.name).toBe("Admin");
|
|
43
|
+
expect(populated.likesIds[1].userId.id).toBe(notAdmin.id);
|
|
44
|
+
expect(populated.likesIds[1].userId.name).toBe("Not Admin");
|
|
45
|
+
|
|
46
|
+
let unpopulated: any = unpopulate(populated, "ownerId");
|
|
47
|
+
expect(spinach.ownerId.name).toBeUndefined();
|
|
48
|
+
expect(unpopulated.ownerId.toString()).toBe(admin.id);
|
|
49
|
+
// Ensure nothing else was touched.
|
|
50
|
+
expect(populated.likesIds[0].userId.id).toBe(admin.id);
|
|
51
|
+
expect(populated.likesIds[0].userId.name).toBe("Admin");
|
|
52
|
+
expect(populated.likesIds[1].userId.id).toBe(notAdmin.id);
|
|
53
|
+
expect(populated.likesIds[1].userId.name).toBe("Not Admin");
|
|
54
|
+
|
|
55
|
+
unpopulated = unpopulate(populated, "eatenBy");
|
|
56
|
+
expect(populated.eatenBy.toString()).toBe(admin.id);
|
|
57
|
+
expect(populated.eatenBy[0]?.name).toBeUndefined();
|
|
58
|
+
|
|
59
|
+
unpopulated = unpopulate(populated, "likesIds.userId");
|
|
60
|
+
expect(populated.likesIds[0].userId.toString()).toBe(admin.id);
|
|
61
|
+
expect(populated.likesIds[0].userId?.name).toBeUndefined();
|
|
62
|
+
expect(populated.likesIds[1].userId.toString()).toBe(notAdmin.id);
|
|
63
|
+
expect(populated.likesIds[1].userId.name).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/populate.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import isArray from "lodash/isArray";
|
|
2
|
+
import type {Document} from "mongoose";
|
|
3
|
+
import m2s from "mongoose-to-swagger";
|
|
4
|
+
|
|
5
|
+
import {APIError} from "./errors";
|
|
6
|
+
|
|
7
|
+
const m2sOptions = {
|
|
8
|
+
props: ["readOnly", "required", "enum", "default"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type PopulatePath = {
|
|
12
|
+
// Mongoose style path population.
|
|
13
|
+
// "ownerId" // populates the User that matches `ownerId`
|
|
14
|
+
// "ownerId.organizationId" Nested. Populates the User that matches `ownerId`, as well as their organization.
|
|
15
|
+
path: string;
|
|
16
|
+
// If provided, type generation will use the already registered component.
|
|
17
|
+
// If not provided and path is provided, will use the path and optionally fields to
|
|
18
|
+
// automatically generate the types. If only generatePathFields is provided, the type will be
|
|
19
|
+
// any.
|
|
20
|
+
openApiComponent?: any;
|
|
21
|
+
// An array of strings to filter on the populated objects, following Mongoose's select
|
|
22
|
+
// rules. If each field starts a preceding "-", will act as a block list and only remove those
|
|
23
|
+
// fields. If each field does not start with a "-", will act as an allow list and only
|
|
24
|
+
// return those fields.
|
|
25
|
+
fields?: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// This function filters an object to only include specified keys.
|
|
29
|
+
// It supports nested keys using dot notation (e.g., 'user.name').
|
|
30
|
+
// If no keys are provided, it returns the original object.
|
|
31
|
+
// The function recursively traverses the object structure to handle nested properties.
|
|
32
|
+
const filterKeys = (obj: Record<string, any>, keysToKeep?: string[]): Record<string, any> => {
|
|
33
|
+
if (!keysToKeep) {
|
|
34
|
+
return obj;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result: Record<string, any> = {};
|
|
38
|
+
|
|
39
|
+
const filterNestedKeys = (
|
|
40
|
+
currentObj: Record<string, any>,
|
|
41
|
+
currentResult: Record<string, any>,
|
|
42
|
+
remainingKeys: string[]
|
|
43
|
+
) => {
|
|
44
|
+
const currentKey = remainingKeys[0];
|
|
45
|
+
const nestedKeys = currentKey.split(".");
|
|
46
|
+
|
|
47
|
+
if (nestedKeys.length > 1) {
|
|
48
|
+
const [firstKey, ...rest] = nestedKeys;
|
|
49
|
+
if (firstKey === "__proto__" || firstKey === "constructor" || firstKey === "prototype") {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!currentResult[firstKey]) {
|
|
53
|
+
currentResult[firstKey] = {};
|
|
54
|
+
}
|
|
55
|
+
filterNestedKeys(currentObj[firstKey], currentResult[firstKey], [
|
|
56
|
+
rest.join("."),
|
|
57
|
+
...remainingKeys.slice(1),
|
|
58
|
+
]);
|
|
59
|
+
} else {
|
|
60
|
+
// biome-ignore lint/suspicious/noPrototypeBuiltins: we need to use the prototype to check if the object has the property
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(currentObj, currentKey)) {
|
|
62
|
+
currentResult[currentKey] = currentObj[currentKey];
|
|
63
|
+
}
|
|
64
|
+
if (remainingKeys.length > 1) {
|
|
65
|
+
filterNestedKeys(currentObj, currentResult, remainingKeys.slice(1));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
filterNestedKeys(obj, result, keysToKeep);
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Helper function to get the path in the OpenAPI schema, so we can swap out the type for the
|
|
75
|
+
// populated model component or generated type.
|
|
76
|
+
function getPathInSchema(schema: any, path: string): string {
|
|
77
|
+
const keys = path.split(".");
|
|
78
|
+
let currentSchema = schema;
|
|
79
|
+
let fullPath = "";
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < keys.length; i++) {
|
|
82
|
+
const key = keys[i];
|
|
83
|
+
|
|
84
|
+
if (currentSchema.properties?.[key]) {
|
|
85
|
+
fullPath += fullPath ? `.${key}` : key;
|
|
86
|
+
currentSchema = currentSchema.properties[key];
|
|
87
|
+
|
|
88
|
+
// If it's an array, add 'items' to the path
|
|
89
|
+
if (currentSchema.type === "array" && currentSchema.items) {
|
|
90
|
+
fullPath += ".items";
|
|
91
|
+
currentSchema = currentSchema.items;
|
|
92
|
+
}
|
|
93
|
+
} else if (i === keys.length - 1 && currentSchema.type === "array") {
|
|
94
|
+
// If we're at the last key and it's an array, we don't need to add anything
|
|
95
|
+
break;
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error(`Path ${path} not found in schema at key ${key}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return fullPath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Replaces populated properties with the populated schema.
|
|
105
|
+
export function getOpenApiSpecForModel(
|
|
106
|
+
model: any,
|
|
107
|
+
{
|
|
108
|
+
populatePaths,
|
|
109
|
+
extraModelProperties,
|
|
110
|
+
}: {populatePaths?: PopulatePath[]; extraModelProperties?: any} = {}
|
|
111
|
+
): {properties: any; required: string[]} {
|
|
112
|
+
const modelSwagger = m2s(model, {
|
|
113
|
+
props: ["required", "enum"],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (populatePaths && isArray(populatePaths)) {
|
|
117
|
+
for (const populatePath of populatePaths) {
|
|
118
|
+
// Get the referenced populate model from the model schema
|
|
119
|
+
let populateModel = model.schema.path(populatePath.path)?.options?.ref;
|
|
120
|
+
const populatePathIsArray = Array.isArray(model.schema.path(populatePath.path).options.type);
|
|
121
|
+
if (populatePathIsArray) {
|
|
122
|
+
populateModel = model.schema.path(populatePath.path).options.type[0].ref;
|
|
123
|
+
}
|
|
124
|
+
if (!populateModel) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get the properties of the referenced model
|
|
129
|
+
const properties = filterKeys(
|
|
130
|
+
m2s(model.db.model(populateModel), m2sOptions).properties,
|
|
131
|
+
populatePath.fields
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Get the OpenAPI path for the current populate path
|
|
135
|
+
const openApiPath = getPathInSchema(modelSwagger, populatePath.path);
|
|
136
|
+
|
|
137
|
+
// Determine the schema to set
|
|
138
|
+
let schemaToSet;
|
|
139
|
+
if (populatePath.openApiComponent) {
|
|
140
|
+
schemaToSet = {
|
|
141
|
+
$ref: `#/components/schemas/${populatePath.openApiComponent}`,
|
|
142
|
+
};
|
|
143
|
+
} else {
|
|
144
|
+
schemaToSet = {
|
|
145
|
+
properties,
|
|
146
|
+
type: "object",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Navigate through the nested structure and set the schema
|
|
151
|
+
const pathParts = openApiPath.split(".");
|
|
152
|
+
let currentSchema = modelSwagger.properties;
|
|
153
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
154
|
+
const part = pathParts[i];
|
|
155
|
+
if (i === pathParts.length - 1) {
|
|
156
|
+
// We're at the last part, merge the schema
|
|
157
|
+
if (currentSchema[part]?.properties) {
|
|
158
|
+
currentSchema[part].properties = {
|
|
159
|
+
...currentSchema[part].properties,
|
|
160
|
+
...(schemaToSet.properties || {[part]: schemaToSet}),
|
|
161
|
+
};
|
|
162
|
+
} else {
|
|
163
|
+
currentSchema[part] = schemaToSet;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// We're still navigating, ensure the path exists
|
|
167
|
+
if (!currentSchema[part]) {
|
|
168
|
+
currentSchema[part] = {};
|
|
169
|
+
}
|
|
170
|
+
if (part === "items" && i < pathParts.length - 1) {
|
|
171
|
+
// If we're at 'items' and it's not the last part, it should be an object
|
|
172
|
+
if (!currentSchema[part].properties) {
|
|
173
|
+
currentSchema[part] = {properties: {}, type: "object"};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
currentSchema = currentSchema[part].properties || currentSchema[part];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add virtuals to the modelSwagger property
|
|
183
|
+
for (const virtual of Object.keys(model.schema.virtuals)) {
|
|
184
|
+
// Skip Mongoose internals
|
|
185
|
+
if (virtual === "id" || virtual === "__v") {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
modelSwagger.properties[virtual] = {
|
|
189
|
+
type: "any",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check subschemas for virtuals (one level deep)
|
|
194
|
+
if (model.schema.childSchemas.length > 0) {
|
|
195
|
+
for (const childSchema of model.schema.childSchemas) {
|
|
196
|
+
for (const virtual of Object.keys(childSchema.schema.virtuals)) {
|
|
197
|
+
if (virtual === "id" || virtual === "__v") {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
modelSwagger.properties[childSchema.model.path].properties[virtual] = {
|
|
201
|
+
type: "any",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
properties: {...modelSwagger.properties, ...extraModelProperties},
|
|
209
|
+
required: modelSwagger.required ?? [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Helper function to unpopulate a document that has been populated.
|
|
214
|
+
// This is helpful for supporting backwards compatibility. E.g. you use populatePaths
|
|
215
|
+
// to populate a document but if the version header for the request is below the version
|
|
216
|
+
// that the populatePath was added, we remove the population and just return the _id.
|
|
217
|
+
export function unpopulate<T>(doc: Document<T>, path: string): Document<T> {
|
|
218
|
+
if (!path) {
|
|
219
|
+
throw new APIError({status: 500, title: "path is required for unpopulate"});
|
|
220
|
+
}
|
|
221
|
+
const pathParts = path.split(".");
|
|
222
|
+
|
|
223
|
+
// Recursive because we need to support nested paths.
|
|
224
|
+
const recursiveUnpopulate = (current: any, parts: string[]): any => {
|
|
225
|
+
const part = parts[0];
|
|
226
|
+
|
|
227
|
+
// If the path doesn't exist, return the original doc
|
|
228
|
+
if (!current[part]) {
|
|
229
|
+
return doc;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (parts.length === 1) {
|
|
233
|
+
// Base case: we've reached the last part of the path
|
|
234
|
+
if (Array.isArray(current[part])) {
|
|
235
|
+
// If the field is an array, recursively unpopulate each element
|
|
236
|
+
current[part] = current[part].map((item: any) => {
|
|
237
|
+
return item?._id ? item._id : item;
|
|
238
|
+
});
|
|
239
|
+
} else if (current[part]?._id) {
|
|
240
|
+
// If the field is a populated document, revert to _id
|
|
241
|
+
current[part] = current[part]._id;
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
// Recursive case: continue down the path
|
|
245
|
+
if (Array.isArray(current[part])) {
|
|
246
|
+
for (const item of current[part]) {
|
|
247
|
+
recursiveUnpopulate(item, parts.slice(1)); // Recursively handle each item in the array
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
recursiveUnpopulate(current[part], parts.slice(1)); // Recursively handle the next part
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return current;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
return recursiveUnpopulate(doc, pathParts);
|
|
258
|
+
}
|
package/src/response.ts
ADDED
|
File without changes
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import {afterAll, afterEach, beforeAll, beforeEach, mock} from "bun:test";
|
|
2
|
+
import {Writable} from "node:stream";
|
|
3
|
+
import mongoose from "mongoose";
|
|
4
|
+
import winston from "winston";
|
|
5
|
+
|
|
6
|
+
import {setupEnvironment} from "../expressServer";
|
|
7
|
+
import {logger, winstonLogger} from "../logger";
|
|
8
|
+
|
|
9
|
+
// Connect to MongoDB once for all tests
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
await mongoose
|
|
12
|
+
.connect("mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000")
|
|
13
|
+
.catch(logger.catch);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Close MongoDB connection after all tests
|
|
17
|
+
afterAll(async () => {
|
|
18
|
+
await mongoose.connection.close();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let logs: string[] = [];
|
|
22
|
+
|
|
23
|
+
const SHOW_ALL_LOGS = process.env.SHOW_ALL_TEST_LOGS === "true";
|
|
24
|
+
|
|
25
|
+
// Create a custom stream that captures logs
|
|
26
|
+
const logStream = new Writable({
|
|
27
|
+
write(chunk: any, _encoding: any, callback: any) {
|
|
28
|
+
logs.push(chunk.toString());
|
|
29
|
+
if (SHOW_ALL_LOGS) {
|
|
30
|
+
process.stdout.write(chunk);
|
|
31
|
+
}
|
|
32
|
+
callback();
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Silence both winston loggers by replacing all transports with our capturing stream
|
|
37
|
+
const silentTransport = new winston.transports.Stream({
|
|
38
|
+
format: winston.format.simple(),
|
|
39
|
+
stream: logStream,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Clear and silence the default winston logger
|
|
43
|
+
winston.clear();
|
|
44
|
+
winston.add(silentTransport);
|
|
45
|
+
|
|
46
|
+
// Clear and silence the custom winstonLogger
|
|
47
|
+
winstonLogger.clear();
|
|
48
|
+
winstonLogger.add(silentTransport);
|
|
49
|
+
|
|
50
|
+
// Capture and silence console methods
|
|
51
|
+
const originalConsole = {
|
|
52
|
+
debug: console.debug,
|
|
53
|
+
error: console.error,
|
|
54
|
+
info: console.info,
|
|
55
|
+
// biome-ignore lint/suspicious/noConsole: We keep the original reference.
|
|
56
|
+
log: console.log,
|
|
57
|
+
warn: console.warn,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const captureConsoleMethod = (method: keyof typeof originalConsole): void => {
|
|
61
|
+
(console as any)[method] = (...args: any[]) => {
|
|
62
|
+
const logMessage = `[console.${method}] ${args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg))).join(" ")}`;
|
|
63
|
+
logs.push(logMessage);
|
|
64
|
+
if (SHOW_ALL_LOGS) {
|
|
65
|
+
originalConsole[method](...args);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
captureConsoleMethod("log");
|
|
71
|
+
captureConsoleMethod("info");
|
|
72
|
+
captureConsoleMethod("warn");
|
|
73
|
+
captureConsoleMethod("error");
|
|
74
|
+
captureConsoleMethod("debug");
|
|
75
|
+
|
|
76
|
+
// Setup before each test
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
process.env.TOKEN_SECRET = "secret";
|
|
79
|
+
process.env.TOKEN_ISSUER = "terreno-api.test";
|
|
80
|
+
process.env.SESSION_SECRET = "sessionSecret";
|
|
81
|
+
process.env.REFRESH_TOKEN_SECRET = "refreshTokenSecret";
|
|
82
|
+
setupEnvironment();
|
|
83
|
+
// Re-silence loggers after setupEnvironment which may reconfigure them
|
|
84
|
+
winston.clear();
|
|
85
|
+
winston.add(silentTransport);
|
|
86
|
+
winstonLogger.clear();
|
|
87
|
+
winstonLogger.add(silentTransport);
|
|
88
|
+
logs = [];
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Clear logs after each test
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
logs = [];
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Mock @sentry/node module
|
|
97
|
+
mock.module("@sentry/node", () => {
|
|
98
|
+
const mockFn = (): ReturnType<typeof mock> => mock(() => {});
|
|
99
|
+
|
|
100
|
+
// Mock Scope
|
|
101
|
+
const mockScope = {
|
|
102
|
+
addBreadcrumb: mockFn(),
|
|
103
|
+
clear: mockFn(),
|
|
104
|
+
getSpan: mockFn(),
|
|
105
|
+
setContext: mockFn(),
|
|
106
|
+
setFingerprint: mockFn(),
|
|
107
|
+
setLevel: mockFn(),
|
|
108
|
+
setSpan: mockFn(),
|
|
109
|
+
setTag: mockFn(),
|
|
110
|
+
setTags: mockFn(),
|
|
111
|
+
setTransactionName: mockFn(),
|
|
112
|
+
setUser: mockFn(),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Mock Hub
|
|
116
|
+
const mockClient = {
|
|
117
|
+
captureException: mockFn(),
|
|
118
|
+
captureMessage: mockFn(),
|
|
119
|
+
close: mock(() => Promise.resolve(true)),
|
|
120
|
+
flush: mock(() => Promise.resolve(true)),
|
|
121
|
+
getOptions: mock(() => ({})),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const mockHub = {
|
|
125
|
+
addBreadcrumb: mockFn(),
|
|
126
|
+
captureException: mockFn(),
|
|
127
|
+
captureMessage: mockFn(),
|
|
128
|
+
configureScope: mockFn(),
|
|
129
|
+
getClient: mock(() => mockClient),
|
|
130
|
+
getScope: mock(() => mockScope),
|
|
131
|
+
popScope: mockFn(),
|
|
132
|
+
pushScope: mockFn(),
|
|
133
|
+
setContext: mockFn(),
|
|
134
|
+
setTag: mockFn(),
|
|
135
|
+
setTags: mockFn(),
|
|
136
|
+
setUser: mockFn(),
|
|
137
|
+
withScope: mockFn(),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const mockSpan: any = {
|
|
141
|
+
finish: mockFn(),
|
|
142
|
+
setData: mockFn(),
|
|
143
|
+
setStatus: mockFn(),
|
|
144
|
+
setTag: mockFn(),
|
|
145
|
+
startChild: mockFn(),
|
|
146
|
+
toTraceparent: mock(() => "mock-trace-parent"),
|
|
147
|
+
};
|
|
148
|
+
mockSpan.startChild = mock(() => mockSpan);
|
|
149
|
+
|
|
150
|
+
const mockTransaction = {
|
|
151
|
+
finish: mockFn(),
|
|
152
|
+
setData: mockFn(),
|
|
153
|
+
setName: mockFn(),
|
|
154
|
+
setStatus: mockFn(),
|
|
155
|
+
setTag: mockFn(),
|
|
156
|
+
startChild: mock(() => mockSpan),
|
|
157
|
+
toTraceparent: mock(() => "mock-trace-parent"),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
addBreadcrumb: mockFn(),
|
|
162
|
+
captureException: mockFn(),
|
|
163
|
+
captureMessage: mockFn(),
|
|
164
|
+
clearScope: mockFn(),
|
|
165
|
+
close: mock(() => Promise.resolve(true)),
|
|
166
|
+
configureScope: mockFn(),
|
|
167
|
+
default: {
|
|
168
|
+
addBreadcrumb: mockFn(),
|
|
169
|
+
captureException: mockFn(),
|
|
170
|
+
captureMessage: mockFn(),
|
|
171
|
+
clearScope: mockFn(),
|
|
172
|
+
close: mock(() => Promise.resolve(true)),
|
|
173
|
+
configureScope: mockFn(),
|
|
174
|
+
flush: mock(() => Promise.resolve(true)),
|
|
175
|
+
getClient: mock(() => mockClient),
|
|
176
|
+
getCurrentHub: mock(() => mockHub),
|
|
177
|
+
getCurrentScope: mock(() => mockScope),
|
|
178
|
+
Handlers: {
|
|
179
|
+
errorHandler: mock(() => (err: any, _req: any, _res: any, next: any) => next(err)),
|
|
180
|
+
requestHandler: mock(() => (_req: any, _res: any, next: any) => next()),
|
|
181
|
+
tracingHandler: mock(() => (_req: any, _res: any, next: any) => next()),
|
|
182
|
+
},
|
|
183
|
+
init: mockFn(),
|
|
184
|
+
isInitialized: mock(() => true),
|
|
185
|
+
popScope: mockFn(),
|
|
186
|
+
pushScope: mockFn(),
|
|
187
|
+
Severity: {
|
|
188
|
+
Debug: "debug",
|
|
189
|
+
Error: "error",
|
|
190
|
+
Fatal: "fatal",
|
|
191
|
+
Info: "info",
|
|
192
|
+
Warning: "warning",
|
|
193
|
+
} as const,
|
|
194
|
+
setContext: mockFn(),
|
|
195
|
+
setFingerprint: mockFn(),
|
|
196
|
+
setLevel: mockFn(),
|
|
197
|
+
setTag: mockFn(),
|
|
198
|
+
setTags: mockFn(),
|
|
199
|
+
setUser: mockFn(),
|
|
200
|
+
setupExpressErrorHandler: mockFn(),
|
|
201
|
+
startTransaction: mock(() => mockTransaction),
|
|
202
|
+
withScope: mock((callback: any) => callback(mockScope)),
|
|
203
|
+
},
|
|
204
|
+
flush: mock(() => Promise.resolve(true)),
|
|
205
|
+
getClient: mock(() => mockClient),
|
|
206
|
+
getCurrentHub: mock(() => mockHub),
|
|
207
|
+
getCurrentScope: mock(() => mockScope),
|
|
208
|
+
Handlers: {
|
|
209
|
+
errorHandler: mock(() => (err: any, _req: any, _res: any, next: any) => next(err)),
|
|
210
|
+
requestHandler: mock(() => (_req: any, _res: any, next: any) => next()),
|
|
211
|
+
tracingHandler: mock(() => (_req: any, _res: any, next: any) => next()),
|
|
212
|
+
},
|
|
213
|
+
init: mockFn(),
|
|
214
|
+
isInitialized: mock(() => true),
|
|
215
|
+
popScope: mockFn(),
|
|
216
|
+
pushScope: mockFn(),
|
|
217
|
+
Severity: {
|
|
218
|
+
Debug: "debug",
|
|
219
|
+
Error: "error",
|
|
220
|
+
Fatal: "fatal",
|
|
221
|
+
Info: "info",
|
|
222
|
+
Warning: "warning",
|
|
223
|
+
} as const,
|
|
224
|
+
setContext: mockFn(),
|
|
225
|
+
setFingerprint: mockFn(),
|
|
226
|
+
setLevel: mockFn(),
|
|
227
|
+
setTag: mockFn(),
|
|
228
|
+
setTags: mockFn(),
|
|
229
|
+
setUser: mockFn(),
|
|
230
|
+
setupExpressErrorHandler: mockFn(),
|
|
231
|
+
startTransaction: mock(() => mockTransaction),
|
|
232
|
+
withScope: mock((callback: any) => callback(mockScope)),
|
|
233
|
+
};
|
|
234
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./bunSetup";
|