feat: add example

This commit is contained in:
2026-08-01 09:31:08 +03:00
parent 15805e28df
commit 26b59686a5
434 changed files with 34975 additions and 4995 deletions

View File

@@ -3,6 +3,7 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
"deleteOutDir": false,
"tsConfigPath": "tsconfig.build.json"
}
}

View File

@@ -40,9 +40,7 @@
}
},
"summary": "Check Simple API availability",
"tags": [
"Health"
]
"tags": ["Health"]
}
},
"/api/v1/auth/login": {
@@ -124,9 +122,7 @@
}
},
"summary": "Login and receive JWT access/refresh tokens",
"tags": [
"Auth"
]
"tags": ["Auth"]
}
},
"/api/v1/auth/refresh": {
@@ -228,9 +224,7 @@
}
},
"summary": "Rotate a refresh token and issue a new token pair",
"tags": [
"Auth"
]
"tags": ["Auth"]
}
},
"/api/v1/auth/logout": {
@@ -305,9 +299,7 @@
}
},
"summary": "Revoke a refresh token; the operation is idempotent",
"tags": [
"Auth"
]
"tags": ["Auth"]
}
},
"/api/v1/users/me": {
@@ -404,9 +396,7 @@
}
],
"summary": "Get the authenticated user",
"tags": [
"Users"
]
"tags": ["Users"]
}
},
"/api/v1/products": {
@@ -488,12 +478,7 @@
"schema": {
"default": "newest",
"type": "string",
"enum": [
"newest",
"price-asc",
"price-desc",
"name"
]
"enum": ["newest", "price-asc", "price-desc", "name"]
}
}
],
@@ -540,9 +525,7 @@
}
},
"summary": "List products using offset pagination, filters and sorting",
"tags": [
"Products"
]
"tags": ["Products"]
},
"post": {
"operationId": "SimpleProducts_create",
@@ -647,9 +630,7 @@
}
],
"summary": "Create a product as an administrator",
"tags": [
"Products"
]
"tags": ["Products"]
}
},
"/api/v1/products/{id}": {
@@ -742,9 +723,7 @@
}
},
"summary": "Get one product with ETag support",
"tags": [
"Products"
]
"tags": ["Products"]
},
"patch": {
"operationId": "SimpleProducts_update",
@@ -877,9 +856,7 @@
}
],
"summary": "Update a product using optimistic locking",
"tags": [
"Products"
]
"tags": ["Products"]
},
"delete": {
"operationId": "SimpleProducts_remove",
@@ -992,9 +969,7 @@
}
],
"summary": "Delete a product as an administrator",
"tags": [
"Products"
]
"tags": ["Products"]
}
},
"/api/v1/categories": {
@@ -1036,9 +1011,7 @@
}
},
"summary": "List product categories",
"tags": [
"Categories"
]
"tags": ["Categories"]
}
},
"/api/v1/categories/{id}": {
@@ -1128,9 +1101,7 @@
}
},
"summary": "Get one category",
"tags": [
"Categories"
]
"tags": ["Categories"]
}
},
"/api/v1/orders": {
@@ -1256,9 +1227,7 @@
}
],
"summary": "List orders visible to the current user",
"tags": [
"Orders"
]
"tags": ["Orders"]
},
"post": {
"operationId": "SimpleOrders_create",
@@ -1346,6 +1315,16 @@
}
}
},
"422": {
"description": "The validated request violates an order invariant.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponseDto"
}
}
}
},
"429": {
"description": "Demo rate limit scenario.",
"content": {
@@ -1373,9 +1352,7 @@
}
],
"summary": "Create an order and validate product stock",
"tags": [
"Orders"
]
"tags": ["Orders"]
}
},
"/api/v1/orders/{id}": {
@@ -1490,9 +1467,7 @@
}
],
"summary": "Get one visible order",
"tags": [
"Orders"
]
"tags": ["Orders"]
}
},
"/api/v1/orders/{id}/cancel": {
@@ -1617,9 +1592,7 @@
}
],
"summary": "Cancel an order if its state permits the transition",
"tags": [
"Orders"
]
"tags": ["Orders"]
}
},
"/api/v1/testing/scenarios": {
@@ -1661,9 +1634,7 @@
}
},
"summary": "List deterministic X-Demo-Scenario values",
"tags": [
"Testing"
]
"tags": ["Testing"]
}
},
"/api/v1/testing/reset": {
@@ -1705,9 +1676,7 @@
}
},
"summary": "Reset all Simple API state and token revocations",
"tags": [
"Testing"
]
"tags": ["Testing"]
}
},
"/api/v1/testing/seed/{preset}": {
@@ -1740,10 +1709,7 @@
"required": true,
"in": "path",
"schema": {
"enum": [
"small",
"large"
],
"enum": ["small", "large"],
"type": "string"
}
}
@@ -1761,9 +1727,7 @@
}
},
"summary": "Select a small or large deterministic dataset",
"tags": [
"Testing"
]
"tags": ["Testing"]
}
},
"/api/v1/testing/users/{userId}/role": {
@@ -1863,9 +1827,7 @@
}
},
"summary": "Change a user role to exercise dynamic access control",
"tags": [
"Testing"
]
"tags": ["Testing"]
}
}
},
@@ -1896,17 +1858,12 @@
"properties": {
"application": {
"type": "string",
"enum": [
"simple",
"complex"
],
"enum": ["simple", "complex"],
"example": "simple"
},
"status": {
"type": "string",
"enum": [
"ok"
],
"enum": ["ok"],
"example": "ok"
},
"timestamp": {
@@ -1918,12 +1875,7 @@
"example": "1.0.0"
}
},
"required": [
"application",
"status",
"timestamp",
"version"
]
"required": ["application", "status", "timestamp", "version"]
},
"HealthResponseDto": {
"type": "object",
@@ -1932,9 +1884,7 @@
"$ref": "#/components/schemas/HealthDataDto"
}
},
"required": [
"data"
]
"required": ["data"]
},
"LoginDto": {
"type": "object",
@@ -1951,10 +1901,7 @@
"minLength": 8
}
},
"required": [
"email",
"password"
]
"required": ["email", "password"]
},
"JwtTokensDto": {
"type": "object",
@@ -1972,18 +1919,11 @@
},
"tokenType": {
"type": "string",
"enum": [
"Bearer"
],
"enum": ["Bearer"],
"example": "Bearer"
}
},
"required": [
"accessToken",
"refreshToken",
"expiresIn",
"tokenType"
]
"required": ["accessToken", "refreshToken", "expiresIn", "tokenType"]
},
"SimpleUserDto": {
"type": "object",
@@ -2003,10 +1943,7 @@
},
"role": {
"type": "string",
"enum": [
"admin",
"customer"
]
"enum": ["admin", "customer"]
},
"avatarUrl": {
"type": "object",
@@ -2014,13 +1951,7 @@
"example": "https://i.pravatar.cc/160?img=12"
}
},
"required": [
"id",
"email",
"name",
"role",
"avatarUrl"
]
"required": ["id", "email", "name", "role", "avatarUrl"]
},
"JwtAuthDataDto": {
"type": "object",
@@ -2032,10 +1963,7 @@
"$ref": "#/components/schemas/SimpleUserDto"
}
},
"required": [
"tokens",
"user"
]
"required": ["tokens", "user"]
},
"JwtAuthResponseDto": {
"type": "object",
@@ -2044,9 +1972,7 @@
"$ref": "#/components/schemas/JwtAuthDataDto"
}
},
"required": [
"data"
]
"required": ["data"]
},
"ErrorDetailDto": {
"type": "object",
@@ -2064,9 +1990,7 @@
"example": "isEmail"
}
},
"required": [
"message"
]
"required": ["message"]
},
"ErrorResponseDto": {
"type": "object",
@@ -2121,9 +2045,7 @@
"description": "Refresh token returned by login or the previous refresh call."
}
},
"required": [
"refreshToken"
]
"required": ["refreshToken"]
},
"SimpleUserResponseDto": {
"type": "object",
@@ -2132,9 +2054,7 @@
"$ref": "#/components/schemas/SimpleUserDto"
}
},
"required": [
"data"
]
"required": ["data"]
},
"Object": {
"type": "object",
@@ -2166,10 +2086,7 @@
},
"currency": {
"type": "string",
"enum": [
"USD",
"EUR"
],
"enum": ["USD", "EUR"],
"example": "USD"
},
"categoryId": {
@@ -2241,12 +2158,7 @@
"minimum": 0
}
},
"required": [
"page",
"limit",
"total",
"totalPages"
]
"required": ["page", "limit", "total", "totalPages"]
},
"ProductsResponseDto": {
"type": "object",
@@ -2261,10 +2173,7 @@
"$ref": "#/components/schemas/PageMetaDto"
}
},
"required": [
"data",
"meta"
]
"required": ["data", "meta"]
},
"ProductResponseDto": {
"type": "object",
@@ -2273,9 +2182,7 @@
"$ref": "#/components/schemas/SimpleProductDto"
}
},
"required": [
"data"
]
"required": ["data"]
},
"CreateProductDto": {
"type": "object",
@@ -2295,10 +2202,7 @@
},
"currency": {
"type": "string",
"enum": [
"USD",
"EUR"
],
"enum": ["USD", "EUR"],
"example": "USD"
},
"categoryId": {
@@ -2344,10 +2248,7 @@
},
"currency": {
"type": "string",
"enum": [
"USD",
"EUR"
],
"enum": ["USD", "EUR"],
"example": "USD"
},
"categoryId": {
@@ -2371,9 +2272,7 @@
"description": "Version last read by the frontend."
}
},
"required": [
"version"
]
"required": ["version"]
},
"MutationResultDto": {
"type": "object",
@@ -2387,10 +2286,7 @@
"example": true
}
},
"required": [
"id",
"success"
]
"required": ["id", "success"]
},
"MutationResponseDto": {
"type": "object",
@@ -2399,9 +2295,7 @@
"$ref": "#/components/schemas/MutationResultDto"
}
},
"required": [
"data"
]
"required": ["data"]
},
"SimpleCategoryDto": {
"type": "object",
@@ -2423,12 +2317,7 @@
"example": 4
}
},
"required": [
"id",
"name",
"slug",
"productCount"
]
"required": ["id", "name", "slug", "productCount"]
},
"CategoriesResponseDto": {
"type": "object",
@@ -2440,9 +2329,7 @@
}
}
},
"required": [
"data"
]
"required": ["data"]
},
"CategoryResponseDto": {
"type": "object",
@@ -2451,9 +2338,7 @@
"$ref": "#/components/schemas/SimpleCategoryDto"
}
},
"required": [
"data"
]
"required": ["data"]
},
"SimpleOrderItemDto": {
"type": "object",
@@ -2475,12 +2360,7 @@
"example": 12990
}
},
"required": [
"productId",
"productName",
"quantity",
"unitPriceCents"
]
"required": ["productId", "productName", "quantity", "unitPriceCents"]
},
"SimpleOrderDto": {
"type": "object",
@@ -2495,12 +2375,7 @@
},
"status": {
"type": "string",
"enum": [
"pending",
"paid",
"shipped",
"cancelled"
]
"enum": ["pending", "paid", "shipped", "cancelled"]
},
"items": {
"type": "array",
@@ -2514,10 +2389,7 @@
},
"currency": {
"type": "string",
"enum": [
"USD",
"EUR"
],
"enum": ["USD", "EUR"],
"example": "USD"
},
"createdAt": {
@@ -2548,10 +2420,7 @@
"$ref": "#/components/schemas/PageMetaDto"
}
},
"required": [
"data",
"meta"
]
"required": ["data", "meta"]
},
"OrderResponseDto": {
"type": "object",
@@ -2560,9 +2429,7 @@
"$ref": "#/components/schemas/SimpleOrderDto"
}
},
"required": [
"data"
]
"required": ["data"]
},
"CreateOrderItemDto": {
"type": "object",
@@ -2576,11 +2443,23 @@
"example": 1,
"minimum": 1,
"maximum": 20
},
"expectedVersion": {
"type": "number",
"example": 1,
"minimum": 1
},
"expectedUnitPriceCents": {
"type": "number",
"example": 12990,
"minimum": 0
}
},
"required": [
"productId",
"quantity"
"quantity",
"expectedVersion",
"expectedUnitPriceCents"
]
},
"CreateOrderDto": {
@@ -2593,9 +2472,7 @@
}
}
},
"required": [
"items"
]
"required": ["items"]
},
"ScenarioDto": {
"type": "object",
@@ -2609,10 +2486,7 @@
"example": "Delays the response to exercise loading and cancellation states."
}
},
"required": [
"name",
"description"
]
"required": ["name", "description"]
},
"ScenariosResponseDto": {
"type": "object",
@@ -2624,9 +2498,7 @@
}
}
},
"required": [
"data"
]
"required": ["data"]
},
"TestingActionDataDto": {
"type": "object",
@@ -2640,10 +2512,7 @@
"example": "State reset to the default deterministic seed."
}
},
"required": [
"success",
"message"
]
"required": ["success", "message"]
},
"TestingActionResponseDto": {
"type": "object",
@@ -2652,25 +2521,18 @@
"$ref": "#/components/schemas/TestingActionDataDto"
}
},
"required": [
"data"
]
"required": ["data"]
},
"ChangeSimpleRoleDto": {
"type": "object",
"properties": {
"role": {
"type": "string",
"enum": [
"admin",
"customer"
],
"enum": ["admin", "customer"],
"example": "customer"
}
},
"required": [
"role"
]
"required": ["role"]
}
}
}

