@terreno/api 0.20.2 → 0.21.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.
Files changed (65) hide show
  1. package/.ai/guidelines/core.md +71 -0
  2. package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
  3. package/README.md +54 -1
  4. package/dist/__tests__/versionCheckPlugin.test.js +29 -7
  5. package/dist/actions.openApi.test.js +13 -11
  6. package/dist/api.js +98 -11
  7. package/dist/api.query.test.js +31 -1
  8. package/dist/api.test.js +211 -0
  9. package/dist/auth.test.js +10 -10
  10. package/dist/betterAuth.d.ts +1 -1
  11. package/dist/consentApp.test.js +1 -0
  12. package/dist/example.js +4 -4
  13. package/dist/expressServer.d.ts +0 -22
  14. package/dist/expressServer.js +1 -125
  15. package/dist/expressServer.test.js +90 -91
  16. package/dist/githubAuth.test.js +22 -22
  17. package/dist/logger.d.ts +154 -0
  18. package/dist/logger.js +445 -26
  19. package/dist/logger.test.js +435 -0
  20. package/dist/middleware.d.ts +7 -0
  21. package/dist/middleware.js +58 -1
  22. package/dist/middleware.test.js +159 -0
  23. package/dist/openApi.test.js +10 -17
  24. package/dist/openApiBuilder.test.js +18 -10
  25. package/dist/realtime/changeStreamWatcher.d.ts +4 -4
  26. package/dist/realtime/changeStreamWatcher.js +2 -4
  27. package/dist/realtime/queryMatcher.d.ts +1 -1
  28. package/dist/realtime/queryMatcher.js +39 -14
  29. package/dist/realtime/types.d.ts +3 -3
  30. package/dist/requestContext.d.ts +61 -0
  31. package/dist/requestContext.js +74 -0
  32. package/dist/secretProviders.test.js +335 -0
  33. package/dist/terrenoApp.d.ts +27 -15
  34. package/dist/terrenoApp.js +24 -14
  35. package/dist/terrenoApp.test.js +52 -0
  36. package/dist/tests/bunSetup.js +61 -7
  37. package/dist/tests.js +27 -4
  38. package/package.json +1 -1
  39. package/src/__tests__/versionCheckPlugin.test.ts +43 -15
  40. package/src/actions.openApi.test.ts +12 -10
  41. package/src/api.query.test.ts +24 -1
  42. package/src/api.test.ts +169 -0
  43. package/src/api.ts +71 -0
  44. package/src/auth.test.ts +10 -10
  45. package/src/betterAuth.ts +1 -1
  46. package/src/consentApp.test.ts +1 -0
  47. package/src/example.ts +4 -4
  48. package/src/expressServer.test.ts +82 -85
  49. package/src/expressServer.ts +1 -213
  50. package/src/githubAuth.test.ts +22 -22
  51. package/src/logger.test.ts +466 -1
  52. package/src/logger.ts +477 -14
  53. package/src/middleware.test.ts +74 -2
  54. package/src/middleware.ts +57 -0
  55. package/src/openApi.test.ts +10 -17
  56. package/src/openApiBuilder.test.ts +18 -10
  57. package/src/realtime/changeStreamWatcher.ts +15 -10
  58. package/src/realtime/queryMatcher.ts +54 -27
  59. package/src/realtime/types.ts +4 -4
  60. package/src/requestContext.ts +86 -0
  61. package/src/secretProviders.test.ts +219 -1
  62. package/src/terrenoApp.test.ts +38 -0
  63. package/src/terrenoApp.ts +37 -15
  64. package/src/tests/bunSetup.ts +16 -3
  65. package/src/tests.ts +17 -4
@@ -1,7 +1,13 @@
1
1
  import {beforeEach, describe, expect, it} from "bun:test";
2
2
 
3
3
  import type {SecretProvider} from "./configurationPlugin";
4
- import {CachingSecretProvider, CompositeSecretProvider, EnvSecretProvider} from "./secretProviders";
4
+ import {APIError} from "./errors";
5
+ import {
6
+ CachingSecretProvider,
7
+ CompositeSecretProvider,
8
+ EnvSecretProvider,
9
+ GcpSecretProvider,
10
+ } from "./secretProviders";
5
11
 
