mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
160 lines
5.3 KiB
TypeScript
160 lines
5.3 KiB
TypeScript
import type { INestApplication } from "@nestjs/common";
|
|
import request from "supertest";
|
|
import { createSimpleApplication } from "../src/apps/simple/bootstrap";
|
|
|
|
describe("Simple API", () => {
|
|
let app: INestApplication;
|
|
|
|
beforeAll(async () => {
|
|
const created = await createSimpleApplication(false);
|
|
app = created.app;
|
|
await app.init();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await request(app.getHttpServer())
|
|
.post("/api/v1/testing/reset")
|
|
.expect(200);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
async function login(email = "admin@demo.local") {
|
|
return request(app.getHttpServer())
|
|
.post("/api/v1/auth/login")
|
|
.send({ email, password: "demo1234" })
|
|
.expect(200);
|
|
}
|
|
|
|
it("serves health, Swagger JSON and observable headers", async () => {
|
|
const health = await request(app.getHttpServer())
|
|
.get("/api/v1/health")
|
|
.set("X-Request-Id", "frontend-request-001")
|
|
.expect(200);
|
|
|
|
expect(health.body.data).toMatchObject({
|
|
application: "simple",
|
|
status: "ok",
|
|
});
|
|
expect(health.headers["x-request-id"]).toBe("frontend-request-001");
|
|
expect(health.headers["x-response-time"]).toMatch(/^\d+ms$/);
|
|
|
|
const contract = await request(app.getHttpServer())
|
|
.get("/openapi.json")
|
|
.expect(200);
|
|
expect(contract.body.info.title).toBe("Demo Simple API");
|
|
expect(contract.body.components.securitySchemes.jwt).toBeDefined();
|
|
});
|
|
|
|
it("supports empty, large, error and rate-limit scenarios deterministically", async () => {
|
|
const empty = await request(app.getHttpServer())
|
|
.get("/api/v1/products")
|
|
.set("X-Demo-Scenario", "empty")
|
|
.expect(200);
|
|
expect(empty.body.data).toEqual([]);
|
|
expect(empty.body.meta.total).toBe(0);
|
|
|
|
const large = await request(app.getHttpServer())
|
|
.get("/api/v1/categories")
|
|
.set("X-Demo-Scenario", "large-dataset")
|
|
.expect(200);
|
|
expect(large.body.data).toHaveLength(250);
|
|
|
|
const limited = await request(app.getHttpServer())
|
|
.get("/api/v1/products")
|
|
.set("X-Demo-Scenario", "rate-limited")
|
|
.expect(429);
|
|
expect(limited.headers["retry-after"]).toBe("3");
|
|
expect(limited.body.code).toBe("DEMO_RATE_LIMITED");
|
|
|
|
const failed = await request(app.getHttpServer())
|
|
.get("/api/v1/products")
|
|
.set("X-Demo-Scenario", "server-error")
|
|
.expect(500);
|
|
expect(failed.body.code).toBe("DEMO_SERVER_ERROR");
|
|
});
|
|
|
|
it("authenticates, rotates refresh tokens and rejects token reuse", async () => {
|
|
const authenticated = await login();
|
|
const { accessToken, refreshToken } = authenticated.body.data.tokens;
|
|
|
|
const me = await request(app.getHttpServer())
|
|
.get("/api/v1/users/me")
|
|
.set("Authorization", `Bearer ${accessToken}`)
|
|
.expect(200);
|
|
expect(me.body.data.email).toBe("admin@demo.local");
|
|
|
|
await request(app.getHttpServer())
|
|
.get("/api/v1/users/me")
|
|
.set("Authorization", `Bearer ${accessToken}`)
|
|
.set("X-Demo-Scenario", "expired-auth")
|
|
.expect(401);
|
|
|
|
const rotated = await request(app.getHttpServer())
|
|
.post("/api/v1/auth/refresh")
|
|
.send({ refreshToken })
|
|
.expect(200);
|
|
expect(rotated.body.data.tokens.refreshToken).not.toBe(refreshToken);
|
|
|
|
const reused = await request(app.getHttpServer())
|
|
.post("/api/v1/auth/refresh")
|
|
.send({ refreshToken })
|
|
.expect(401);
|
|
expect(reused.body.code).toBe("REFRESH_TOKEN_REUSED");
|
|
});
|
|
|
|
it("returns 304 for a current product ETag", async () => {
|
|
const first = await request(app.getHttpServer())
|
|
.get("/api/v1/products/product-keyboard")
|
|
.expect(200);
|
|
expect(first.headers.etag).toBeDefined();
|
|
|
|
await request(app.getHttpServer())
|
|
.get("/api/v1/products/product-keyboard")
|
|
.set("If-None-Match", first.headers.etag)
|
|
.expect(304);
|
|
});
|
|
|
|
it("detects stale product updates and returns structured conflicts", async () => {
|
|
const authenticated = await login();
|
|
const accessToken = authenticated.body.data.tokens.accessToken as string;
|
|
const authorization = `Bearer ${accessToken}`;
|
|
|
|
const updated = await request(app.getHttpServer())
|
|
.patch("/api/v1/products/product-keyboard")
|
|
.set("Authorization", authorization)
|
|
.send({ version: 1, name: "Mechanical Keyboard Updated" })
|
|
.expect(200);
|
|
expect(updated.body.data.version).toBe(2);
|
|
|
|
const conflict = await request(app.getHttpServer())
|
|
.patch("/api/v1/products/product-keyboard")
|
|
.set("Authorization", authorization)
|
|
.send({ version: 1, name: "Stale Update" })
|
|
.expect(409);
|
|
expect(conflict.body.code).toBe("PRODUCT_VERSION_CONFLICT");
|
|
expect(conflict.body.requestId).toMatch(/^req-/);
|
|
});
|
|
|
|
it("enforces roles and validates nested orders", async () => {
|
|
const customer = await login("customer@demo.local");
|
|
const authorization = `Bearer ${customer.body.data.tokens.accessToken as string}`;
|
|
|
|
await request(app.getHttpServer())
|
|
.post("/api/v1/products")
|
|
.set("Authorization", authorization)
|
|
.send({})
|
|
.expect(403);
|
|
|
|
const order = await request(app.getHttpServer())
|
|
.post("/api/v1/orders")
|
|
.set("Authorization", authorization)
|
|
.send({ items: [{ productId: "product-keyboard", quantity: 1 }] })
|
|
.expect(201);
|
|
expect(order.body.data.userId).toBe("user-customer");
|
|
expect(order.body.data.totalCents).toBe(12990);
|
|
});
|
|
});
|