@nest-arch/tui 0.2.8 → 0.4.3

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 (92) hide show
  1. package/README.md +20 -42
  2. package/dist/cli.js +1 -1
  3. package/dist/constants-B5MuV-iH.js +1 -0
  4. package/dist/index.js +365 -98
  5. package/dist/templates/addons/husky/pre-commit.hbs +11 -26
  6. package/dist/templates/base/.env.example.hbs +13 -2
  7. package/dist/templates/base/.env.hbs +11 -2
  8. package/dist/templates/base/.gitignore.hbs +2 -1
  9. package/dist/templates/base/.yarnrc.yml.hbs +0 -5
  10. package/dist/templates/base/README.md.hbs +49 -5
  11. package/dist/templates/base/nest-cli.json.hbs +2 -1
  12. package/dist/templates/base/pnpm-workspace.yaml.hbs +15 -0
  13. package/dist/templates/base/prisma.config.ts.hbs +32 -6
  14. package/dist/templates/base/src/app.controller.spec.ts.hbs +3 -11
  15. package/dist/templates/base/src/app.controller.ts.hbs +2 -2
  16. package/dist/templates/base/src/app.module.ts.hbs +76 -5
  17. package/dist/templates/base/src/app.router.ts.hbs +3 -4
  18. package/dist/templates/base/src/database/adapters/interfaces/db-adapter.interface.ts.hbs +7 -0
  19. package/dist/templates/base/src/database/adapters/mongo/prisma-mongo.adapter.ts.hbs +36 -4
  20. package/dist/templates/base/src/database/adapters/mongo/raw-mongo.adapter.ts.hbs +17 -12
  21. package/dist/templates/base/src/database/adapters/sql/drizzle-sql.adapter.ts.hbs +9 -0
  22. package/dist/templates/base/src/database/adapters/sql/prisma-sql.adapter.ts.hbs +8 -5
  23. package/dist/templates/base/src/database/adapters/sql/raw-sql.adapter.ts.hbs +37 -0
  24. package/dist/templates/base/src/database/adapters/sql/typeorm-sql.adapter.ts.hbs +8 -3
  25. package/dist/templates/base/src/database/db.module.ts.hbs +55 -42
  26. package/dist/templates/base/src/database/db.service.ts.hbs +60 -6
  27. package/dist/templates/base/src/database/entities/app.entity.ts.hbs +25 -0
  28. package/dist/templates/base/src/database/entities/todo.entity.ts.hbs +12 -7
  29. package/dist/templates/base/src/database/index.ts.hbs +20 -0
  30. package/dist/templates/base/src/database/orm/drizzle/schema.ts.hbs +80 -7
  31. package/dist/templates/base/src/database/orm/typeorm/data-source.ts.hbs +23 -4
  32. package/dist/templates/base/src/libs/middlewares/auth.trpc.middleware.ts.hbs +47 -0
  33. package/dist/templates/base/src/libs/middlewares/rate-limit.trpc.middleware.ts.hbs +43 -0
  34. package/dist/templates/base/src/libs/trpc/error.handler.ts.hbs +1 -1
  35. package/dist/templates/base/src/libs/trpc/inner.context.ts.hbs +6 -3
  36. package/dist/templates/base/src/main.ts.hbs +34 -10
  37. package/dist/templates/base/src/modules/users/entities/user.entity.ts.hbs +22 -0
  38. package/dist/templates/base/test/app.e2e-spec.ts.hbs +841 -6
  39. package/dist/templates/base/test/bootstrap.e2e-spec.ts.hbs +84 -0
  40. package/dist/templates/base/tsconfig.build.json.hbs +2 -0
  41. package/dist/templates/base/vitest.config.e2e.ts.hbs +4 -3
  42. package/dist/templates/base/vitest.config.ts.hbs +4 -3
  43. package/dist/templates/base/yarn.lock.hbs +1 -0
  44. package/dist/templates/extras/docker/.dockerignore.hbs +8 -1
  45. package/dist/templates/extras/docker/Dockerfile.hbs +11 -10
  46. package/dist/templates/extras/health/src/health/health.controller.ts.hbs +54 -0
  47. package/dist/templates/extras/health/src/health/health.module.ts.hbs +9 -0
  48. package/dist/templates/extras/swagger/create-docs.ts.hbs +13 -22
  49. package/dist/templates/feature/graphql/src/graphql/graphql.module.ts.hbs +6 -10
  50. package/dist/templates/feature/graphql/src/graphql/models/todo.model.ts.hbs +4 -0
  51. package/dist/templates/feature/graphql/src/graphql/orm/drizzle/drizzle-graphql.module.ts.hbs +6 -4
  52. package/dist/templates/feature/graphql/src/graphql/resolvers/health.resolver.ts.hbs +14 -0
  53. package/dist/templates/feature/graphql/src/graphql/resolvers/todo.resolver.ts.hbs +210 -47
  54. package/dist/templates/feature/rest/todo-example/src/api/todo/dto/create-todo.dto.ts.hbs +12 -0
  55. package/dist/templates/feature/rest/todo-example/src/api/todo/dto/update-todo.dto.ts.hbs +12 -0
  56. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.controller.ts.hbs +21 -9
  57. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.module.ts.hbs +1 -3
  58. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.router.ts.hbs +41 -13
  59. package/dist/templates/feature/rest/todo-example/src/api/todo/todo.service.ts.hbs +157 -33
  60. package/dist/templates/monorepo/apps/api-gateway/package.json.hbs +2 -3
  61. package/dist/templates/monorepo/apps/api-gateway/src/app.controller.spec.ts.hbs +6 -1
  62. package/dist/templates/monorepo/apps/api-gateway/src/app.controller.ts.hbs +6 -0
  63. package/dist/templates/monorepo/apps/api-gateway/src/app.module.ts.hbs +32 -1
  64. package/dist/templates/monorepo/apps/api-gateway/src/app.service.ts.hbs +1 -1
  65. package/dist/templates/monorepo/apps/api-gateway/src/main.ts.hbs +13 -4
  66. package/dist/templates/monorepo/apps/api-gateway/test/app.e2e-spec.ts.hbs +45 -1
  67. package/dist/templates/monorepo/apps/api-gateway/tsconfig.build.json.hbs +2 -1
  68. package/dist/templates/monorepo/apps/api-gateway/vitest.config.e2e.ts.hbs +3 -2
  69. package/dist/templates/monorepo/apps/api-gateway/vitest.config.ts.hbs +3 -2
  70. package/dist/templates/monorepo/feature/graphql/src/graphql/graphql.module.ts.hbs +6 -10
  71. package/dist/templates/monorepo/feature/graphql/src/graphql/resolvers/health.resolver.ts.hbs +14 -0
  72. package/dist/templates/monorepo/feature/graphql/src/graphql/resolvers/todo.resolver.ts.hbs +151 -42
  73. package/dist/templates/monorepo/packages/env/src/api-gateway.ts.hbs +16 -3
  74. package/dist/templates/monorepo/packages/shared/package.json.hbs +1 -0
  75. package/dist/templates/monorepo/packages/shared/src/auth.ts.hbs +6 -0
  76. package/dist/templates/monorepo/root/.gitignore.hbs +1 -0
  77. package/dist/templates/monorepo/root/pnpm-workspace.yaml.hbs +19 -2
  78. package/dist/templates/monorepo/root/tsconfig.json.hbs +1 -1
  79. package/dist/templates/monorepo/root/turbo.json.hbs +2 -0
  80. package/dist/templates/single-project/architecture/nest-api/.env.hbs +9 -1
  81. package/dist/templates/single-project/architecture/nest-api/package.json.hbs +1 -2
  82. package/dist/templates/single-project/architecture/nest-api/src/main.ts.hbs +33 -9
  83. package/dist/templates/single-project/architecture/nest-gateway/src/main.ts.hbs +3 -3
  84. package/dist/templates/single-project/architecture/nest-microservice/.env.hbs +18 -0
  85. package/dist/templates/single-project/architecture/nest-microservice/proto/app.proto.hbs +26 -0
  86. package/dist/templates/single-project/architecture/nest-microservice/src/app.controller.ts.hbs +23 -4
  87. package/dist/templates/single-project/architecture/nest-microservice/src/app.service.ts.hbs +6 -2
  88. package/dist/templates/single-project/architecture/nest-microservice/src/main.ts.hbs +16 -5
  89. package/dist/templates/single-project/architecture/nest-microservice/test/microservice.e2e-spec.ts.hbs +70 -0
  90. package/dist/templates/single-project/shared/config/env.ts.hbs +38 -3
  91. package/package.json +5 -2
  92. package/dist/constants-Cgtoxhmd.js +0 -1
