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