chore: Новый черновик DRAFT, удалить старые docs-v

This commit is contained in:
2026-07-30 09:19:59 +03:00
parent 6f6e4896af
commit 590fb63ca7
122 changed files with 29145 additions and 3253 deletions

View File

@@ -0,0 +1,288 @@
import type { AddressInfo } from "node:net";
import type { INestApplication } from "@nestjs/common";
import { io, type Socket } from "socket.io-client";
import request from "supertest";
import { createComplexApplication } from "../src/apps/complex/bootstrap";
interface AuthenticatedAgent {
agent: ReturnType<typeof request.agent>;
csrfToken: string;
cookieHeader: string;
}
describe("Complex API", () => {
let app: INestApplication;
let baseUrl: string;
beforeAll(async () => {
const created = await createComplexApplication(false);
app = created.app;
await app.listen(0, "127.0.0.1");
const address = app.getHttpServer().address() as AddressInfo;
baseUrl = `http://127.0.0.1:${address.port}`;
});
beforeEach(async () => {
await request(app.getHttpServer())
.post("/api/v1/testing/reset")
.expect(200);
});
afterAll(async () => {
await app.close();
});
async function login(
email = "admin@complex.demo",
): Promise<AuthenticatedAgent> {
const agent = request.agent(app.getHttpServer());
const response = await agent
.post("/api/v1/auth/login")
.send({ email, password: "demo1234" })
.expect(200);
const setCookies = response.headers["set-cookie"] as unknown as string[];
return {
agent,
csrfToken: response.body.data.csrfToken as string,
cookieHeader: setCookies
.map((cookie) => cookie.split(";", 1)[0])
.join("; "),
};
}
it("serves an isolated cookie-auth OpenAPI document", async () => {
const contract = await request(app.getHttpServer())
.get("/openapi.json")
.expect(200);
expect(contract.body.info.title).toBe("Demo Complex API");
expect(
contract.body.components.securitySchemes.cookieSession,
).toBeDefined();
expect(contract.body.components.securitySchemes.csrf).toBeDefined();
expect(contract.body.paths["/api/v1/organizations"]).toBeDefined();
});
it("establishes a cookie session and enforces tenant context", async () => {
await request(app.getHttpServer()).get("/api/v1/users/me").expect(401);
const authenticated = await login();
const me = await authenticated.agent.get("/api/v1/users/me").expect(200);
expect(me.body.data.email).toBe("admin@complex.demo");
const missingTenant = await authenticated.agent
.get("/api/v1/products")
.expect(400);
expect(missingTenant.body.code).toBe("ORGANIZATION_REQUIRED");
const products = await authenticated.agent
.get("/api/v1/products")
.set("X-Organization-Id", "org-acme")
.expect(200);
expect(products.body.data[0].organizationId).toBe("org-acme");
await authenticated.agent
.get("/api/v1/products")
.set("X-Organization-Id", "org-unknown")
.expect(403);
});
it("enforces CSRF and optimistic product versions", async () => {
const authenticated = await login();
const url = "/api/v1/products/complex-product-keyboard";
await authenticated.agent
.patch(url)
.set("X-Organization-Id", "org-acme")
.send({ version: 3, name: "No CSRF" })
.expect(403);
const updated = await authenticated.agent
.patch(url)
.set("X-Organization-Id", "org-acme")
.set("X-CSRF-Token", authenticated.csrfToken)
.send({ version: 3, name: "Keyboard Enterprise" })
.expect(200);
expect(updated.body.data.version).toBe(4);
const stale = await authenticated.agent
.patch(url)
.set("X-Organization-Id", "org-acme")
.set("X-CSRF-Token", authenticated.csrfToken)
.send({ version: 3, name: "Stale Keyboard" })
.expect(409);
expect(stale.body.code).toBe("PRODUCT_VERSION_CONFLICT");
});
it("creates orders idempotently", async () => {
const authenticated = await login();
const body = {
customerId: "customer-ada",
items: [
{
productId: "complex-product-keyboard",
variantId: "variant-complex-product-keyboard-default",
quantity: 1,
},
],
promotionCode: "WELCOME10",
};
const create = () =>
authenticated.agent
.post("/api/v1/orders")
.set("X-Organization-Id", "org-acme")
.set("X-CSRF-Token", authenticated.csrfToken)
.set("Idempotency-Key", "checkout-test-001")
.send(body)
.expect(201);
const first = await create();
const second = await create();
expect(second.body.data.id).toBe(first.body.data.id);
expect(first.body.data.total.amount).toBe("116.91");
});
it("supports role changes without recreating the session", async () => {
const authenticated = await login("viewer@complex.demo");
await authenticated.agent
.post("/api/v1/products")
.set("X-Organization-Id", "org-acme")
.set("X-CSRF-Token", authenticated.csrfToken)
.send({})
.expect(403);
await request(app.getHttpServer())
.post("/api/v1/testing/users/complex-user-viewer/role")
.send({ role: "manager" })
.expect(200);
const allowedToReachValidation = await authenticated.agent
.post("/api/v1/products")
.set("X-Organization-Id", "org-acme")
.set("X-CSRF-Token", authenticated.csrfToken)
.send({})
.expect(400);
expect(allowedToReachValidation.body.code).toBe("BAD_REQUEST");
});
it("expires an active session on demand", async () => {
const authenticated = await login();
await authenticated.agent
.post("/api/v1/testing/session/expire")
.set("X-CSRF-Token", authenticated.csrfToken)
.expect(200);
await authenticated.agent.get("/api/v1/users/me").expect(401);
});
it("runs a 202 background export and exposes the completed result", async () => {
const authenticated = await login();
const started = await authenticated.agent
.post("/api/v1/exports/orders")
.set("X-Organization-Id", "org-acme")
.set("X-CSRF-Token", authenticated.csrfToken)
.expect(202);
const jobId = started.body.data.id as string;
await authenticated.agent
.get(`/api/v1/jobs/${jobId}/result`)
.set("X-Organization-Id", "org-acme")
.expect(409);
await new Promise((resolve) => setTimeout(resolve, 850));
const completed = await authenticated.agent
.get(`/api/v1/jobs/${jobId}`)
.set("X-Organization-Id", "org-acme")
.expect(200);
expect(completed.body.data).toMatchObject({
status: "completed",
progress: 100,
});
const result = await authenticated.agent
.get(`/api/v1/jobs/${jobId}/result`)
.set("X-Organization-Id", "org-acme")
.expect(200);
expect(result.headers["content-type"]).toMatch(/text\/csv/);
});
it("uploads and downloads a multipart file", async () => {
const authenticated = await login();
const uploaded = await authenticated.agent
.post("/api/v1/files")
.set("X-Organization-Id", "org-acme")
.set("X-CSRF-Token", authenticated.csrfToken)
.attach("file", Buffer.from("sku,stock\nABC,10\n"), "inventory.csv")
.expect(201);
expect(uploaded.body.data.name).toBe("inventory.csv");
const downloaded = await authenticated.agent
.get(`/api/v1/files/${uploaded.body.data.id as string}/download`)
.set("X-Organization-Id", "org-acme")
.expect(200);
expect(downloaded.headers["content-disposition"]).toContain(
"inventory.csv",
);
const tooLarge = await authenticated.agent
.post("/api/v1/files")
.set("X-Organization-Id", "org-acme")
.set("X-CSRF-Token", authenticated.csrfToken)
.attach("file", Buffer.alloc(5 * 1024 * 1024 + 1), "too-large.bin")
.expect(413);
expect(tooLarge.body.code).toBe("FILE_TOO_LARGE");
});
it("authenticates Socket.IO with the session cookie and deduplicates messages", async () => {
const authenticated = await login();
const socket = io(`${baseUrl}/chat`, {
transports: ["websocket"],
reconnection: false,
forceNew: true,
extraHeaders: { Cookie: authenticated.cookieHeader },
});
await waitForSocketEvent(socket, "connect");
const joined = waitForSocketEvent<{ conversationId: string }>(
socket,
"chat:joined",
);
socket.emit("chat:join", {
organizationId: "org-acme",
conversationId: "conversation-support",
});
expect((await joined).conversationId).toBe("conversation-support");
const messagePayload = {
organizationId: "org-acme",
conversationId: "conversation-support",
text: "Socket E2E message",
clientMessageId: "socket-e2e-001",
};
const firstAck = waitForSocketEvent<{ id: string }>(socket, "message:ack");
socket.emit("message:send", messagePayload);
const first = await firstAck;
const secondAck = waitForSocketEvent<{ id: string }>(socket, "message:ack");
socket.emit("message:send", messagePayload);
const second = await secondAck;
expect(second.id).toBe(first.id);
socket.disconnect();
}, 10_000);
});
function waitForSocketEvent<T = void>(
socket: Socket,
event: string,
timeoutMs = 3000,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
socket.off(event, handler);
reject(new Error(`Timed out waiting for Socket.IO event ${event}.`));
}, timeoutMs);
const handler = (payload: T) => {
clearTimeout(timer);
resolve(payload);
};
socket.once(event, handler);
});
}

View File

@@ -0,0 +1,159 @@
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);
});
});