@nest-arch/tui 0.4.0 → 0.4.4

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 (64) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/{constants-C3xmDWeK.js → constants-7dJLZpB2.js} +1 -1
  3. package/dist/index.js +188 -134
  4. package/dist/templates/addons/husky/pre-commit.hbs +11 -26
  5. package/dist/templates/base/.env.example.hbs +9 -0
  6. package/dist/templates/base/.env.hbs +7 -0
  7. package/dist/templates/base/README.md.hbs +16 -0
  8. package/dist/templates/base/pnpm-workspace.yaml.hbs +6 -0
  9. package/dist/templates/base/src/app.module.ts.hbs +68 -1
  10. package/dist/templates/base/src/app.router.ts.hbs +1 -2
  11. package/dist/templates/base/src/database/adapters/mongo/prisma-mongo.adapter.ts.hbs +11 -3
  12. package/dist/templates/base/src/database/adapters/sql/prisma-sql.adapter.ts.hbs +1 -1
  13. package/dist/templates/base/src/database/db.module.ts.hbs +24 -8
  14. package/dist/templates/base/src/database/db.service.ts.hbs +20 -0
  15. package/dist/templates/base/src/database/entities/app.entity.ts.hbs +25 -0
  16. package/dist/templates/base/src/database/index.ts.hbs +19 -0
  17. package/dist/templates/base/src/database/orm/drizzle/schema.ts.hbs +42 -11
  18. package/dist/templates/base/src/database/orm/typeorm/data-source.ts.hbs +12 -1
  19. package/dist/templates/base/src/libs/middlewares/auth.trpc.middleware.ts.hbs +47 -0
  20. package/dist/templates/base/src/libs/middlewares/rate-limit.trpc.middleware.ts.hbs +43 -0
  21. package/dist/templates/base/src/libs/trpc/inner.context.ts.hbs +6 -3
  22. package/dist/templates/base/src/main.ts.hbs +13 -4
  23. package/dist/templates/base/src/modules/users/entities/user.entity.ts.hbs +22 -0
  24. package/dist/templates/base/test/app.e2e-spec.ts.hbs +670 -37
  25. package/dist/templates/base/test/bootstrap.e2e-spec.ts.hbs +11 -1
  26. package/dist/templates/extras/docker/.dockerignore.hbs +8 -1
  27. package/dist/templates/extras/docker/Dockerfile.hbs +8 -10
  28. package/dist/templates/extras/health/src/health/health.controller.ts.hbs +54 -0
  29. package/dist/templates/extras/health/src/health/health.module.ts.hbs +9 -0
  30. package/dist/templates/extras/swagger/create-docs.ts.hbs +9 -3
  31. package/dist/templates/feature/graphql/src/graphql/graphql.module.ts.hbs +6 -17
  32. package/dist/templates/feature/graphql/src/graphql/orm/drizzle/drizzle-graphql.module.ts.hbs +3 -2
  33. package/dist/templates/feature/graphql/src/graphql/resolvers/health.resolver.ts.hbs +14 -0
  34. package/dist/templates/feature/graphql/src/graphql/resolvers/todo.resolver.ts.hbs +165 -53
  35. package/dist/templates/feature/rest/todo-example/src/api/todo/dto/create-todo.dto.ts.hbs +12 -0
  36. package/dist/templates/feature/rest/todo-example/src/api/todo/dto/update-todo.dto.ts.hbs +12 -0
  37. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.controller.ts.hbs +18 -8
  38. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.router.ts.hbs +41 -13
  39. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.service.ts.hbs +68 -10
  40. package/dist/templates/monorepo/apps/api-gateway/package.json.hbs +2 -2
  41. package/dist/templates/monorepo/apps/api-gateway/src/app.controller.spec.ts.hbs +5 -0
  42. package/dist/templates/monorepo/apps/api-gateway/src/app.controller.ts.hbs +6 -0
  43. package/dist/templates/monorepo/apps/api-gateway/src/app.module.ts.hbs +32 -1
  44. package/dist/templates/monorepo/apps/api-gateway/src/app.service.ts.hbs +1 -1
  45. package/dist/templates/monorepo/apps/api-gateway/src/main.ts.hbs +13 -4
  46. package/dist/templates/monorepo/apps/api-gateway/test/app.e2e-spec.ts.hbs +26 -1
  47. package/dist/templates/monorepo/feature/graphql/src/graphql/graphql.module.ts.hbs +6 -10
  48. package/dist/templates/monorepo/feature/graphql/src/graphql/resolvers/health.resolver.ts.hbs +14 -0
  49. package/dist/templates/monorepo/feature/graphql/src/graphql/resolvers/todo.resolver.ts.hbs +147 -47
  50. package/dist/templates/monorepo/packages/env/src/api-gateway.ts.hbs +7 -0
  51. package/dist/templates/monorepo/packages/shared/package.json.hbs +1 -0
  52. package/dist/templates/monorepo/packages/shared/src/auth.ts.hbs +6 -0
  53. package/dist/templates/monorepo/root/pnpm-workspace.yaml.hbs +10 -2
  54. package/dist/templates/single-project/architecture/nest-api/.env.hbs +6 -0
  55. package/dist/templates/single-project/architecture/nest-api/package.json.hbs +1 -1
  56. package/dist/templates/single-project/architecture/nest-api/src/main.ts.hbs +13 -4
  57. package/dist/templates/single-project/architecture/nest-gateway/src/main.ts.hbs +3 -3
  58. package/dist/templates/single-project/architecture/nest-microservice/proto/app.proto.hbs +13 -0
  59. package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs +10 -4
  60. package/dist/templates/single-project/architecture/nest-microservice/src/app.service.ts.hbs +2 -2
  61. package/dist/templates/single-project/architecture/nest-microservice/src/main.ts.hbs +3 -3
  62. package/dist/templates/single-project/architecture/nest-microservice/test/microservice.e2e-spec.ts.hbs +7 -1
  63. package/dist/templates/single-project/shared/config/env.ts.hbs +12 -1
  64. package/package.json +2 -2
