@nest-arch/tui 0.4.6 → 0.5.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 (42) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/constants-DX5wiqrq.js +1 -0
  3. package/dist/index.js +145 -133
  4. package/dist/templates/addons/husky/pre-commit.hbs +1 -1
  5. package/dist/templates/base/.env.example.hbs +7 -2
  6. package/dist/templates/base/.env.hbs +7 -2
  7. package/dist/templates/base/README.md.hbs +11 -2
  8. package/dist/templates/base/pnpm-workspace.yaml.hbs +9 -4
  9. package/dist/templates/base/prisma.config.ts.hbs +5 -0
  10. package/dist/templates/base/src/app.module.ts.hbs +5 -22
  11. package/dist/templates/base/src/database/adapters/mongo/raw-mongo.adapter.ts.hbs +4 -2
  12. package/dist/templates/base/src/database/db.module.ts.hbs +15 -1
  13. package/dist/templates/base/src/database/db.service.ts.hbs +6 -3
  14. package/dist/templates/base/src/database/entities/todo.entity.ts.hbs +2 -2
  15. package/dist/templates/base/src/database/index.ts.hbs +2 -0
  16. package/dist/templates/base/src/database/orm/drizzle/schema.ts.hbs +48 -12
  17. package/dist/templates/base/src/database/orm/typeorm/data-source.ts.hbs +26 -10
  18. package/dist/templates/base/src/libs/middlewares/graphql-throttler.guard.ts.hbs +22 -0
  19. package/dist/templates/base/src/libs/trpc/inner.context.ts.hbs +2 -2
  20. package/dist/templates/base/src/main.ts.hbs +2 -2
  21. package/dist/templates/base/test/app.e2e-spec.ts.hbs +368 -196
  22. package/dist/templates/extras/docker/Dockerfile.hbs +8 -1
  23. package/dist/templates/extras/health/src/health/health.controller.ts.hbs +7 -1
  24. package/dist/templates/extras/swagger/create-docs.ts.hbs +6 -1
  25. package/dist/templates/feature/graphql/src/graphql/models/todo.model.ts.hbs +1 -1
  26. package/dist/templates/feature/graphql/src/graphql/resolvers/todo.resolver.ts.hbs +1 -1
  27. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.controller.ts.hbs +80 -7
  28. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.service.ts.hbs +181 -25
  29. package/dist/templates/monorepo/apps/api-gateway/src/main.ts.hbs +2 -2
  30. package/dist/templates/monorepo/root/README.md.hbs +8 -13
  31. package/dist/templates/single-project/architecture/nest-api/.env.hbs +6 -2
  32. package/dist/templates/single-project/architecture/nest-api/pnpm-workspace.yaml.hbs +6 -0
  33. package/dist/templates/single-project/architecture/nest-api/src/main.ts.hbs +2 -2
  34. package/dist/templates/single-project/architecture/nest-gateway/src/main.ts.hbs +2 -2
  35. package/dist/templates/single-project/architecture/nest-microservice/.env.hbs +6 -0
  36. package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs +2 -2
  37. package/dist/templates/single-project/architecture/nest-microservice/src/app.module.ts.hbs +28 -1
  38. package/dist/templates/single-project/architecture/nest-microservice/src/http-throttler.guard.ts.hbs +13 -0
  39. package/dist/templates/single-project/architecture/nest-microservice/src/main.ts.hbs +2 -2
  40. package/dist/templates/single-project/architecture/nest-microservice/test/microservice.e2e-spec.ts.hbs +35 -0
  41. package/package.json +5 -5
  42. package/dist/constants-DVhgS48N.js +0 -1