6
12
  describe("EnvSecretProvider", () => {
7
13
  beforeEach(() => {
@@ -184,3 +190,215 @@ describe("CachingSecretProvider", () => {
184
190
  expect(calls).toBe(1);
185
191
  });
186
192
  });
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // GcpSecretProvider
196
+ // ---------------------------------------------------------------------------
197
+
198
+ interface MockSecretManagerClient {
199
+ accessSecretVersion: (request: {
200
+ name: string;
201
+ }) => Promise<[{payload?: {data?: string | Uint8Array}}]>;
202
+ }
203
+
204
+ /** Inject a pre-built mock client into a GcpSecretProvider, bypassing getClient(). */
205
+ const injectClient = (provider: GcpSecretProvider, client: MockSecretManagerClient): void => {
206
+ // Bypass the private `client` field for testing — avoids the dynamic import of
207
+ // @google-cloud/secret-manager which is an optional peer dependency.
208
+ Object.defineProperty(provider, "client", {configurable: true, value: client, writable: true});
209
+ };
210
+
211
+ describe("GcpSecretProvider", () => {
212
+ it("has the name 'gcp'", () => {
213
+ const provider = new GcpSecretProvider({projectId: "my-project"});
214
+ expect(provider.name).toBe("gcp");
215
+ });
216
+
217
+ it("throws APIError when @google-cloud/secret-manager is not installed", async () => {
218
+ const provider = new GcpSecretProvider({projectId: "my-project"});
219
+ try {
220
+ await provider.getSecret("some-secret");
221
+ expect.unreachable("should have thrown");
222
+ } catch (error) {
223
+ expect(error).toBeInstanceOf(APIError);
224
+ expect((error as APIError).title).toContain(
225
+ "GcpSecretProvider requires @google-cloud/secret-manager"
226
+ );
227
+ }
228
+ });
229
+
230
+ it("resolves a short secret name to the full resource path with default version", async () => {
231
+ const calls: string[] = [];
232
+ const mockClient: MockSecretManagerClient = {
233
+ accessSecretVersion: async (req) => {
234
+ calls.push(req.name);
235
+ return [{payload: {data: "secret-value"}}];
236
+ },
237
+ };
238
+ const provider = new GcpSecretProvider({projectId: "my-project"});
239
+ injectClient(provider, mockClient);
240
+
241
+ const result = await provider.getSecret("openai-api-key");
242
+ expect(result).toBe("secret-value");
243
+ expect(calls).toEqual(["projects/my-project/secrets/openai-api-key/versions/latest"]);
244
+ });
245
+
246
+ it("resolves a short secret name with an explicit version", async () => {
247
+ const calls: string[] = [];
248
+ const mockClient: MockSecretManagerClient = {
249
+ accessSecretVersion: async (req) => {
250
+ calls.push(req.name);
251
+ return [{payload: {data: "v3-value"}}];
252
+ },
253
+ };
254
+ const provider = new GcpSecretProvider({projectId: "p"});
255
+ injectClient(provider, mockClient);
256
+
257
+ const result = await provider.getSecret("my-key", "3");
258
+ expect(result).toBe("v3-value");
259
+ expect(calls).toEqual(["projects/p/secrets/my-key/versions/3"]);
260
+ });
261
+
262
+ it("honors a full resource path that already contains /versions/", async () => {
263
+ const calls: string[] = [];
264
+ const mockClient: MockSecretManagerClient = {
265
+ accessSecretVersion: async (req) => {
266
+ calls.push(req.name);
267
+ return [{payload: {data: "pinned"}}];
268
+ },
269
+ };
270
+ const provider = new GcpSecretProvider({projectId: "ignored"});
271
+ injectClient(provider, mockClient);
272
+
273
+ const result = await provider.getSecret("projects/p/secrets/s/versions/7");
274
+ expect(result).toBe("pinned");
275
+ expect(calls).toEqual(["projects/p/secrets/s/versions/7"]);
276
+ });
277
+
278
+ it("appends /versions/latest to a full resource path without a version suffix", async () => {
279
+ const calls: string[] = [];
280
+ const mockClient: MockSecretManagerClient = {
281
+ accessSecretVersion: async (req) => {
282
+ calls.push(req.name);
283
+ return [{payload: {data: "latest-value"}}];
284
+ },
285
+ };
286
+ const provider = new GcpSecretProvider({projectId: "ignored"});
287
+ injectClient(provider, mockClient);
288
+
289
+ const result = await provider.getSecret("projects/p/secrets/s");
290
+ expect(result).toBe("latest-value");
291
+ expect(calls).toEqual(["projects/p/secrets/s/versions/latest"]);
292
+ });
293
+
294
+ it("appends the explicit version when full path lacks /versions/", async () => {
295
+ const calls: string[] = [];
296
+ const mockClient: MockSecretManagerClient = {
297
+ accessSecretVersion: async (req) => {
298
+ calls.push(req.name);
299
+ return [{payload: {data: "v5"}}];
300
+ },
301
+ };
302
+ const provider = new GcpSecretProvider({projectId: "ignored"});
303
+ injectClient(provider, mockClient);
304
+
305
+ const result = await provider.getSecret("projects/p/secrets/s", "5");
306
+ expect(result).toBe("v5");
307
+ expect(calls).toEqual(["projects/p/secrets/s/versions/5"]);
308
+ });
309
+
310
+ it("decodes a Uint8Array payload", async () => {
311
+ const encoded = new TextEncoder().encode("binary-secret");
312
+ const mockClient: MockSecretManagerClient = {
313
+ accessSecretVersion: async () => [{payload: {data: encoded}}],
314
+ };
315
+ const provider = new GcpSecretProvider({projectId: "p"});
316
+ injectClient(provider, mockClient);
317
+
318
+ expect(await provider.getSecret("bin-key")).toBe("binary-secret");
319
+ });
320
+
321
+ it("returns null when the payload is empty", async () => {
322
+ const mockClient: MockSecretManagerClient = {
323
+ accessSecretVersion: async () => [{payload: {}}],
324
+ };
325
+ const provider = new GcpSecretProvider({projectId: "p"});
326
+ injectClient(provider, mockClient);
327
+
328
+ expect(await provider.getSecret("empty-payload")).toBeNull();
329
+ });
330
+
331
+ it("returns null when the payload field is missing entirely", async () => {
332
+ const mockClient: MockSecretManagerClient = {
333
+ accessSecretVersion: async () => [{}],
334
+ };
335
+ const provider = new GcpSecretProvider({projectId: "p"});
336
+ injectClient(provider, mockClient);
337
+
338
+ expect(await provider.getSecret("no-payload")).toBeNull();
339
+ });
340
+
341
+ it("returns null on NOT_FOUND (gRPC code 5)", async () => {
342
+ const notFound = Object.assign(new Error("NOT_FOUND"), {code: 5});
343
+ const mockClient: MockSecretManagerClient = {
344
+ accessSecretVersion: async () => {
345
+ throw notFound;
346
+ },
347
+ };
348
+ const provider = new GcpSecretProvider({projectId: "p"});
349
+ injectClient(provider, mockClient);
350
+
351
+ expect(await provider.getSecret("missing-secret")).toBeNull();
352
+ });
353
+
354
+ it("re-throws non-NOT_FOUND errors", async () => {
355
+ const permissionDenied = Object.assign(new Error("PERMISSION_DENIED"), {code: 7});
356
+ const mockClient: MockSecretManagerClient = {
357
+ accessSecretVersion: async () => {
358
+ throw permissionDenied;
359
+ },
360
+ };
361
+ const provider = new GcpSecretProvider({projectId: "p"});
362
+ injectClient(provider, mockClient);
363
+
364
+ try {
365
+ await provider.getSecret("forbidden-secret");
366
+ expect.unreachable("should have thrown");
367
+ } catch (error) {
368
+ expect(error).toBe(permissionDenied);
369
+ }
370
+ });
371
+
372
+ it("re-throws non-Error throwables", async () => {
373
+ const mockClient: MockSecretManagerClient = {
374
+ accessSecretVersion: async () => {
375
+ throw "string-error";
376
+ },
377
+ };
378
+ const provider = new GcpSecretProvider({projectId: "p"});
379
+ injectClient(provider, mockClient);
380
+
381
+ try {
382
+ await provider.getSecret("x");
383
+ expect.unreachable("should have thrown");
384
+ } catch (error) {
385
+ expect(error).toBe("string-error");
386
+ }
387
+ });
388
+
389
+ it("caches the client across multiple getSecret calls", async () => {
390
+ let callCount = 0;
391
+ const mockClient: MockSecretManagerClient = {
392
+ accessSecretVersion: async () => {
393
+ callCount++;
394
+ return [{payload: {data: `call-${callCount}`}}];
395
+ },
396
+ };
397
+ const provider = new GcpSecretProvider({projectId: "p"});
398
+ injectClient(provider, mockClient);
399
+
400
+ expect(await provider.getSecret("a")).toBe("call-1");
401
+ expect(await provider.getSecret("b")).toBe("call-2");
402
+ expect(callCount).toBe(2);
403
+ });
404
+ });
@@ -6,6 +6,7 @@ import supertest from "supertest";
6
6
  import {modelRouter} from "./api";
7
7
  import type {UserModel as UserModelType} from "./auth";
8
8
  import {configurationPlugin} from "./configurationPlugin";
9
+ import {APIError} from "./errors";
9
10
  import {Permissions} from "./permissions";
10
11
  import {createdUpdatedPlugin} from "./plugins";
11
12
  import {TerrenoApp} from "./terrenoApp";
@@ -42,6 +43,17 @@ describe("TerrenoApp", () => {
42
43
  expect(app).toBeDefined();
43
44
  });
44
45
 
46
+ it("does not add requestId to GET /openapi.json document bodies", async () => {
47
+ const app = new TerrenoApp({
48
+ skipListen: true,
49
+ userModel: typedUserModel,
50
+ }).build();
51
+
52
+ const res = await supertest(app).get("/openapi.json").expect(200);
53
+ expect(res.body.openapi).toBe("3.0.0");
54
+ expect(res.body.requestId).toBeUndefined();
55
+ });
56
+
45
57
  it("creates server with custom corsOrigin", () => {
46
58
  const app = new TerrenoApp({
47
59
  corsOrigin: "https://example.com",
@@ -105,6 +117,7 @@ describe("TerrenoApp", () => {
105
117
  const res = await agent.get("/food").expect(200);
106
118
  expect(res.body.data).toHaveLength(1);
107
119
  expect(res.body.data[0].name).toBe("Apple");
120
+ expect(res.body.requestId).toBe(res.headers["x-request-id"]);
108
121
  });
109
122
 
110
123
  it("supports chaining multiple registrations", async () => {
@@ -196,6 +209,7 @@ describe("TerrenoApp", () => {
196
209
  const agent = await authAsUser(app, "admin");
197
210
  const res = await agent.get("/configuration/meta");
198
211
  expect(res.status).toBe(200);
212
+ expect(res.body.requestId).toBe(res.headers["x-request-id"]);
199
213
  });
200
214
 
201
215
  it("supports custom basePath via configure options", async () => {
@@ -240,6 +254,30 @@ describe("TerrenoApp", () => {
240
254
 
241
255
  const res = await supertest(app).get("/trigger-fallthrough");
242
256
  expect(res.status).toBe(500);
257
+ expect(res.body.requestId).toBe(res.headers["x-request-id"]);
258
+ expect(res.body.status).toBe(500);
259
+ expect(res.body.title).toBe("Internal server error");
260
+ });
261
+
262
+ it("adds requestId to APIError JSON responses", async () => {
263
+ const plugin: TerrenoPlugin = {
264
+ register: (pluginApp) => {
265
+ pluginApp.get("/api-error-route", () => {
266
+ throw new APIError({status: 400, title: "Bad request test"});
267
+ });
268
+ },
269
+ };
270
+ const app = new TerrenoApp({
271
+ skipListen: true,
272
+ userModel: typedUserModel,
273
+ })
274
+ .register(plugin)
275
+ .build();
276
+
277
+ const res = await supertest(app).get("/api-error-route").set("X-Request-ID", "api-err-rid");
278
+ expect(res.status).toBe(400);
279
+ expect(res.body.requestId).toBe("api-err-rid");
280
+ expect(res.body.title).toBe("Bad request test");
243
281
  });
244
282
  });
245
283
 
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 provides an alternative to `setupServer` using a registration
77
- * pattern instead of callbacks. Build applications by registering model
78
- * routers and plugins, then calling `start()` to begin listening.
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. Custom middleware (via addMiddleware)
83
- * 3. JSON body parser
84
- * 4. Auth routes (/auth/login, /auth/signup, etc.)
85
- * 5. JWT authentication setup
86
- * 6. Request logging
87
- * 7. Sentry scopes
88
- * 8. OpenAPI middleware
89
- * 9. /auth/me routes
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. Registered model routers and plugins
92
- * 12. Error handling middleware
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);
@@ -9,12 +9,22 @@ import {logger, winstonLogger} from "../logger";
9
9
 
10
10
  const shouldConnectToTestDb = process.env.BUN_TEST_DISABLE_DB !== "true";
11
11
 
12
+ const defaultLocalMongoUri = "mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000";
13
+
14
+ /** When set by {@link TERRENO_TEST_USE_MEMORY_MONGO}, holds the server to stop in afterAll. */
15
+ let memoryMongo: {getUri: () => string; stop: () => Promise<boolean>} | undefined;
16
+
12
17
  // Connect to MongoDB once for all tests
13
18
  if (shouldConnectToTestDb) {
14
19
  beforeAll(async () => {
15
- await mongoose
16
- .connect("mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000")
17
- .catch(logger.catch);
20
+ let uri = process.env.TERRENO_TEST_MONGODB_URI?.trim();
21
+ if (!uri && process.env.TERRENO_TEST_USE_MEMORY_MONGO === "true") {
22
+ const {MongoMemoryServer} = await import("mongodb-memory-server");
23
+ memoryMongo = await MongoMemoryServer.create();
24
+ uri = memoryMongo.getUri();
25
+ }
26
+ const connectUri = uri ?? defaultLocalMongoUri;
27
+ await mongoose.connect(connectUri).catch(logger.catch);
18
28
  });
19
29
  }
20
30
 
@@ -22,6 +32,9 @@ if (shouldConnectToTestDb) {
22
32
  if (shouldConnectToTestDb) {
23
33
  afterAll(async () => {
24
34
  await mongoose.connection.close();
35
+ if (memoryMongo) {
36
+ await memoryMongo.stop();
37
+ }
25
38
  });
26
39
  }
27
40
 
package/src/tests.ts CHANGED
@@ -177,7 +177,7 @@ export const getBaseServer = (): Express => {
177
177
 
178
178
  // Express 5 defaults to 'simple' query parser (Node querystring) which doesn't
179
179
  // support nested bracket notation like name[$regex]=Green. Use qs to match
180
- // what setupServer() configures.
180
+ // what TerrenoApp.build() configures.
181
181
  app.set("query parser", (str: string) => qs.parse(str, {arrayLimit: 200}));
182
182
 
183
183
  // Record mount paths on layers for Express 5 → OpenAPI compat
@@ -210,10 +210,23 @@ export const authAsUser = async (
210
210
  return agent;
211
211
  };
212
212
 
213
+ const defaultTestMongoUri = "mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000";
214
+
215
+ /** Ensures Mongoose is connected without replacing an existing test connection (e.g. MongoMemoryServer from bunSetup). */
216
+ const ensureTestMongooseConnected = async (): Promise<void> => {
217
+ if (mongoose.connection.readyState === 1) {
218
+ return;
219
+ }
220
+ if (mongoose.connection.readyState === 2) {
221
+ await mongoose.connection.asPromise();
222
+ return;
223
+ }
224
+ const uri = process.env.TERRENO_TEST_MONGODB_URI?.trim() || defaultTestMongoUri;
225
+ await mongoose.connect(uri).catch(logger.catch);
226
+ };
227
+
213
228
  export const setupDb = async () => {
214
- await mongoose
215
- .connect("mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000")
216
- .catch(logger.catch);
229
+ await ensureTestMongooseConnected();
217
230
 
218
231
  process.env.REFRESH_TOKEN_SECRET = "refresh_secret";
219
232
  process.env.TOKEN_SECRET = "secret";