diff --git a/test-example-app/src/App.twd.test.ts b/test-example-app/src/App.twd.test.ts index 5612de8..1a96ed1 100644 --- a/test-example-app/src/App.twd.test.ts +++ b/test-example-app/src/App.twd.test.ts @@ -1,942 +1,944 @@ import { twd, userEvent, screenDom } from "twd-js"; import { describe, it, beforeEach } from "twd-js/runner"; -describe("App Component", () => { - beforeEach(async () => { - await twd.visit("/"); - }); +describe("Test example app", () => { + describe("App Component", () => { + beforeEach(async () => { + await twd.visit("/"); + }); - it("should render the main heading", async () => { - const heading = screenDom.getByRole("heading", { level: 1 }); - twd.should(heading, "be.visible"); - twd.should(heading, "have.text", "Vite + React"); - }); + it("should render the main heading", async () => { + const heading = screenDom.getByRole("heading", { level: 1 }); + twd.should(heading, "be.visible"); + twd.should(heading, "have.text", "Vite + React"); + }); - it("should handle button clicks and increment counter", async () => { - const user = userEvent.setup(); - const button = screenDom.getByRole("button", { name: /count is/i }); + it("should handle button clicks and increment counter", async () => { + const user = userEvent.setup(); + const button = screenDom.getByRole("button", { name: /count is/i }); - twd.should(button, "have.text", "count is 0"); + twd.should(button, "have.text", "count is 0"); - await user.click(button); - twd.should(button, "have.text", "count is 1"); + await user.click(button); + twd.should(button, "have.text", "count is 1"); - await user.click(button); - twd.should(button, "have.text", "count is 2"); - }); + await user.click(button); + twd.should(button, "have.text", "count is 2"); + }); - it("should display the logos", async () => { - const viteLogo = screenDom.getByAltText("Vite logo"); - const reactLogo = screenDom.getByAltText("React logo"); + it("should display the logos", async () => { + const viteLogo = screenDom.getByAltText("Vite logo"); + const reactLogo = screenDom.getByAltText("React logo"); - twd.should(viteLogo, "be.visible"); - twd.should(reactLogo, "be.visible"); + twd.should(viteLogo, "be.visible"); + twd.should(reactLogo, "be.visible"); + }); }); -}); -// --- Valid mocks --- + // --- Valid mocks --- -describe("Contract Validation - Users API (OpenAPI 3.0)", () => { - it("should mock GET /api/users with nested address and oneOf role", async () => { - twd.mockRequest("getUsers", { - method: "GET", - url: "/api/users", - status: 200, - response: [ - { + describe("Contract Validation - Users API (OpenAPI 3.0)", () => { + it("should mock GET /api/users with nested address and oneOf role", async () => { + await twd.mockRequest("getUsers", { + method: "GET", + url: "/api/users", + status: 200, + response: [ + { + id: 1, name: "Alice", email: "alice@example.com", + address: { street: "123 Main St", city: "Springfield", country: "US" }, + role: { type: "admin", permissions: ["read", "write"] }, + }, + { + id: 2, name: "Bob", email: "bob@example.com", + address: { street: "456 Oak Ave", city: "Portland", country: "US" }, + role: { type: "viewer" }, + }, + ], + }); + }); + + it("should mock GET /api/users/:id", async () => { + await twd.mockRequest("getUser", { + method: "GET", + url: "/api/users/1", + status: 200, + response: { id: 1, name: "Alice", email: "alice@example.com", address: { street: "123 Main St", city: "Springfield", country: "US" }, - role: { type: "admin", permissions: ["read", "write"] }, }, - { - id: 2, name: "Bob", email: "bob@example.com", - address: { street: "456 Oak Ave", city: "Portland", country: "US" }, - role: { type: "viewer" }, - }, - ], - }); - }); - - it("should mock GET /api/users/:id", async () => { - twd.mockRequest("getUser", { - method: "GET", - url: "/api/users/1", - status: 200, - response: { - id: 1, name: "Alice", email: "alice@example.com", - address: { street: "123 Main St", city: "Springfield", country: "US" }, - }, - }); - }); -}); - -describe("Contract Validation - Posts API (OpenAPI 3.1)", () => { - it("should mock GET /api/posts with nested author and oneOf metadata", async () => { - twd.mockRequest("getPosts", { - method: "GET", - url: "/api/posts", - status: 200, - response: [ - { + }); + }); + }); + + describe("Contract Validation - Posts API (OpenAPI 3.1)", () => { + it("should mock GET /api/posts with nested author and oneOf metadata", async () => { + await twd.mockRequest("getPosts", { + method: "GET", + url: "/api/posts", + status: 200, + response: [ + { + id: 1, title: "First Post", body: "Hello world", + author: { id: 1, name: "Alice" }, + metadata: { type: "article", category: "tech" }, + }, + { + id: 2, title: "Second Post", body: "Quick note", + author: { id: 2, name: "Bob" }, + metadata: { type: "note", tags: ["draft", "personal"] }, + }, + ], + }); + }); + + it("should mock GET /api/posts/:id", async () => { + await twd.mockRequest("getPost", { + method: "GET", + url: "/api/posts/1", + status: 200, + response: { id: 1, title: "First Post", body: "Hello world", author: { id: 1, name: "Alice" }, - metadata: { type: "article", category: "tech" }, - }, - { - id: 2, title: "Second Post", body: "Quick note", - author: { id: 2, name: "Bob" }, - metadata: { type: "note", tags: ["draft", "personal"] }, }, - ], - }); - }); - - it("should mock GET /api/posts/:id", async () => { - twd.mockRequest("getPost", { - method: "GET", - url: "/api/posts/1", - status: 200, - response: { - id: 1, title: "First Post", body: "Hello world", - author: { id: 1, name: "Alice" }, - }, + }); }); }); -}); -// --- Invalid mocks (contract mismatches) --- + // --- Invalid mocks (contract mismatches) --- -describe("Contract Validation - Mismatches", () => { - it("should fail: missing nested address field", async () => { - twd.mockRequest("getUserNoAddress", { - method: "GET", - url: "/api/users/10", - status: 200, - response: { id: 10, name: "Ghost", email: "ghost@example.com" }, + describe("Contract Validation - Mismatches", () => { + it("should fail: missing nested address field", async () => { + await twd.mockRequest("getUserNoAddress", { + method: "GET", + url: "/api/users/10", + status: 200, + response: { id: 10, name: "Ghost", email: "ghost@example.com" }, + }); }); - }); - it("should fail: nested address missing required city", async () => { - twd.mockRequest("getUserBadAddress", { - method: "GET", - url: "/api/users/11", - status: 200, - response: { - id: 11, name: "Broken", email: "broken@example.com", - address: { street: "789 Elm St" }, - }, - }); - }); - - it("should fail: oneOf role with invalid variant", async () => { - twd.mockRequest("getUserBadRole", { - method: "GET", - url: "/api/users/12", - status: 200, - response: { - id: 12, name: "BadRole", email: "bad@example.com", - address: { street: "1 St", city: "X", country: "US" }, - role: { type: "editor" }, - }, - }); - }); - - it("should fail: post missing nested author object", async () => { - twd.mockRequest("getPostNoAuthor", { - method: "GET", - url: "/api/posts/50", - status: 200, - response: { id: 50, title: "Orphan", body: "No author here" }, - }); - }); - - it("should fail: post oneOf metadata matches neither variant", async () => { - twd.mockRequest("getPostBadMeta", { - method: "GET", - url: "/api/posts/51", - status: 200, - response: { - id: 51, title: "Bad Meta", body: "Wrong metadata", - author: { id: 1, name: "Alice" }, - metadata: { type: "video", duration: 120 }, - }, - }); - }); - - it("should warn: undocumented 404 status", async () => { - twd.mockRequest("getUserNotFound", { - method: "GET", - url: "/api/users/999", - status: 404, - response: { message: "Not found" }, + it("should fail: nested address missing required city", async () => { + await twd.mockRequest("getUserBadAddress", { + method: "GET", + url: "/api/users/11", + status: 200, + response: { + id: 11, name: "Broken", email: "broken@example.com", + address: { street: "789 Elm St" }, + }, + }); + }); + + it("should fail: oneOf role with invalid variant", async () => { + await twd.mockRequest("getUserBadRole", { + method: "GET", + url: "/api/users/12", + status: 200, + response: { + id: 12, name: "BadRole", email: "bad@example.com", + address: { street: "1 St", city: "X", country: "US" }, + role: { type: "editor" }, + }, + }); }); - }); - it("should skip: endpoint not in any spec", async () => { - twd.mockRequest("getUnknown", { - method: "GET", - url: "/api/unknown", - status: 200, - response: { theme: "dark" }, + it("should fail: post missing nested author object", async () => { + await twd.mockRequest("getPostNoAuthor", { + method: "GET", + url: "/api/posts/50", + status: 200, + response: { id: 50, title: "Orphan", body: "No author here" }, + }); }); - }); -}); - -// ── Products API (OpenAPI 3.0) — Valid mocks ──────────────────────── -describe("Contract Validation - Products API (OpenAPI 3.0)", () => { - it("should mock GET /api/products with all valid fields", async () => { - twd.mockRequest("getProductsFull", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { - id: "550e8400-e29b-41d4-a716-446655440000", - name: "Premium Widget", - sku: "ABCD-12345678", - description: "The best widget ever", - price: 49.99, - compareAtPrice: 59.99, - quantity: 100, - weight: 2.50, - rating: 4.5, - inStock: true, - currency: "GBP", - category: "toys", - tags: ["new", "featured"], - createdAt: "2026-03-31T10:00:00Z", - releaseDate: "2026-04-01", - website: "https://widgets.example.com", - contactEmail: "sales@widgets.example.com", - origin: "widgets.example.com", - serverIp: "10.0.0.1", - serverIpV6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", - metadata: { color: "blue", material: "steel" }, + it("should fail: post oneOf metadata matches neither variant", async () => { + await twd.mockRequest("getPostBadMeta", { + method: "GET", + url: "/api/posts/51", + status: 200, + response: { + id: 51, title: "Bad Meta", body: "Wrong metadata", + author: { id: 1, name: "Alice" }, + metadata: { type: "video", duration: 120 }, }, - ], + }); + }); + + it("should warn: undocumented 404 status", async () => { + await twd.mockRequest("getUserNotFound", { + method: "GET", + url: "/api/users/999", + status: 404, + response: { message: "Not found" }, + }); + }); + + it("should skip: endpoint not in any spec", async () => { + await twd.mockRequest("getUnknown", { + method: "GET", + url: "/api/unknown", + status: 200, + response: { theme: "dark" }, + }); + }); + }); + + // ── Products API (OpenAPI 3.0) — Valid mocks ──────────────────────── + + describe("Contract Validation - Products API (OpenAPI 3.0)", () => { + it("should mock GET /api/products with all valid fields", async () => { + await twd.mockRequest("getProductsFull", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { + id: "550e8400-e29b-41d4-a716-446655440000", + name: "Premium Widget", + sku: "ABCD-12345678", + description: "The best widget ever", + price: 49.99, + compareAtPrice: 59.99, + quantity: 100, + weight: 2.50, + rating: 4.5, + inStock: true, + currency: "GBP", + category: "toys", + tags: ["new", "featured"], + createdAt: "2026-03-31T10:00:00Z", + releaseDate: "2026-04-01", + website: "https://widgets.example.com", + contactEmail: "sales@widgets.example.com", + origin: "widgets.example.com", + serverIp: "10.0.0.1", + serverIpV6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + metadata: { color: "blue", material: "steel" }, + }, + ], + }); + }); + + it("should mock GET /api/products with minimal required fields", async () => { + await twd.mockRequest("getProductsMinimal", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { + id: "550e8400-e29b-41d4-a716-446655440000", + name: "W", + price: 0.01, + currency: "USD", + inStock: true, + category: "electronics", + }, + ], + }); + }); + + it("should mock POST /api/products with valid create payload", async () => { + await twd.mockRequest("createProduct", { + method: "POST", + url: "/api/products", + status: 201, + response: { + id: "660e8400-e29b-41d4-a716-446655440000", + name: "New Product", + price: 19.99, + currency: "EUR", + inStock: false, + category: "clothing", + }, + }); }); - }); - it("should mock GET /api/products with minimal required fields", async () => { - twd.mockRequest("getProductsMinimal", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { + it("should accept null for nullable description (3.0 nullable)", async () => { + await twd.mockRequest("getProductNullDesc", { + method: "GET", + url: "/api/products/550e8400-e29b-41d4-a716-446655440000", + status: 200, + response: { id: "550e8400-e29b-41d4-a716-446655440000", - name: "W", - price: 0.01, + name: "Widget", + price: 9.99, currency: "USD", inStock: true, category: "electronics", + description: null, }, - ], - }); - }); - - it("should mock POST /api/products with valid create payload", async () => { - twd.mockRequest("createProduct", { - method: "POST", - url: "/api/products", - status: 201, - response: { - id: "660e8400-e29b-41d4-a716-446655440000", - name: "New Product", - price: 19.99, - currency: "EUR", - inStock: false, - category: "clothing", - }, - }); - }); - - it("should accept null for nullable description (3.0 nullable)", async () => { - twd.mockRequest("getProductNullDesc", { - method: "GET", - url: "/api/products/550e8400-e29b-41d4-a716-446655440000", - status: 200, - response: { - id: "550e8400-e29b-41d4-a716-446655440000", - name: "Widget", - price: 9.99, - currency: "USD", - inStock: true, - category: "electronics", - description: null, - }, - }); - }); - - it("should accept null for nullable compareAtPrice (3.0 nullable number)", async () => { - twd.mockRequest("getProductNullPrice", { - method: "GET", - url: "/api/products/550e8400-e29b-41d4-a716-446655440001", - status: 200, - response: { - id: "550e8400-e29b-41d4-a716-446655440001", - name: "Widget", - price: 9.99, - currency: "USD", - inStock: true, - category: "electronics", - compareAtPrice: null, - }, - }); - }); - - it("should accept all valid currency enum values", async () => { - twd.mockRequest("getProductsAllCurrencies", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440010", name: "A", price: 1, currency: "USD", inStock: true, category: "food" }, - { id: "550e8400-e29b-41d4-a716-446655440011", name: "B", price: 1, currency: "EUR", inStock: true, category: "food" }, - { id: "550e8400-e29b-41d4-a716-446655440012", name: "C", price: 1, currency: "GBP", inStock: true, category: "food" }, - { id: "550e8400-e29b-41d4-a716-446655440013", name: "D", price: 1, currency: "JPY", inStock: true, category: "food" }, - ], - }); - }); - - it("should accept boolean inStock values", async () => { - twd.mockRequest("getProductsBool", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440020", name: "In Stock", price: 5, currency: "USD", inStock: true, category: "books" }, - { id: "550e8400-e29b-41d4-a716-446655440021", name: "Out of Stock", price: 5, currency: "USD", inStock: false, category: "books" }, - ], - }); - }); - - it("should accept unique tags within limit", async () => { - twd.mockRequest("getProductTags", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { - id: "550e8400-e29b-41d4-a716-446655440030", - name: "Tagged", - price: 5, + }); + }); + + it("should accept null for nullable compareAtPrice (3.0 nullable number)", async () => { + await twd.mockRequest("getProductNullPrice", { + method: "GET", + url: "/api/products/550e8400-e29b-41d4-a716-446655440001", + status: 200, + response: { + id: "550e8400-e29b-41d4-a716-446655440001", + name: "Widget", + price: 9.99, currency: "USD", inStock: true, category: "electronics", - tags: ["sale", "new", "featured"], + compareAtPrice: null, }, - ], - }); - }); - - it("should accept metadata with string additionalProperties", async () => { - twd.mockRequest("getProductMeta", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { - id: "550e8400-e29b-41d4-a716-446655440040", - name: "With Meta", - price: 5, - currency: "USD", - inStock: true, - category: "electronics", - metadata: { color: "red", size: "large", weight: "1kg" }, + }); + }); + + it("should accept all valid currency enum values", async () => { + await twd.mockRequest("getProductsAllCurrencies", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440010", name: "A", price: 1, currency: "USD", inStock: true, category: "food" }, + { id: "550e8400-e29b-41d4-a716-446655440011", name: "B", price: 1, currency: "EUR", inStock: true, category: "food" }, + { id: "550e8400-e29b-41d4-a716-446655440012", name: "C", price: 1, currency: "GBP", inStock: true, category: "food" }, + { id: "550e8400-e29b-41d4-a716-446655440013", name: "D", price: 1, currency: "JPY", inStock: true, category: "food" }, + ], + }); + }); + + it("should accept boolean inStock values", async () => { + await twd.mockRequest("getProductsBool", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440020", name: "In Stock", price: 5, currency: "USD", inStock: true, category: "books" }, + { id: "550e8400-e29b-41d4-a716-446655440021", name: "Out of Stock", price: 5, currency: "USD", inStock: false, category: "books" }, + ], + }); + }); + + it("should accept unique tags within limit", async () => { + await twd.mockRequest("getProductTags", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { + id: "550e8400-e29b-41d4-a716-446655440030", + name: "Tagged", + price: 5, + currency: "USD", + inStock: true, + category: "electronics", + tags: ["sale", "new", "featured"], + }, + ], + }); + }); + + it("should accept metadata with string additionalProperties", async () => { + await twd.mockRequest("getProductMeta", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { + id: "550e8400-e29b-41d4-a716-446655440040", + name: "With Meta", + price: 5, + currency: "USD", + inStock: true, + category: "electronics", + metadata: { color: "red", size: "large", weight: "1kg" }, + }, + ], + }); + }); + + it("should mock GET /api/settings with valid properties only", async () => { + await twd.mockRequest("getSettings", { + method: "GET", + url: "/api/settings", + status: 200, + response: { + theme: "dark", + notifications: true, + language: "en-US", }, - ], - }); - }); - - it("should mock GET /api/settings with valid properties only", async () => { - twd.mockRequest("getSettings", { - method: "GET", - url: "/api/settings", - status: 200, - response: { - theme: "dark", - notifications: true, - language: "en-US", - }, - }); - }); - - it("should accept null for nullable Settings customCss (3.0 nullable)", async () => { - twd.mockRequest("getSettingsNullCss", { - method: "GET", - url: "/api/settings", - status: 200, - response: { - theme: "auto", - notifications: false, - language: "es", - customCss: null, - }, - }); - }); -}); - -// ── Products API (OpenAPI 3.0) — Invalid mocks (error mode) ───────── - -describe("Contract Validation - Products Mismatches (OpenAPI 3.0 — error mode)", () => { - it("should fail: empty name violates minLength", async () => { - twd.mockRequest("getProductEmptyName", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "", price: 1, currency: "USD", inStock: true, category: "food" }, - ], - }); - }); - - it("should fail: invalid SKU pattern", async () => { - twd.mockRequest("getProductBadSku", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", sku: "invalid-sku" }, - ], - }); - }); - - it("should fail: invalid uuid format for id", async () => { - twd.mockRequest("getProductBadUuid", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "not-a-uuid", name: "W", price: 1, currency: "USD", inStock: true, category: "food" }, - ], - }); - }); - - it("should fail: invalid date-time format", async () => { - twd.mockRequest("getProductBadDateTime", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", createdAt: "31-03-2026" }, - ], - }); - }); - - it("should fail: invalid date format", async () => { - twd.mockRequest("getProductBadDate", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", releaseDate: "March 31, 2026" }, - ], - }); - }); - - it("should fail: invalid email format", async () => { - twd.mockRequest("getProductBadEmail", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", contactEmail: "not-an-email" }, - ], - }); - }); - - it("should fail: invalid uri format", async () => { - twd.mockRequest("getProductBadUri", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", website: "not a uri" }, - ], - }); - }); - - it("should fail: invalid ipv4 format", async () => { - twd.mockRequest("getProductBadIp", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", serverIp: "999.999.999.999" }, - ], - }); - }); - - it("should fail: invalid ipv6 format", async () => { - twd.mockRequest("getProductBadIpV6", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", serverIpV6: "not-ipv6" }, - ], - }); - }); - - it("should fail: price of 0 violates exclusiveMinimum", async () => { - twd.mockRequest("getProductZeroPrice", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 0, currency: "USD", inStock: true, category: "food" }, - ], - }); - }); - - it("should fail: negative quantity violates minimum", async () => { - twd.mockRequest("getProductNegQty", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", quantity: -1 }, - ], - }); - }); - - it("should fail: quantity exceeds maximum", async () => { - twd.mockRequest("getProductOverQty", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", quantity: 1000000 }, - ], - }); - }); - - it("should fail: weight not multipleOf 0.01", async () => { - twd.mockRequest("getProductBadWeight", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", weight: 1.005 }, - ], - }); - }); - - it("should fail: rating above maximum (5)", async () => { - twd.mockRequest("getProductBadRating", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", rating: 5.1 }, - ], - }); - }); - - it("should fail: invalid enum value for currency", async () => { - twd.mockRequest("getProductBadCurrency", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "BTC", inStock: true, category: "food" }, - ], - }); - }); - - it("should fail: invalid enum value for category", async () => { - twd.mockRequest("getProductBadCategory", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "furniture" }, - ], - }); - }); - - it("should fail: string value for boolean inStock", async () => { - twd.mockRequest("getProductBadBool", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: "yes", category: "food" }, - ], - }); - }); - - it("should fail: duplicate tags violates uniqueItems", async () => { - twd.mockRequest("getProductDupTags", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", tags: ["sale", "sale"] }, - ], - }); - }); - - it("should fail: tags exceeds maxItems (10)", async () => { - twd.mockRequest("getProductTooManyTags", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { - id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", - tags: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "t9", "t10"], + }); + }); + + it("should accept null for nullable Settings customCss (3.0 nullable)", async () => { + await twd.mockRequest("getSettingsNullCss", { + method: "GET", + url: "/api/settings", + status: 200, + response: { + theme: "auto", + notifications: false, + language: "es", + customCss: null, }, - ], - }); - }); - - it("should fail: non-string value in metadata additionalProperties", async () => { - twd.mockRequest("getProductBadMeta", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", metadata: { count: 5 } }, - ], - }); - }); - - it("should fail: extra property on Settings (additionalProperties: false)", async () => { - twd.mockRequest("getSettingsBadExtra", { - method: "GET", - url: "/api/settings", - status: 200, - response: { theme: "dark", notifications: true, language: "en", extraField: "oops" }, - }); - }); - - it("should fail: invalid language pattern in Settings", async () => { - twd.mockRequest("getSettingsBadLang", { - method: "GET", - url: "/api/settings", - status: 200, - response: { theme: "light", notifications: false, language: "ENGLISH" }, - }); - }); - - it("should fail: wrong type for nullable description (number instead of string|null)", async () => { - twd.mockRequest("getProductBadNullable", { - method: "GET", - url: "/api/products", - status: 200, - response: [ - { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", description: 123 }, - ], - }); - }); -}); - -// ── Events API (OpenAPI 3.1) — Valid mocks ────────────────────────── - -describe("Contract Validation - Events API (OpenAPI 3.1)", () => { - it("should mock GET /api/events with all valid fields", async () => { - twd.mockRequest("getEventsFull", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { + }); + }); + }); + + // ── Products API (OpenAPI 3.0) — Invalid mocks (error mode) ───────── + + describe("Contract Validation - Products Mismatches (OpenAPI 3.0 — error mode)", () => { + it("should fail: empty name violates minLength", async () => { + await twd.mockRequest("getProductEmptyName", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "", price: 1, currency: "USD", inStock: true, category: "food" }, + ], + }); + }); + + it("should fail: invalid SKU pattern", async () => { + await twd.mockRequest("getProductBadSku", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", sku: "invalid-sku" }, + ], + }); + }); + + it("should fail: invalid uuid format for id", async () => { + await twd.mockRequest("getProductBadUuid", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "not-a-uuid", name: "W", price: 1, currency: "USD", inStock: true, category: "food" }, + ], + }); + }); + + it("should fail: invalid date-time format", async () => { + await twd.mockRequest("getProductBadDateTime", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", createdAt: "31-03-2026" }, + ], + }); + }); + + it("should fail: invalid date format", async () => { + await twd.mockRequest("getProductBadDate", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", releaseDate: "March 31, 2026" }, + ], + }); + }); + + it("should fail: invalid email format", async () => { + await twd.mockRequest("getProductBadEmail", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", contactEmail: "not-an-email" }, + ], + }); + }); + + it("should fail: invalid uri format", async () => { + await twd.mockRequest("getProductBadUri", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", website: "not a uri" }, + ], + }); + }); + + it("should fail: invalid ipv4 format", async () => { + await twd.mockRequest("getProductBadIp", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", serverIp: "999.999.999.999" }, + ], + }); + }); + + it("should fail: invalid ipv6 format", async () => { + await twd.mockRequest("getProductBadIpV6", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", serverIpV6: "not-ipv6" }, + ], + }); + }); + + it("should fail: price of 0 violates exclusiveMinimum", async () => { + await twd.mockRequest("getProductZeroPrice", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 0, currency: "USD", inStock: true, category: "food" }, + ], + }); + }); + + it("should fail: negative quantity violates minimum", async () => { + await twd.mockRequest("getProductNegQty", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", quantity: -1 }, + ], + }); + }); + + it("should fail: quantity exceeds maximum", async () => { + await twd.mockRequest("getProductOverQty", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", quantity: 1000000 }, + ], + }); + }); + + it("should fail: weight not multipleOf 0.01", async () => { + await twd.mockRequest("getProductBadWeight", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", weight: 1.005 }, + ], + }); + }); + + it("should fail: rating above maximum (5)", async () => { + await twd.mockRequest("getProductBadRating", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", rating: 5.1 }, + ], + }); + }); + + it("should fail: invalid enum value for currency", async () => { + await twd.mockRequest("getProductBadCurrency", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "BTC", inStock: true, category: "food" }, + ], + }); + }); + + it("should fail: invalid enum value for category", async () => { + await twd.mockRequest("getProductBadCategory", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "furniture" }, + ], + }); + }); + + it("should fail: string value for boolean inStock", async () => { + await twd.mockRequest("getProductBadBool", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: "yes", category: "food" }, + ], + }); + }); + + it("should fail: duplicate tags violates uniqueItems", async () => { + await twd.mockRequest("getProductDupTags", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", tags: ["sale", "sale"] }, + ], + }); + }); + + it("should fail: tags exceeds maxItems (10)", async () => { + await twd.mockRequest("getProductTooManyTags", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { + id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", + tags: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "t9", "t10"], + }, + ], + }); + }); + + it("should fail: non-string value in metadata additionalProperties", async () => { + await twd.mockRequest("getProductBadMeta", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", metadata: { count: 5 } }, + ], + }); + }); + + it("should fail: extra property on Settings (additionalProperties: false)", async () => { + await twd.mockRequest("getSettingsBadExtra", { + method: "GET", + url: "/api/settings", + status: 200, + response: { theme: "dark", notifications: true, language: "en", extraField: "oops" }, + }); + }); + + it("should fail: invalid language pattern in Settings", async () => { + await twd.mockRequest("getSettingsBadLang", { + method: "GET", + url: "/api/settings", + status: 200, + response: { theme: "light", notifications: false, language: "ENGLISH" }, + }); + }); + + it("should fail: wrong type for nullable description (number instead of string|null)", async () => { + await twd.mockRequest("getProductBadNullable", { + method: "GET", + url: "/api/products", + status: 200, + response: [ + { id: "550e8400-e29b-41d4-a716-446655440000", name: "W", price: 1, currency: "USD", inStock: true, category: "food", description: 123 }, + ], + }); + }); + }); + + // ── Events API (OpenAPI 3.1) — Valid mocks ────────────────────────── + + describe("Contract Validation - Events API (OpenAPI 3.1)", () => { + it("should mock GET /api/events with all valid fields", async () => { + await twd.mockRequest("getEventsFull", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { + id: 1, + name: "Tech Conference", + startDate: "2026-06-15T09:00:00Z", + endDate: "2026-06-17T18:00:00Z", + active: true, + priority: 3, + score: 85.5, + status: "published", + attendees: ["alice@example.com", "bob@example.com"], + description: "Annual tech conference", + capacity: 500, + }, + ], + }); + }); + + it("should mock GET /api/events with minimal required fields", async () => { + await twd.mockRequest("getEventsMinimal", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { + id: 2, + name: "Meetup", + startDate: "2026-07-01T19:00:00Z", + active: false, + status: "draft", + }, + ], + }); + }); + + it("should accept null for nullable endDate (3.1 type array)", async () => { + await twd.mockRequest("getEventNullEnd", { + method: "GET", + url: "/api/events/1", + status: 200, + response: { id: 1, - name: "Tech Conference", - startDate: "2026-06-15T09:00:00Z", - endDate: "2026-06-17T18:00:00Z", + name: "Open-ended", + startDate: "2026-08-01T10:00:00Z", active: true, - priority: 3, - score: 85.5, status: "published", - attendees: ["alice@example.com", "bob@example.com"], - description: "Annual tech conference", - capacity: 500, + endDate: null, }, - ], + }); }); - }); - it("should mock GET /api/events with minimal required fields", async () => { - twd.mockRequest("getEventsMinimal", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { + it("should accept null for nullable description (3.1 type array)", async () => { + await twd.mockRequest("getEventNullDesc", { + method: "GET", + url: "/api/events/2", + status: 200, + response: { id: 2, - name: "Meetup", - startDate: "2026-07-01T19:00:00Z", - active: false, + name: "Silent event", + startDate: "2026-09-01T10:00:00Z", + active: true, status: "draft", + description: null, }, - ], - }); - }); - - it("should accept null for nullable endDate (3.1 type array)", async () => { - twd.mockRequest("getEventNullEnd", { - method: "GET", - url: "/api/events/1", - status: 200, - response: { - id: 1, - name: "Open-ended", - startDate: "2026-08-01T10:00:00Z", - active: true, - status: "published", - endDate: null, - }, - }); - }); - - it("should accept null for nullable description (3.1 type array)", async () => { - twd.mockRequest("getEventNullDesc", { - method: "GET", - url: "/api/events/2", - status: 200, - response: { - id: 2, - name: "Silent event", - startDate: "2026-09-01T10:00:00Z", - active: true, - status: "draft", - description: null, - }, - }); - }); - - it("should accept null for nullable capacity (3.1 nullable integer)", async () => { - twd.mockRequest("getEventNullCap", { - method: "GET", - url: "/api/events/3", - status: 200, - response: { - id: 3, - name: "Unlimited", - startDate: "2026-10-01T10:00:00Z", - active: true, - status: "published", - capacity: null, - }, - }); - }); - - it("should accept all valid status enum values", async () => { - twd.mockRequest("getEventsAllStatus", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { id: 10, name: "Draft Event", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft" }, - { id: 11, name: "Published Event", startDate: "2026-06-02T10:00:00Z", active: true, status: "published" }, - { id: 12, name: "Archived Event", startDate: "2026-06-03T10:00:00Z", active: false, status: "archived" }, - ], - }); - }); -}); - -// ── Events API (OpenAPI 3.1) — Invalid mocks (error mode) ────────── - -describe("Contract Validation - Events Mismatches (OpenAPI 3.1 — error mode)", () => { - it("should fail: empty events array violates minItems (1)", async () => { - twd.mockRequest("getEventsEmpty", { - method: "GET", - url: "/api/events", - status: 200, - response: [], - }); - }); - - it("should fail: event name too short (minLength: 3)", async () => { - twd.mockRequest("getEventShortName", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { id: 1, name: "Hi", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft" }, - ], - }); - }); - - it("should fail: invalid date-time format for startDate", async () => { - twd.mockRequest("getEventBadDate", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { id: 1, name: "Bad Date", startDate: "June 15, 2026", active: true, status: "draft" }, - ], - }); - }); - - it("should fail: float value for integer id", async () => { - twd.mockRequest("getEventFloatId", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { id: 1.5, name: "Float ID", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft" }, - ], - }); - }); - - it("should fail: number value for boolean active", async () => { - twd.mockRequest("getEventBadBool", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { id: 1, name: "Bad Bool", startDate: "2026-06-01T10:00:00Z", active: 1, status: "draft" }, - ], - }); - }); - - it("should fail: invalid enum value for status", async () => { - twd.mockRequest("getEventBadStatus", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { id: 1, name: "Bad Status", startDate: "2026-06-01T10:00:00Z", active: true, status: "deleted" }, - ], - }); - }); - - it("should fail: score at exclusiveMaximum boundary (100)", async () => { - twd.mockRequest("getEventScoreMax", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { id: 1, name: "Max Score", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", score: 100 }, - ], - }); - }); - - it("should fail: priority below minimum (1)", async () => { - twd.mockRequest("getEventLowPriority", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { id: 1, name: "Low Priority", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", priority: 0 }, - ], - }); - }); - - it("should fail: priority above maximum (5)", async () => { - twd.mockRequest("getEventHighPriority", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { id: 1, name: "High Priority", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", priority: 6 }, - ], - }); - }); - - it("should fail: duplicate attendees violates uniqueItems", async () => { - twd.mockRequest("getEventDupAttendees", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { - id: 1, name: "Dup Attendees", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", - attendees: ["same@test.com", "same@test.com"], - }, - ], - }); - }); - - it("should fail: empty attendees array violates minItems (1)", async () => { - twd.mockRequest("getEventNoAttendees", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { - id: 1, name: "No Attendees", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", - attendees: [], + }); + }); + + it("should accept null for nullable capacity (3.1 nullable integer)", async () => { + await twd.mockRequest("getEventNullCap", { + method: "GET", + url: "/api/events/3", + status: 200, + response: { + id: 3, + name: "Unlimited", + startDate: "2026-10-01T10:00:00Z", + active: true, + status: "published", + capacity: null, }, - ], - }); - }); - - it("should fail: invalid email format in attendees", async () => { - twd.mockRequest("getEventBadAttendee", { - method: "GET", - url: "/api/events", - status: 200, - response: [ - { - id: 1, name: "Bad Attendee", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", - attendees: ["not-an-email"], + }); + }); + + it("should accept all valid status enum values", async () => { + await twd.mockRequest("getEventsAllStatus", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { id: 10, name: "Draft Event", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft" }, + { id: 11, name: "Published Event", startDate: "2026-06-02T10:00:00Z", active: true, status: "published" }, + { id: 12, name: "Archived Event", startDate: "2026-06-03T10:00:00Z", active: false, status: "archived" }, + ], + }); + }); + }); + + // ── Events API (OpenAPI 3.1) — Invalid mocks (error mode) ────────── + + describe("Contract Validation - Events Mismatches (OpenAPI 3.1 — error mode)", () => { + it("should fail: empty events array violates minItems (1)", async () => { + await twd.mockRequest("getEventsEmpty", { + method: "GET", + url: "/api/events", + status: 200, + response: [], + }); + }); + + it("should fail: event name too short (minLength: 3)", async () => { + await twd.mockRequest("getEventShortName", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { id: 1, name: "Hi", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft" }, + ], + }); + }); + + it("should fail: invalid date-time format for startDate", async () => { + await twd.mockRequest("getEventBadDate", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { id: 1, name: "Bad Date", startDate: "June 15, 2026", active: true, status: "draft" }, + ], + }); + }); + + it("should fail: float value for integer id", async () => { + await twd.mockRequest("getEventFloatId", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { id: 1.5, name: "Float ID", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft" }, + ], + }); + }); + + it("should fail: number value for boolean active", async () => { + await twd.mockRequest("getEventBadBool", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { id: 1, name: "Bad Bool", startDate: "2026-06-01T10:00:00Z", active: 1, status: "draft" }, + ], + }); + }); + + it("should fail: invalid enum value for status", async () => { + await twd.mockRequest("getEventBadStatus", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { id: 1, name: "Bad Status", startDate: "2026-06-01T10:00:00Z", active: true, status: "deleted" }, + ], + }); + }); + + it("should fail: score at exclusiveMaximum boundary (100)", async () => { + await twd.mockRequest("getEventScoreMax", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { id: 1, name: "Max Score", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", score: 100 }, + ], + }); + }); + + it("should fail: priority below minimum (1)", async () => { + await twd.mockRequest("getEventLowPriority", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { id: 1, name: "Low Priority", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", priority: 0 }, + ], + }); + }); + + it("should fail: priority above maximum (5)", async () => { + await twd.mockRequest("getEventHighPriority", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { id: 1, name: "High Priority", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", priority: 6 }, + ], + }); + }); + + it("should fail: duplicate attendees violates uniqueItems", async () => { + await twd.mockRequest("getEventDupAttendees", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { + id: 1, name: "Dup Attendees", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", + attendees: ["same@test.com", "same@test.com"], + }, + ], + }); + }); + + it("should fail: empty attendees array violates minItems (1)", async () => { + await twd.mockRequest("getEventNoAttendees", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { + id: 1, name: "No Attendees", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", + attendees: [], + }, + ], + }); + }); + + it("should fail: invalid email format in attendees", async () => { + await twd.mockRequest("getEventBadAttendee", { + method: "GET", + url: "/api/events", + status: 200, + response: [ + { + id: 1, name: "Bad Attendee", startDate: "2026-06-01T10:00:00Z", active: true, status: "draft", + attendees: ["not-an-email"], + }, + ], + }); + }); + + it("should fail: wrong type for nullable description (number instead of string|null)", async () => { + await twd.mockRequest("getEventBadNullable", { + method: "GET", + url: "/api/events/99", + status: 200, + response: { + id: 99, + name: "Bad Nullable", + startDate: "2026-06-01T10:00:00Z", + active: true, + status: "draft", + description: 42, }, - ], - }); - }); - - it("should fail: wrong type for nullable description (number instead of string|null)", async () => { - twd.mockRequest("getEventBadNullable", { - method: "GET", - url: "/api/events/99", - status: 200, - response: { - id: 99, - name: "Bad Nullable", - startDate: "2026-06-01T10:00:00Z", - active: true, - status: "draft", - description: 42, - }, - }); - }); -}); - -// ── Content-Type forwarding — binary mocks vs image/* spec ─────────── - -describe("Contract Validation - Content-Type forwarding (Products API)", () => { - it("should match image/png mock against image/* spec entry", async () => { - twd.mockRequest("getProductThumbnailPng", { - method: "GET", - url: "/api/products/550e8400-e29b-41d4-a716-446655440000/thumbnail", - status: 200, - response: "fake-png-bytes", - responseHeaders: { "Content-Type": "image/png" }, - }); - }); - - it("should match image/jpeg mock against image/* spec entry", async () => { - twd.mockRequest("getProductThumbnailJpeg", { - method: "GET", - url: "/api/products/550e8400-e29b-41d4-a716-446655440001/thumbnail", - status: 200, - response: "fake-jpeg-bytes", - responseHeaders: { "content-type": "image/jpeg" }, - }); - }); - - it("should warn when non-binary Content-Type has no matching spec entry", async () => { - twd.mockRequest("getProductsAsXml", { - method: "GET", - url: "/api/products", - status: 200, - response: "", - responseHeaders: { "Content-Type": "application/xml" }, - }); - }); - - it("should warn when mock has no responseHeaders against image-only endpoint", async () => { - twd.mockRequest("getProductThumbnailNoHeader", { - method: "GET", - url: "/api/products/550e8400-e29b-41d4-a716-446655440002/thumbnail", - status: 200, - response: "fake-bytes", + }); + }); + }); + + // ── Content-Type forwarding — binary mocks vs image/* spec ─────────── + + describe("Contract Validation - Content-Type forwarding (Products API)", () => { + it("should match image/png mock against image/* spec entry", async () => { + await twd.mockRequest("getProductThumbnailPng", { + method: "GET", + url: "/api/products/550e8400-e29b-41d4-a716-446655440000/thumbnail", + status: 200, + response: "fake-png-bytes", + responseHeaders: { "Content-Type": "image/png" }, + }); + }); + + it("should match image/jpeg mock against image/* spec entry", async () => { + await twd.mockRequest("getProductThumbnailJpeg", { + method: "GET", + url: "/api/products/550e8400-e29b-41d4-a716-446655440001/thumbnail", + status: 200, + response: "fake-jpeg-bytes", + responseHeaders: { "content-type": "image/jpeg" }, + }); + }); + + it("should warn when non-binary Content-Type has no matching spec entry", async () => { + await twd.mockRequest("getProductsAsXml", { + method: "GET", + url: "/api/products", + status: 200, + response: "", + responseHeaders: { "Content-Type": "application/xml" }, + }); + }); + + it("should warn when mock has no responseHeaders against image-only endpoint", async () => { + await twd.mockRequest("getProductThumbnailNoHeader", { + method: "GET", + url: "/api/products/550e8400-e29b-41d4-a716-446655440002/thumbnail", + status: 200, + response: "fake-bytes", + }); }); }); });