@@ -6,6 +6,7 @@ import { Test, TestingModule } from "@nestjs/testing";
6
6
  {{#if (or (eq auth "better-auth:stateless") (and isPassportAuth (not hasDatabase)))}}
7
7
  import { createServer } from "node:http";
8
8
  import { generateKeyPairSync, createSign } from "node:crypto";
9
+ import { once } from "node:events";
9
10
  {{/if}}
10
11
  {{#if (and hasTrustedProxy isPassportAuth hasRateLimiting hasDatabase)}}
11
12
  import { createServer, request as httpRequest } from "node:http";
@@ -17,7 +18,7 @@ import { env } from "../src/config/env.js";
17
18
  import { FastifyAdapter } from "@nestjs/platform-fastify";
18
19
  {{/if}}
19
20
  import request from "supertest";
20
- import { afterAll, beforeAll, describe, expect, it{{#if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}, vi{{/if}} } from "vitest";
21
+ import { afterAll, beforeAll{{#if (and hasAuth hasRateLimiting (not (eq httpProvider "fastify")))}}, beforeEach{{/if}}, describe, expect, it{{#if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}, vi{{/if}} } from "vitest";
21
22
  import type { App } from "supertest/types";
22
23
  {{#if swagger}}
23
24
  import { createDocs } from "../src/docs/create-docs.js";
@@ -32,9 +33,9 @@ import { apiReference } from "@scalar/nestjs-api-reference";
32
33
  {{#unless (or (eq auth "better-auth:stateless") (and isPassportAuth (not hasDatabase)))}}
33
34
  import { AppModule } from "./../src/app.module.js";
34
35
  {{/unless}}
35
- {{#if (and useDrizzle (eq auth "none"))}}
36
+ {{#if (and useDrizzle (eq auth "none") (not hasSqlserver) hasTodoExample)}}
36
37
  import { DbService } from "../src/database/db.service.js";
37
- import { users } from "../src/database/orm/drizzle/schema.js";
38
+ import { todos } from "../src/database/orm/drizzle/schema.js";
38
39
  import { eq } from "drizzle-orm";
39
40
  {{else if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}
40
41
  import { DbService } from "../src/database/db.service.js";
@@ -42,9 +43,35 @@ import { DbService } from "../src/database/db.service.js";
42
43
 
43
44
  describe("AppController (e2e)", () => {
44
45
  let app: INestApplication<App>;
46
+ {{#if (and hasAuth hasRateLimiting (not (eq httpProvider "fastify")))}}
47
+ let activeRateLimitTestIp = "198.51.100.1";
48
+ let rateLimitTestIp = 1;
49
+ beforeEach(() => {
50
+ activeRateLimitTestIp = `198.51.100.${rateLimitTestIp}`;
51
+ rateLimitTestIp = (rateLimitTestIp % 254) + 1;
52
+ });
53
+ {{/if}}
45
54
  {{#if swagger}}
46
55
  let generatedOpenApi: Awaited<ReturnType<typeof createDocs>>;
47
56
  {{/if}}
57
+ {{#if (eq httpProvider "fastify")}}
58
+ let fastifyPort = 0;
59
+ {{/if}}
60
+ function getRequestTarget(_server = app.getHttpServer()) {
61
+ {{#if (eq httpProvider "fastify")}}
62
+ return `http://127.0.0.1:${fastifyPort}`;
63
+ {{else}}
64
+ return _server;
65
+ {{/if}}
66
+ }
67
+ {{#if (and hasAuth hasRateLimiting (eq httpProvider "fastify") (not hasTrustedProxy))}}
68
+ let activeRateLimitTestIp = "198.51.100.1";
69
+ let rateLimitTestIp = 1;
70
+ beforeEach(() => {
71
+ activeRateLimitTestIp = `198.51.100.${rateLimitTestIp}`;
72
+ rateLimitTestIp = (rateLimitTestIp % 254) + 1;
73
+ });
74
+ {{/if}}
48
75
  {{#if (eq auth "better-auth:stateless")}}
49
76
  const oidcOrigin = "http://127.0.0.1:4010";
50
77
  const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
@@ -63,8 +90,8 @@ describe("AppController (e2e)", () => {
63
90
  }
64
91
 
65
92
  async function startOidcFixture(): Promise<void> {
66
- oidcServer = createServer((request, response) => {
67
- const url = new URL(request.url ?? "/", oidcOrigin);
93
+ oidcServer = createServer((incomingRequest, response) => {
94
+ const url = new URL(incomingRequest.url ?? "/", oidcOrigin);
68
95
  if (url.pathname === "/.well-known/openid-configuration") {
69
96
  response.setHeader("content-type", "application/json");
70
97
  response.end(JSON.stringify({ authorization_endpoint: `${oidcOrigin}/authorize`, issuer: oidcOrigin, jwks_uri: `${oidcOrigin}/jwks`, token_endpoint: `${oidcOrigin}/token`, userinfo_endpoint: `${oidcOrigin}/userinfo` }));
@@ -86,8 +113,8 @@ describe("AppController (e2e)", () => {
86
113
  }
87
114
  if (url.pathname === "/token") {
88
115
  let body = "";
89
- request.on("data", (chunk: Buffer) => { body += chunk.toString(); });
90
- request.on("end", () => {
116
+ incomingRequest.on("data", (chunk: Buffer) => { body += chunk.toString(); });
117
+ incomingRequest.on("end", () => {
91
118
  response.setHeader("content-type", "application/json");
92
119
  response.end(JSON.stringify({ access_token: "scenario-access-token", id_token: signIdToken(authorizationNonce), token_type: "Bearer" }));
93
120
  });
@@ -101,7 +128,8 @@ describe("AppController (e2e)", () => {
101
128
  response.writeHead(404);
102
129
  response.end();
103
130
  });
104
- await new Promise<void>((resolve) => oidcServer.listen(4010, "127.0.0.1", resolve));
131
+ oidcServer.listen(4010, "127.0.0.1");
132
+ await once(oidcServer, "listening");
105
133
  }
106
134
  {{/if}}
107
135
  {{#if (and isPassportAuth (not hasDatabase))}}
@@ -122,9 +150,9 @@ describe("AppController (e2e)", () => {
122
150
  return `${signingInput}.${signer.sign(privateKey).toString("base64url")}`;
123
151
  }
124
152
 
125
- async function startIdentityProvider(): Promise<void> {
126
- identityServer = createServer((request, response) => {
127
- if (request.url !== "/.well-known/jwks.json") {
153
+ function startIdentityProvider(): Promise<void> {
154
+ identityServer = createServer((incomingRequest, response) => {
155
+ if (incomingRequest.url !== "/.well-known/jwks.json") {
128
156
  response.writeHead(404);
129
157
  response.end();
130
158
  return;
@@ -132,7 +160,12 @@ describe("AppController (e2e)", () => {
132
160
  response.setHeader("content-type", "application/json");
133
161
  response.end(JSON.stringify({ keys: [{ ...publicJwk, alg: "RS256", kid: "scenario-key", use: "sig" }] }));
134
162
  });
135
- await new Promise<void>((resolve) => identityServer.listen(4011, "127.0.0.1", resolve));
163
+ identityServer.listen(4011, "127.0.0.1");
164
+ return once(identityServer, "listening").then(() => {
165
+ if (!identityServer.listening) {
166
+ throw new Error("OIDC identity fixture failed to start");
167
+ }
168
+ });
136
169
  }
137
170
  {{/if}}
138
171
 
@@ -155,10 +188,23 @@ describe("AppController (e2e)", () => {
155
188
  }).compile();
156
189
 
157
190
  {{#if (eq httpProvider "fastify")}}
158
- app = moduleFixture.createNestApplication(new FastifyAdapter(){{#if isBetterAuth}}, { bodyParser: false }{{/if}});
191
+ app = moduleFixture.createNestApplication(new FastifyAdapter({{#if (and hasAuth hasRateLimiting)}}{ trustProxy: true }{{/if}}){{#if isBetterAuth}}, { bodyParser: false }{{/if}});
159
192
  {{else}}
160
193
  app = moduleFixture.createNestApplication({{#if isBetterAuth}}{ bodyParser: false }{{/if}});
161
194
  {{/if}}
195
+ {{#if (and hasAuth hasRateLimiting (not hasTrustedProxy) (not (eq httpProvider "fastify")))}}
196
+ app.getHttpAdapter().getInstance().set("trust proxy", true);
197
+ app.use((incomingRequest, _response, next) => {
198
+ incomingRequest.headers["x-forwarded-for"] = activeRateLimitTestIp;
199
+ next();
200
+ });
201
+ {{/if}}
202
+ {{#if (and hasAuth hasRateLimiting (eq httpProvider "fastify") (not hasTrustedProxy))}}
203
+ app.getHttpAdapter().getInstance().addHook("onRequest", (incomingRequest, _reply, done) => {
204
+ incomingRequest.headers["x-forwarded-for"] = activeRateLimitTestIp;
205
+ done();
206
+ });
207
+ {{/if}}
162
208
  {{#if hasTrustedProxy}}
163
209
  {{#unless (eq httpProvider "fastify")}}
164
210
  const trustedProxyIps = env.TRUSTED_PROXY_IPS.split(",").map((ip) => ip.trim()).filter(Boolean);
@@ -172,7 +218,6 @@ describe("AppController (e2e)", () => {
172
218
  {{/if}}
173
219
  {{#if swagger}}
174
220
  generatedOpenApi = await createDocs(app, {
175
- title: "Generated API",
176
221
  integrations: {
177
222
  {{#if isBetterAuth}}
178
223
  betterAuth: { auth: app.get(AuthService).instance, pathPrefix: "/api/auth" },
@@ -182,6 +227,7 @@ describe("AppController (e2e)", () => {
182
227
  {{/if}}
183
228
  zod: { withFastify: false },
184
229
  },
230
+ title: "Generated API",
185
231
  });
186
232
  {{#if useSwaggerScalar}}
187
233
  app.use("/docs", apiReference({
@@ -191,9 +237,18 @@ describe("AppController (e2e)", () => {
191
237
  }));
192
238
  {{/if}}
193
239
  {{/if}}
240
+ {{#if (eq httpProvider "fastify")}}
194
241
  await app.init();
242
+ {{else}}
243
+ return app.init();
244
+ {{/if}}
195
245
  {{#if (eq httpProvider "fastify")}}
196
- await app.getHttpAdapter().getInstance().ready();
246
+ await app.listen(fastifyPort, "127.0.0.1");
247
+ const address = app.getHttpAdapter().getInstance().server.address();
248
+ if (!address || typeof address === "string") {
249
+ throw new Error("Could not allocate a Fastify test port.");
250
+ }
251
+ fastifyPort = address.port;
197
252
  {{/if}}
198
253
  }
199
254
  {{#if (eq auth "better-auth:stateless")}}
@@ -202,20 +257,18 @@ describe("AppController (e2e)", () => {
202
257
  await startApplication();
203
258
  });
204
259
  {{else if (and isPassportAuth (not hasDatabase))}}
205
- beforeAll(async () => {
206
- await startIdentityProvider();
207
- await startApplication();
208
- });
260
+ beforeAll(startIdentityProvider);
261
+ beforeAll(startApplication, 30_000);
209
262
  {{else}}
210
- beforeAll(startApplication);
263
+ beforeAll(startApplication, 30_000);
211
264
  {{/if}}
212
265
 
213
266
  {{#if (and isPassportAuth (not hasDatabase))}}
214
267
  it("accepts only RS256 access tokens from the configured external issuer", async () => {
215
268
  const server = app.getHttpServer();
216
- await request(server).get("/auth/profile").expect(401);
217
- await request(server).post("/auth/login").send({ id: "attacker" }).expect(404);
218
- const authenticated = await request(server)
269
+ await request(getRequestTarget(server)).get("/auth/profile").expect(401);
270
+ await request(getRequestTarget(server)).post("/auth/login").send({ id: "attacker" }).expect(404);
271
+ const authenticated = await request(getRequestTarget(server))
219
272
  .get("/auth/profile")
220
273
  .set("Authorization", `Bearer ${signAccessToken()}`)
221
274
  .expect(200);
@@ -226,16 +279,18 @@ describe("AppController (e2e)", () => {
226
279
  userId: "external-user-1",
227
280
  });
228
281
 
229
- for (const claims of [
230
- { iss: "http://attacker.example/" },
231
- { aud: "another-resource-server" },
232
- { exp: Math.floor(Date.now() / 1000) - 30 },
233
- ]) {
234
- await request(server)
235
- .get("/auth/profile")
236
- .set("Authorization", `Bearer ${signAccessToken(claims)}`)
237
- .expect(401);
238
- }
282
+ const invalidClaimResponses = await Promise.all(
283
+ [
284
+ { iss: "http://attacker.example/" },
285
+ { aud: "another-resource-server" },
286
+ { exp: Math.floor(Date.now() / 1000) - 30 },
287
+ ].map((claims) =>
288
+ request(getRequestTarget(server))
289
+ .get("/auth/profile")
290
+ .set("Authorization", `Bearer ${signAccessToken(claims)}`)
291
+ )
292
+ );
293
+ expect(invalidClaimResponses.every((response) => response.status === 401)).toBe(true);
239
294
  });
240
295
  {{/if}}
241
296
 
@@ -245,18 +300,22 @@ describe("AppController (e2e)", () => {
245
300
  {{#if isBetterAuth}}
246
301
  expect(generatedOpenApi.paths?.["/api/auth/sign-up/email"]).toBeDefined();
247
302
  {{else if isPassportAuth}}
248
- {{#if (and usePrisma hasRest (not hasMongodb))}}
303
+ {{#if (not hasDatabase)}}
304
+ expect(generatedOpenApi.paths?.["/auth/profile"]).toBeDefined();
305
+ {{else if (and usePrisma hasRest (not hasMongodb))}}
249
306
  expect(generatedOpenApi.paths?.["/auth/register"]).toBeDefined();
250
307
  {{else}}
251
308
  expect(generatedOpenApi.paths?.["/auth/signup"]).toBeDefined();
252
309
  {{/if}}
253
310
  expect(generatedOpenApi.components?.securitySchemes?.bearer).toBeDefined();
254
311
  {{/if}}
255
- {{#if hasTodoExample}}
312
+ {{#if (and hasTodoExample hasRest)}}
256
313
  expect(generatedOpenApi.paths?.["/todos"]?.post?.requestBody).toBeDefined();
314
+ {{else if (and hasHealthCheck (not hasRest))}}
315
+ expect(generatedOpenApi.paths?.["/health"]?.get).toBeDefined();
257
316
  {{/if}}
258
317
  {{#if useSwaggerScalar}}
259
- const reference = await request(app.getHttpServer()).get("/docs").expect(200);
318
+ const reference = await request(getRequestTarget()).get("/docs").expect(200);
260
319
  expect(reference.text).toMatch(/scalar/u);
261
320
  {{/if}}
262
321
  });
@@ -264,7 +323,7 @@ describe("AppController (e2e)", () => {
264
323
 
265
324
  {{#if hasRest}}
266
325
  it("/ (GET)", async () => {
267
- const response = await request(app.getHttpServer()).get("/").expect(200);
326
+ const response = await request(getRequestTarget()).get("/").expect(200);
268
327
  expect(response.text).toBe("Hello World!");
269
328
  });
270
329
  {{/if}}
@@ -275,31 +334,31 @@ describe("AppController (e2e)", () => {
275
334
  const password = "Scenario-password-123!";
276
335
  async function register(prefix: string) {
277
336
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
278
- await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}})
337
+ await request(getRequestTarget(server)).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}})
279
338
  .send({ email, password }).expect(201);
280
- const response = await request(server).post("/auth/login")
339
+ const response = await request(getRequestTarget(server)).post("/auth/login")
281
340
  .send({ email, password }).expect(200);
282
341
  expect(response.body.accessToken).toBeTruthy();
283
342
  return response.body.accessToken as string;
284
343
  }
285
344
  const alice = await register("passport-alice");
286
345
  const bob = await register("passport-bob");
287
- const created = await request(server).post("/todos")
346
+ const created = await request(getRequestTarget(server)).post("/todos")
288
347
  .set("Authorization", `Bearer ${alice}`)
289
348
  .send({ title: "Private Passport todo", completed: false }).expect(201);
290
349
  const todo = Array.isArray(created.body) ? created.body[0] : created.body;
291
350
  const { id } = todo;
292
- const bobList = await request(server).get("/todos")
351
+ const bobList = await request(getRequestTarget(server)).get("/todos")
293
352
  .set("Authorization", `Bearer ${bob}`).expect(200);
294
353
  expect(bobList.body).not.toEqual(
295
354
  expect.arrayContaining([expect.objectContaining({ id })])
296
355
  );
297
- await request(server).put(`/todos/${id}`)
356
+ await request(getRequestTarget(server)).put(`/todos/${id}`)
298
357
  .set("Authorization", `Bearer ${bob}`)
299
358
  .send({ completed: true }).expect(404);
300
- await request(server).delete(`/todos/${id}`)
359
+ await request(getRequestTarget(server)).delete(`/todos/${id}`)
301
360
  .set("Authorization", `Bearer ${bob}`).expect(404);
302
- const aliceList = await request(server).get("/todos")
361
+ const aliceList = await request(getRequestTarget(server)).get("/todos")
303
362
  .set("Authorization", `Bearer ${alice}`).expect(200);
304
363
  expect(aliceList.body).toEqual(
305
364
  expect.arrayContaining([
@@ -309,9 +368,45 @@ describe("AppController (e2e)", () => {
309
368
  });
310
369
  {{/if}}
311
370
 
371
+ {{#if (and isPassportAuth (not hasDatabase) hasTodoExample hasRest)}}
372
+ it("protects external-identity REST todos and isolates owners", async () => {
373
+ const server = app.getHttpServer();
374
+ await request(getRequestTarget(server)).get("/todos").expect(401);
375
+ const aliceToken = signAccessToken();
376
+ const bobToken = signAccessToken({ sub: "external-user-2" });
377
+ const created = await request(getRequestTarget(server))
378
+ .post("/todos")
379
+ .set("Authorization", `Bearer ${aliceToken}`)
380
+ .send({ title: "External identity todo", completed: false })
381
+ .expect(201);
382
+ const todo = created.body as { id: string };
383
+ const bobTodos = await request(getRequestTarget(server))
384
+ .get("/todos")
385
+ .set("Authorization", `Bearer ${bobToken}`)
386
+ .expect(200);
387
+ expect(bobTodos.body).toEqual([]);
388
+ const aliceTodos = await request(getRequestTarget(server))
389
+ .get("/todos")
390
+ .set("Authorization", `Bearer ${aliceToken}`)
391
+ .expect(200);
392
+ expect(aliceTodos.body).toEqual(
393
+ expect.arrayContaining([expect.objectContaining({ id: todo.id })])
394
+ );
395
+ await request(getRequestTarget(server))
396
+ .put(`/todos/${todo.id}`)
397
+ .set("Authorization", `Bearer ${aliceToken}`)
398
+ .send({ completed: true })
399
+ .expect(200);
400
+ await request(getRequestTarget(server))
401
+ .delete(`/todos/${todo.id}`)
402
+ .set("Authorization", `Bearer ${aliceToken}`)
403
+ .expect(200);
404
+ });
405
+ {{/if}}
406
+
312
407
  {{#if hasHealthCheck}}
313
408
  it("exposes unauthenticated health and readiness", async () => {
314
- const response = await request(app.getHttpServer()).get("/health").expect(200);
409
+ const response = await request(getRequestTarget()).get("/health").expect(200);
315
410
  expect(response.body.status).toBe("ok");
316
411
  expect(response.body.info.app.status).toBe("up");
317
412
  {{#if hasDatabase}}
@@ -323,11 +418,11 @@ describe("AppController (e2e)", () => {
323
418
  {{#if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}
324
419
  it("reports database readiness failure with HTTP 503", async () => {
325
420
  const db = app.get(DbService);
326
- const database = {{#if hasMongodb}}db.raw.mongo{{else if usePrisma}}db.prisma{{else if useTypeorm}}db.typeorm{{else}}db.drizzle{{/if}};
327
- const query = vi.spyOn(database, {{#if hasMongodb}}{{#if usePrisma}}"$runCommandRaw"{{else}}"command"{{/if}}{{else if usePrisma}}"$queryRaw"{{else if useTypeorm}}"query"{{else if hasSqlite}}"get"{{else}}"execute"{{/if}})
421
+ const database = {{#if hasMongodb}}db.raw.mongo{{else if isPrisma8}}db.prisma.orm.public.Todo{{else if usePrisma}}db.prisma{{else if useTypeorm}}db.typeorm{{else}}db.drizzle{{/if}};
422
+ const query = vi.spyOn(database, {{#if hasMongodb}}{{#if usePrisma}}"$runCommandRaw"{{else}}"command"{{/if}}{{else if isPrisma8}}"all"{{else if usePrisma}}"$queryRaw"{{else if useTypeorm}}"query"{{else if hasSqlite}}"get"{{else}}"execute"{{/if}})
328
423
  .mockRejectedValue(new Error("scenario database outage"));
329
424
  try {
330
- const response = await request(app.getHttpServer()).get("/health").expect(503);
425
+ const response = await request(getRequestTarget()).get("/health").expect(503);
331
426
  expect(response.body.status).toBe("error");
332
427
  expect(response.body.error.db.status).toBe("down");
333
428
  } finally {
@@ -338,20 +433,51 @@ describe("AppController (e2e)", () => {
338
433
 
339
434
  {{#if hasTrpc}}
340
435
  it("executes a tRPC query through HTTP", async () => {
341
- const response = await request(app.getHttpServer())
436
+ const response = await request(getRequestTarget())
342
437
  .get("/trpc/appRouter.getHello").expect(200);
343
438
  expect(response.body.result.data).toBe("Hello World!");
344
439
  });
345
440
  {{/if}}
346
441
 
347
- {{#if (and hasTrpc hasTodoExample hasAuth)}}
442
+ {{#if (and hasTrpc hasTodoExample (eq auth "better-auth:stateless"))}}
443
+ it("protects stateless tRPC todo CRUD with an OIDC session", async () => {
444
+ const server = app.getHttpServer();
445
+ const call = (procedure: string, input?: unknown, cookies?: string[]) => {
446
+ const operation = procedure === "findAll"
447
+ ? request(getRequestTarget(server)).get("/trpc/todoRouter.findAll")
448
+ : request(getRequestTarget(server)).post(`/trpc/todoRouter.${procedure}`);
449
+ if (cookies) operation.set("Cookie", cookies);
450
+ if (input !== undefined) operation.send(input);
451
+ return operation;
452
+ };
453
+ await call("findAll").expect(401);
454
+ const signIn = await request(getRequestTarget(server)).post("/api/auth/sign-in/social")
455
+ .set("Origin", "http://localhost:3000")
456
+ .send({ callbackURL: "/", provider: "scenario-oidc" }).expect(200);
457
+ const authorization = new URL(signIn.body.url);
458
+ const providerResponse = await request(oidcServer)
459
+ .get(`${authorization.pathname}${authorization.search}`).expect(302);
460
+ const callback = new URL(providerResponse.headers.location);
461
+ const session = await request(getRequestTarget(server))
462
+ .get(`${callback.pathname}${callback.search}`)
463
+ .set("Cookie", signIn.headers["set-cookie"] as string[])
464
+ .set("Origin", "http://localhost:3000").expect(302);
465
+ const cookies = session.headers["set-cookie"] as string[];
466
+ expect(cookies).toBeTruthy();
467
+ const created = await call("create", { title: "OIDC tRPC todo", completed: false }, cookies).expect(200);
468
+ const todo = (created.body.result?.data?.json ?? created.body.result?.data) as { id: string | number };
469
+ await call("findAll", undefined, cookies).expect(200);
470
+ await call("update", { id: todo.id, data: { completed: true } }, cookies).expect(200);
471
+ await call("delete", { id: todo.id }, cookies).expect(200);
472
+ });
473
+ {{else if (and hasTrpc hasTodoExample hasAuth)}}
348
474
  it("enforces todo ownership through authenticated tRPC procedures", async () => {
349
475
  const server = app.getHttpServer();
350
476
  const password = "Scenario-password-123!";
351
477
  const call = (procedure: string, input?: unknown, credential?: { cookie?: string[]; token?: string }) => {
352
478
  const operation = procedure === "findAll"
353
- ? request(server).get("/trpc/todoRouter.findAll")
354
- : request(server).post(`/trpc/todoRouter.${procedure}`);
479
+ ? request(getRequestTarget(server)).get("/trpc/todoRouter.findAll")
480
+ : request(getRequestTarget(server)).post(`/trpc/todoRouter.${procedure}`);
355
481
  if (credential?.cookie) operation.set("Cookie", credential.cookie);
356
482
  if (credential?.token) operation.set("Authorization", `Bearer ${credential.token}`);
357
483
  if (input !== undefined) operation.send(input);
@@ -361,18 +487,18 @@ describe("AppController (e2e)", () => {
361
487
  async function register(prefix: string) {
362
488
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
363
489
  const origin = "http://localhost:3000";
364
- const response = await request(server).post("/api/auth/sign-up/email")
490
+ const response = await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
365
491
  .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
366
492
  return { cookie: response.headers["set-cookie"] as string[] };
367
493
  }
368
494
  {{else if isPassportAuth}}
369
495
  async function register(prefix: string) {
370
496
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
371
- await request(server)
497
+ await request(getRequestTarget(server))
372
498
  .post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}})
373
499
  {{#if usePrisma}}.send({ email, password }){{else}}.send({ email, password, name: prefix }){{/if}}
374
500
  .expect(201);
375
- const login = await request(server).post("/auth/login").send({ email, password }).expect(200);
501
+ const login = await request(getRequestTarget(server)).post("/auth/login").send({ email, password }).expect(200);
376
502
  return { token: login.body.accessToken as string };
377
503
  }
378
504
  {{/if}}
@@ -402,7 +528,7 @@ describe("AppController (e2e)", () => {
402
528
  {{#if (and hasTrpc hasTodoExample (not hasAuth))}}
403
529
  it("creates, reads, updates, and deletes tRPC todos", async () => {
404
530
  const server = app.getHttpServer();
405
- const create = await request(server)
531
+ const create = await request(getRequestTarget(server))
406
532
  .post("/trpc/todoRouter.create")
407
533
  .send({ title: "Public tRPC todo", completed: false })
408
534
  .expect(200);
@@ -410,18 +536,18 @@ describe("AppController (e2e)", () => {
410
536
  expect(created.title).toBe("Public tRPC todo");
411
537
  const id = created.id as string | number;
412
538
 
413
- const list = await request(server).get("/trpc/todoRouter.findAll").expect(200);
539
+ const list = await request(getRequestTarget(server)).get("/trpc/todoRouter.findAll").expect(200);
414
540
  const todos = list.body.result?.data?.json ?? list.body.result?.data;
415
541
  expect(todos).toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
416
542
 
417
- const update = await request(server)
543
+ const update = await request(getRequestTarget(server))
418
544
  .post("/trpc/todoRouter.update")
419
545
  .send({ id, data: { completed: true } })
420
546
  .expect(200);
421
547
  const updated = update.body.result?.data?.json ?? update.body.result?.data;
422
548
  expect(updated.completed).toBe(true);
423
549
 
424
- await request(server).post("/trpc/todoRouter.delete").send({ id }).expect(200);
550
+ await request(getRequestTarget(server)).post("/trpc/todoRouter.delete").send({ id }).expect(200);
425
551
  });
426
552
  {{/if}}
427
553
 
@@ -432,73 +558,49 @@ describe("AppController (e2e)", () => {
432
558
  expect(password.length).toBeGreaterThanOrEqual(8);
433
559
  const origin = "http://localhost:3000";
434
560
  let server = app.getHttpServer();
435
- const signedUp = await request(server).post("/api/auth/sign-up/email")
561
+ const signedUp = await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
436
562
  .set("Origin", origin).send({ email, password, name: "Scenario" }).expect(200);
437
563
  expect(signedUp.body.user.email).toBe(email);
438
564
  expect(signedUp.body.user.password).toBeUndefined();
439
- await request(server).post("/api/auth/sign-up/email")
565
+ await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
440
566
  .set("Origin", origin).send({ email, password, name: "Duplicate" }).expect(422);
441
- await request(server).post("/api/auth/sign-in/email")
567
+ await request(getRequestTarget(server)).post("/api/auth/sign-in/email")
442
568
  .set("Origin", origin).send({ email, password: "wrong-password" }).expect(401);
443
- const signedIn = await request(server).post("/api/auth/sign-in/email")
569
+ const signedIn = await request(getRequestTarget(server)).post("/api/auth/sign-in/email")
444
570
  .set("Origin", origin).send({ email, password }).expect(200);
445
571
  const cookies = signedIn.headers["set-cookie"];
446
572
  expect(cookies).toBeTruthy();
447
- const session = await request(server).get("/api/auth/get-session")
573
+ const session = await request(getRequestTarget(server)).get("/api/auth/get-session")
448
574
  .set("Cookie", cookies).expect(200);
449
575
  expect(session.body.user.email).toBe(email);
450
576
  {{#if (or usePrisma useDrizzle)}}
451
- await request(server).get("/auth-profile").expect(401);
452
- const protectedProfile = await request(server).get("/auth-profile").set("Cookie", cookies).expect(200);
577
+ await request(getRequestTarget(server)).get("/auth-profile").expect(401);
578
+ const protectedProfile = await request(getRequestTarget(server)).get("/auth-profile").set("Cookie", cookies).expect(200);
453
579
  expect(protectedProfile.body.email).toBe(email);
454
580
  await app.close();
455
581
  await startApplication();
456
582
  server = app.getHttpServer();
457
- const persisted = await request(app.getHttpServer()).get("/api/auth/get-session").set("Cookie", cookies).expect(200);
583
+ const persisted = await request(getRequestTarget()).get("/api/auth/get-session").set("Cookie", cookies).expect(200);
458
584
  expect(persisted.body.user.email).toBe(email);
459
585
  {{/if}}
460
586
  {{#if hasTrpc}}
461
- await request(server).get("/trpc/appRouter.testAuth").expect(401);
462
- const authenticated = await request(server).get("/trpc/appRouter.testAuth")
587
+ await request(getRequestTarget(server)).get("/trpc/appRouter.testAuth").expect(401);
588
+ const authenticated = await request(getRequestTarget(server)).get("/trpc/appRouter.testAuth")
463
589
  .set("Cookie", cookies).expect(200);
464
590
  expect(authenticated.body.result.data.user.email).toBe(email);
465
591
  {{/if}}
466
- await request(server).post("/api/auth/sign-out").set("Origin", origin)
592
+ await request(getRequestTarget(server)).post("/api/auth/sign-out").set("Origin", origin)
467
593
  .set("Cookie", cookies).send({}).expect(200);
468
- const revoked = await request(server).get("/api/auth/get-session")
594
+ const revoked = await request(getRequestTarget(server)).get("/api/auth/get-session")
469
595
  .set("Cookie", cookies).expect(200);
470
596
  expect(revoked.body).toBeNull();
471
597
  });
472
598
  {{/if}}
473
599
 
474
- {{#if (and isBetterAuth hasRateLimiting)}}
475
- it("keeps normal sign-in usable and throttles repeated authentication requests", async () => {
476
- const server = app.getHttpServer();
477
- const origin = "http://localhost:3000";
478
- const email = `ratelimit-${Date.now()}@example.com`;
479
- const password = "Scenario-password-123!";
480
- await request(server).post("/api/auth/sign-up/email")
481
- .set("Origin", origin).send({ email, password, name: "Rate limit" }).expect(200);
482
- await request(server).post("/api/auth/sign-in/email")
483
- .set("Origin", origin).send({ email, password }).expect(200);
484
- const responses = await Promise.all(
485
- Array.from({ length: 80 }, () =>
486
- request(server)
487
- .post("/api/auth/sign-in/email")
488
- .set("Origin", origin)
489
- .send({ email: "rate-limited@example.com", password: "wrong-password" })
490
- )
491
- );
492
- expect(responses.some((response) => response.status === 429)).toBe(true);
493
- await app.close();
494
- await startApplication();
495
- }, 30_000);
496
- {{/if}}
497
-
498
600
  {{#if (eq auth "better-auth:stateless")}}
499
601
  it("completes an OIDC authorization-code flow without an application database", async () => {
500
602
  const server = app.getHttpServer();
501
- const signIn = await request(server).post("/api/auth/sign-in/social")
603
+ const signIn = await request(getRequestTarget(server)).post("/api/auth/sign-in/social")
502
604
  .set("Origin", "http://localhost:3000")
503
605
  .send({ callbackURL: "/", provider: "scenario-oidc" }).expect(200);
504
606
  const authorization = new URL(signIn.body.url);
@@ -507,19 +609,19 @@ expect(revoked.body).toBeNull();
507
609
  expect(stateCookies).toBeTruthy();
508
610
  const providerResponse = await request(oidcServer).get(`${authorization.pathname}${authorization.search}`).expect(302);
509
611
  const callback = new URL(providerResponse.headers.location);
510
- const session = await request(server).get(`${callback.pathname}${callback.search}`)
612
+ const session = await request(getRequestTarget(server)).get(`${callback.pathname}${callback.search}`)
511
613
  .set("Cookie", stateCookies).set("Origin", "http://localhost:3000").expect(302);
512
614
  const cookies = session.headers["set-cookie"];
513
615
  expect(cookies).toBeTruthy();
514
- const currentSession = await request(server).get("/api/auth/get-session").set("Cookie", cookies).expect(200);
616
+ const currentSession = await request(getRequestTarget(server)).get("/api/auth/get-session").set("Cookie", cookies).expect(200);
515
617
  expect(currentSession.body.user.email).toBe("oidc-scenario@example.com");
516
618
  {{#if hasTrpc}}
517
- await request(server).get("/trpc/appRouter.testAuth").expect(401);
518
- const authenticated = await request(server).get("/trpc/appRouter.testAuth")
619
+ await request(getRequestTarget(server)).get("/trpc/appRouter.testAuth").expect(401);
620
+ const authenticated = await request(getRequestTarget(server)).get("/trpc/appRouter.testAuth")
519
621
  .set("Cookie", cookies).expect(200);
520
622
  expect(authenticated.body.result.data.user.email).toBe("oidc-scenario@example.com");
521
623
  {{/if}}
522
- await request(server).get("/api/auth/callback/scenario-oidc?code=scenario-code&state=tampered")
624
+ await request(getRequestTarget(server)).get("/api/auth/callback/scenario-oidc?code=scenario-code&state=tampered")
523
625
  .set("Origin", "http://localhost:3000").expect(302);
524
626
  });
525
627
  {{/if}}
@@ -527,25 +629,10 @@ expect(revoked.body).toBeNull();
527
629
  {{#if hasGraphql}}
528
630
  it("queries and mutates persisted GraphQL todos with input validation", async () => {
529
631
  const server = app.getHttpServer();
530
- const execute = (query: string, variables: Record<string, unknown> = {}) => request(server).post("/graphql").send({ query, variables });
632
+ const execute = (query: string, variables: Record<string, unknown> = {}) => request(getRequestTarget(server)).post("/graphql").send({ query, variables });
531
633
  const hello = await execute("{ hello }");
532
634
  expect(hello.body.errors).toBeUndefined();
533
635
  expect(hello.body.data.hello).toBe("Hello GraphQL!");
534
- {{#if (and hasTodoExample (eq auth "none"))}}
535
- const created = await execute('mutation($input: CreateTodoInput!) { createTodo(input: $input) { id title completed } }', { input: { title: "GraphQL integration" } });
536
- expect(created.body.errors).toBeUndefined();
537
- const { id } = created.body.data.createTodo;
538
- const listed = await execute("{ todos { id title } }");
539
- expect(listed.body.data.todos).toContainEqual({ id, title: "GraphQL integration" });
540
- const updated = await execute('mutation($input: UpdateTodoInput!) { updateTodo(input: $input) { completed } }', { input: { id, completed: true } });
541
- expect(updated.body.data.updateTodo.completed).toBe(true);
542
- const invalid = await execute('mutation { createTodo(input: { title: " " }) { id } }');
543
- expect(invalid.body.errors).toBeTruthy();
544
- const deleted = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
545
- expect(deleted.body.data.deleteTodo).toBe(true);
546
- const missing = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
547
- expect(missing.body.errors).toBeTruthy();
548
- {{/if}}
549
636
  {{#if (and hasTodoExample (eq auth "none"))}}
550
637
  const created = await execute('mutation($input: CreateTodoInput!) { createTodo(input: $input) { id title completed } }', { input: { title: "GraphQL integration" } });
551
638
  expect(created.body.errors).toBeUndefined();
@@ -568,13 +655,15 @@ expect(revoked.body).toBeNull();
568
655
  it("rejects anonymous GraphQL todo access and isolates authenticated owners", async () => {
569
656
  const server = app.getHttpServer();
570
657
  const password = "Scenario-password-123!";
658
+ {{#if isBetterAuth}}
571
659
  const origin = "http://localhost:3000";
660
+ {{/if}}
572
661
  const execute = (
573
662
  query: string,
574
663
  variables: Record<string, unknown> = {},
575
664
  credentials: { cookie?: string[]; token?: string } = {}
576
665
  ) => {
577
- const operation = request(server).post("/graphql").send({ query, variables });
666
+ const operation = request(getRequestTarget(server)).post("/graphql").send({ query, variables });
578
667
  if (credentials.cookie) operation.set("Cookie", credentials.cookie);
579
668
  if (credentials.token) operation.set("Authorization", `Bearer ${credentials.token}`);
580
669
  return operation;
@@ -582,14 +671,14 @@ expect(revoked.body).toBeNull();
582
671
  {{#if isBetterAuth}}
583
672
  async function register(prefix: string) {
584
673
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
585
- const response = await request(server).post("/api/auth/sign-up/email")
674
+ const response = await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
586
675
  .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
587
676
  return { cookie: response.headers["set-cookie"] };
588
677
  }
589
678
  {{else if isPassportAuth}}
590
679
  async function register(prefix: string) {
591
680
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
592
- const response = await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}})
681
+ const response = await request(getRequestTarget(server)).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}})
593
682
  .send({ email, password }).expect(201);
594
683
  return { token: response.body.accessToken as string };
595
684
  }
@@ -628,18 +717,26 @@ expect(revoked.body).toBeNull();
628
717
 
629
718
  {{#if (and hasGraphql hasRateLimiting)}}
630
719
  it("throttles repeated GraphQL requests to 429", async () => {
631
- let throttled = false;
632
- let lastResponse = "";
633
720
  app.getHttpServer().setMaxListeners(100);
634
- for (let attempt = 0; attempt < 80; attempt += 1) {
635
- const response = await request(app.getHttpServer()).post("/graphql").send({ query: "{ hello }" });
636
- lastResponse = JSON.stringify({ errors: response.body.errors, status: response.status });
637
- throttled ||= response.status === 429 || response.body.errors?.some((error: { message?: string; extensions?: { originalError?: { statusCode?: number } } }) =>
638
- error.extensions?.originalError?.statusCode === 429 ||
639
- error.message?.includes("ThrottlerException: Too Many Requests") === true
640
- ) === true;
641
- }
642
- expect(throttled, lastResponse).toBe(true);
721
+ const responses = await Promise.all(
722
+ Array.from({ length: 80 }, () =>
723
+ request(getRequestTarget()).post("/graphql").send({ query: "{ hello }" })
724
+ )
725
+ );
726
+ const lastResponse = responses.at(-1);
727
+ const responseSummary = lastResponse
728
+ ? JSON.stringify({ errors: lastResponse.body.errors, status: lastResponse.status })
729
+ : "GraphQL rate-limit probe produced no responses";
730
+ const throttled = responses.some(
731
+ (response) =>
732
+ response.status === 429 ||
733
+ response.body.errors?.some(
734
+ (error: { message?: string; extensions?: { originalError?: { statusCode?: number } } }) =>
735
+ error.extensions?.originalError?.statusCode === 429 ||
736
+ error.message?.includes("ThrottlerException: Too Many Requests") === true
737
+ ) === true
738
+ );
739
+ expect(throttled, responseSummary).toBe(true);
643
740
  }, 30_000);
644
741
  {{/if}}
645
742
 
@@ -648,14 +745,17 @@ expect(revoked.body).toBeNull();
648
745
  const server = app.getHttpServer();
649
746
  const email = `passport-ratelimit-${Date.now()}@example.com`;
650
747
  const password = "Scenario-password-123!";
651
- await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
652
- await request(server).post("/auth/login").send({ email, password }).expect(200);
653
- const responses = await Promise.all(
654
- Array.from({ length: 80 }, () =>
655
- request(server).post("/auth/login").send({ email, password: "Invalid-password-123!" })
656
- )
657
- );
658
- expect(responses.some((response) => response.status === 429)).toBe(true);
748
+ await request(getRequestTarget(server)).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
749
+ await request(getRequestTarget(server)).post("/auth/login").send({ email, password }).expect(200);
750
+ async function tryLoginUntilThrottled(attemptsRemaining: number): Promise<boolean> {
751
+ if (attemptsRemaining === 0) {
752
+ return false;
753
+ }
754
+ const response = await request(getRequestTarget(server)).post("/auth/login").send({ email, password: "Invalid-password-123!" });
755
+ return response.status === 429 || tryLoginUntilThrottled(attemptsRemaining - 1);
756
+ }
757
+ const throttled = await tryLoginUntilThrottled(80);
758
+ expect(throttled).toBe(true);
659
759
  await app.close();
660
760
  await startApplication();
661
761
  }, 30_000);
@@ -665,14 +765,11 @@ expect(revoked.body).toBeNull();
665
765
  it("throttles repeated tRPC procedure requests", async () => {
666
766
  const server = app.getHttpServer();
667
767
  server.setMaxListeners(100);
668
- const procedure = () => request(server).get(
768
+ const procedure = () => request(getRequestTarget(server)).get(
669
769
  "/trpc/appRouter.{{#if hasTrpcAndAuth}}testAuth{{else}}getHello{{/if}}"
670
770
  );
671
- {{#if hasTrpcAndAuth}}
672
- expect((await procedure()).status).toBe(401);
673
- {{else}}
674
- expect((await procedure()).status).toBe(200);
675
- {{/if}}
771
+ const initialResponse = await procedure();
772
+ expect(initialResponse.status).toBe({{#if hasTrpcAndAuth}}401{{else}}200{{/if}});
676
773
  const responses = await Promise.all(
677
774
  Array.from({ length: 80 }, () => procedure())
678
775
  );
@@ -685,8 +782,10 @@ expect(revoked.body).toBeNull();
685
782
  const appServer = app.getHttpServer();
686
783
  const email = `passport-proxy-${Date.now()}@example.com`;
687
784
  const password = "Scenario-password-123!";
688
- await request(appServer).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
785
+ await request(getRequestTarget(appServer)).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
786
+ {{#unless (eq httpProvider "fastify")}}
689
787
  await new Promise<void>((resolve) => appServer.listen(0, "127.0.0.1", resolve));
788
+ {{/unless}}
690
789
  const appAddress = appServer.address();
691
790
  if (!appAddress || typeof appAddress === "string") {
692
791
  throw new Error("Application did not bind a TCP port for the proxy scenario");
@@ -745,44 +844,44 @@ expect(revoked.body).toBeNull();
745
844
  const server = app.getHttpServer();
746
845
  const email = `passport-${Date.now()}@example.com`;
747
846
  const password = "Scenario-password-123!";
748
- const registered = await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
847
+ const registered = await request(getRequestTarget(server)).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
749
848
  {{#if (and usePrisma hasRest (not hasMongodb))}}
750
849
  expect(registered.body.email).toBe(email);
751
850
  {{else}}
752
851
  expect(registered.body.accessToken).toBeTruthy();
753
852
  {{/if}}
754
853
  expect(registered.body.password).toBeUndefined();
755
- await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(409);
756
- await request(server).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
757
- const login = await request(server).post("/auth/login").send({ email, password }).expect(200);
854
+ await request(getRequestTarget(server)).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(409);
855
+ await request(getRequestTarget(server)).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
856
+ const login = await request(getRequestTarget(server)).post("/auth/login").send({ email, password }).expect(200);
758
857
  expect(login.body.accessToken).toBeTruthy();
759
858
  expect(login.body.refreshToken).toBeTruthy();
760
859
  {{#if hasTodoExample}}
761
- await request(server).get("/todos").expect(401);
762
- const todos = await request(server).get("/todos")
860
+ await request(getRequestTarget(server)).get("/todos").expect(401);
861
+ const todos = await request(getRequestTarget(server)).get("/todos")
763
862
  .auth(login.body.accessToken, { type: "bearer" }).expect(200);
764
863
  expect(todos.body).toEqual([]);
765
- await request(server).post("/todos").auth(login.body.accessToken, { type: "bearer" })
864
+ await request(getRequestTarget(server)).post("/todos").auth(login.body.accessToken, { type: "bearer" })
766
865
  .send({ title: "" }).expect(400);
767
866
  {{/if}}
768
867
  {{#if (not hasMongodb)}}
769
- await request(server).get("/auth/profile").expect(401);
770
- const profile = await request(server).get("/auth/profile")
868
+ await request(getRequestTarget(server)).get("/auth/profile").expect(401);
869
+ const profile = await request(getRequestTarget(server)).get("/auth/profile")
771
870
  .auth(login.body.accessToken, { type: "bearer" }).expect(200);
772
871
  expect(profile.body.email).toBe(email);
773
872
  {{/if}}
774
- const rotated = await request(server).post("/auth/refresh").send({{#if hasMongodb}}{ token: login.body.refreshToken }{{else}}{ refreshToken: login.body.refreshToken }{{/if}}).expect(200);
873
+ const rotated = await request(getRequestTarget(server)).post("/auth/refresh").send({{#if hasMongodb}}{ token: login.body.refreshToken }{{else}}{ refreshToken: login.body.refreshToken }{{/if}}).expect(200);
775
874
  expect(rotated.body.accessToken).toBeTruthy();
776
875
  expect(rotated.body.refreshToken).toBeTruthy();
777
876
  {{#if hasMongodb}}
778
- await request(server).post("/auth/refresh").send({ token: "invalid-refresh-token" }).expect(401);
779
- const fresh = await request(server).post("/auth/login").send({ email, password }).expect(200);
877
+ await request(getRequestTarget(server)).post("/auth/refresh").send({ token: "invalid-refresh-token" }).expect(401);
878
+ const fresh = await request(getRequestTarget(server)).post("/auth/login").send({ email, password }).expect(200);
780
879
  expect(fresh.body.refreshToken).toBeTruthy();
781
880
  {{else}}
782
- await request(server).post("/auth/refresh").send({ refreshToken: login.body.refreshToken }).expect(401);
783
- const fresh = await request(server).post("/auth/login").send({ email, password }).expect(200);
784
- await request(server).post("/auth/logout").send({ refreshToken: fresh.body.refreshToken }).expect(200);
785
- await request(server).post("/auth/refresh").send({ refreshToken: fresh.body.refreshToken }).expect(401);
881
+ await request(getRequestTarget(server)).post("/auth/refresh").send({ refreshToken: login.body.refreshToken }).expect(401);
882
+ const fresh = await request(getRequestTarget(server)).post("/auth/login").send({ email, password }).expect(200);
883
+ await request(getRequestTarget(server)).post("/auth/logout").send({ refreshToken: fresh.body.refreshToken }).expect(200);
884
+ await request(getRequestTarget(server)).post("/auth/refresh").send({ refreshToken: fresh.body.refreshToken }).expect(401);
786
885
  {{/if}}
787
886
  });
788
887
  {{/if}}
@@ -791,73 +890,73 @@ expect(revoked.body).toBeNull();
791
890
  const server = app.getHttpServer();
792
891
  const email = `passport-${Date.now()}@example.com`;
793
892
  const password = "Scenario-password-123!";
794
- const signedUp = await request(server).post("/auth/signup").send({ email, password }).expect(201);
893
+ const signedUp = await request(getRequestTarget(server)).post("/auth/signup").send({ email, password }).expect(201);
795
894
  expect(signedUp.body.accessToken).toBeTruthy();
796
895
  expect(signedUp.body.refreshToken).toBeTruthy();
797
- await request(server).post("/auth/signup").send({ email, password }).expect(409);
798
- await request(server).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
799
- const login = await request(server).post("/auth/login").send({ email, password }).expect(200);
800
- const rotated = await request(server).post("/auth/refresh").send({ token: login.body.refreshToken }).expect(200);
896
+ await request(getRequestTarget(server)).post("/auth/signup").send({ email, password }).expect(409);
897
+ await request(getRequestTarget(server)).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
898
+ const login = await request(getRequestTarget(server)).post("/auth/login").send({ email, password }).expect(200);
899
+ const rotated = await request(getRequestTarget(server)).post("/auth/refresh").send({ token: login.body.refreshToken }).expect(200);
801
900
  expect(rotated.body.accessToken).toBeTruthy();
802
901
  expect(rotated.body.refreshToken).toBeTruthy();
803
902
  });
804
903
  {{/if}}
805
904
  {{#if (and hasMongodb (eq auth "none"))}}
806
905
  it("rejects malformed and missing MongoDB identifiers", async () => {
807
- await request(app.getHttpServer()).delete("/todos/invalid").expect(400);
808
- await request(app.getHttpServer()).delete("/todos/000000000000000000000000").expect(404);
809
- await request(app.getHttpServer()).post("/todos").send({ title: "" }).expect(400);
906
+ await request(getRequestTarget()).delete("/todos/invalid").expect(400);
907
+ await request(getRequestTarget()).delete("/todos/000000000000000000000000").expect(404);
908
+ await request(getRequestTarget()).post("/todos").send({ title: "" }).expect(400);
810
909
  });
811
910
  {{/if}}
812
911
 
813
912
  it("returns 404 for an unknown route", async () => {
814
- await request(app.getHttpServer()).get("/__missing_route__").expect(404);
913
+ await request(getRequestTarget()).get("/__missing_route__").expect(404);
815
914
  });
816
915
 
817
- {{#if (and useDrizzle (eq auth "none"))}}
916
+ {{#if (and useDrizzle (eq auth "none") (not hasSqlserver) hasTodoExample)}}
818
917
  it("writes and reads the migrated Drizzle schema", async () => {
819
918
  const db = app.get(DbService).drizzle;
820
- const email = `drizzle-${Date.now()}@example.com`;
919
+ const title = `Drizzle integration ${Date.now()}`;
821
920
  try {
822
- await db.insert(users).values({ email, name: "Database integration" });
823
- const rows = await db.select().from(users).where(eq(users.email, email));
921
+ await db.insert(todos).values({ title });
922
+ const rows = await db.select().from(todos).where(eq(todos.title, title));
824
923
  expect(rows).toHaveLength(1);
825
- expect(rows[0].name).toBe("Database integration");
924
+ expect(rows[0].title).toBe(title);
826
925
  } finally {
827
- await db.delete(users).where(eq(users.email, email));
926
+ await db.delete(todos).where(eq(todos.title, title));
828
927
  }
829
928
  });
830
929
  {{/if}}
831
930
 
832
931
  {{#if (and hasTodoExample hasRest (eq auth "none"))}}
833
932
  it("persists, updates and deletes a todo through HTTP", async () => {
834
- const created = await request(app.getHttpServer()).post("/todos")
835
- .send({ title: "Database integration", completed: false }).expect(201);
933
+ const created = await request(getRequestTarget()).post("/todos")
934
+ .send({ completed: false, title: "Database integration" }).expect(201);
836
935
  const { id } = created.body;
837
936
  expect(id).toBeTruthy();
838
937
  {{#if hasMongodb}}
839
938
  await app.close();
840
939
  await startApplication();
841
940
  {{/if}}
842
- const listed = await request(app.getHttpServer()).get("/todos").expect(200);
941
+ const listed = await request(getRequestTarget()).get("/todos").expect(200);
843
942
  expect(listed.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, title: "Database integration" })]));
844
- await request(app.getHttpServer()).put(`/todos/${id}`).send({ completed: true }).expect(200);
845
- const updated = await request(app.getHttpServer()).get("/todos").expect(200);
846
- expect(updated.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, completed: true })]));
847
- await request(app.getHttpServer()).delete(`/todos/${id}`).expect(200);
848
- const deleted = await request(app.getHttpServer()).get("/todos").expect(200);
943
+ await request(getRequestTarget()).put(`/todos/${id}`).send({ completed: true }).expect(200);
944
+ const updated = await request(getRequestTarget()).get("/todos").expect(200);
945
+ expect(updated.body).toEqual(expect.arrayContaining([expect.objectContaining({ completed: true, id })]));
946
+ await request(getRequestTarget()).delete(`/todos/${id}`).expect(200);
947
+ const deleted = await request(getRequestTarget()).get("/todos").expect(200);
849
948
  expect(deleted.body).not.toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
850
949
  });
851
950
  {{/if}}
852
951
 
853
- {{#if (and isBetterAuth hasTodoExample hasDatabase hasRest)}}
952
+ {{#if (and (eq auth "better-auth") hasTodoExample hasDatabase hasRest)}}
854
953
  it("enforces todo ownership across two authenticated users", async () => {
855
954
  const server = app.getHttpServer();
856
955
  const origin = "http://localhost:3000";
857
956
  const password = "Scenario-password-123!";
858
957
  async function register(prefix: string) {
859
958
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
860
- const response = await request(server).post("/api/auth/sign-up/email")
959
+ const response = await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
861
960
  .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
862
961
  const cookies = response.headers["set-cookie"];
863
962
  expect(cookies).toBeTruthy();
@@ -866,34 +965,107 @@ expect(revoked.body).toBeNull();
866
965
  const alice = await register("alice");
867
966
  const bob = await register("bob");
868
967
  {{#if (includes addons "nestjs-zod")}}
869
- await request(server).post("/todos").set("Cookie", alice)
968
+ await request(getRequestTarget(server)).post("/todos").set("Cookie", alice)
870
969
  .send({ title: "" }).expect(400);
871
970
  {{/if}}
872
- const created = await request(server).post("/todos").set("Cookie", alice)
971
+ const created = await request(getRequestTarget(server)).post("/todos").set("Cookie", alice)
873
972
  .send({ title: "Private alice todo", completed: false }).expect(201);
874
973
  const { id } = created.body;
875
974
  expect(id).toBeTruthy();
876
- const bobList = await request(server).get("/todos").set("Cookie", bob).expect(200);
975
+ const bobList = await request(getRequestTarget(server)).get("/todos").set("Cookie", bob).expect(200);
877
976
  expect(bobList.body).toEqual(expect.not.arrayContaining([expect.objectContaining({ id })]));
878
- await request(server).put(`/todos/${id}`).set("Cookie", bob)
977
+ await request(getRequestTarget(server)).put(`/todos/${id}`).set("Cookie", bob)
879
978
  .send({ completed: true }).expect(404);
880
- await request(server).delete(`/todos/${id}`).set("Cookie", bob).expect(404);
881
- const aliceList = await request(server).get("/todos").set("Cookie", alice).expect(200);
979
+ await request(getRequestTarget(server)).delete(`/todos/${id}`).set("Cookie", bob).expect(404);
980
+ const aliceList = await request(getRequestTarget(server)).get("/todos").set("Cookie", alice).expect(200);
882
981
  expect(aliceList.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, completed: false })]));
883
982
  });
884
983
  {{/if}}
885
984
 
985
+ {{#if (and (eq auth "better-auth:stateless") hasTodoExample hasRest)}}
986
+ it("protects stateless REST todo CRUD with an OIDC session", async () => {
987
+ const server = app.getHttpServer();
988
+ await request(getRequestTarget(server)).get("/todos").expect(401);
989
+ const signIn = await request(getRequestTarget(server)).post("/api/auth/sign-in/social")
990
+ .set("Origin", "http://localhost:3000")
991
+ .send({ callbackURL: "/", provider: "scenario-oidc" }).expect(200);
992
+ const authorization = new URL(signIn.body.url);
993
+ const providerResponse = await request(oidcServer)
994
+ .get(`${authorization.pathname}${authorization.search}`).expect(302);
995
+ const callback = new URL(providerResponse.headers.location);
996
+ const session = await request(getRequestTarget(server))
997
+ .get(`${callback.pathname}${callback.search}`)
998
+ .set("Cookie", signIn.headers["set-cookie"] as string[])
999
+ .set("Origin", "http://localhost:3000").expect(302);
1000
+ const cookies = session.headers["set-cookie"] as string[];
1001
+ const created = await request(getRequestTarget(server)).post("/todos")
1002
+ .set("Cookie", cookies)
1003
+ .send({ title: "OIDC protected todo", completed: false }).expect(201);
1004
+ const { id } = created.body;
1005
+ const list = await request(getRequestTarget(server)).get("/todos")
1006
+ .set("Cookie", cookies).expect(200);
1007
+ expect(list.body).toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
1008
+ await request(getRequestTarget(server)).put(`/todos/${id}`)
1009
+ .set("Cookie", cookies).send({ completed: true }).expect(200);
1010
+ await request(getRequestTarget(server)).delete(`/todos/${id}`)
1011
+ .set("Cookie", cookies).expect(200);
1012
+ });
1013
+ {{/if}}
1014
+
1015
+ {{#if (and isBetterAuth hasRateLimiting)}}
1016
+ it("keeps normal sign-in usable and throttles repeated authentication requests", async () => {
1017
+ const server = app.getHttpServer();
1018
+ {{#if (eq auth "better-auth:stateless")}}
1019
+ const origin = "http://localhost:3000";
1020
+ const signIn = () => request(getRequestTarget(server))
1021
+ .post("/api/auth/sign-in/social")
1022
+ .set("Origin", origin)
1023
+ .send({ callbackURL: "/", provider: "scenario-oidc" });
1024
+ await signIn().expect(200);
1025
+ const responses = await Promise.all(
1026
+ Array.from({ length: 80 }, () => signIn())
1027
+ );
1028
+ expect(responses.some((response) => response.status === 429)).toBe(true);
1029
+ {{else}}
1030
+ const origin = "http://localhost:3000";
1031
+ const email = `ratelimit-${Date.now()}@example.com`;
1032
+ const password = "Scenario-password-123!";
1033
+ await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
1034
+ .set("Origin", origin).send({ email, password, name: "Rate limit" }).expect(200);
1035
+ await request(getRequestTarget(server)).post("/api/auth/sign-in/email")
1036
+ .set("Origin", origin).send({ email, password }).expect(200);
1037
+ const responses = await Promise.all(
1038
+ Array.from({ length: 80 }, () =>
1039
+ request(getRequestTarget(server))
1040
+ .post("/api/auth/sign-in/email")
1041
+ .set("Origin", origin)
1042
+ .send({ email: "rate-limited@example.com", password: "wrong-password" })
1043
+ )
1044
+ );
1045
+ expect(responses.some((response) => response.status === 429)).toBe(true);
1046
+ {{/if}}
1047
+ }, 30_000);
1048
+ {{/if}}
1049
+
886
1050
  it("rejects unsupported methods", async () => {
887
- await request(app.getHttpServer()).post("/").send({ unexpected: true }).expect(404);
1051
+ await request(getRequestTarget()).post("/").send({ unexpected: true }).expect(404);
888
1052
  });
889
1053
 
890
1054
  afterAll(async () => {
891
1055
  await app?.close();
892
1056
  {{#if (eq auth "better-auth:stateless")}}
893
- await new Promise<void>((resolve, reject) => oidcServer?.close((error) => error ? reject(error) : resolve()));
1057
+ if (oidcServer?.listening) {
1058
+ const closed = once(oidcServer, "close");
1059
+ oidcServer.close();
1060
+ await closed;
1061
+ }
894
1062
  {{/if}}
895
1063
  {{#if (and isPassportAuth (not hasDatabase))}}
896
- await new Promise<void>((resolve, reject) => identityServer?.close((error) => error ? reject(error) : resolve()));
1064
+ if (identityServer?.listening) {
1065
+ const closed = once(identityServer, "close");
1066
+ identityServer.close();
1067
+ await closed;
1068
+ }
897
1069
  {{/if}}
898
1070
  });
899
1071
  });