@@ -3,29 +3,153 @@ import { INestApplication{{#unless (includes addons "nestjs-zod")}}, StandardSch
3
3
  import { ZodValidationPipe } from "nestjs-zod";
4
4
  {{/if}}
5
5
  import { Test, TestingModule } from "@nestjs/testing";
6
+ {{#if (or (eq auth "better-auth:stateless") (and isPassportAuth (not hasDatabase)))}}
7
+ import { createServer } from "node:http";
8
+ import { generateKeyPairSync, createSign } from "node:crypto";
9
+ {{/if}}
10
+ {{#if (and isPassportAuth hasRateLimiting hasDatabase)}}
11
+ import { createServer, request as httpRequest } from "node:http";
12
+ {{/if}}
13
+ {{#if hasTrustedProxy}}
14
+ import { env } from "../src/config/env.js";
15
+ {{/if}}
6
16
  {{#if (eq httpProvider "fastify")}}
7
17
  import { FastifyAdapter } from "@nestjs/platform-fastify";
8
18
  {{/if}}
9
19
  import request from "supertest";
10
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
20
+ import { afterAll, beforeAll, describe, expect, it{{#if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}, vi{{/if}} } from "vitest";
11
21
  import type { App } from "supertest/types";
12
-
13
- {{#if isPassportAuth}}
14
- import { JwtService } from "@nestjs/jwt";
15
- import { DbService } from "../src/database/db.service.js";
16
- import { env } from "../src/config/env.js";
22
+ {{#if swagger}}
23
+ import { createDocs } from "../src/docs/create-docs.js";
24
+ {{#if isBetterAuth}}
25
+ import { AuthService } from "@thallesp/nestjs-better-auth";
26
+ {{/if}}
17
27
  {{/if}}
28
+ {{#if (and swagger useSwaggerScalar)}}
29
+ import { apiReference } from "@scalar/nestjs-api-reference";
30
+ {{/if}}
31
+
32
+ {{#unless (or (eq auth "better-auth:stateless") (and isPassportAuth (not hasDatabase)))}}
18
33
  import { AppModule } from "./../src/app.module.js";
34
+ {{/unless}}
19
35
  {{#if (and useDrizzle (eq auth "none"))}}
20
36
  import { DbService } from "../src/database/db.service.js";
21
37
  import { users } from "../src/database/orm/drizzle/schema.js";
22
38
  import { eq } from "drizzle-orm";
39
+ {{else if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}
40
+ import { DbService } from "../src/database/db.service.js";
23
41
  {{/if}}
24
42
 
25
43
  describe("AppController (e2e)", () => {
26
44
  let app: INestApplication<App>;
45
+ {{#if swagger}}
46
+ let generatedOpenApi: Awaited<ReturnType<typeof createDocs>>;
47
+ {{/if}}
48
+ {{#if (eq auth "better-auth:stateless")}}
49
+ const oidcOrigin = "http://127.0.0.1:4010";
50
+ const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
51
+ const publicJwk = publicKey.export({ format: "jwk" });
52
+ let authorizationNonce = "";
53
+ let oidcServer: ReturnType<typeof createServer>;
54
+
55
+ function signIdToken(nonce: string): string {
56
+ const encodedHeader = Buffer.from(JSON.stringify({ alg: "RS256", kid: "scenario-key", typ: "JWT" })).toString("base64url");
57
+ const encodedPayload = Buffer.from(JSON.stringify({ aud: "generated-scenario-client", email: "oidc-scenario@example.com", exp: Math.floor(Date.now() / 1000) + 300, iat: Math.floor(Date.now() / 1000), iss: oidcOrigin, name: "OIDC Scenario", nonce, sub: "oidc-scenario-user" })).toString("base64url");
58
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
59
+ const signer = createSign("RSA-SHA256");
60
+ signer.update(signingInput);
61
+ signer.end();
62
+ return `${signingInput}.${signer.sign(privateKey).toString("base64url")}`;
63
+ }
64
+
65
+ async function startOidcFixture(): Promise<void> {
66
+ oidcServer = createServer((request, response) => {
67
+ const url = new URL(request.url ?? "/", oidcOrigin);
68
+ if (url.pathname === "/.well-known/openid-configuration") {
69
+ response.setHeader("content-type", "application/json");
70
+ response.end(JSON.stringify({ authorization_endpoint: `${oidcOrigin}/authorize`, issuer: oidcOrigin, jwks_uri: `${oidcOrigin}/jwks`, token_endpoint: `${oidcOrigin}/token`, userinfo_endpoint: `${oidcOrigin}/userinfo` }));
71
+ return;
72
+ }
73
+ if (url.pathname === "/jwks") {
74
+ response.setHeader("content-type", "application/json");
75
+ response.end(JSON.stringify({ keys: [{ ...publicJwk, alg: "RS256", kid: "scenario-key", use: "sig" }] }));
76
+ return;
77
+ }
78
+ if (url.pathname === "/authorize") {
79
+ authorizationNonce = url.searchParams.get("nonce") ?? "";
80
+ const redirect = new URL(url.searchParams.get("redirect_uri") ?? "");
81
+ redirect.searchParams.set("code", "scenario-code");
82
+ redirect.searchParams.set("state", url.searchParams.get("state") ?? "");
83
+ response.writeHead(302, { location: redirect.toString() });
84
+ response.end();
85
+ return;
86
+ }
87
+ if (url.pathname === "/token") {
88
+ let body = "";
89
+ request.on("data", (chunk: Buffer) => { body += chunk.toString(); });
90
+ request.on("end", () => {
91
+ response.setHeader("content-type", "application/json");
92
+ response.end(JSON.stringify({ access_token: "scenario-access-token", id_token: signIdToken(authorizationNonce), token_type: "Bearer" }));
93
+ });
94
+ return;
95
+ }
96
+ if (url.pathname === "/userinfo") {
97
+ response.setHeader("content-type", "application/json");
98
+ response.end(JSON.stringify({ email: "oidc-scenario@example.com", name: "OIDC Scenario", sub: "oidc-scenario-user" }));
99
+ return;
100
+ }
101
+ response.writeHead(404);
102
+ response.end();
103
+ });
104
+ await new Promise<void>((resolve) => oidcServer.listen(4010, "127.0.0.1", resolve));
105
+ }
106
+ {{/if}}
107
+ {{#if (and isPassportAuth (not hasDatabase))}}
108
+ const identityIssuer = "http://127.0.0.1:4011/";
109
+ const identityAudience = "generated-resource-server";
110
+ const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
111
+ const publicJwk = publicKey.export({ format: "jwk" });
112
+ let identityServer: ReturnType<typeof createServer>;
113
+
114
+ function signAccessToken(claims: Record<string, unknown> = {}): string {
115
+ const issuedAt = Math.floor(Date.now() / 1000);
116
+ const encodedHeader = Buffer.from(JSON.stringify({ alg: "RS256", kid: "scenario-key", typ: "at+jwt" })).toString("base64url");
117
+ const encodedPayload = Buffer.from(JSON.stringify({ aud: identityAudience, email: "external-user@example.com", exp: issuedAt + 300, iat: issuedAt, iss: identityIssuer, sub: "external-user-1", ...claims })).toString("base64url");
118
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
119
+ const signer = createSign("RSA-SHA256");
120
+ signer.update(signingInput);
121
+ signer.end();
122
+ return `${signingInput}.${signer.sign(privateKey).toString("base64url")}`;
123
+ }
124
+
125
+ async function startIdentityProvider(): Promise<void> {
126
+ identityServer = createServer((request, response) => {
127
+ if (request.url !== "/.well-known/jwks.json") {
128
+ response.writeHead(404);
129
+ response.end();
130
+ return;
131
+ }
132
+ response.setHeader("content-type", "application/json");
133
+ response.end(JSON.stringify({ keys: [{ ...publicJwk, alg: "RS256", kid: "scenario-key", use: "sig" }] }));
134
+ });
135
+ await new Promise<void>((resolve) => identityServer.listen(4011, "127.0.0.1", resolve));
136
+ }
137
+ {{/if}}
27
138
 
28
139
  async function startApplication() {
140
+ {{#if (eq auth "better-auth:stateless")}}
141
+ process.env.OIDC_CLIENT_ID = "generated-scenario-client";
142
+ process.env.OIDC_CLIENT_SECRET = "generated-scenario-secret";
143
+ process.env.OIDC_DISCOVERY_URL = `${oidcOrigin}/.well-known/openid-configuration`;
144
+ process.env.OIDC_PROVIDER_ID = "scenario-oidc";
145
+ const { AppModule } = await import("./../src/app.module.js");
146
+ {{/if}}
147
+ {{#if (and isPassportAuth (not hasDatabase))}}
148
+ process.env.OIDC_AUDIENCE = identityAudience;
149
+ process.env.OIDC_ISSUER = identityIssuer;
150
+ process.env.OIDC_JWKS_URI = `${identityIssuer}.well-known/jwks.json`;
151
+ const { AppModule } = await import("./../src/app.module.js");
152
+ {{/if}}
29
153
  const moduleFixture: TestingModule = await Test.createTestingModule({
30
154
  imports: [AppModule],
31
155
  }).compile();
@@ -35,17 +159,104 @@ describe("AppController (e2e)", () => {
35
159
  {{else}}
36
160
  app = moduleFixture.createNestApplication({{#if isBetterAuth}}{ bodyParser: false }{{/if}});
37
161
  {{/if}}
162
+ {{#if hasTrustedProxy}}
163
+ {{#unless (eq httpProvider "fastify")}}
164
+ const trustedProxyIps = env.TRUSTED_PROXY_IPS.split(",").map((ip) => ip.trim()).filter(Boolean);
165
+ app.getHttpAdapter().getInstance().set("trust proxy", trustedProxyIps);
166
+ {{/unless}}
167
+ {{/if}}
38
168
  {{#if (includes addons "nestjs-zod")}}
39
169
  app.useGlobalPipes(new ZodValidationPipe());
40
170
  {{else}}
41
171
  app.useGlobalPipes(new StandardSchemaValidationPipe());
172
+ {{/if}}
173
+ {{#if swagger}}
174
+ generatedOpenApi = await createDocs(app, {
175
+ title: "Generated API",
176
+ integrations: {
177
+ {{#if isBetterAuth}}
178
+ betterAuth: { auth: app.get(AuthService).instance, pathPrefix: "/api/auth" },
179
+ {{/if}}
180
+ {{#if isPassportAuth}}
181
+ bearerAuth: true,
182
+ {{/if}}
183
+ zod: { withFastify: false },
184
+ },
185
+ });
186
+ {{#if useSwaggerScalar}}
187
+ app.use("/docs", apiReference({ content: generatedOpenApi, theme: "default" }));
188
+ {{/if}}
42
189
  {{/if}}
43
190
  await app.init();
44
191
  {{#if (eq httpProvider "fastify")}}
45
192
  await app.getHttpAdapter().getInstance().ready();
46
193
  {{/if}}
47
194
  }
195
+ {{#if (eq auth "better-auth:stateless")}}
196
+ beforeAll(async () => {
197
+ await startOidcFixture();
198
+ await startApplication();
199
+ });
200
+ {{else if (and isPassportAuth (not hasDatabase))}}
201
+ beforeAll(async () => {
202
+ await startIdentityProvider();
203
+ await startApplication();
204
+ });
205
+ {{else}}
48
206
  beforeAll(startApplication);
207
+ {{/if}}
208
+
209
+ {{#if (and isPassportAuth (not hasDatabase))}}
210
+ it("accepts only RS256 access tokens from the configured external issuer", async () => {
211
+ 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)
215
+ .get("/auth/profile")
216
+ .set("Authorization", `Bearer ${signAccessToken()}`)
217
+ .expect(200);
218
+ expect(authenticated.body).toMatchObject({
219
+ email: "external-user@example.com",
220
+ id: "external-user-1",
221
+ issuer: identityIssuer,
222
+ userId: "external-user-1",
223
+ });
224
+
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
+ }
235
+ });
236
+ {{/if}}
237
+
238
+ {{#if swagger}}
239
+ it("generates authenticated OpenAPI docs and serves the Scalar reference", async () => {
240
+ expect(generatedOpenApi.openapi).toMatch(/^3\./u);
241
+ {{#if isBetterAuth}}
242
+ expect(generatedOpenApi.paths?.["/api/auth/sign-up/email"]).toBeDefined();
243
+ {{else if isPassportAuth}}
244
+ {{#if (and usePrisma hasRest (not hasMongodb))}}
245
+ expect(generatedOpenApi.paths?.["/auth/register"]).toBeDefined();
246
+ {{else}}
247
+ expect(generatedOpenApi.paths?.["/auth/signup"]).toBeDefined();
248
+ {{/if}}
249
+ expect(generatedOpenApi.components?.securitySchemes?.bearer).toBeDefined();
250
+ {{/if}}
251
+ {{#if hasTodoExample}}
252
+ expect(generatedOpenApi.paths?.["/todos"]?.post?.requestBody).toBeDefined();
253
+ {{/if}}
254
+ {{#if useSwaggerScalar}}
255
+ const reference = await request(app.getHttpServer()).get("/docs").expect(200);
256
+ expect(reference.text).toMatch(/scalar/u);
257
+ {{/if}}
258
+ });
259
+ {{/if}}
49
260
 
50
261
  {{#if hasRest}}
51
262
  it("/ (GET)", async () => {
@@ -54,6 +265,73 @@ describe("AppController (e2e)", () => {
54
265
  });
55
266
  {{/if}}
56
267
 
268
+ {{#if (and isPassportAuth hasTodoExample hasDatabase hasRest)}}
269
+ it("enforces REST todo ownership across two Passport users", async () => {
270
+ const server = app.getHttpServer();
271
+ const password = "Scenario-password-123!";
272
+ async function register(prefix: string) {
273
+ 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}})
275
+ .send({ email, password }).expect(201);
276
+ const response = await request(server).post("/auth/login")
277
+ .send({ email, password }).expect(200);
278
+ expect(response.body.accessToken).toBeTruthy();
279
+ return response.body.accessToken as string;
280
+ }
281
+ const alice = await register("passport-alice");
282
+ const bob = await register("passport-bob");
283
+ const created = await request(server).post("/todos")
284
+ .set("Authorization", `Bearer ${alice}`)
285
+ .send({ title: "Private Passport todo", completed: false }).expect(201);
286
+ const todo = Array.isArray(created.body) ? created.body[0] : created.body;
287
+ const { id } = todo;
288
+ const bobList = await request(server).get("/todos")
289
+ .set("Authorization", `Bearer ${bob}`).expect(200);
290
+ expect(bobList.body).not.toEqual(
291
+ expect.arrayContaining([expect.objectContaining({ id })])
292
+ );
293
+ await request(server).put(`/todos/${id}`)
294
+ .set("Authorization", `Bearer ${bob}`)
295
+ .send({ completed: true }).expect(404);
296
+ await request(server).delete(`/todos/${id}`)
297
+ .set("Authorization", `Bearer ${bob}`).expect(404);
298
+ const aliceList = await request(server).get("/todos")
299
+ .set("Authorization", `Bearer ${alice}`).expect(200);
300
+ expect(aliceList.body).toEqual(
301
+ expect.arrayContaining([
302
+ expect.objectContaining({ id, completed: false }),
303
+ ])
304
+ );
305
+ });
306
+ {{/if}}
307
+
308
+ {{#if hasHealthCheck}}
309
+ it("exposes unauthenticated health and readiness", async () => {
310
+ const response = await request(app.getHttpServer()).get("/health").expect(200);
311
+ expect(response.body.status).toBe("ok");
312
+ expect(response.body.info.app.status).toBe("up");
313
+ {{#if hasDatabase}}
314
+ expect(response.body.info.db.status).toBe("up");
315
+ {{/if}}
316
+ });
317
+ {{/if}}
318
+
319
+ {{#if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}
320
+ it("reports database readiness failure with HTTP 503", async () => {
321
+ const db = app.get(DbService);
322
+ const database = {{#if usePrisma}}db.prisma{{else if useTypeorm}}db.typeorm{{else}}db.drizzle{{/if}};
323
+ const query = vi.spyOn(database, {{#if usePrisma}}"$queryRaw"{{else if useTypeorm}}"query"{{else if hasSqlite}}"get"{{else}}"execute"{{/if}})
324
+ .mockRejectedValue(new Error("scenario database outage"));
325
+ try {
326
+ const response = await request(app.getHttpServer()).get("/health").expect(503);
327
+ expect(response.body.status).toBe("error");
328
+ expect(response.body.error.db.status).toBe("down");
329
+ } finally {
330
+ query.mockRestore();
331
+ }
332
+ });
333
+ {{/if}}
334
+
57
335
  {{#if hasTrpc}}
58
336
  it("executes a tRPC query through HTTP", async () => {
59
337
  const response = await request(app.getHttpServer())
@@ -62,10 +340,65 @@ describe("AppController (e2e)", () => {
62
340
  });
63
341
  {{/if}}
64
342
 
343
+ {{#if (and hasTrpc hasTodoExample hasAuth)}}
344
+ it("enforces todo ownership through authenticated tRPC procedures", async () => {
345
+ const server = app.getHttpServer();
346
+ const password = "Scenario-password-123!";
347
+ const origin = "http://localhost:3000";
348
+ const call = (procedure: string, input?: unknown, credential?: { cookie?: string[]; token?: string }) => {
349
+ const operation = procedure === "findAll"
350
+ ? request(server).get("/trpc/todoRouter.findAll")
351
+ : request(server).post(`/trpc/todoRouter.${procedure}`);
352
+ if (credential?.cookie) operation.set("Cookie", credential.cookie);
353
+ if (credential?.token) operation.set("Authorization", `Bearer ${credential.token}`);
354
+ if (input !== undefined) operation.send(input);
355
+ return operation;
356
+ };
357
+ const unwrap = (body: { result?: { data?: { json?: unknown } } }) =>
358
+ body.result?.data?.json ?? body.result?.data;
359
+ {{#if isBetterAuth}}
360
+ async function register(prefix: string) {
361
+ const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
362
+ const response = await request(server).post("/api/auth/sign-up/email")
363
+ .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
364
+ return { cookie: response.headers["set-cookie"] as string[] };
365
+ }
366
+ {{else if isPassportAuth}}
367
+ async function register(prefix: string) {
368
+ const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
369
+ await request(server)
370
+ .post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}})
371
+ {{#if usePrisma}}.send({ email, password }){{else}}.send({ email, password, name: prefix }){{/if}}
372
+ .expect(201);
373
+ const login = await request(server).post("/auth/login").send({ email, password }).expect(200);
374
+ return { token: login.body.accessToken as string };
375
+ }
376
+ {{/if}}
377
+ const anonymous = await call("findAll");
378
+ expect(anonymous.status).toBe(401);
379
+ const alice = await register("trpc-alice");
380
+ const bob = await register("trpc-bob");
381
+ const created = await call("create", { title: "Private tRPC todo", completed: false }, alice).expect(200);
382
+ const createdTodo = unwrap(created.body) as { id: string; title: string };
383
+ expect(createdTodo.title).toBe("Private tRPC todo");
384
+ const id = createdTodo.id;
385
+ const aliceTodos = unwrap((await call("findAll", undefined, alice).expect(200)).body) as { id: string }[];
386
+ expect(aliceTodos).toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
387
+ const bobTodos = unwrap((await call("findAll", undefined, bob).expect(200)).body) as { id: string }[];
388
+ expect(bobTodos).not.toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
389
+ await call("update", { id, data: { completed: true } }, bob).expect(404);
390
+ await call("delete", { id }, bob).expect(404);
391
+ const updated = await call("update", { id, data: { completed: true } }, alice).expect(200);
392
+ expect((unwrap(updated.body) as { completed: boolean }).completed).toBe(true);
393
+ await call("delete", { id }, alice).expect(200);
394
+ });
395
+ {{/if}}
396
+
65
397
  {{#if (eq auth "better-auth")}}
66
398
  it("registers, authenticates and revokes a persisted session", async () => {
67
399
  const email = `scenario-${Date.now()}@example.com`;
68
400
  const password = "Scenario-password-123!";
401
+ expect(password.length).toBeGreaterThanOrEqual(8);
69
402
  const origin = "http://localhost:3000";
70
403
  let server = app.getHttpServer();
71
404
  const signedUp = await request(server).post("/api/auth/sign-up/email")
@@ -103,7 +436,60 @@ describe("AppController (e2e)", () => {
103
436
  .set("Cookie", cookies).send({}).expect(200);
104
437
  const revoked = await request(server).get("/api/auth/get-session")
105
438
  .set("Cookie", cookies).expect(200);
106
- expect(revoked.body).toBeNull();
439
+ expect(revoked.body).toBeNull();
440
+ });
441
+ {{/if}}
442
+
443
+ {{#if (and isBetterAuth hasRateLimiting)}}
444
+ it("keeps normal sign-in usable and throttles repeated authentication requests", async () => {
445
+ const server = app.getHttpServer();
446
+ const origin = "http://localhost:3000";
447
+ const email = `ratelimit-${Date.now()}@example.com`;
448
+ const password = "Scenario-password-123!";
449
+ await request(server).post("/api/auth/sign-up/email")
450
+ .set("Origin", origin).send({ email, password, name: "Rate limit" }).expect(200);
451
+ await request(server).post("/api/auth/sign-in/email")
452
+ .set("Origin", origin).send({ email, password }).expect(200);
453
+ const responses = await Promise.all(
454
+ Array.from({ length: 80 }, () =>
455
+ request(server)
456
+ .post("/api/auth/sign-in/email")
457
+ .set("Origin", origin)
458
+ .send({ email: "rate-limited@example.com", password: "wrong-password" })
459
+ )
460
+ );
461
+ expect(responses.some((response) => response.status === 429)).toBe(true);
462
+ await app.close();
463
+ await startApplication();
464
+ }, 30_000);
465
+ {{/if}}
466
+
467
+ {{#if (eq auth "better-auth:stateless")}}
468
+ it("completes an OIDC authorization-code flow without an application database", async () => {
469
+ const server = app.getHttpServer();
470
+ const signIn = await request(server).post("/api/auth/sign-in/social")
471
+ .set("Origin", "http://localhost:3000")
472
+ .send({ callbackURL: "/", provider: "scenario-oidc" }).expect(200);
473
+ const authorization = new URL(signIn.body.url);
474
+ expect(authorization.origin).toBe(oidcOrigin);
475
+ const stateCookies = signIn.headers["set-cookie"];
476
+ expect(stateCookies).toBeTruthy();
477
+ const providerResponse = await request(oidcServer).get(`${authorization.pathname}${authorization.search}`).expect(302);
478
+ const callback = new URL(providerResponse.headers.location);
479
+ const session = await request(server).get(`${callback.pathname}${callback.search}`)
480
+ .set("Cookie", stateCookies).set("Origin", "http://localhost:3000").expect(302);
481
+ const cookies = session.headers["set-cookie"];
482
+ expect(cookies).toBeTruthy();
483
+ const currentSession = await request(server).get("/api/auth/get-session").set("Cookie", cookies).expect(200);
484
+ expect(currentSession.body.user.email).toBe("oidc-scenario@example.com");
485
+ {{#if hasTrpc}}
486
+ await request(server).get("/trpc/appRouter.testAuth").expect(401);
487
+ const authenticated = await request(server).get("/trpc/appRouter.testAuth")
488
+ .set("Cookie", cookies).expect(200);
489
+ expect(authenticated.body.result.data.user.email).toBe("oidc-scenario@example.com");
490
+ {{/if}}
491
+ await request(server).get("/api/auth/callback/scenario-oidc?code=scenario-code&state=tampered")
492
+ .set("Origin", "http://localhost:3000").expect(302);
107
493
  });
108
494
  {{/if}}
109
495
 
@@ -112,8 +498,24 @@ describe("AppController (e2e)", () => {
112
498
  const server = app.getHttpServer();
113
499
  const execute = (query: string, variables: Record<string, unknown> = {}) => request(server).post("/graphql").send({ query, variables });
114
500
  const hello = await execute("{ hello }");
501
+ expect(hello.body.errors).toBeUndefined();
115
502
  expect(hello.body.data.hello).toBe("Hello GraphQL!");
116
- {{#if hasTodoExample}}
503
+ {{#if (and hasTodoExample (eq auth "none"))}}
504
+ const created = await execute('mutation($input: CreateTodoInput!) { createTodo(input: $input) { id title completed } }', { input: { title: "GraphQL integration" } });
505
+ expect(created.body.errors).toBeUndefined();
506
+ const { id } = created.body.data.createTodo;
507
+ const listed = await execute("{ todos { id title } }");
508
+ expect(listed.body.data.todos).toContainEqual({ id, title: "GraphQL integration" });
509
+ const updated = await execute('mutation($input: UpdateTodoInput!) { updateTodo(input: $input) { completed } }', { input: { id, completed: true } });
510
+ expect(updated.body.data.updateTodo.completed).toBe(true);
511
+ const invalid = await execute('mutation { createTodo(input: { title: " " }) { id } }');
512
+ expect(invalid.body.errors).toBeTruthy();
513
+ const deleted = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
514
+ expect(deleted.body.data.deleteTodo).toBe(true);
515
+ const missing = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
516
+ expect(missing.body.errors).toBeTruthy();
517
+ {{/if}}
518
+ {{#if (and hasTodoExample (eq auth "none"))}}
117
519
  const created = await execute('mutation($input: CreateTodoInput!) { createTodo(input: $input) { id title completed } }', { input: { title: "GraphQL integration" } });
118
520
  expect(created.body.errors).toBeUndefined();
119
521
  const { id } = created.body.data.createTodo;
@@ -131,47 +533,239 @@ describe("AppController (e2e)", () => {
131
533
  });
132
534
  {{/if}}
133
535
 
134
- {{#if isPassportAuth}}
135
- it("verifies credentials, rotates refresh tokens, detects replay and revokes access", async () => {
536
+ {{#if (and hasGraphql hasTodoExample hasAuth)}}
537
+ it("rejects anonymous GraphQL todo access and isolates authenticated owners", async () => {
538
+ const server = app.getHttpServer();
539
+ const password = "Scenario-password-123!";
540
+ const origin = "http://localhost:3000";
541
+ const execute = (
542
+ query: string,
543
+ variables: Record<string, unknown> = {},
544
+ credentials: { cookie?: string[]; token?: string } = {}
545
+ ) => {
546
+ const operation = request(server).post("/graphql").send({ query, variables });
547
+ if (credentials.cookie) operation.set("Cookie", credentials.cookie);
548
+ if (credentials.token) operation.set("Authorization", `Bearer ${credentials.token}`);
549
+ return operation;
550
+ };
551
+ {{#if isBetterAuth}}
552
+ async function register(prefix: string) {
553
+ const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
554
+ const response = await request(server).post("/api/auth/sign-up/email")
555
+ .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
556
+ return { cookie: response.headers["set-cookie"] };
557
+ }
558
+ {{else if isPassportAuth}}
559
+ async function register(prefix: string) {
560
+ const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
561
+ const response = await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}})
562
+ .send({ email, password }).expect(201);
563
+ return { token: response.body.accessToken as string };
564
+ }
565
+ {{/if}}
566
+ const anonymous = await execute("{ todos { id title } }");
567
+ expect(anonymous.body.errors).toBeTruthy();
568
+ const firstUser = await register("graphql-owner-one");
569
+ const secondUser = await register("graphql-owner-two");
570
+ const created = await execute(
571
+ 'mutation($input: CreateTodoInput!) { createTodo(input: $input) { id title } }',
572
+ { input: { title: "Private GraphQL todo" } },
573
+ firstUser
574
+ );
575
+ expect(created.body.errors).toBeUndefined();
576
+ const { id } = created.body.data.createTodo;
577
+ const firstList = await execute("{ todos { id title } }", {}, firstUser);
578
+ expect(firstList.body.data.todos).toContainEqual({ id, title: "Private GraphQL todo" });
579
+ const secondList = await execute("{ todos { id title } }", {}, secondUser);
580
+ expect(secondList.body.data.todos).toEqual([]);
581
+ const unauthorizedUpdate = await execute(
582
+ 'mutation($input: UpdateTodoInput!) { updateTodo(input: $input) { id } }',
583
+ { input: { id, completed: true } },
584
+ secondUser
585
+ );
586
+ expect(unauthorizedUpdate.body.errors).toBeTruthy();
587
+ const unauthorizedDelete = await execute(
588
+ 'mutation($id: String!) { deleteTodo(id: $id) }',
589
+ { id },
590
+ secondUser
591
+ );
592
+ expect(unauthorizedDelete.body.errors).toBeTruthy();
593
+ const ownerTodo = await execute("{ todos { id title } }", {}, firstUser);
594
+ expect(ownerTodo.body.data.todos).toContainEqual({ id, title: "Private GraphQL todo" });
595
+ });
596
+ {{/if}}
597
+
598
+ {{#if (and hasGraphql hasRateLimiting)}}
599
+ it("throttles repeated GraphQL requests to 429", async () => {
600
+ let throttled = false;
601
+ let lastResponse = "";
602
+ app.getHttpServer().setMaxListeners(100);
603
+ for (let attempt = 0; attempt < 80; attempt += 1) {
604
+ const response = await request(app.getHttpServer()).post("/graphql").send({ query: "{ hello }" });
605
+ lastResponse = JSON.stringify({ errors: response.body.errors, status: response.status });
606
+ throttled ||= response.status === 429 || response.body.errors?.some((error: { message?: string; extensions?: { originalError?: { statusCode?: number } } }) =>
607
+ error.extensions?.originalError?.statusCode === 429 ||
608
+ error.message?.includes("ThrottlerException: Too Many Requests") === true
609
+ ) === true;
610
+ }
611
+ expect(throttled, lastResponse).toBe(true);
612
+ }, 30_000);
613
+ {{/if}}
614
+
615
+ {{#if (and isPassportAuth hasRateLimiting hasDatabase)}}
616
+ it("keeps Passport sign-in usable and throttles repeated login attempts", async () => {
617
+ const server = app.getHttpServer();
618
+ const email = `passport-ratelimit-${Date.now()}@example.com`;
619
+ const password = "Scenario-password-123!";
620
+ await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
621
+ await request(server).post("/auth/login").send({ email, password }).expect(200);
622
+ const responses = await Promise.all(
623
+ Array.from({ length: 80 }, () =>
624
+ request(server).post("/auth/login").send({ email, password: "Invalid-password-123!" })
625
+ )
626
+ );
627
+ expect(responses.some((response) => response.status === 429)).toBe(true);
628
+ await app.close();
629
+ await startApplication();
630
+ }, 30_000);
631
+ {{/if}}
632
+
633
+ {{#if (and hasTrpc hasRateLimiting)}}
634
+ it("throttles repeated tRPC procedure requests", async () => {
635
+ const server = app.getHttpServer();
636
+ server.setMaxListeners(100);
637
+ const procedure = () => request(server).get("/trpc/appRouter.testAuth");
638
+ expect((await procedure()).status).toBe(401);
639
+ const responses = await Promise.all(
640
+ Array.from({ length: 80 }, () => procedure())
641
+ );
642
+ expect(responses.some((response) => response.status === 429)).toBe(true);
643
+ }, 30_000);
644
+ {{/if}}
645
+
646
+ {{#if (and hasTrustedProxy isPassportAuth hasRateLimiting hasDatabase)}}
647
+ it("isolates rate limits by client IP behind a trusted reverse proxy", async () => {
648
+ const appServer = app.getHttpServer();
649
+ const email = `passport-proxy-${Date.now()}@example.com`;
650
+ const password = "Scenario-password-123!";
651
+ await request(appServer).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
652
+ await new Promise<void>((resolve) => appServer.listen(0, "127.0.0.1", resolve));
653
+ const appAddress = appServer.address();
654
+ if (!appAddress || typeof appAddress === "string") {
655
+ throw new Error("Application did not bind a TCP port for the proxy scenario");
656
+ }
657
+
658
+ const proxy = createServer((incoming, outgoing) => {
659
+ const forwardedClientIp = incoming.headers["x-scenario-client-ip"]?.toString() ?? "198.51.100.10";
660
+ const upstream = httpRequest({
661
+ headers: { ...incoming.headers, "x-forwarded-for": forwardedClientIp },
662
+ hostname: "127.0.0.1",
663
+ method: incoming.method,
664
+ path: incoming.url,
665
+ port: appAddress.port,
666
+ }, (upstreamResponse) => {
667
+ outgoing.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
668
+ upstreamResponse.pipe(outgoing);
669
+ });
670
+ upstream.on("error", () => {
671
+ outgoing.writeHead(502);
672
+ outgoing.end();
673
+ });
674
+ incoming.pipe(upstream);
675
+ });
676
+ await new Promise<void>((resolve) => proxy.listen(0, "127.0.0.1", resolve));
677
+ const proxyAddress = proxy.address();
678
+ if (!proxyAddress || typeof proxyAddress === "string") {
679
+ throw new Error("Reverse proxy did not bind a TCP port");
680
+ }
681
+
682
+ const proxyLogin = async (clientIp: string, suppliedPassword: string): Promise<number> => {
683
+ const response = await fetch(`http://127.0.0.1:${proxyAddress.port}/auth/login`, {
684
+ body: JSON.stringify({ email, password: suppliedPassword }),
685
+ headers: {
686
+ "content-type": "application/json",
687
+ "x-scenario-client-ip": clientIp,
688
+ },
689
+ method: "POST",
690
+ });
691
+ return response.status;
692
+ };
693
+
694
+ expect(await proxyLogin("198.51.100.10", password)).toBe(200);
695
+ const invalidAttempts = await Promise.all(
696
+ Array.from({ length: 80 }, () => proxyLogin("198.51.100.10", "Invalid-password-123!"))
697
+ );
698
+ expect(invalidAttempts).toContain(429);
699
+ expect(await proxyLogin("198.51.100.20", password)).toBe(200);
700
+
701
+ await new Promise<void>((resolve, reject) => proxy.close((error) => error ? reject(error) : resolve()));
702
+ await app.close();
703
+ }, 30_000);
704
+ {{/if}}
705
+
706
+ {{#if (and isPassportAuth usePrisma hasRest)}}
707
+ it("registers, authenticates and refreshes Prisma Passport tokens", async () => {
136
708
  const server = app.getHttpServer();
137
709
  const email = `passport-${Date.now()}@example.com`;
138
710
  const password = "Scenario-password-123!";
139
- const registered = await request(server).post("/auth/register").send({ email, password }).expect(201);
711
+ const registered = await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(201);
712
+ {{#if (and usePrisma hasRest (not hasMongodb))}}
713
+ expect(registered.body.email).toBe(email);
714
+ {{else}}
715
+ expect(registered.body.accessToken).toBeTruthy();
716
+ {{/if}}
140
717
  expect(registered.body.password).toBeUndefined();
141
- expect(registered.body.role).toBe("USER");
142
- await request(server).post("/auth/register").send({ email, password }).expect(409);
143
- await request(server).post("/auth/login").send({ email, password, role: "ADMIN" }).expect(400);
718
+ await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(409);
144
719
  await request(server).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
145
720
  const login = await request(server).post("/auth/login").send({ email, password }).expect(200);
146
- const { accessToken, refreshToken } = login.body;
721
+ expect(login.body.accessToken).toBeTruthy();
722
+ expect(login.body.refreshToken).toBeTruthy();
723
+ {{#if hasTodoExample}}
724
+ await request(server).get("/todos").expect(401);
725
+ const todos = await request(server).get("/todos")
726
+ .auth(login.body.accessToken, { type: "bearer" }).expect(200);
727
+ expect(todos.body).toEqual([]);
728
+ await request(server).post("/todos").auth(login.body.accessToken, { type: "bearer" })
729
+ .send({ title: "" }).expect(400);
730
+ {{/if}}
731
+ {{#if (not hasMongodb)}}
147
732
  await request(server).get("/auth/profile").expect(401);
148
- const profile = await request(server).get("/auth/profile").auth(accessToken, { type: "bearer" }).expect(200);
733
+ const profile = await request(server).get("/auth/profile")
734
+ .auth(login.body.accessToken, { type: "bearer" }).expect(200);
149
735
  expect(profile.body.email).toBe(email);
150
- const jwt = new JwtService();
151
- const decoded = jwt.decode(accessToken);
152
- for (const overrides of [{ iss: "wrong" }, { aud: "wrong" }, { exp: 1 }, { sub: undefined }, { sid: "wrong" }]) {
153
- const token = jwt.sign({ ...decoded, ...overrides }, { secret: env.JWT_ACCESS_SECRET, algorithm: "HS256" });
154
- await request(server).get("/auth/profile").auth(token, { type: "bearer" }).expect(401);
155
- }
156
- const wrongSignature = jwt.sign(decoded, { secret: "another-secret-that-is-at-least-32-characters" });
157
- await request(server).get("/auth/profile").auth(wrongSignature, { type: "bearer" }).expect(401);
158
- const rotated = await request(server).post("/auth/refresh").send({ refreshToken }).expect(200);
159
- expect(rotated.body.refreshToken).not.toBe(refreshToken);
160
- await request(server).post("/auth/refresh").send({ refreshToken }).expect(401);
161
- await request(server).post("/auth/refresh").send({ refreshToken: rotated.body.refreshToken }).expect(401);
162
- await request(server).get("/auth/profile").auth(rotated.body.accessToken, { type: "bearer" }).expect(401);
736
+ {{/if}}
737
+ const rotated = await request(server).post("/auth/refresh").send({{#if hasMongodb}}{ token: login.body.refreshToken }{{else}}{ refreshToken: login.body.refreshToken }{{/if}}).expect(200);
738
+ expect(rotated.body.accessToken).toBeTruthy();
739
+ expect(rotated.body.refreshToken).toBeTruthy();
740
+ {{#if hasMongodb}}
741
+ await request(server).post("/auth/refresh").send({ token: "invalid-refresh-token" }).expect(401);
742
+ const fresh = await request(server).post("/auth/login").send({ email, password }).expect(200);
743
+ expect(fresh.body.refreshToken).toBeTruthy();
744
+ {{else}}
745
+ await request(server).post("/auth/refresh").send({ refreshToken: login.body.refreshToken }).expect(401);
163
746
  const fresh = await request(server).post("/auth/login").send({ email, password }).expect(200);
164
747
  await request(server).post("/auth/logout").send({ refreshToken: fresh.body.refreshToken }).expect(200);
165
- await request(server).get("/auth/profile").auth(fresh.body.accessToken, { type: "bearer" }).expect(401);
166
- const user = await app.get(DbService).prisma.user.findUniqueOrThrow({ where: { email }, include: { refreshTokens: true } });
167
- expect(user.password).not.toBe(password);
168
- expect(user.refreshTokens.every((token) => token.tokenHash !== refreshToken)).toBe(true);
169
- const concurrentLogin = await request(server).post("/auth/login").send({ email, password }).expect(200);
170
- const results = await Promise.all([1, 2].map(() => request(server).post("/auth/refresh").send({ refreshToken: concurrentLogin.body.refreshToken })));
171
- expect(results.map(({ status }) => status).sort()).toEqual([200, 401]);
748
+ await request(server).post("/auth/refresh").send({ refreshToken: fresh.body.refreshToken }).expect(401);
749
+ {{/if}}
172
750
  });
173
751
  {{/if}}
174
- {{#if hasMongodb}}
752
+ {{#if (and isPassportAuth (or useTypeorm useDrizzle))}}
753
+ it("registers, authenticates and refreshes passport sessions", async () => {
754
+ const server = app.getHttpServer();
755
+ const email = `passport-${Date.now()}@example.com`;
756
+ const password = "Scenario-password-123!";
757
+ const signedUp = await request(server).post("/auth/signup").send({ email, password }).expect(201);
758
+ expect(signedUp.body.accessToken).toBeTruthy();
759
+ expect(signedUp.body.refreshToken).toBeTruthy();
760
+ await request(server).post("/auth/signup").send({ email, password }).expect(409);
761
+ await request(server).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
762
+ const login = await request(server).post("/auth/login").send({ email, password }).expect(200);
763
+ const rotated = await request(server).post("/auth/refresh").send({ token: login.body.refreshToken }).expect(200);
764
+ expect(rotated.body.accessToken).toBeTruthy();
765
+ expect(rotated.body.refreshToken).toBeTruthy();
766
+ });
767
+ {{/if}}
768
+ {{#if (and hasMongodb (eq auth "none"))}}
175
769
  it("rejects malformed and missing MongoDB identifiers", async () => {
176
770
  await request(app.getHttpServer()).delete("/todos/invalid").expect(400);
177
771
  await request(app.getHttpServer()).delete("/todos/000000000000000000000000").expect(404);
@@ -219,11 +813,50 @@ describe("AppController (e2e)", () => {
219
813
  });
220
814
  {{/if}}
221
815
 
816
+ {{#if (and isBetterAuth hasTodoExample hasDatabase hasRest)}}
817
+ it("enforces todo ownership across two authenticated users", async () => {
818
+ const server = app.getHttpServer();
819
+ const origin = "http://localhost:3000";
820
+ const password = "Scenario-password-123!";
821
+ async function register(prefix: string) {
822
+ const email = `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
823
+ const response = await request(server).post("/api/auth/sign-up/email")
824
+ .set("Origin", origin).send({ email, password, name: prefix }).expect(200);
825
+ const cookies = response.headers["set-cookie"];
826
+ expect(cookies).toBeTruthy();
827
+ return cookies;
828
+ }
829
+ const alice = await register("alice");
830
+ const bob = await register("bob");
831
+ {{#if (includes addons "nestjs-zod")}}
832
+ await request(server).post("/todos").set("Cookie", alice)
833
+ .send({ title: "" }).expect(400);
834
+ {{/if}}
835
+ const created = await request(server).post("/todos").set("Cookie", alice)
836
+ .send({ title: "Private alice todo", completed: false }).expect(201);
837
+ const { id } = created.body;
838
+ expect(id).toBeTruthy();
839
+ const bobList = await request(server).get("/todos").set("Cookie", bob).expect(200);
840
+ expect(bobList.body).toEqual(expect.not.arrayContaining([expect.objectContaining({ id })]));
841
+ await request(server).put(`/todos/${id}`).set("Cookie", bob)
842
+ .send({ completed: true }).expect(404);
843
+ await request(server).delete(`/todos/${id}`).set("Cookie", bob).expect(404);
844
+ const aliceList = await request(server).get("/todos").set("Cookie", alice).expect(200);
845
+ expect(aliceList.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, completed: false })]));
846
+ });
847
+ {{/if}}
848
+
222
849
  it("rejects unsupported methods", async () => {
223
850
  await request(app.getHttpServer()).post("/").send({ unexpected: true }).expect(404);
224
851
  });
225
852
 
226
853
  afterAll(async () => {
227
854
  await app?.close();
855
+ {{#if (eq auth "better-auth:stateless")}}
856
+ await new Promise<void>((resolve, reject) => oidcServer?.close((error) => error ? reject(error) : resolve()));
857
+ {{/if}}
858
+ {{#if (and isPassportAuth (not hasDatabase))}}
859
+ await new Promise<void>((resolve, reject) => identityServer?.close((error) => error ? reject(error) : resolve()));
860
+ {{/if}}
228
861
  });
229
862
  });