@terreno/api 0.20.2 → 0.22.0
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/.ai/guidelines/core.md +71 -0
- package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
- package/README.md +54 -1
- package/bunfig.toml +1 -1
- package/dist/__tests__/versionCheckPlugin.test.js +29 -7
- package/dist/actions.openApi.test.js +13 -11
- package/dist/api.js +98 -11
- package/dist/api.query.test.js +31 -1
- package/dist/api.test.js +211 -0
- package/dist/auth.test.js +418 -43
- package/dist/betterAuth.d.ts +1 -1
- package/dist/consentApp.test.js +1 -0
- package/dist/example.js +4 -4
- package/dist/expressServer.d.ts +0 -22
- package/dist/expressServer.js +1 -125
- package/dist/expressServer.test.js +90 -91
- package/dist/githubAuth.test.js +22 -22
- package/dist/logger.d.ts +154 -0
- package/dist/logger.js +445 -26
- package/dist/logger.test.js +435 -0
- package/dist/middleware.d.ts +7 -0
- package/dist/middleware.js +58 -1
- package/dist/middleware.test.js +159 -0
- package/dist/models/consentForm.js +2 -1
- package/dist/models/consentResponse.js +2 -1
- package/dist/models/versionConfig.js +2 -1
- package/dist/openApi.test.js +10 -17
- package/dist/openApiBuilder.d.ts +18 -0
- package/dist/openApiBuilder.js +21 -0
- package/dist/openApiBuilder.test.js +34 -10
- package/dist/permissions.test.js +10 -43
- package/dist/populate.test.js +10 -42
- package/dist/realtime/changeStreamWatcher.d.ts +4 -4
- package/dist/realtime/changeStreamWatcher.js +2 -4
- package/dist/realtime/queryMatcher.d.ts +1 -1
- package/dist/realtime/queryMatcher.js +39 -14
- package/dist/realtime/types.d.ts +3 -3
- package/dist/requestContext.d.ts +61 -0
- package/dist/requestContext.js +74 -0
- package/dist/secretProviders.test.js +335 -0
- package/dist/syncConsents.test.js +2 -2
- package/dist/terrenoApp.d.ts +27 -15
- package/dist/terrenoApp.js +24 -14
- package/dist/terrenoApp.test.js +52 -0
- package/dist/tests/bunSetup.js +66 -262
- package/dist/tests/createTestData.d.ts +9 -0
- package/dist/tests/createTestData.js +272 -0
- package/dist/tests/models.d.ts +71 -0
- package/dist/tests/models.js +134 -0
- package/dist/tests/mongoTestSetup.d.ts +7 -0
- package/dist/tests/mongoTestSetup.js +150 -0
- package/dist/tests/testEnv.d.ts +0 -0
- package/dist/tests/testEnv.js +6 -0
- package/dist/tests/testHelper.d.ts +22 -0
- package/dist/tests/testHelper.js +115 -0
- package/dist/tests/types.d.ts +29 -0
- package/dist/tests/types.js +2 -0
- package/dist/tests.d.ts +10 -78
- package/dist/tests.js +24 -241
- package/dist/transformers.test.js +14 -50
- package/package.json +18 -4
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1 -0
- package/src/__tests__/versionCheckPlugin.test.ts +43 -15
- package/src/actions.openApi.test.ts +12 -10
- package/src/api.query.test.ts +24 -1
- package/src/api.test.ts +169 -0
- package/src/api.ts +71 -0
- package/src/auth.test.ts +287 -39
- package/src/betterAuth.ts +1 -1
- package/src/consentApp.test.ts +1 -0
- package/src/example.ts +4 -4
- package/src/expressServer.test.ts +82 -85
- package/src/expressServer.ts +1 -213
- package/src/githubAuth.test.ts +22 -22
- package/src/logger.test.ts +466 -1
- package/src/logger.ts +477 -14
- package/src/middleware.test.ts +74 -2
- package/src/middleware.ts +57 -0
- package/src/models/consentForm.ts +3 -4
- package/src/models/consentResponse.ts +6 -4
- package/src/models/versionConfig.ts +3 -4
- package/src/openApi.test.ts +10 -17
- package/src/openApiBuilder.test.ts +27 -10
- package/src/openApiBuilder.ts +24 -0
- package/src/permissions.test.ts +8 -23
- package/src/populate.test.ts +7 -22
- package/src/realtime/changeStreamWatcher.ts +15 -10
- package/src/realtime/queryMatcher.ts +54 -27
- package/src/realtime/types.ts +4 -4
- package/src/requestContext.ts +86 -0
- package/src/secretProviders.test.ts +219 -1
- package/src/syncConsents.test.ts +1 -1
- package/src/terrenoApp.test.ts +38 -0
- package/src/terrenoApp.ts +37 -15
- package/src/tests/bunSetup.ts +22 -236
- package/src/tests/createTestData.ts +176 -0
- package/src/tests/models.ts +164 -0
- package/src/tests/mongoTestSetup.ts +69 -0
- package/src/tests/testEnv.ts +4 -0
- package/src/tests/testHelper.ts +57 -0
- package/src/tests/types.ts +35 -0
- package/src/tests.ts +40 -231
- package/src/transformers.test.ts +11 -30
- package/tsconfig.typedoc.json +4 -0
- package/dist/tests/index.d.ts +0 -1
- package/dist/tests/index.js +0 -17
- package/src/tests/index.ts +0 -1
package/src/terrenoApp.ts
CHANGED
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
apiFallthroughErrorMiddleware,
|
|
12
12
|
apiUnauthorizedMiddleware,
|
|
13
13
|
} from "./errors";
|
|
14
|
-
import {type AuthOptions, logRequests} from "./expressServer";
|
|
14
|
+
import {type AddRoutes, type AuthOptions, logRequests} from "./expressServer";
|
|
15
15
|
import {addGitHubAuthRoutes, type GitHubAuthOptions, setupGitHubAuth} from "./githubAuth";
|
|
16
16
|
import {type LoggingOptions, logger, setupLogging} from "./logger";
|
|
17
|
+
import {jsonResponseRequestIdMiddleware} from "./middleware";
|
|
17
18
|
import {openApiCompatMiddleware, patchAppUse} from "./openApiCompat";
|
|
18
19
|
import {openApiEtagMiddleware} from "./openApiEtag";
|
|
19
20
|
import {RealtimeApp} from "./realtime/realtimeApp";
|
|
@@ -68,28 +69,41 @@ export interface TerrenoAppOptions {
|
|
|
68
69
|
* Set to `true` for defaults, or pass a RealtimeAppOptions object for full control.
|
|
69
70
|
*/
|
|
70
71
|
realtime?: boolean | RealtimeAppOptions;
|
|
72
|
+
/**
|
|
73
|
+
* Runs after CORS and before the `addMiddleware` chain and JSON body parsing.
|
|
74
|
+
* Use to attach early middleware via `app.use(...)` before JSON parsing.
|
|
75
|
+
*/
|
|
76
|
+
beforeJsonSetup?: (app: express.Application) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Invoked after registered plugins/model routers and before `/auth/me`.
|
|
79
|
+
* Receives the Express app and OpenAPI bundle for `modelRouter` / `createOpenApiBuilder` wiring.
|
|
80
|
+
*/
|
|
81
|
+
configureApp?: AddRoutes;
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
/**
|
|
74
85
|
* Fluent API for building Express applications with Terreno framework.
|
|
75
86
|
*
|
|
76
|
-
* TerrenoApp
|
|
77
|
-
*
|
|
78
|
-
*
|
|
87
|
+
* TerrenoApp is the supported way to assemble the Terreno Express stack.
|
|
88
|
+
* Build applications by registering model routers and plugins (and/or
|
|
89
|
+
* `configureApp`), then calling `start()` to listen.
|
|
79
90
|
*
|
|
80
91
|
* The middleware stack is configured in this order:
|
|
81
92
|
* 1. CORS
|
|
82
|
-
* 2.
|
|
83
|
-
* 3.
|
|
84
|
-
* 4.
|
|
85
|
-
* 5.
|
|
86
|
-
* 6.
|
|
87
|
-
* 7.
|
|
88
|
-
* 8.
|
|
89
|
-
* 9.
|
|
93
|
+
* 2. Optional `beforeJsonSetup` (configure the app before JSON parsing)
|
|
94
|
+
* 3. Custom middleware (via addMiddleware)
|
|
95
|
+
* 4. JSON body parser
|
|
96
|
+
* 5. Auth routes (/auth/login, /auth/signup, etc.)
|
|
97
|
+
* 6. JWT authentication setup
|
|
98
|
+
* 7. Request logging
|
|
99
|
+
* 8. Sentry scopes
|
|
100
|
+
* 9. OpenAPI middleware (including JSON `requestId` on object responses)
|
|
90
101
|
* 10. GitHub OAuth routes (if enabled)
|
|
91
|
-
* 11.
|
|
92
|
-
* 12.
|
|
102
|
+
* 11. Configuration app (if any)
|
|
103
|
+
* 12. Registered model routers and plugins
|
|
104
|
+
* 13. Optional `configureApp` callback
|
|
105
|
+
* 14. /auth/me routes
|
|
106
|
+
* 15. Error handling middleware
|
|
93
107
|
*
|
|
94
108
|
* @example
|
|
95
109
|
* ```typescript
|
|
@@ -126,7 +140,6 @@ export interface TerrenoAppOptions {
|
|
|
126
140
|
* .start();
|
|
127
141
|
* ```
|
|
128
142
|
*
|
|
129
|
-
* @see setupServer for the callback-based alternative
|
|
130
143
|
* @see TerrenoPlugin for creating reusable plugins
|
|
131
144
|
* @see modelRouter for creating CRUD route registrations
|
|
132
145
|
*/
|
|
@@ -263,6 +276,10 @@ export class TerrenoApp {
|
|
|
263
276
|
|
|
264
277
|
app.use(cors({credentials: true, origin: options.corsOrigin ?? "*"}));
|
|
265
278
|
|
|
279
|
+
if (options.beforeJsonSetup) {
|
|
280
|
+
options.beforeJsonSetup(app);
|
|
281
|
+
}
|
|
282
|
+
|
|
266
283
|
// Apply custom middleware before JSON parsing
|
|
267
284
|
for (const fn of this.middlewareFns) {
|
|
268
285
|
if (fn.length <= 3) {
|
|
@@ -317,6 +334,7 @@ export class TerrenoApp {
|
|
|
317
334
|
// OpenAPI
|
|
318
335
|
app.use(openApiCompatMiddleware);
|
|
319
336
|
app.use(openApiEtagMiddleware);
|
|
337
|
+
app.use(jsonResponseRequestIdMiddleware);
|
|
320
338
|
const oapi = openapi({
|
|
321
339
|
info: {
|
|
322
340
|
description: "Generated docs from an Express api",
|
|
@@ -352,6 +370,10 @@ export class TerrenoApp {
|
|
|
352
370
|
}
|
|
353
371
|
}
|
|
354
372
|
|
|
373
|
+
if (options.configureApp) {
|
|
374
|
+
options.configureApp(app, {openApi: oapi});
|
|
375
|
+
}
|
|
376
|
+
|
|
355
377
|
// /auth/me must be registered after plugins so that session middleware
|
|
356
378
|
// (e.g. Better Auth) has a chance to populate req.user first.
|
|
357
379
|
addMeRoutes(app, options.userModel, options.authOptions);
|
package/src/tests/bunSetup.ts
CHANGED
|
@@ -1,241 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
import {afterAll, afterEach, beforeAll, beforeEach, mock} from "bun:test";
|
|
3
|
-
import {Writable} from "node:stream";
|
|
4
|
-
import mongoose from "mongoose";
|
|
5
|
-
import winston from "winston";
|
|
1
|
+
import {registerBackendPreload, registerSimpleMongoPreload} from "@terreno/test";
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
import {logger, winstonLogger} from "../logger";
|
|
3
|
+
const useFixtureCache = process.env.TERRENO_TEST_USE_FIXTURE_CACHE === "true";
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
if (useFixtureCache) {
|
|
6
|
+
registerBackendPreload({
|
|
7
|
+
connectMongoInBeforeAll: true,
|
|
8
|
+
loadTestDataFromCache: async () => {
|
|
9
|
+
const {loadTestDataFromCache} = await import("./mongoTestSetup");
|
|
10
|
+
await loadTestDataFromCache();
|
|
11
|
+
},
|
|
12
|
+
mongo: {
|
|
13
|
+
baseDatabaseName: "terrenoTest_base",
|
|
14
|
+
useReplSet: true,
|
|
15
|
+
},
|
|
16
|
+
testEnv: {
|
|
17
|
+
tokenIssuer: "terreno-api.test",
|
|
18
|
+
},
|
|
19
|
+
useTransactions: true,
|
|
18
20
|
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
await mongoose.connection.close();
|
|
21
|
+
} else {
|
|
22
|
+
registerSimpleMongoPreload({
|
|
23
|
+
testEnv: {
|
|
24
|
+
tokenIssuer: "terreno-api.test",
|
|
25
|
+
},
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
let logs: string[] = [];
|
|
29
|
-
|
|
30
|
-
const SHOW_ALL_LOGS = process.env.SHOW_ALL_TEST_LOGS === "true";
|
|
31
|
-
|
|
32
|
-
// Create a custom stream that captures logs
|
|
33
|
-
const logStream = new Writable({
|
|
34
|
-
write(chunk: any, _encoding: any, callback: any) {
|
|
35
|
-
logs.push(chunk.toString());
|
|
36
|
-
if (SHOW_ALL_LOGS) {
|
|
37
|
-
process.stdout.write(chunk);
|
|
38
|
-
}
|
|
39
|
-
callback();
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Silence both winston loggers by replacing all transports with our capturing stream
|
|
44
|
-
const silentTransport = new winston.transports.Stream({
|
|
45
|
-
format: winston.format.simple(),
|
|
46
|
-
stream: logStream,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Clear and silence the default winston logger
|
|
50
|
-
winston.clear();
|
|
51
|
-
winston.add(silentTransport);
|
|
52
|
-
|
|
53
|
-
// Clear and silence the custom winstonLogger
|
|
54
|
-
winstonLogger.clear();
|
|
55
|
-
winstonLogger.add(silentTransport);
|
|
56
|
-
|
|
57
|
-
// Capture and silence console methods
|
|
58
|
-
const originalConsole = {
|
|
59
|
-
debug: console.debug,
|
|
60
|
-
error: console.error,
|
|
61
|
-
info: console.info,
|
|
62
|
-
// biome-ignore lint/suspicious/noConsole: We keep the original reference.
|
|
63
|
-
log: console.log,
|
|
64
|
-
warn: console.warn,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const captureConsoleMethod = (method: keyof typeof originalConsole): void => {
|
|
68
|
-
(console as any)[method] = (...args: any[]) => {
|
|
69
|
-
const logMessage = `[console.${method}] ${args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg))).join(" ")}`;
|
|
70
|
-
logs.push(logMessage);
|
|
71
|
-
if (SHOW_ALL_LOGS) {
|
|
72
|
-
originalConsole[method](...args);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
captureConsoleMethod("log");
|
|
78
|
-
captureConsoleMethod("info");
|
|
79
|
-
captureConsoleMethod("warn");
|
|
80
|
-
captureConsoleMethod("error");
|
|
81
|
-
captureConsoleMethod("debug");
|
|
82
|
-
|
|
83
|
-
// Setup before each test
|
|
84
|
-
beforeEach(() => {
|
|
85
|
-
process.env.TOKEN_SECRET = "secret";
|
|
86
|
-
process.env.TOKEN_ISSUER = "terreno-api.test";
|
|
87
|
-
process.env.SESSION_SECRET = "sessionSecret";
|
|
88
|
-
process.env.REFRESH_TOKEN_SECRET = "refreshTokenSecret";
|
|
89
|
-
setupEnvironment();
|
|
90
|
-
// Re-silence loggers after setupEnvironment which may reconfigure them
|
|
91
|
-
winston.clear();
|
|
92
|
-
winston.add(silentTransport);
|
|
93
|
-
winstonLogger.clear();
|
|
94
|
-
winstonLogger.add(silentTransport);
|
|
95
|
-
logs = [];
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Clear logs after each test
|
|
99
|
-
afterEach(() => {
|
|
100
|
-
logs = [];
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Mock @sentry/bun module
|
|
104
|
-
mock.module("@sentry/bun", () => {
|
|
105
|
-
const mockFn = (): ReturnType<typeof mock> => mock(() => {});
|
|
106
|
-
|
|
107
|
-
// Mock Scope
|
|
108
|
-
const mockScope = {
|
|
109
|
-
addBreadcrumb: mockFn(),
|
|
110
|
-
clear: mockFn(),
|
|
111
|
-
getSpan: mockFn(),
|
|
112
|
-
setContext: mockFn(),
|
|
113
|
-
setFingerprint: mockFn(),
|
|
114
|
-
setLevel: mockFn(),
|
|
115
|
-
setSpan: mockFn(),
|
|
116
|
-
setTag: mockFn(),
|
|
117
|
-
setTags: mockFn(),
|
|
118
|
-
setTransactionName: mockFn(),
|
|
119
|
-
setUser: mockFn(),
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// Mock Hub
|
|
123
|
-
const mockClient = {
|
|
124
|
-
captureException: mockFn(),
|
|
125
|
-
captureMessage: mockFn(),
|
|
126
|
-
close: mock(() => Promise.resolve(true)),
|
|
127
|
-
flush: mock(() => Promise.resolve(true)),
|
|
128
|
-
getOptions: mock(() => ({})),
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const mockHub = {
|
|
132
|
-
addBreadcrumb: mockFn(),
|
|
133
|
-
captureException: mockFn(),
|
|
134
|
-
captureMessage: mockFn(),
|
|
135
|
-
configureScope: mockFn(),
|
|
136
|
-
getClient: mock(() => mockClient),
|
|
137
|
-
getScope: mock(() => mockScope),
|
|
138
|
-
popScope: mockFn(),
|
|
139
|
-
pushScope: mockFn(),
|
|
140
|
-
setContext: mockFn(),
|
|
141
|
-
setTag: mockFn(),
|
|
142
|
-
setTags: mockFn(),
|
|
143
|
-
setUser: mockFn(),
|
|
144
|
-
withScope: mockFn(),
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const mockSpan: any = {
|
|
148
|
-
finish: mockFn(),
|
|
149
|
-
setData: mockFn(),
|
|
150
|
-
setStatus: mockFn(),
|
|
151
|
-
setTag: mockFn(),
|
|
152
|
-
startChild: mockFn(),
|
|
153
|
-
toTraceparent: mock(() => "mock-trace-parent"),
|
|
154
|
-
};
|
|
155
|
-
mockSpan.startChild = mock(() => mockSpan);
|
|
156
|
-
|
|
157
|
-
const mockTransaction = {
|
|
158
|
-
finish: mockFn(),
|
|
159
|
-
setData: mockFn(),
|
|
160
|
-
setName: mockFn(),
|
|
161
|
-
setStatus: mockFn(),
|
|
162
|
-
setTag: mockFn(),
|
|
163
|
-
startChild: mock(() => mockSpan),
|
|
164
|
-
toTraceparent: mock(() => "mock-trace-parent"),
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
addBreadcrumb: mockFn(),
|
|
169
|
-
captureException: mockFn(),
|
|
170
|
-
captureMessage: mockFn(),
|
|
171
|
-
clearScope: mockFn(),
|
|
172
|
-
close: mock(() => Promise.resolve(true)),
|
|
173
|
-
configureScope: mockFn(),
|
|
174
|
-
default: {
|
|
175
|
-
addBreadcrumb: mockFn(),
|
|
176
|
-
captureException: mockFn(),
|
|
177
|
-
captureMessage: mockFn(),
|
|
178
|
-
clearScope: mockFn(),
|
|
179
|
-
close: mock(() => Promise.resolve(true)),
|
|
180
|
-
configureScope: mockFn(),
|
|
181
|
-
flush: mock(() => Promise.resolve(true)),
|
|
182
|
-
getClient: mock(() => mockClient),
|
|
183
|
-
getCurrentHub: mock(() => mockHub),
|
|
184
|
-
getCurrentScope: mock(() => mockScope),
|
|
185
|
-
Handlers: {
|
|
186
|
-
errorHandler: mock(() => (err: any, _req: any, _res: any, next: any) => next(err)),
|
|
187
|
-
requestHandler: mock(() => (_req: any, _res: any, next: any) => next()),
|
|
188
|
-
tracingHandler: mock(() => (_req: any, _res: any, next: any) => next()),
|
|
189
|
-
},
|
|
190
|
-
init: mockFn(),
|
|
191
|
-
isInitialized: mock(() => true),
|
|
192
|
-
popScope: mockFn(),
|
|
193
|
-
pushScope: mockFn(),
|
|
194
|
-
Severity: {
|
|
195
|
-
Debug: "debug",
|
|
196
|
-
Error: "error",
|
|
197
|
-
Fatal: "fatal",
|
|
198
|
-
Info: "info",
|
|
199
|
-
Warning: "warning",
|
|
200
|
-
} as const,
|
|
201
|
-
setContext: mockFn(),
|
|
202
|
-
setFingerprint: mockFn(),
|
|
203
|
-
setLevel: mockFn(),
|
|
204
|
-
setTag: mockFn(),
|
|
205
|
-
setTags: mockFn(),
|
|
206
|
-
setUser: mockFn(),
|
|
207
|
-
setupExpressErrorHandler: mockFn(),
|
|
208
|
-
startTransaction: mock(() => mockTransaction),
|
|
209
|
-
withScope: mock((callback: any) => callback(mockScope)),
|
|
210
|
-
},
|
|
211
|
-
flush: mock(() => Promise.resolve(true)),
|
|
212
|
-
getClient: mock(() => mockClient),
|
|
213
|
-
getCurrentHub: mock(() => mockHub),
|
|
214
|
-
getCurrentScope: mock(() => mockScope),
|
|
215
|
-
Handlers: {
|
|
216
|
-
errorHandler: mock(() => (err: any, _req: any, _res: any, next: any) => next(err)),
|
|
217
|
-
requestHandler: mock(() => (_req: any, _res: any, next: any) => next()),
|
|
218
|
-
tracingHandler: mock(() => (_req: any, _res: any, next: any) => next()),
|
|
219
|
-
},
|
|
220
|
-
init: mockFn(),
|
|
221
|
-
isInitialized: mock(() => true),
|
|
222
|
-
popScope: mockFn(),
|
|
223
|
-
pushScope: mockFn(),
|
|
224
|
-
Severity: {
|
|
225
|
-
Debug: "debug",
|
|
226
|
-
Error: "error",
|
|
227
|
-
Fatal: "fatal",
|
|
228
|
-
Info: "info",
|
|
229
|
-
Warning: "warning",
|
|
230
|
-
} as const,
|
|
231
|
-
setContext: mockFn(),
|
|
232
|
-
setFingerprint: mockFn(),
|
|
233
|
-
setLevel: mockFn(),
|
|
234
|
-
setTag: mockFn(),
|
|
235
|
-
setTags: mockFn(),
|
|
236
|
-
setUser: mockFn(),
|
|
237
|
-
setupExpressErrorHandler: mockFn(),
|
|
238
|
-
startTransaction: mock(() => mockTransaction),
|
|
239
|
-
withScope: mock((callback: any) => callback(mockScope)),
|
|
240
|
-
};
|
|
241
|
-
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type {HydratedDocument} from "mongoose";
|
|
2
|
+
import type {PassportLocalMongooseDocument} from "passport-local-mongoose";
|
|
3
|
+
|
|
4
|
+
import {logger} from "../logger";
|
|
5
|
+
import {FoodModel, RequiredModel, type User, UserModel} from "./models";
|
|
6
|
+
import type {CachedTestData, TestData, TestFoods, TestRequired, TestUsers} from "./types";
|
|
7
|
+
|
|
8
|
+
const setPassword = async (user: HydratedDocument<User>, password: string): Promise<void> => {
|
|
9
|
+
await (user as unknown as PassportLocalMongooseDocument).setPassword(password);
|
|
10
|
+
await user.save();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const clearTestCollections = async (): Promise<void> => {
|
|
14
|
+
await Promise.all([
|
|
15
|
+
UserModel.deleteMany({}),
|
|
16
|
+
FoodModel.deleteMany({}),
|
|
17
|
+
RequiredModel.deleteMany({}),
|
|
18
|
+
]).catch(logger.catch);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const createTestUsers = async (): Promise<TestUsers> => {
|
|
22
|
+
const [notAdmin, admin, adminOther] = await Promise.all([
|
|
23
|
+
UserModel.create({email: "notAdmin@example.com", name: "Not Admin"}),
|
|
24
|
+
UserModel.create({admin: true, email: "admin@example.com", name: "Admin"}),
|
|
25
|
+
UserModel.create({admin: true, email: "admin+other@example.com", name: "Admin Other"}),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
await Promise.all([
|
|
29
|
+
setPassword(notAdmin, "password"),
|
|
30
|
+
setPassword(admin, "securePassword"),
|
|
31
|
+
setPassword(adminOther, "otherPassword"),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
return {admin, adminOther, notAdmin};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const createStandardFoods = async (users: TestUsers): Promise<TestFoods> => {
|
|
38
|
+
const {admin, adminOther, notAdmin} = users;
|
|
39
|
+
|
|
40
|
+
const [spinach, apple, carrots, pizza] = await Promise.all([
|
|
41
|
+
FoodModel.create({
|
|
42
|
+
calories: 1,
|
|
43
|
+
categories: [{name: "Vegetables", show: true}],
|
|
44
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
45
|
+
eatenBy: [admin._id],
|
|
46
|
+
expiration: "2026-12-31",
|
|
47
|
+
hidden: false,
|
|
48
|
+
lastEatenWith: {
|
|
49
|
+
dressing: new Date("2021-12-03T19:00:30.000Z"),
|
|
50
|
+
},
|
|
51
|
+
likesIds: [
|
|
52
|
+
{likes: true, userId: admin._id},
|
|
53
|
+
{likes: false, userId: notAdmin._id},
|
|
54
|
+
],
|
|
55
|
+
name: "Spinach",
|
|
56
|
+
ownerId: notAdmin._id,
|
|
57
|
+
source: {
|
|
58
|
+
dateAdded: "2023-12-13T12:30:00.000Z",
|
|
59
|
+
href: "https://www.example.com/spinach",
|
|
60
|
+
name: "Brand",
|
|
61
|
+
},
|
|
62
|
+
tags: ["healthy"],
|
|
63
|
+
}),
|
|
64
|
+
FoodModel.create({
|
|
65
|
+
calories: 100,
|
|
66
|
+
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
67
|
+
expiration: "2026-12-31",
|
|
68
|
+
hidden: true,
|
|
69
|
+
likesIds: [{likes: true, userId: admin._id}],
|
|
70
|
+
name: "Apple",
|
|
71
|
+
ownerId: admin._id,
|
|
72
|
+
source: {name: "Orchard"},
|
|
73
|
+
tags: ["healthy"],
|
|
74
|
+
}),
|
|
75
|
+
FoodModel.create({
|
|
76
|
+
calories: 100,
|
|
77
|
+
created: new Date("2021-12-03T00:00:00.000Z"),
|
|
78
|
+
eatenBy: [admin._id, notAdmin._id],
|
|
79
|
+
expiration: "2026-12-31",
|
|
80
|
+
hidden: false,
|
|
81
|
+
likesIds: [{likes: false, userId: notAdmin._id}],
|
|
82
|
+
name: "Carrots",
|
|
83
|
+
ownerId: admin._id,
|
|
84
|
+
source: {name: "Farm"},
|
|
85
|
+
tags: ["vegetable"],
|
|
86
|
+
}),
|
|
87
|
+
FoodModel.create({
|
|
88
|
+
calories: 800,
|
|
89
|
+
created: new Date("2022-01-01T00:00:00.000Z"),
|
|
90
|
+
expiration: "2026-12-31",
|
|
91
|
+
hidden: false,
|
|
92
|
+
likesIds: [{likes: true, userId: adminOther._id}],
|
|
93
|
+
name: "Pizza",
|
|
94
|
+
ownerId: adminOther._id,
|
|
95
|
+
source: {name: "Pizzeria"},
|
|
96
|
+
tags: ["comfort"],
|
|
97
|
+
}),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
return {apple, carrots, pizza, spinach};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const createRequiredFixtures = async (): Promise<TestRequired> => {
|
|
104
|
+
const [sample, withAbout] = await Promise.all([
|
|
105
|
+
RequiredModel.create({name: "Sample Required"}),
|
|
106
|
+
RequiredModel.create({about: "Optional about text", name: "Required With About"}),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
return {sample, withAbout};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Builds the standard Terreno API test database (users, foods, required docs). */
|
|
113
|
+
export const createTestData = async (): Promise<TestData> => {
|
|
114
|
+
await clearTestCollections();
|
|
115
|
+
|
|
116
|
+
const users = await createTestUsers();
|
|
117
|
+
const [foods, required] = await Promise.all([
|
|
118
|
+
createStandardFoods(users),
|
|
119
|
+
createRequiredFixtures(),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
return {foods, required, users};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const toCachedTestData = (testData: TestData): CachedTestData => ({
|
|
126
|
+
foods: {
|
|
127
|
+
apple: testData.foods.apple.id,
|
|
128
|
+
carrots: testData.foods.carrots.id,
|
|
129
|
+
pizza: testData.foods.pizza.id,
|
|
130
|
+
spinach: testData.foods.spinach.id,
|
|
131
|
+
},
|
|
132
|
+
required: {
|
|
133
|
+
sample: testData.required.sample.id,
|
|
134
|
+
withAbout: testData.required.withAbout.id,
|
|
135
|
+
},
|
|
136
|
+
users: {
|
|
137
|
+
admin: testData.users.admin.id,
|
|
138
|
+
adminOther: testData.users.adminOther.id,
|
|
139
|
+
notAdmin: testData.users.notAdmin.id,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export const loadTestDataFromDocuments = async (cached: CachedTestData): Promise<TestData> => {
|
|
144
|
+
const [admin, notAdmin, adminOther, spinach, apple, carrots, pizza, sample, withAbout] =
|
|
145
|
+
await Promise.all([
|
|
146
|
+
UserModel.findById(cached.users.admin),
|
|
147
|
+
UserModel.findById(cached.users.notAdmin),
|
|
148
|
+
UserModel.findById(cached.users.adminOther),
|
|
149
|
+
FoodModel.findById(cached.foods.spinach),
|
|
150
|
+
FoodModel.findById(cached.foods.apple),
|
|
151
|
+
FoodModel.findById(cached.foods.carrots),
|
|
152
|
+
FoodModel.findById(cached.foods.pizza),
|
|
153
|
+
RequiredModel.findById(cached.required.sample),
|
|
154
|
+
RequiredModel.findById(cached.required.withAbout),
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
if (
|
|
158
|
+
!admin ||
|
|
159
|
+
!notAdmin ||
|
|
160
|
+
!adminOther ||
|
|
161
|
+
!spinach ||
|
|
162
|
+
!apple ||
|
|
163
|
+
!carrots ||
|
|
164
|
+
!pizza ||
|
|
165
|
+
!sample ||
|
|
166
|
+
!withAbout
|
|
167
|
+
) {
|
|
168
|
+
throw new Error("[createTestData] Cached test data references missing documents");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
foods: {apple, carrots, pizza, spinach},
|
|
173
|
+
required: {sample, withAbout},
|
|
174
|
+
users: {admin, adminOther, notAdmin},
|
|
175
|
+
};
|
|
176
|
+
};
|