@nest-arch/tui 0.4.5 → 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 (46) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/constants-DX5wiqrq.js +1 -0
  3. package/dist/index.js +158 -145
  4. package/dist/templates/addons/husky/noop-hook.hbs +2 -0
  5. package/dist/templates/addons/husky/pre-commit.hbs +1 -1
  6. package/dist/templates/base/.env.example.hbs +16 -2
  7. package/dist/templates/base/.env.hbs +16 -2
  8. package/dist/templates/base/README.md.hbs +23 -2
  9. package/dist/templates/base/pnpm-workspace.yaml.hbs +9 -4
  10. package/dist/templates/base/prisma.config.ts.hbs +6 -0
  11. package/dist/templates/base/src/app.module.ts.hbs +6 -22
  12. package/dist/templates/base/src/database/adapters/interfaces/db-adapter.interface.ts.hbs +1 -1
  13. package/dist/templates/base/src/database/adapters/mongo/raw-mongo.adapter.ts.hbs +4 -2
  14. package/dist/templates/base/src/database/db.module.ts.hbs +30 -3
  15. package/dist/templates/base/src/database/db.service.ts.hbs +11 -8
  16. package/dist/templates/base/src/database/entities/todo.entity.ts.hbs +2 -2
  17. package/dist/templates/base/src/database/index.ts.hbs +2 -0
  18. package/dist/templates/base/src/database/orm/drizzle/schema.ts.hbs +48 -12
  19. package/dist/templates/base/src/database/orm/typeorm/data-source.ts.hbs +26 -10
  20. package/dist/templates/base/src/libs/middlewares/graphql-throttler.guard.ts.hbs +22 -0
  21. package/dist/templates/base/src/libs/trpc/inner.context.ts.hbs +2 -2
  22. package/dist/templates/base/src/main.ts.hbs +7 -6
  23. package/dist/templates/base/test/app.e2e-spec.ts.hbs +374 -198
  24. package/dist/templates/base/test/bootstrap.e2e-spec.ts.hbs +2 -2
  25. package/dist/templates/extras/docker/Dockerfile.hbs +8 -1
  26. package/dist/templates/extras/health/src/health/health.controller.ts.hbs +7 -1
  27. package/dist/templates/extras/swagger/create-docs.ts.hbs +6 -1
  28. package/dist/templates/feature/graphql/src/graphql/models/todo.model.ts.hbs +1 -1
  29. package/dist/templates/feature/graphql/src/graphql/resolvers/todo.resolver.ts.hbs +1 -1
  30. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.controller.ts.hbs +80 -7
  31. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.service.ts.hbs +181 -25
  32. package/dist/templates/monorepo/apps/api-gateway/src/main.ts.hbs +10 -9
  33. package/dist/templates/monorepo/root/README.md.hbs +8 -13
  34. package/dist/templates/single-project/architecture/nest-api/.env.hbs +15 -2
  35. package/dist/templates/single-project/architecture/nest-api/pnpm-workspace.yaml.hbs +6 -0
  36. package/dist/templates/single-project/architecture/nest-api/src/main.ts.hbs +10 -9
  37. package/dist/templates/single-project/architecture/nest-gateway/src/main.ts.hbs +10 -9
  38. package/dist/templates/single-project/architecture/nest-microservice/.env.hbs +6 -0
  39. package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs +2 -2
  40. package/dist/templates/single-project/architecture/nest-microservice/src/app.module.ts.hbs +28 -1
  41. package/dist/templates/single-project/architecture/nest-microservice/src/http-throttler.guard.ts.hbs +13 -0
  42. package/dist/templates/single-project/architecture/nest-microservice/src/main.ts.hbs +10 -9
  43. package/dist/templates/single-project/architecture/nest-microservice/test/microservice.e2e-spec.ts.hbs +35 -0
  44. package/dist/templates/single-project/shared/config/env.ts.hbs +9 -0
  45. package/package.json +5 -5
  46. package/dist/constants-BbC7KTI7.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,14 +227,28 @@ describe("AppController (e2e)", () => {
182
227
  {{/if}}
183
228
  zod: { withFastify: false },
184
229
  },
230
+ title: "Generated API",
185
231
  });