View File

@@ -3020,9 +3020,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.17",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz",
"integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==",
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4685,9 +4685,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -8,8 +8,8 @@
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"start:dev": "concurrently -k -n simple,complex -c blue,magenta \"npm:dev:simple\" \"npm:dev:complex\"",
"dev:simple": "nest start --watch --entryFile apps/simple/main",
"dev:complex": "nest start --watch --entryFile apps/complex/main",
"dev:simple": "nest start --watch --path tsconfig.build.json --entryFile apps/simple/main",
"dev:complex": "nest start --watch --path tsconfig.build.json --entryFile apps/complex/main",
"start:simple": "node dist/apps/simple/main.js",
"start:complex": "node dist/apps/complex/main.js",
"openapi:generate": "npm run build && node dist/scripts/generate-openapi.js",

View File

@@ -270,7 +270,7 @@ export class SimpleOrdersController {
@Post()
@ApiOperation({ summary: "Create an order and validate product stock" })
@ApiCreatedResponse({ type: OrderResponseDto })
@ApiStandardErrors({ auth: true, conflict: true })
@ApiStandardErrors({ auth: true, conflict: true, unprocessable: true })
create(
@Req() request: DemoRequest,
@Body() dto: CreateOrderDto,

View File

@@ -9,6 +9,7 @@ import {
IsString,
Max,
MaxLength,
Matches,
Min,
MinLength,
ValidateNested,
@@ -243,6 +244,9 @@ export class CreateProductDto {
example: "https://picsum.photos/seed/dock/640/480",
})
@IsString()
@Matches(/^https:\/\/picsum\.photos\//, {
message: "imageUrl must use the https://picsum.photos host",
})
imageUrl!: string;
}
@@ -298,6 +302,16 @@ export class CreateOrderItemDto {
@Min(1)
@Max(20)
quantity!: number;
@ApiProperty({ example: 1, minimum: 1 })
@IsInt()
@Min(1)
expectedVersion!: number;
@ApiProperty({ example: 12990, minimum: 0 })
@IsInt()
@Min(0)
expectedUnitPriceCents!: number;
}
export class CreateOrderDto {

View File

@@ -351,21 +351,54 @@ export class SimpleStore {
details: [{ field: "items", message: "Add at least one item." }],
});
}
const items = dto.items.map(({ productId, quantity }) => {
const product = this.getProduct(productId);
if (product.stock < quantity) {
throw new ConflictException({
code: "INSUFFICIENT_STOCK",
message: `Not enough stock for ${product.name}.`,
const productIds = new Set<string>();
dto.items.forEach((item) => {
if (productIds.has(item.productId)) {
throw new UnprocessableEntityException({
code: "DUPLICATE_ORDER_PRODUCT",
message: "Each product may appear only once in an order.",
});
}
return {
productId,
productName: product.name,
quantity,
unitPriceCents: product.priceCents,
};
productIds.add(item.productId);
});
const items = dto.items.map(
({ productId, quantity, expectedVersion, expectedUnitPriceCents }) => {
const product = this.getProduct(productId);
if (
product.version !== expectedVersion ||
product.priceCents !== expectedUnitPriceCents
) {
throw new ConflictException({
code: "PRODUCT_CHANGED",
message: `${product.name} changed before checkout.`,
});
}
if (product.currency !== "USD") {
throw new UnprocessableEntityException({
code: "UNSUPPORTED_ORDER_CURRENCY",
message: "Simple API checkout accepts USD products only.",
});
}
if (product.stock < quantity) {
throw new ConflictException({
code: "INSUFFICIENT_STOCK",
message: `Not enough stock for ${product.name}.`,
});
}
return {
productId,
productName: product.name,
quantity,
unitPriceCents: product.priceCents,
};
},
);
const order: SimpleOrderDto = {
id: `order-${String(this.orderSequence++).padStart(3, "0")}`,
userId,
@@ -417,7 +450,7 @@ export class SimpleStore {
stock,
rating,
imageUrl: `https://picsum.photos/seed/${slug}/640/480`,
createdAt: `2026-07-${String(10 + this.products.length).padStart(2, "0")}T09:00:00.000Z`,
createdAt: "2026-07-10T09:00:00.000Z",
version: 1,
};
}

View File

@@ -9,11 +9,17 @@ import {
ApiResponse,
ApiTooManyRequestsResponse,
ApiUnauthorizedResponse,
ApiUnprocessableEntityResponse,
} from "@nestjs/swagger";
import { ErrorResponseDto } from "./api.dto";
export function ApiStandardErrors(
options: { auth?: boolean; notFound?: boolean; conflict?: boolean } = {},
options: {
auth?: boolean;
notFound?: boolean;
conflict?: boolean;
unprocessable?: boolean;
} = {},
) {
const decorators: Array<
ClassDecorator | MethodDecorator | PropertyDecorator
@@ -63,6 +69,15 @@ export function ApiStandardErrors(
);
}
if (options.unprocessable) {
decorators.push(
ApiUnprocessableEntityResponse({
description: "The validated request violates an order invariant.",
type: ErrorResponseDto,
}),
);
}
return applyDecorators(...decorators);
}

View File

@@ -10,8 +10,8 @@ import {
UnauthorizedException,
} from "@nestjs/common";
import type { Request, Response } from "express";
import { Observable } from "rxjs";
import { delay, map } from "rxjs/operators";
import { fromEvent, Observable, timer } from "rxjs";
import { delay, map, mergeMap, takeUntil } from "rxjs/operators";
import { DEMO_SCENARIOS, type DemoScenario } from "./api.dto";
function scenarioFromRequest(request: Request): DemoScenario {
@@ -80,9 +80,20 @@ export class ScenarioInterceptor implements NestInterceptor {
: scenario === "timeout"
? Number(process.env.MOCK_TIMEOUT_DELAY_MS ?? 30000)
: 0;
const isMutation = !["GET", "HEAD", "OPTIONS"].includes(request.method);
const source =
configuredDelay > 0 && isMutation
? timer(configuredDelay).pipe(
takeUntil(fromEvent(response, "close")),
mergeMap(() => next.handle()),
)
: next
.handle()
.pipe(
configuredDelay > 0 ? delay(configuredDelay) : (value) => value,
);
return next.handle().pipe(
configuredDelay > 0 ? delay(configuredDelay) : (source) => source,
return source.pipe(
map((payload) => this.transformPayload(payload, scenario)),
);
}

View File

@@ -151,9 +151,137 @@ describe("Simple API", () => {
const order = await request(app.getHttpServer())
.post("/api/v1/orders")
.set("Authorization", authorization)
.send({ items: [{ productId: "product-keyboard", quantity: 1 }] })
.send({
items: [
{
productId: "product-keyboard",
quantity: 1,
expectedVersion: 1,
expectedUnitPriceCents: 12990,
},
],
})
.expect(201);
expect(order.body.data.userId).toBe("user-customer");
expect(order.body.data.totalCents).toBe(12990);
});
it("does not execute a timeout mutation after the client disconnects", async () => {
const customer = await login("customer@demo.local");
const authorization = `Bearer ${customer.body.data.tokens.accessToken as string}`;
const ordersBefore = await request(app.getHttpServer())
.get("/api/v1/orders")
.set("Authorization", authorization)
.expect(200);
const previousDelay = process.env.MOCK_TIMEOUT_DELAY_MS;
process.env.MOCK_TIMEOUT_DELAY_MS = "100";
try {
await request(app.getHttpServer())
.post("/api/v1/orders")
.set("Authorization", authorization)
.set("X-Demo-Scenario", "timeout")
.send({
items: [
{
productId: "product-keyboard",
quantity: 1,
expectedVersion: 1,
expectedUnitPriceCents: 12990,
},
],
})
.timeout({ deadline: 10 });
} catch {
// The client intentionally closes the response before the mutation delay elapses.
} finally {
if (previousDelay === undefined) {
delete process.env.MOCK_TIMEOUT_DELAY_MS;
} else {
process.env.MOCK_TIMEOUT_DELAY_MS = previousDelay;
}
}
await new Promise((resolve) => setTimeout(resolve, 150));
const ordersAfter = await request(app.getHttpServer())
.get("/api/v1/orders")
.set("Authorization", authorization)
.expect(200);
expect(ordersAfter.body.meta.total).toBe(ordersBefore.body.meta.total);
});
it("rejects an order when the confirmed product snapshot changed", async () => {
const customer = await login("customer@demo.local");
const authorization = `Bearer ${customer.body.data.tokens.accessToken as string}`;
const response = await request(app.getHttpServer())
.post("/api/v1/orders")
.set("Authorization", authorization)
.send({
items: [
{
productId: "product-keyboard",
quantity: 1,
expectedVersion: 1,
expectedUnitPriceCents: 1,
},
],
})
.expect(409);
expect(response.body.code).toBe("PRODUCT_CHANGED");
});
it("rejects duplicate product lines and unsupported order currency", async () => {
const admin = await login();
const adminAuthorization = `Bearer ${admin.body.data.tokens.accessToken as string}`;
const customer = await login("customer@demo.local");
const customerAuthorization = `Bearer ${customer.body.data.tokens.accessToken as string}`;
const line = {
productId: "product-keyboard",
quantity: 20,
expectedVersion: 1,
expectedUnitPriceCents: 12990,
};
const duplicateResponse = await request(app.getHttpServer())
.post("/api/v1/orders")
.set("Authorization", customerAuthorization)
.send({ items: [line, line] })
.expect(422);
expect(duplicateResponse.body.code).toBe("DUPLICATE_ORDER_PRODUCT");
const eurProduct = await request(app.getHttpServer())
.post("/api/v1/products")
.set("Authorization", adminAuthorization)
.send({
name: "Euro Product",
description: "A deterministic product priced in euros.",
priceCents: 1000,
currency: "EUR",
categoryId: "category-books",
stock: 5,
imageUrl: "https://picsum.photos/seed/euro-product/640/480",
})
.expect(201);
const currencyResponse = await request(app.getHttpServer())
.post("/api/v1/orders")
.set("Authorization", customerAuthorization)
.send({
items: [
{
productId: eurProduct.body.data.id,
quantity: 1,
expectedVersion: eurProduct.body.data.version,
expectedUnitPriceCents: eurProduct.body.data.priceCents,
},
],
})
.expect(422);
expect(currencyResponse.body.code).toBe("UNSUPPORTED_ORDER_CURRENCY");
});
});

View File

@@ -1,6 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"incremental": false,
"rootDir": "./src"
},
"include": ["src/**/*.ts"],