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