186
232
  {{#if useSwaggerScalar}}
187
- app.use("/docs", apiReference({ content: generatedOpenApi, theme: "default" }));
233
+ app.use("/docs", apiReference({
234
+ content: generatedOpenApi,
235
+ {{#if (eq httpProvider "fastify")}}withFastify: true,{{/if}}
236
+ theme: "default",
237
+ }));
188
238
  {{/if}}
189
239
  {{/if}}
240
+ {{#if (eq httpProvider "fastify")}}
190
241
  await app.init();
242
+ {{else}}
243
+ return app.init();
244
+ {{/if}}
191
245
  {{#if (eq httpProvider "fastify")}}
192
- 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;
193
252
  {{/if}}
194
253
  }
195
254
  {{#if (eq auth "better-auth:stateless")}}
@@ -198,20 +257,18 @@ describe("AppController (e2e)", () => {
198
257
  await startApplication();
199
258
  });
200
259
  {{else if (and isPassportAuth (not hasDatabase))}}
201
- beforeAll(async () => {
202
- await startIdentityProvider();
203
- await startApplication();
204
- });
260
+ beforeAll(startIdentityProvider);
261
+ beforeAll(startApplication, 30_000);
205
262
  {{else}}
206
- beforeAll(startApplication);
263
+ beforeAll(startApplication, 30_000);
207
264
  {{/if}}
208
265
 
209
266
  {{#if (and isPassportAuth (not hasDatabase))}}
210
267
  it("accepts only RS256 access tokens from the configured external issuer", async () => {
211
268
  const server = app.getHttpServer();
212
- await request(server).get("/auth/profile").expect(401);
213
- await request(server).post("/auth/login").send({ id: "attacker" }).expect(404);
214
- 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))
215
272
  .get("/auth/profile")
216
273
  .set("Authorization", `Bearer ${signAccessToken()}`)
217
274
  .expect(200);
@@ -222,37 +279,43 @@ describe("AppController (e2e)", () => {
222
279
  userId: "external-user-1",
223
280
  });
224
281
 
225
- for (const claims of [
226
- { iss: "http://attacker.example/" },
227
- { aud: "another-resource-server" },
228
- { exp: Math.floor(Date.now() / 1000) - 30 },
229
- ]) {
230
- await request(server)
231
- .get("/auth/profile")
232
- .set("Authorization", `Bearer ${signAccessToken(claims)}`)
233
- .expect(401);
234
- }
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);
235
294
  });
236
295
  {{/if}}
237
296
 
238
297
  {{#if swagger}}
239
- it("generates authenticated OpenAPI docs and serves the Scalar reference", async () => {
298
+ it("generates authenticated OpenAPI docs and serves the Scalar reference", {{#if useSwaggerScalar}}async {{/if}}() => {
240
299
  expect(generatedOpenApi.openapi).toMatch(/^3\./u);
241
300
  {{#if isBetterAuth}}
242
301
  expect(generatedOpenApi.paths?.["/api/auth/sign-up/email"]).toBeDefined();
243
302
  {{else if isPassportAuth}}
244
- {{#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))}}
245
306
  expect(generatedOpenApi.paths?.["/auth/register"]).toBeDefined();
246
307
  {{else}}
247
308
  expect(generatedOpenApi.paths?.["/auth/signup"]).toBeDefined();
248
309
  {{/if}}
249
310
  expect(generatedOpenApi.components?.securitySchemes?.bearer).toBeDefined();
250
311
  {{/if}}
251
- {{#if hasTodoExample}}
312
+ {{#if (and hasTodoExample hasRest)}}
252
313
  expect(generatedOpenApi.paths?.["/todos"]?.post?.requestBody).toBeDefined();
314
+ {{else if (and hasHealthCheck (not hasRest))}}
315
+ expect(generatedOpenApi.paths?.["/health"]?.get).toBeDefined();
253
316
  {{/if}}
254
317
  {{#if useSwaggerScalar}}
255
- const reference = await request(app.getHttpServer()).get("/docs").expect(200);
318
+ const reference = await request(getRequestTarget()).get("/docs").expect(200);
256
319
  expect(reference.text).toMatch(/scalar/u);
257
320
  {{/if}}
258
321
  });
@@ -260,7 +323,7 @@ describe("AppController (e2e)", () => {
260
323
 
261
324
  {{#if hasRest}}
262
325
  it("/ (GET)", async () => {
263
- const response = await request(app.getHttpServer()).get("/").expect(200);
326
+ const response = await request(getRequestTarget()).get("/").expect(200);
264
327
  expect(response.text).toBe("Hello World!");
265
328
  });
266
329
  {{/if}}
@@ -271,31 +334,31 @@ describe("AppController (e2e)", () => {
271
334
  const password = "Scenario-password-123!";
272
335
  async function register(prefix: string) {
273
336
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
274
- 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}})
275
338
  .send({ email, password }).expect(201);
276
- const response = await request(server).post("/auth/login")
339
+ const response = await request(getRequestTarget(server)).post("/auth/login")
277
340
  .send({ email, password }).expect(200);
278
341
  expect(response.body.accessToken).toBeTruthy();
279
342
  return response.body.accessToken as string;
280
343
  }
281
344
  const alice = await register("passport-alice");
282
345
  const bob = await register("passport-bob");
283
- const created = await request(server).post("/todos")
346
+ const created = await request(getRequestTarget(server)).post("/todos")
284
347
  .set("Authorization", `Bearer ${alice}`)
285
348
  .send({ title: "Private Passport todo", completed: false }).expect(201);
286
349
  const todo = Array.isArray(created.body) ? created.body[0] : created.body;
287
350
  const { id } = todo;
288
- const bobList = await request(server).get("/todos")
351
+ const bobList = await request(getRequestTarget(server)).get("/todos")
289
352
  .set("Authorization", `Bearer ${bob}`).expect(200);
290
353
  expect(bobList.body).not.toEqual(
291
354
  expect.arrayContaining([expect.objectContaining({ id })])
292
355
  );
293
- await request(server).put(`/todos/${id}`)
356
+ await request(getRequestTarget(server)).put(`/todos/${id}`)
294
357
  .set("Authorization", `Bearer ${bob}`)
295
358
  .send({ completed: true }).expect(404);
296
- await request(server).delete(`/todos/${id}`)
359
+ await request(getRequestTarget(server)).delete(`/todos/${id}`)
297
360
  .set("Authorization", `Bearer ${bob}`).expect(404);
298
- const aliceList = await request(server).get("/todos")
361
+ const aliceList = await request(getRequestTarget(server)).get("/todos")
299
362
  .set("Authorization", `Bearer ${alice}`).expect(200);
300
363
  expect(aliceList.body).toEqual(
301
364
  expect.arrayContaining([
@@ -305,9 +368,45 @@ describe("AppController (e2e)", () => {
305
368
  });
306
369
  {{/if}}
307
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
+
308
407
  {{#if hasHealthCheck}}
309
408
  it("exposes unauthenticated health and readiness", async () => {
310
- const response = await request(app.getHttpServer()).get("/health").expect(200);
409
+ const response = await request(getRequestTarget()).get("/health").expect(200);
311
410
  expect(response.body.status).toBe("ok");
312
411
  expect(response.body.info.app.status).toBe("up");
313
412
  {{#if hasDatabase}}
@@ -319,11 +418,11 @@ describe("AppController (e2e)", () => {
319
418
  {{#if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}
320
419
  it("reports database readiness failure with HTTP 503", async () => {
321
420
  const db = app.get(DbService);
322
- const database = {{#if hasMongodb}}db.raw.mongo{{else if usePrisma}}db.prisma{{else if useTypeorm}}db.typeorm{{else}}db.drizzle{{/if}};
323
- 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}})
324
423
  .mockRejectedValue(new Error("scenario database outage"));
325
424
  try {
326
- const response = await request(app.getHttpServer()).get("/health").expect(503);
425
+ const response = await request(getRequestTarget()).get("/health").expect(503);
327
426
  expect(response.body.status).toBe("error");
328
427
  expect(response.body.error.db.status).toBe("down");
329
428
  } finally {
@@ -334,20 +433,51 @@ describe("AppController (e2e)", () => {
334
433
 
335
434
  {{#if hasTrpc}}
336
435
  it("executes a tRPC query through HTTP", async () => {
337
- const response = await request(app.getHttpServer())
436
+ const response = await request(getRequestTarget())
338
437
  .get("/trpc/appRouter.getHello").expect(200);
339
438
  expect(response.body.result.data).toBe("Hello World!");
340
439
  });
341
440
  {{/if}}
342
441
 
343
- {{#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)}}
344
474
  it("enforces todo ownership through authenticated tRPC procedures", async () => {
345
475
  const server = app.getHttpServer();
346
476
  const password = "Scenario-password-123!";
347
477
  const call = (procedure: string, input?: unknown, credential?: { cookie?: string[]; token?: string }) => {
348
478
  const operation = procedure === "findAll"
349
- ? request(server).get("/trpc/todoRouter.findAll")
350
- : request(server).post(`/trpc/todoRouter.${procedure}`);
479
+ ? request(getRequestTarget(server)).get("/trpc/todoRouter.findAll")
480
+ : request(getRequestTarget(server)).post(`/trpc/todoRouter.${procedure}`);
351
481
  if (credential?.cookie) operation.set("Cookie", credential.cookie);
352
482
  if (credential?.token) operation.set("Authorization", `Bearer ${credential.token}`);
353
483
  if (input !== undefined) operation.send(input);
@@ -357,18 +487,18 @@ describe("AppController (e2e)", () => {
357
487
  async function register(prefix: string) {
358
488
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
359
489
  const origin = "http://localhost:3000";
360
- const response = await request(server).post("/api/auth/sign-up/email")
490
+ const response = await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
361
491
  .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
362
492
  return { cookie: response.headers["set-cookie"] as string[] };
363
493
  }
364
494
  {{else if isPassportAuth}}
365
495
  async function register(prefix: string) {
366
496
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
367
- await request(server)
497
+ await request(getRequestTarget(server))
368
498
  .post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}})
369
499
  {{#if usePrisma}}.send({ email, password }){{else}}.send({ email, password, name: prefix }){{/if}}
370
500
  .expect(201);
371
- 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);
372
502
  return { token: login.body.accessToken as string };
373
503
  }
374
504
  {{/if}}
@@ -398,7 +528,7 @@ describe("AppController (e2e)", () => {
398
528
  {{#if (and hasTrpc hasTodoExample (not hasAuth))}}
399
529
  it("creates, reads, updates, and deletes tRPC todos", async () => {
400
530
  const server = app.getHttpServer();
401
- const create = await request(server)
531
+ const create = await request(getRequestTarget(server))
402
532
  .post("/trpc/todoRouter.create")
403
533
  .send({ title: "Public tRPC todo", completed: false })
404
534
  .expect(200);
@@ -406,18 +536,18 @@ describe("AppController (e2e)", () => {
406
536
  expect(created.title).toBe("Public tRPC todo");
407
537
  const id = created.id as string | number;
408
538
 
409
- const list = await request(server).get("/trpc/todoRouter.findAll").expect(200);
539
+ const list = await request(getRequestTarget(server)).get("/trpc/todoRouter.findAll").expect(200);
410
540
  const todos = list.body.result?.data?.json ?? list.body.result?.data;
411
541
  expect(todos).toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
412
542
 
413
- const update = await request(server)
543
+ const update = await request(getRequestTarget(server))
414
544
  .post("/trpc/todoRouter.update")
415
545
  .send({ id, data: { completed: true } })
416
546
  .expect(200);
417
547
  const updated = update.body.result?.data?.json ?? update.body.result?.data;
418
548
  expect(updated.completed).toBe(true);
419
549
 
420
- await request(server).post("/trpc/todoRouter.delete").send({ id }).expect(200);
550
+ await request(getRequestTarget(server)).post("/trpc/todoRouter.delete").send({ id }).expect(200);
421
551
  });
422
552
  {{/if}}
423
553
 
@@ -428,73 +558,49 @@ describe("AppController (e2e)", () => {
428
558
  expect(password.length).toBeGreaterThanOrEqual(8);
429
559
  const origin = "http://localhost:3000";
430
560
  let server = app.getHttpServer();
431
- const signedUp = await request(server).post("/api/auth/sign-up/email")
561
+ const signedUp = await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
432
562
  .set("Origin", origin).send({ email, password, name: "Scenario" }).expect(200);
433
563
  expect(signedUp.body.user.email).toBe(email);
434
564
  expect(signedUp.body.user.password).toBeUndefined();
435
- await request(server).post("/api/auth/sign-up/email")
565
+ await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
436
566
  .set("Origin", origin).send({ email, password, name: "Duplicate" }).expect(422);
437
- await request(server).post("/api/auth/sign-in/email")
567
+ await request(getRequestTarget(server)).post("/api/auth/sign-in/email")
438
568
  .set("Origin", origin).send({ email, password: "wrong-password" }).expect(401);
439
- const signedIn = await request(server).post("/api/auth/sign-in/email")
569
+ const signedIn = await request(getRequestTarget(server)).post("/api/auth/sign-in/email")
440
570
  .set("Origin", origin).send({ email, password }).expect(200);
441
571
  const cookies = signedIn.headers["set-cookie"];
442
572
  expect(cookies).toBeTruthy();
443
- const session = await request(server).get("/api/auth/get-session")
573
+ const session = await request(getRequestTarget(server)).get("/api/auth/get-session")
444
574
  .set("Cookie", cookies).expect(200);
445
575
  expect(session.body.user.email).toBe(email);
446
576
  {{#if (or usePrisma useDrizzle)}}
447
- await request(server).get("/auth-profile").expect(401);
448
- 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);
449
579
  expect(protectedProfile.body.email).toBe(email);
450
580
  await app.close();
451
581
  await startApplication();
452
582
  server = app.getHttpServer();
453
- 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);
454
584
  expect(persisted.body.user.email).toBe(email);
455
585
  {{/if}}
456
586
  {{#if hasTrpc}}
457
- await request(server).get("/trpc/appRouter.testAuth").expect(401);
458
- 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")
459
589
  .set("Cookie", cookies).expect(200);
460
590
  expect(authenticated.body.result.data.user.email).toBe(email);
461
591
  {{/if}}
462
- await request(server).post("/api/auth/sign-out").set("Origin", origin)
592
+ await request(getRequestTarget(server)).post("/api/auth/sign-out").set("Origin", origin)
463
593
  .set("Cookie", cookies).send({}).expect(200);
464
- const revoked = await request(server).get("/api/auth/get-session")
594
+ const revoked = await request(getRequestTarget(server)).get("/api/auth/get-session")
465
595
  .set("Cookie", cookies).expect(200);
466
596
  expect(revoked.body).toBeNull();
467
597
  });
468
598
  {{/if}}
469
599
 
470
- {{#if (and isBetterAuth hasRateLimiting)}}
471
- it("keeps normal sign-in usable and throttles repeated authentication requests", async () => {
472
- const server = app.getHttpServer();
473
- const origin = "http://localhost:3000";
474
- const email = `ratelimit-${Date.now()}@example.com`;
475
- const password = "Scenario-password-123!";
476
- await request(server).post("/api/auth/sign-up/email")
477
- .set("Origin", origin).send({ email, password, name: "Rate limit" }).expect(200);
478
- await request(server).post("/api/auth/sign-in/email")
479
- .set("Origin", origin).send({ email, password }).expect(200);
480
- const responses = await Promise.all(
481
- Array.from({ length: 80 }, () =>
482
- request(server)
483
- .post("/api/auth/sign-in/email")
484
- .set("Origin", origin)
485
- .send({ email: "rate-limited@example.com", password: "wrong-password" })
486
- )
487
- );
488
- expect(responses.some((response) => response.status === 429)).toBe(true);
489
- await app.close();
490
- await startApplication();
491
- }, 30_000);
492
- {{/if}}
493
-
494
600
  {{#if (eq auth "better-auth:stateless")}}
495
601
  it("completes an OIDC authorization-code flow without an application database", async () => {
496
602
  const server = app.getHttpServer();
497
- const signIn = await request(server).post("/api/auth/sign-in/social")
603
+ const signIn = await request(getRequestTarget(server)).post("/api/auth/sign-in/social")
498
604
  .set("Origin", "http://localhost:3000")
499
605
  .send({ callbackURL: "/", provider: "scenario-oidc" }).expect(200);
500
606
  const authorization = new URL(signIn.body.url);
@@ -503,19 +609,19 @@ expect(revoked.body).toBeNull();
503
609
  expect(stateCookies).toBeTruthy();
504
610
  const providerResponse = await request(oidcServer).get(`${authorization.pathname}${authorization.search}`).expect(302);
505
611
  const callback = new URL(providerResponse.headers.location);
506
- const session = await request(server).get(`${callback.pathname}${callback.search}`)
612
+ const session = await request(getRequestTarget(server)).get(`${callback.pathname}${callback.search}`)
507
613
  .set("Cookie", stateCookies).set("Origin", "http://localhost:3000").expect(302);
508
614
  const cookies = session.headers["set-cookie"];
509
615
  expect(cookies).toBeTruthy();
510
- 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);
511
617
  expect(currentSession.body.user.email).toBe("oidc-scenario@example.com");
512
618
  {{#if hasTrpc}}
513
- await request(server).get("/trpc/appRouter.testAuth").expect(401);
514
- 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")
515
621
  .set("Cookie", cookies).expect(200);
516
622
  expect(authenticated.body.result.data.user.email).toBe("oidc-scenario@example.com");
517
623
  {{/if}}
518
- 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")
519
625
  .set("Origin", "http://localhost:3000").expect(302);
520
626
  });
521
627
  {{/if}}
@@ -523,25 +629,10 @@ expect(revoked.body).toBeNull();
523
629
  {{#if hasGraphql}}
524
630
  it("queries and mutates persisted GraphQL todos with input validation", async () => {
525
631
  const server = app.getHttpServer();
526
- 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 });
527
633
  const hello = await execute("{ hello }");
528
634
  expect(hello.body.errors).toBeUndefined();
529
635
  expect(hello.body.data.hello).toBe("Hello GraphQL!");
530
- {{#if (and hasTodoExample (eq auth "none"))}}
531
- const created = await execute('mutation($input: CreateTodoInput!) { createTodo(input: $input) { id title completed } }', { input: { title: "GraphQL integration" } });
532
- expect(created.body.errors).toBeUndefined();
533
- const { id } = created.body.data.createTodo;
534
- const listed = await execute("{ todos { id title } }");
535
- expect(listed.body.data.todos).toContainEqual({ id, title: "GraphQL integration" });
536
- const updated = await execute('mutation($input: UpdateTodoInput!) { updateTodo(input: $input) { completed } }', { input: { id, completed: true } });
537
- expect(updated.body.data.updateTodo.completed).toBe(true);
538
- const invalid = await execute('mutation { createTodo(input: { title: " " }) { id } }');
539
- expect(invalid.body.errors).toBeTruthy();
540
- const deleted = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
541
- expect(deleted.body.data.deleteTodo).toBe(true);
542
- const missing = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
543
- expect(missing.body.errors).toBeTruthy();
544
- {{/if}}
545
636
  {{#if (and hasTodoExample (eq auth "none"))}}
546
637
  const created = await execute('mutation($input: CreateTodoInput!) { createTodo(input: $input) { id title completed } }', { input: { title: "GraphQL integration" } });
547
638
  expect(created.body.errors).toBeUndefined();
@@ -564,13 +655,15 @@ expect(revoked.body).toBeNull();
564
655
  it("rejects anonymous GraphQL todo access and isolates authenticated owners", async () => {
565
656
  const server = app.getHttpServer();
566
657
  const password = "Scenario-password-123!";
658
+ {{#if isBetterAuth}}
567
659
  const origin = "http://localhost:3000";
660
+ {{/if}}
568
661
  const execute = (
569
662
  query: string,
570
663
  variables: Record<string, unknown> = {},
571
664
  credentials: { cookie?: string[]; token?: string } = {}
572
665
  ) => {
573
- const operation = request(server).post("/graphql").send({ query, variables });
666
+ const operation = request(getRequestTarget(server)).post("/graphql").send({ query, variables });
574
667
  if (credentials.cookie) operation.set("Cookie", credentials.cookie);
575
668
  if (credentials.token) operation.set("Authorization", `Bearer ${credentials.token}`);
576
669
  return operation;
@@ -578,14 +671,14 @@ expect(revoked.body).toBeNull();
578
671
  {{#if isBetterAuth}}
579
672
  async function register(prefix: string) {
580
673
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
581
- const response = await request(server).post("/api/auth/sign-up/email")
674
+ const response = await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
582
675
  .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
583
676
  return { cookie: response.headers["set-cookie"] };
584
677
  }
585
678
  {{else if isPassportAuth}}
586
679
  async function register(prefix: string) {
587
680
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
588
- 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}})
589
682
  .send({ email, password }).expect(201);
590
683
  return { token: response.body.accessToken as string };
591
684
  }
@@ -624,18 +717,26 @@ expect(revoked.body).toBeNull();
624
717
 
625
718
  {{#if (and hasGraphql hasRateLimiting)}}
626
719
  it("throttles repeated GraphQL requests to 429", async () => {
627
- let throttled = false;
628
- let lastResponse = "";
629
720
  app.getHttpServer().setMaxListeners(100);
630
- for (let attempt = 0; attempt < 80; attempt += 1) {
631
- const response = await request(app.getHttpServer()).post("/graphql").send({ query: "{ hello }" });
632
- lastResponse = JSON.stringify({ errors: response.body.errors, status: response.status });
633
- throttled ||= response.status === 429 || response.body.errors?.some((error: { message?: string; extensions?: { originalError?: { statusCode?: number } } }) =>
634
- error.extensions?.originalError?.statusCode === 429 ||
635
- error.message?.includes("ThrottlerException: Too Many Requests") === true
636
- ) === true;
637
- }
638
- 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);
639
740
  }, 30_000);
640
741
  {{/if}}
641
742
 
@@ -644,14 +745,17 @@ expect(revoked.body).toBeNull();
644
745
  const server = app.getHttpServer();
645
746
  const email = `passport-ratelimit-${Date.now()}@example.com`;
646
747
  const password = "Scenario-password-123!";
647
- await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
648
- await request(server).post("/auth/login").send({ email, password }).expect(200);
649
- const responses = await Promise.all(
650
- Array.from({ length: 80 }, () =>
651
- request(server).post("/auth/login").send({ email, password: "Invalid-password-123!" })
652
- )
653
- );
654
- 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);
655
759
  await app.close();
656
760
  await startApplication();
657
761
  }, 30_000);
@@ -661,14 +765,11 @@ expect(revoked.body).toBeNull();
661
765
  it("throttles repeated tRPC procedure requests", async () => {
662
766
  const server = app.getHttpServer();
663
767
  server.setMaxListeners(100);
664
- const procedure = () => request(server).get(
768
+ const procedure = () => request(getRequestTarget(server)).get(
665
769
  "/trpc/appRouter.{{#if hasTrpcAndAuth}}testAuth{{else}}getHello{{/if}}"
666
770
  );
667
- {{#if hasTrpcAndAuth}}
668
- expect((await procedure()).status).toBe(401);
669
- {{else}}
670
- expect((await procedure()).status).toBe(200);
671
- {{/if}}
771
+ const initialResponse = await procedure();
772
+ expect(initialResponse.status).toBe({{#if hasTrpcAndAuth}}401{{else}}200{{/if}});
672
773
  const responses = await Promise.all(
673
774
  Array.from({ length: 80 }, () => procedure())
674
775
  );
@@ -681,8 +782,10 @@ expect(revoked.body).toBeNull();
681
782
  const appServer = app.getHttpServer();
682
783
  const email = `passport-proxy-${Date.now()}@example.com`;
683
784
  const password = "Scenario-password-123!";
684
- 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")}}
685
787
  await new Promise<void>((resolve) => appServer.listen(0, "127.0.0.1", resolve));
788
+ {{/unless}}
686
789
  const appAddress = appServer.address();
687
790
  if (!appAddress || typeof appAddress === "string") {
688
791
  throw new Error("Application did not bind a TCP port for the proxy scenario");
@@ -741,44 +844,44 @@ expect(revoked.body).toBeNull();
741
844
  const server = app.getHttpServer();
742
845
  const email = `passport-${Date.now()}@example.com`;
743
846
  const password = "Scenario-password-123!";
744
- 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);
745
848
  {{#if (and usePrisma hasRest (not hasMongodb))}}
746
849
  expect(registered.body.email).toBe(email);
747
850
  {{else}}
748
851
  expect(registered.body.accessToken).toBeTruthy();
749
852
  {{/if}}
750
853
  expect(registered.body.password).toBeUndefined();
751
- await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(409);
752
- await request(server).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
753
- 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);
754
857
  expect(login.body.accessToken).toBeTruthy();
755
858
  expect(login.body.refreshToken).toBeTruthy();
756
859
  {{#if hasTodoExample}}
757
- await request(server).get("/todos").expect(401);
758
- 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")
759
862
  .auth(login.body.accessToken, { type: "bearer" }).expect(200);
760
863
  expect(todos.body).toEqual([]);
761
- await request(server).post("/todos").auth(login.body.accessToken, { type: "bearer" })
864
+ await request(getRequestTarget(server)).post("/todos").auth(login.body.accessToken, { type: "bearer" })
762
865
  .send({ title: "" }).expect(400);
763
866
  {{/if}}
764
867
  {{#if (not hasMongodb)}}
765
- await request(server).get("/auth/profile").expect(401);
766
- 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")
767
870
  .auth(login.body.accessToken, { type: "bearer" }).expect(200);
768
871
  expect(profile.body.email).toBe(email);
769
872
  {{/if}}
770
- 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);
771
874
  expect(rotated.body.accessToken).toBeTruthy();
772
875
  expect(rotated.body.refreshToken).toBeTruthy();
773
876
  {{#if hasMongodb}}
774
- await request(server).post("/auth/refresh").send({ token: "invalid-refresh-token" }).expect(401);
775
- 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);
776
879
  expect(fresh.body.refreshToken).toBeTruthy();
777
880
  {{else}}
778
- await request(server).post("/auth/refresh").send({ refreshToken: login.body.refreshToken }).expect(401);
779
- const fresh = await request(server).post("/auth/login").send({ email, password }).expect(200);
780
- await request(server).post("/auth/logout").send({ refreshToken: fresh.body.refreshToken }).expect(200);
781
- 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);
782
885
  {{/if}}
783
886
  });
784
887
  {{/if}}
@@ -787,73 +890,73 @@ expect(revoked.body).toBeNull();
787
890
  const server = app.getHttpServer();
788
891
  const email = `passport-${Date.now()}@example.com`;
789
892
  const password = "Scenario-password-123!";
790
- 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);
791
894
  expect(signedUp.body.accessToken).toBeTruthy();
792
895
  expect(signedUp.body.refreshToken).toBeTruthy();
793
- await request(server).post("/auth/signup").send({ email, password }).expect(409);
794
- await request(server).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
795
- const login = await request(server).post("/auth/login").send({ email, password }).expect(200);
796
- 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);
797
900
  expect(rotated.body.accessToken).toBeTruthy();
798
901
  expect(rotated.body.refreshToken).toBeTruthy();
799
902
  });
800
903
  {{/if}}
801
904
  {{#if (and hasMongodb (eq auth "none"))}}
802
905
  it("rejects malformed and missing MongoDB identifiers", async () => {
803
- await request(app.getHttpServer()).delete("/todos/invalid").expect(400);
804
- await request(app.getHttpServer()).delete("/todos/000000000000000000000000").expect(404);
805
- 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);
806
909
  });
807
910
  {{/if}}
808
911
 
809
912
  it("returns 404 for an unknown route", async () => {
810
- await request(app.getHttpServer()).get("/__missing_route__").expect(404);
913
+ await request(getRequestTarget()).get("/__missing_route__").expect(404);
811
914
  });
812
915
 
813
- {{#if (and useDrizzle (eq auth "none"))}}
916
+ {{#if (and useDrizzle (eq auth "none") (not hasSqlserver) hasTodoExample)}}
814
917
  it("writes and reads the migrated Drizzle schema", async () => {
815
918
  const db = app.get(DbService).drizzle;
816
- const email = `drizzle-${Date.now()}@example.com`;
919
+ const title = `Drizzle integration ${Date.now()}`;
817
920
  try {
818
- await db.insert(users).values({ email, name: "Database integration" });
819
- 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));
820
923
  expect(rows).toHaveLength(1);
821
- expect(rows[0].name).toBe("Database integration");
924
+ expect(rows[0].title).toBe(title);
822
925
  } finally {
823
- await db.delete(users).where(eq(users.email, email));
926
+ await db.delete(todos).where(eq(todos.title, title));
824
927
  }
825
928
  });
826
929
  {{/if}}
827
930
 
828
931
  {{#if (and hasTodoExample hasRest (eq auth "none"))}}
829
932
  it("persists, updates and deletes a todo through HTTP", async () => {
830
- const created = await request(app.getHttpServer()).post("/todos")
831
- .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);
832
935
  const { id } = created.body;
833
936
  expect(id).toBeTruthy();
834
937
  {{#if hasMongodb}}
835
938
  await app.close();
836
939
  await startApplication();
837
940
  {{/if}}
838
- const listed = await request(app.getHttpServer()).get("/todos").expect(200);
941
+ const listed = await request(getRequestTarget()).get("/todos").expect(200);
839
942
  expect(listed.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, title: "Database integration" })]));
840
- await request(app.getHttpServer()).put(`/todos/${id}`).send({ completed: true }).expect(200);
841
- const updated = await request(app.getHttpServer()).get("/todos").expect(200);
842
- expect(updated.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, completed: true })]));
843
- await request(app.getHttpServer()).delete(`/todos/${id}`).expect(200);
844
- 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);
845
948
  expect(deleted.body).not.toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
846
949
  });
847
950
  {{/if}}
848
951
 
849
- {{#if (and isBetterAuth hasTodoExample hasDatabase hasRest)}}
952
+ {{#if (and (eq auth "better-auth") hasTodoExample hasDatabase hasRest)}}
850
953
  it("enforces todo ownership across two authenticated users", async () => {
851
954
  const server = app.getHttpServer();
852
955
  const origin = "http://localhost:3000";
853
956
  const password = "Scenario-password-123!";
854
957
  async function register(prefix: string) {
855
958
  const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
856
- const response = await request(server).post("/api/auth/sign-up/email")
959
+ const response = await request(getRequestTarget(server)).post("/api/auth/sign-up/email")
857
960
  .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
858
961
  const cookies = response.headers["set-cookie"];
859
962
  expect(cookies).toBeTruthy();
@@ -862,34 +965,107 @@ expect(revoked.body).toBeNull();
862
965
  const alice = await register("alice");
863
966
  const bob = await register("bob");
864
967
  {{#if (includes addons "nestjs-zod")}}
865
- await request(server).post("/todos").set("Cookie", alice)
968
+ await request(getRequestTarget(server)).post("/todos").set("Cookie", alice)
866
969
  .send({ title: "" }).expect(400);
867
970
  {{/if}}
868
- const created = await request(server).post("/todos").set("Cookie", alice)
971
+ const created = await request(getRequestTarget(server)).post("/todos").set("Cookie", alice)
869
972
  .send({ title: "Private alice todo", completed: false }).expect(201);
870
973
  const { id } = created.body;
871
974
  expect(id).toBeTruthy();
872
- 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);
873
976
  expect(bobList.body).toEqual(expect.not.arrayContaining([expect.objectContaining({ id })]));
874
- await request(server).put(`/todos/${id}`).set("Cookie", bob)
977
+ await request(getRequestTarget(server)).put(`/todos/${id}`).set("Cookie", bob)
875
978
  .send({ completed: true }).expect(404);
876
- await request(server).delete(`/todos/${id}`).set("Cookie", bob).expect(404);
877
- 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);
878
981
  expect(aliceList.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, completed: false })]));
879
982
  });
880
983
  {{/if}}
881
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
+
882
1050
  it("rejects unsupported methods", async () => {
883
- await request(app.getHttpServer()).post("/").send({ unexpected: true }).expect(404);
1051
+ await request(getRequestTarget()).post("/").send({ unexpected: true }).expect(404);
884
1052
  });
885
1053
 
886
1054
  afterAll(async () => {
887
1055
  await app?.close();
888
1056
  {{#if (eq auth "better-auth:stateless")}}
889
- 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
+ }
890
1062
  {{/if}}
891
1063
  {{#if (and isPassportAuth (not hasDatabase))}}
892
- 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
+ }
893
1069
  {{/if}}
894
1070
  });
895
1071
  });