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