@@ -1,27 +1,862 @@
1
- import { INestApplication } from "@nestjs/common";
1
+ import { INestApplication{{#unless (includes addons "nestjs-zod")}}, StandardSchemaValidationPipe{{/unless}} } from "@nestjs/common";
2
+ {{#if (includes addons "nestjs-zod")}}
3
+ import { ZodValidationPipe } from "nestjs-zod";
4
+ {{/if}}
2
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}}
16
+ {{#if (eq httpProvider "fastify")}}
17
+ import { FastifyAdapter } from "@nestjs/platform-fastify";
18
+ {{/if}}
3
19
  import request from "supertest";
20
+ import { afterAll, beforeAll, describe, expect, it{{#if (and hasHealthCheck hasDatabase (or usePrisma useTypeorm useDrizzle))}}, vi{{/if}} } from "vitest";
4
21
  import type { App } from "supertest/types";
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}}
27
+ {{/if}}
28
+ {{#if (and swagger useSwaggerScalar)}}
29
+ import { apiReference } from "@scalar/nestjs-api-reference";
30
+ {{/if}}
5
31
 
32
+ {{#unless (or (eq auth "better-auth:stateless") (and isPassportAuth (not hasDatabase)))}}
6
33
  import { AppModule } from "./../src/app.module.js";
34
+ {{/unless}}
35
+ {{#if (and useDrizzle (eq auth "none"))}}
36
+ import { DbService } from "../src/database/db.service.js";
37
+ import { users } from "../src/database/orm/drizzle/schema.js";
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";
41
+ {{/if}}
7
42
 
8
43
  describe("AppController (e2e)", () => {
9
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>;
10
54
 
11
- beforeEach(async () => {
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}}
138
+
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}}
12
153
  const moduleFixture: TestingModule = await Test.createTestingModule({
13
154
  imports: [AppModule],
14
155
  }).compile();
15
156
 
16
- app = moduleFixture.createNestApplication();
157
+ {{#if (eq httpProvider "fastify")}}
158
+ app = moduleFixture.createNestApplication(new FastifyAdapter(){{#if isBetterAuth}}, { bodyParser: false }{{/if}});
159
+ {{else}}
160
+ app = moduleFixture.createNestApplication({{#if isBetterAuth}}{ bodyParser: false }{{/if}});
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}}
168
+ {{#if (includes addons "nestjs-zod")}}
169
+ app.useGlobalPipes(new ZodValidationPipe());
170
+ {{else}}
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}}
189
+ {{/if}}
17
190
  await app.init();
191
+ {{#if (eq httpProvider "fastify")}}
192
+ await app.getHttpAdapter().getInstance().ready();
193
+ {{/if}}
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}}
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}}
260
+
261
+ {{#if hasRest}}
262
+ it("/ (GET)", async () => {
263
+ const response = await request(app.getHttpServer()).get("/").expect(200);
264
+ expect(response.text).toBe("Hello World!");
265
+ });
266
+ {{/if}}
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
+ );
18
305
  });
306
+ {{/if}}
19
307
 
20
- it("/ (GET)", () => {
21
- return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
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}}
22
316
  });
317
+ {{/if}}
23
318
 
24
- afterEach(async () => {
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
+
335
+ {{#if hasTrpc}}
336
+ it("executes a tRPC query through HTTP", async () => {
337
+ const response = await request(app.getHttpServer())
338
+ .get("/trpc/appRouter.getHello").expect(200);
339
+ expect(response.body.result.data).toBe("Hello World!");
340
+ });
341
+ {{/if}}
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
+
397
+ {{#if (eq auth "better-auth")}}
398
+ it("registers, authenticates and revokes a persisted session", async () => {
399
+ const email = `scenario-${Date.now()}@example.com`;
400
+ const password = "Scenario-password-123!";
401
+ expect(password.length).toBeGreaterThanOrEqual(8);
402
+ const origin = "http://localhost:3000";
403
+ let server = app.getHttpServer();
404
+ const signedUp = await request(server).post("/api/auth/sign-up/email")
405
+ .set("Origin", origin).send({ email, password, name: "Scenario" }).expect(200);
406
+ expect(signedUp.body.user.email).toBe(email);
407
+ expect(signedUp.body.user.password).toBeUndefined();
408
+ await request(server).post("/api/auth/sign-up/email")
409
+ .set("Origin", origin).send({ email, password, name: "Duplicate" }).expect(422);
410
+ await request(server).post("/api/auth/sign-in/email")
411
+ .set("Origin", origin).send({ email, password: "wrong-password" }).expect(401);
412
+ const signedIn = await request(server).post("/api/auth/sign-in/email")
413
+ .set("Origin", origin).send({ email, password }).expect(200);
414
+ const cookies = signedIn.headers["set-cookie"];
415
+ expect(cookies).toBeTruthy();
416
+ const session = await request(server).get("/api/auth/get-session")
417
+ .set("Cookie", cookies).expect(200);
418
+ expect(session.body.user.email).toBe(email);
419
+ {{#if (or usePrisma useDrizzle)}}
420
+ await request(server).get("/auth-profile").expect(401);
421
+ const protectedProfile = await request(server).get("/auth-profile").set("Cookie", cookies).expect(200);
422
+ expect(protectedProfile.body.email).toBe(email);
423
+ await app.close();
424
+ await startApplication();
425
+ server = app.getHttpServer();
426
+ const persisted = await request(app.getHttpServer()).get("/api/auth/get-session").set("Cookie", cookies).expect(200);
427
+ expect(persisted.body.user.email).toBe(email);
428
+ {{/if}}
429
+ {{#if hasTrpc}}
430
+ await request(server).get("/trpc/appRouter.testAuth").expect(401);
431
+ const authenticated = await request(server).get("/trpc/appRouter.testAuth")
432
+ .set("Cookie", cookies).expect(200);
433
+ expect(authenticated.body.result.data.user.email).toBe(email);
434
+ {{/if}}
435
+ await request(server).post("/api/auth/sign-out").set("Origin", origin)
436
+ .set("Cookie", cookies).send({}).expect(200);
437
+ const revoked = await request(server).get("/api/auth/get-session")
438
+ .set("Cookie", cookies).expect(200);
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);
25
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);
493
+ });
494
+ {{/if}}
495
+
496
+ {{#if hasGraphql}}
497
+ it("queries and mutates persisted GraphQL todos with input validation", async () => {
498
+ const server = app.getHttpServer();
499
+ const execute = (query: string, variables: Record<string, unknown> = {}) => request(server).post("/graphql").send({ query, variables });
500
+ const hello = await execute("{ hello }");
501
+ expect(hello.body.errors).toBeUndefined();
502
+ expect(hello.body.data.hello).toBe("Hello GraphQL!");
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"))}}
519
+ const created = await execute('mutation($input: CreateTodoInput!) { createTodo(input: $input) { id title completed } }', { input: { title: "GraphQL integration" } });
520
+ expect(created.body.errors).toBeUndefined();
521
+ const { id } = created.body.data.createTodo;
522
+ const listed = await execute("{ todos { id title } }");
523
+ expect(listed.body.data.todos).toContainEqual({ id, title: "GraphQL integration" });
524
+ const updated = await execute('mutation($input: UpdateTodoInput!) { updateTodo(input: $input) { completed } }', { input: { id, completed: true } });
525
+ expect(updated.body.data.updateTodo.completed).toBe(true);
526
+ const invalid = await execute('mutation { createTodo(input: { title: " " }) { id } }');
527
+ expect(invalid.body.errors).toBeTruthy();
528
+ const deleted = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
529
+ expect(deleted.body.data.deleteTodo).toBe(true);
530
+ const missing = await execute('mutation($id: String!) { deleteTodo(id: $id) }', { id });
531
+ expect(missing.body.errors).toBeTruthy();
532
+ {{/if}}
533
+ });
534
+ {{/if}}
535
+
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 () => {
708
+ const server = app.getHttpServer();
709
+ const email = `passport-${Date.now()}@example.com`;
710
+ const password = "Scenario-password-123!";
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}}
717
+ expect(registered.body.password).toBeUndefined();
718
+ await request(server).post({{#if (and usePrisma hasRest (not hasMongodb))}}"/auth/register"{{else}}"/auth/signup"{{/if}}).send({ email, password }).expect(409);
719
+ await request(server).post("/auth/login").send({ email, password: "wrong-password-123" }).expect(401);
720
+ const login = await request(server).post("/auth/login").send({ email, password }).expect(200);
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)}}
732
+ await request(server).get("/auth/profile").expect(401);
733
+ const profile = await request(server).get("/auth/profile")
734
+ .auth(login.body.accessToken, { type: "bearer" }).expect(200);
735
+ expect(profile.body.email).toBe(email);
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);
746
+ const fresh = await request(server).post("/auth/login").send({ email, password }).expect(200);
747
+ await request(server).post("/auth/logout").send({ refreshToken: fresh.body.refreshToken }).expect(200);
748
+ await request(server).post("/auth/refresh").send({ refreshToken: fresh.body.refreshToken }).expect(401);
749
+ {{/if}}
750
+ });
751
+ {{/if}}
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"))}}
769
+ it("rejects malformed and missing MongoDB identifiers", async () => {
770
+ await request(app.getHttpServer()).delete("/todos/invalid").expect(400);
771
+ await request(app.getHttpServer()).delete("/todos/000000000000000000000000").expect(404);
772
+ await request(app.getHttpServer()).post("/todos").send({ title: "" }).expect(400);
773
+ });
774
+ {{/if}}
775
+
776
+ it("returns 404 for an unknown route", async () => {
777
+ await request(app.getHttpServer()).get("/__missing_route__").expect(404);
778
+ });
779
+
780
+ {{#if (and useDrizzle (eq auth "none"))}}
781
+ it("writes and reads the migrated Drizzle schema", async () => {
782
+ const db = app.get(DbService).drizzle;
783
+ const email = `drizzle-${Date.now()}@example.com`;
784
+ try {
785
+ await db.insert(users).values({ email, name: "Database integration" });
786
+ const rows = await db.select().from(users).where(eq(users.email, email));
787
+ expect(rows).toHaveLength(1);
788
+ expect(rows[0].name).toBe("Database integration");
789
+ } finally {
790
+ await db.delete(users).where(eq(users.email, email));
791
+ }
792
+ });
793
+ {{/if}}
794
+
795
+ {{#if (and hasTodoExample hasRest (eq auth "none"))}}
796
+ it("persists, updates and deletes a todo through HTTP", async () => {
797
+ const created = await request(app.getHttpServer()).post("/todos")
798
+ .send({ title: "Database integration", completed: false }).expect(201);
799
+ const { id } = created.body;
800
+ expect(id).toBeTruthy();
801
+ {{#if hasMongodb}}
802
+ await app.close();
803
+ await startApplication();
804
+ {{/if}}
805
+ const listed = await request(app.getHttpServer()).get("/todos").expect(200);
806
+ expect(listed.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, title: "Database integration" })]));
807
+ await request(app.getHttpServer()).put(`/todos/${id}`).send({ completed: true }).expect(200);
808
+ const updated = await request(app.getHttpServer()).get("/todos").expect(200);
809
+ expect(updated.body).toEqual(expect.arrayContaining([expect.objectContaining({ id, completed: true })]));
810
+ await request(app.getHttpServer()).delete(`/todos/${id}`).expect(200);
811
+ const deleted = await request(app.getHttpServer()).get("/todos").expect(200);
812
+ expect(deleted.body).not.toEqual(expect.arrayContaining([expect.objectContaining({ id })]));
813
+ });
814
+ {{/if}}
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
+
849
+ it("rejects unsupported methods", async () => {
850
+ await request(app.getHttpServer()).post("/").send({ unexpected: true }).expect(404);
851
+ });
852
+
853
+ afterAll(async () => {
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}}
26
861
  });
27
862
  });