@nest-arch/tui 0.4.6 → 0.5.1

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