From 21883e8674e6cf7353282c8c34dab64e5b304783 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 29 Jun 2024 18:49:47 +0200 Subject: [PATCH] feat: use "seed-json-schema" instead --- package.json | 6 +- pnpm-lock.yaml | 38 +-- src/open-api/schema/evolve.ts | 43 --- src/open-api/schema/types/array.ts | 34 --- src/open-api/schema/types/boolean.ts | 5 - src/open-api/schema/types/integer.ts | 22 -- src/open-api/schema/types/number.ts | 9 - src/open-api/schema/types/object.ts | 58 ---- src/open-api/schema/types/string.ts | 89 ------ src/open-api/utils/open-api-utils.ts | 35 ++- src/open-api/utils/repeat.ts | 9 - test/oas/evolve-json-schema.test.ts | 437 --------------------------- test/oas/oas-json-schema.test.ts | 3 +- test/oas/petstore.test.ts | 30 +- vitest.setup.ts | 5 + 15 files changed, 72 insertions(+), 751 deletions(-) delete mode 100644 src/open-api/schema/evolve.ts delete mode 100644 src/open-api/schema/types/array.ts delete mode 100644 src/open-api/schema/types/boolean.ts delete mode 100644 src/open-api/schema/types/integer.ts delete mode 100644 src/open-api/schema/types/number.ts delete mode 100644 src/open-api/schema/types/object.ts delete mode 100644 src/open-api/schema/types/string.ts delete mode 100644 src/open-api/utils/repeat.ts delete mode 100644 test/oas/evolve-json-schema.test.ts diff --git a/package.json b/package.json index cfbb84c..62e127f 100644 --- a/package.json +++ b/package.json @@ -65,11 +65,9 @@ }, "dependencies": { "@apidevtools/swagger-parser": "^10.0.2", - "@types/faker": "^5.5.9", "@types/har-format": "^1.2.7", - "faker": "5.x", + "@yellow-ticket/seed-json-schema": "^0.1.6", "openapi-types": "^7.2.3", - "outvariant": "^1.2.1", - "randexp": "^0.5.3" + "outvariant": "^1.2.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a74ecd2..5c26361 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,24 +8,18 @@ dependencies: '@apidevtools/swagger-parser': specifier: ^10.0.2 version: 10.1.0(openapi-types@7.2.3) - '@types/faker': - specifier: ^5.5.9 - version: 5.5.9 '@types/har-format': specifier: ^1.2.7 version: 1.2.15 - faker: - specifier: 5.x - version: 5.5.3 + '@yellow-ticket/seed-json-schema': + specifier: ^0.1.6 + version: 0.1.6 openapi-types: specifier: ^7.2.3 version: 7.2.3 outvariant: specifier: ^1.2.1 version: 1.4.2 - randexp: - specifier: ^0.5.3 - version: 0.5.3 devDependencies: '@commitlint/cli': @@ -531,6 +525,11 @@ packages: dev: true optional: true + /@faker-js/faker@8.4.1: + resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + dev: false + /@inquirer/confirm@3.1.8: resolution: {integrity: sha512-f3INZ+ca4dQdn+MQiq1yP/mOIR/Oc8BLRYuDh6ciToWd6z4W8yArfzjBCMQ0BPY8PcJKwZxGIt8Z6yNT32eSTw==} engines: {node: '>=18'} @@ -874,10 +873,6 @@ packages: '@types/serve-static': 1.15.7 dev: true - /@types/faker@5.5.9: - resolution: {integrity: sha512-uCx6mP3UY5SIO14XlspxsGjgaemrxpssJI0Ol+GfhxtcKpv9pgRZYsS4eeKeHVLje6Qtc8lGszuBI461+gVZBA==} - dev: false - /@types/har-format@1.2.15: resolution: {integrity: sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==} dev: false @@ -886,6 +881,10 @@ packages: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: false + /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} dev: true @@ -994,6 +993,15 @@ packages: pretty-format: 29.7.0 dev: true + /@yellow-ticket/seed-json-schema@0.1.6: + resolution: {integrity: sha512-RtI85ohEQpARt8qRLeqglOpPFu/00YGmw3Mi7iNphbjoDCwm4QrZWnVBy9uEn2veUC6fuw0GHjHAFzAfJxaEZQ==} + dependencies: + '@faker-js/faker': 8.4.1 + '@types/json-schema': 7.0.15 + outvariant: 1.4.2 + randexp: 0.5.3 + dev: false + /JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -1858,10 +1866,6 @@ packages: - supports-color dev: true - /faker@5.5.3: - resolution: {integrity: sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==} - dev: false - /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} diff --git a/src/open-api/schema/evolve.ts b/src/open-api/schema/evolve.ts deleted file mode 100644 index 090cee1..0000000 --- a/src/open-api/schema/evolve.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { OpenAPIV3 } from 'openapi-types' -import { evolveString } from './types/string.js' -import { evolveInteger } from './types/integer.js' -import { evolveBoolean } from './types/boolean.js' -import { evolveNumber } from './types/number.js' -import { evolveArray } from './types/array.js' -import { evolveObject } from './types/object.js' - -export function evolveJsonSchema( - schema: OpenAPIV3.SchemaObject, -): string | number | boolean | unknown[] | Record | undefined { - // Always use an explicit example first. - // A "schema" field may equal an example if it's a resolved reference. - if (schema.example) { - return schema.example - } - - switch (schema.type) { - case 'string': { - return evolveString(schema) - } - - case 'integer': { - return evolveInteger(schema) - } - - case 'boolean': { - return evolveBoolean() - } - - case 'number': { - return evolveNumber(schema) - } - - case 'array': { - return evolveArray(schema) - } - - case 'object': { - return evolveObject(schema) - } - } -} diff --git a/src/open-api/schema/types/array.ts b/src/open-api/schema/types/array.ts deleted file mode 100644 index e8842db..0000000 --- a/src/open-api/schema/types/array.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { datatype } from 'faker' -import { OpenAPIV3 } from 'openapi-types' -import { invariant } from 'outvariant' -import { evolveJsonSchema } from '../evolve.js' - -export function evolveArray(schema: OpenAPIV3.ArraySchemaObject): unknown[] { - const { items: arraySchema } = schema - - invariant( - !('$ref' in arraySchema), - 'Failed to generate mock from schema array (%j): found unresolved reference.', - arraySchema, - ) - - const minLength = schema.minLength || 2 - const arrayLength = datatype.number({ - min: minLength, - max: schema.maxLength || minLength + 4, - }) - - const value: unknown[] = new Array(arrayLength) - .fill(null) - .reduce((array) => { - const value = evolveJsonSchema(arraySchema) - if (value) { - // Push instead of concating to support - // nested arrays. - array.push(value) - } - return array - }, []) - - return value -} diff --git a/src/open-api/schema/types/boolean.ts b/src/open-api/schema/types/boolean.ts deleted file mode 100644 index a848220..0000000 --- a/src/open-api/schema/types/boolean.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { datatype } from 'faker' - -export function evolveBoolean(): boolean { - return datatype.boolean() -} diff --git a/src/open-api/schema/types/integer.ts b/src/open-api/schema/types/integer.ts deleted file mode 100644 index da2e74b..0000000 --- a/src/open-api/schema/types/integer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { datatype } from 'faker' -import { OpenAPIV3 } from 'openapi-types' - -export function evolveInteger(schema: OpenAPIV3.SchemaObject): number { - switch (schema.format) { - case 'int16': - case 'int32': - case 'int64': { - return datatype.number({ - min: schema.minimum, - max: schema.maximum, - }) - } - - default: { - return datatype.float({ - min: schema.minimum, - max: schema.maximum, - }) - } - } -} diff --git a/src/open-api/schema/types/number.ts b/src/open-api/schema/types/number.ts deleted file mode 100644 index aa43fe7..0000000 --- a/src/open-api/schema/types/number.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { datatype } from 'faker' -import { OpenAPIV3 } from 'openapi-types' - -export function evolveNumber(schema: OpenAPIV3.SchemaObject): number { - return datatype.number({ - min: schema.minimum, - max: schema.maximum, - }) -} diff --git a/src/open-api/schema/types/object.ts b/src/open-api/schema/types/object.ts deleted file mode 100644 index 0cd9ab0..0000000 --- a/src/open-api/schema/types/object.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { datatype, random } from 'faker' -import { OpenAPIV3 } from 'openapi-types' -import { invariant } from 'outvariant' -import { repeat } from '../../utils/repeat.js' -import { evolveJsonSchema } from '../evolve.js' - -export function evolveObject( - schema: OpenAPIV3.NonArraySchemaObject, -): Record { - // Always us an explicit example, if provided. - if (schema.example) { - return schema.example - } - - const json: Record = {} - - // Support explicit "properties". - if (schema.properties) { - for (const [key, propertyDefinition] of Object.entries(schema.properties)) { - invariant( - !('$ref' in propertyDefinition), - 'Failed to generate mock from the schema property definition (%j): found unresolved reference.', - propertyDefinition, - ) - - const value = evolveJsonSchema(propertyDefinition) - if (typeof value !== 'undefined') { - json[key] = value - } - } - } - - // Support "additionalProperties". - if (schema.additionalProperties) { - const additionalPropertiesSchema = schema.additionalProperties - - if (additionalPropertiesSchema === true) { - repeat(0, 4, () => { - const propertyName = random.word().toLowerCase() - json[propertyName] = datatype.string() - }) - - return json - } - - invariant( - !('$ref' in additionalPropertiesSchema), - 'Failed to generate mock from the "additionalProperties" schema: found unresolved reference.', - ) - - repeat(0, 4, () => { - const propertyName = random.word().toLowerCase() - json[propertyName] = evolveJsonSchema(additionalPropertiesSchema) - }) - } - - return json -} diff --git a/src/open-api/schema/types/string.ts b/src/open-api/schema/types/string.ts deleted file mode 100644 index 441bf55..0000000 --- a/src/open-api/schema/types/string.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { datatype, internet, finance } from 'faker' -import { OpenAPIV3 } from 'openapi-types' -import { randexp } from 'randexp' -import { toBinary } from '../../utils/to-binary.js' - -export function evolveString(schema: OpenAPIV3.SchemaObject): string { - if (schema.pattern) { - return randexp(schema.pattern) - } - - switch (schema.format?.toLowerCase()) { - case 'byte': { - return btoa(datatype.string()) - } - - case 'binary': { - return toBinary([ - datatype.number({ min: 0, max: 255 }), - datatype.number({ min: 0, max: 255 }), - datatype.number({ min: 0, max: 255 }), - datatype.number({ min: 0, max: 255 }), - ]) - } - - case 'uuid': { - return datatype.uuid() - } - - case 'email': { - return internet.email() - } - - case 'password': { - return internet.password() - } - - case 'date': { - return datatype - .datetime(schema.maximum) - .toISOString() - .replace(/T.+$/g, '') - } - - case 'date-time': { - return datatype.datetime(schema.maximum).toISOString() - } - - case 'uri': { - return internet.url() - } - - case 'hostname': { - return internet.domainName() - } - - case 'ipv4': { - return internet.ip() - } - - case 'ipv6': { - return internet.ipv6() - } - - case 'creditcard': { - return finance.creditCardNumber() - } - - case 'hexcolor': { - return internet.color() - } - - case 'mac': { - return internet.mac() - } - } - - // Use a random value from the specified enums list. - if (schema.enum) { - const enumIndex = datatype.number({ - min: 0, - max: schema.enum.length - 1, - }) - - return schema.enum[enumIndex] - } - - const value = datatype.string(schema.minLength) - return value.slice(0, schema.maxLength) -} diff --git a/src/open-api/utils/open-api-utils.ts b/src/open-api/utils/open-api-utils.ts index e970bed..d97cfc3 100644 --- a/src/open-api/utils/open-api-utils.ts +++ b/src/open-api/utils/open-api-utils.ts @@ -1,7 +1,7 @@ import { STATUS_CODES } from 'node:http' import type { ResponseResolver } from 'msw' import { OpenAPIV3 } from 'openapi-types' -import { evolveJsonSchema } from '../schema/evolve' +import { seedSchema } from '@yellow-ticket/seed-json-schema' import { toString } from './to-string.js' export function createResponseResolver( @@ -17,6 +17,7 @@ export function createResponseResolver( statusText: 'Not Implemented', }) } + if (Object.keys(responses).length === 0) { return new Response('Not Implemented', { status: 501, @@ -58,6 +59,7 @@ export function createResponseResolver( } const status = Number(explicitResponseStatus || '200') + return new Response(toBody(request, responseObject), { status, statusText: STATUS_CODES[status], @@ -127,11 +129,13 @@ export function toHeaders( const headerSchema = (headerObject as OpenAPIV3.HeaderObject).schema as | OpenAPIV3.SchemaObject | undefined + if (!headerSchema) { continue } - const headerValue = evolveJsonSchema(headerSchema) + const headerValue = seedSchema(headerSchema as any) + if (typeof headerValue === 'undefined') { continue } @@ -154,7 +158,8 @@ export function toBody( responseObject: OpenAPIV3.ResponseObject, ): RequestInit['body'] { const { content } = responseObject - if (!content) { + + if (content == null) { return null } @@ -196,15 +201,15 @@ export function toBody( return null } - // If the response object has the body example, use it. + // First, if the response has a literal example, use it. if (mediaTypeObject.example) { if (typeof mediaTypeObject.example === 'object') { return JSON.stringify(mediaTypeObject.example) } - return mediaTypeObject.example } + // If the response has multiple literal examples, use the first one. if (mediaTypeObject.examples) { // Support exact response example specified in the // "example" request URL search parameter. @@ -232,11 +237,25 @@ export function toBody( return firstExample.value } + /** + * Then, if the response has a schema example, use it. + * @note `example` is always nested under `schema`. + * `examples` is a sibling to `schema`. + */ + const schemaExample = (mediaTypeObject.schema as OpenAPIV3.SchemaObject) + ?.example + + if (schemaExample) { + if (typeof schemaExample === 'object') { + return JSON.stringify(schemaExample) + } + + return schemaExample + } + // If the response is a JSON Schema, evolve and use it. if (mediaTypeObject.schema) { - const resolvedResponse = evolveJsonSchema( - mediaTypeObject.schema as OpenAPIV3.SchemaObject, - ) + const resolvedResponse = seedSchema(mediaTypeObject.schema as any) return JSON.stringify(resolvedResponse) } diff --git a/src/open-api/utils/repeat.ts b/src/open-api/utils/repeat.ts deleted file mode 100644 index 7aa8fdb..0000000 --- a/src/open-api/utils/repeat.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { datatype } from 'faker' - -export function repeat(min: number, max: number, callback: () => void): void { - const count = datatype.number({ min, max }) - - for (let i = 0; i < count; i++) { - callback() - } -} diff --git a/test/oas/evolve-json-schema.test.ts b/test/oas/evolve-json-schema.test.ts deleted file mode 100644 index d1578a0..0000000 --- a/test/oas/evolve-json-schema.test.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { evolveJsonSchema } from '../../src/open-api/schema/evolve.js' - -describe('string', () => { - it('supports a plain string value', () => { - const value = evolveJsonSchema({ - type: 'string', - }) - expect(value).toMatch(/^\S+$/) - }) - - it('supports the "example" value', () => { - const value = evolveJsonSchema({ - type: 'string', - example: 'John', - }) - // Explicit examples are always used as-is. - expect(value).toEqual('John') - }) - - it('supports "minLength"', () => { - const value = evolveJsonSchema({ - type: 'string', - minLength: 10, - }) - expect(value).toMatch(/^\S{10,}$/) - }) - - it('supports the "pattern" expression', () => { - const value = evolveJsonSchema({ - type: 'string', - pattern: '^[0-9]{3}-[0-9]{3}$', - }) - expect(value).toMatch(/^[0-9]{3}-[0-9]{3}$/) - }) - - it('supports the "enum" value', () => { - const value = evolveJsonSchema({ - type: 'string', - enum: ['active', 'pending', 'stale'], - }) - - expect(value).toMatch(/^(active|pending|stale)$/) - }) - - it('supports the "uuid" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'uuid', - }) - expect(value).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ) - }) - - it('supports the "email" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'email', - }) - expect(value).toMatch(/^\S+@\S+\.\w+$/) - }) - - it('supports the "password" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'password', - }) - expect(value).toMatch(/^\S+$/) - }) - - it('supports the "date" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'date', - }) - expect(value).toMatch(/^\d{4}-\d{2}-\d{2}$/) - }) - - it('supports the "date-time" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'date-time', - }) - expect(value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+?Z$/) - }) - - it('supports the "uri" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'uri', - }) - expect(value).toMatch(/^https?:\/\/\w+\.\w+?$/) - }) - - it('supports the "byte" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'byte', - }) - expect(value).toMatch(/^.+?==$/) - }) - - it('supports the "binary" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'binary', - }) - expect(value).toMatch(/^\d{1,8} \d{1,8} \d{1,8} \d{1,8}$/) - }) - - it('supports the "hostname" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'hostname', - }) - expect(value).toMatch(/^\w+?\.\w{2,}$/) - }) - - it('supports the "ipv4" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'ipv4', - }) - expect(value).toMatch(/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) - }) - - it('supports the "ipv6" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'ipv6', - }) - expect(value).toMatch(/^([a-f0-9:]+:+)+[a-f0-9]+$/) - }) - - it('supports the "creditcard" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'creditcard', - }) - expect(value).toMatch(/[0-9]+(-)?/) - }) - - it('supports the "hexcolor" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'hexcolor', - }) - expect(value).toMatch(/^#[a-f0-9]{6}$/) - }) - - it('supports the "mac" format', () => { - const value = evolveJsonSchema({ - type: 'string', - format: 'mac', - }) - expect(value).toMatch(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/) - }) -}) - -describe('boolean', () => { - it('supports a plain boolean value', () => { - const value = evolveJsonSchema({ - type: 'boolean', - }) as boolean - expect(typeof value).toEqual('boolean') - }) - - it('supports the "example" value', () => { - const value = evolveJsonSchema({ - type: 'boolean', - example: true, - }) - expect(value).toEqual(true) - }) -}) - -describe('number', () => { - it('supports a plain number value', () => { - const number = evolveJsonSchema({ type: 'number' }) - expect(typeof number).toEqual('number') - }) - - it('supports the "minimum" option', () => { - const number = evolveJsonSchema({ - type: 'number', - minimum: 100, - }) - expect(number).toBeGreaterThanOrEqual(100) - }) - - it('supports the "maximum" option', () => { - const number = evolveJsonSchema({ - type: 'number', - maximum: 50, - }) - expect(number).toBeLessThanOrEqual(50) - }) - - it('supports both the "minimum" and "maximum" options', () => { - const number = evolveJsonSchema({ - type: 'number', - minimum: 25, - maximum: 50, - }) - expect(number).toBeGreaterThanOrEqual(25) - expect(number).toBeLessThanOrEqual(50) - }) -}) - -describe('integer', () => { - it('supports a plain integer', () => { - const number = evolveJsonSchema({ - type: 'integer', - }) as number - expect(number.toString()).toMatch(/^\d+\.\d+$/) - }) - - it('supports the "minimum" option', () => { - const number = evolveJsonSchema({ - type: 'integer', - minimum: 100, - }) - expect(number).toBeGreaterThanOrEqual(100) - }) - - it('supports the "maximum" option', () => { - const number = evolveJsonSchema({ - type: 'integer', - maximum: 100, - }) - expect(number).toBeLessThanOrEqual(100) - }) - - it('suports both the "minimum" and "maximum" options', () => { - const number = evolveJsonSchema({ - type: 'integer', - minimum: 100, - maximum: 100, - }) - expect(number).toBeGreaterThanOrEqual(100) - expect(number).toBeLessThanOrEqual(100) - }) - - it('supports a "int64" format integer', () => { - const number = evolveJsonSchema({ - type: 'integer', - format: 'int64', - }) as number - expect(number.toString()).toMatch(/^\d+$/) - }) -}) - -describe('array', () => { - it('supports a plain array of numbers', () => { - const array = evolveJsonSchema({ - type: 'array', - items: { type: 'number' }, - }) as number[] - - expect(array).toBeInstanceOf(Array) - array.forEach((value) => { - expect(typeof value).toEqual('number') - }) - }) - - it('supports a plain array of strings', () => { - const array = evolveJsonSchema({ - type: 'array', - items: { type: 'string' }, - }) as string[] - - expect(array).toBeInstanceOf(Array) - array.forEach((value) => { - expect(value).toMatch(/^\S+$/) - }) - }) - - it('supports a plain array of objects', () => { - const array = evolveJsonSchema({ - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'uuid', - }, - title: { type: 'string' }, - }, - }, - }) as Array<{ id: string; title: string }> - - expect(array).toBeInstanceOf(Array) - array.forEach((value) => { - expect(value.id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ) - expect(value.title).toMatch(/^\S+$/) - }) - }) - - it('supports nested arrays', () => { - const data = evolveJsonSchema({ - type: 'array', - items: { - type: 'array', - items: { type: 'number' }, - }, - }) as number[][] - - expect(data).toBeInstanceOf(Array) - data.forEach((value) => { - expect(value).toBeInstanceOf(Array) - value.forEach((number) => { - expect(typeof number).toEqual('number') - }) - }) - }) - - it('supports the "minLength" option', () => { - const data = evolveJsonSchema({ - type: 'array', - minLength: 10, - items: { type: 'number' }, - }) as number[] - expect(data.length).toBeGreaterThanOrEqual(10) - }) - - it('supports the "maxLength" option', () => { - const data = evolveJsonSchema({ - type: 'array', - maxLength: 10, - items: { type: 'number' }, - }) as number[] - expect(data.length).toBeLessThanOrEqual(10) - }) -}) - -describe('object', () => { - it('supports a plain object', () => { - const data = evolveJsonSchema({ - type: 'object', - properties: { - id: { - type: 'string', - format: 'uuid', - }, - firstName: { - type: 'string', - }, - age: { - type: 'number', - minimum: 18, - maximum: 99, - }, - }, - }) as Record - expect(Object.keys(data)).toEqual(['id', 'firstName', 'age']) - - expect(data.id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ) - expect(data.firstName).toMatch(/^\S+$/) - expect(data.age).toBeGreaterThanOrEqual(18) - expect(data.age).toBeLessThanOrEqual(99) - }) - - it('supports a nested object', () => { - const value = evolveJsonSchema({ - type: 'object', - properties: { - firstName: { - type: 'string', - }, - nested: { - type: 'object', - properties: { - isNested: { - type: 'boolean', - }, - }, - }, - }, - }) as { firstName: string; nested: { isNested: boolean } } - - expect(Object.keys(value)).toEqual(['firstName', 'nested']) - expect(value.firstName).toMatch(/^\S+$/) - expect(Object.keys(value.nested)).toEqual(['isNested']) - expect(typeof value.nested.isNested).toEqual('boolean') - }) - - it('supports the "example" value', () => { - const value = evolveJsonSchema({ - type: 'object', - example: { - id: 'abc-123', - firstName: 'John', - }, - }) - expect(value).toEqual({ - id: 'abc-123', - firstName: 'John', - }) - }) - - it('supports "additionalProperties"', () => { - const value = evolveJsonSchema({ - type: 'object', - additionalProperties: true, - }) as Record - - const keys = Object.keys(value) - if (keys.length > 0) { - expect(keys).toEqual(expect.arrayContaining([expect.any(String)])) - expect(Object.values(value)).toEqual( - expect.arrayContaining([expect.anything()]), - ) - } - }) - - it('supports "additionalProperties" with a strict type', () => { - const value = evolveJsonSchema({ - type: 'object', - additionalProperties: { - type: 'string', - }, - }) as Record - - const keys = Object.keys(value) - if (keys.length > 0) { - expect(keys).toEqual(expect.arrayContaining([expect.any(String)])) - expect(Object.values(value)).toEqual( - expect.arrayContaining([expect.any(String)]), - ) - } - }) -}) diff --git a/test/oas/oas-json-schema.test.ts b/test/oas/oas-json-schema.test.ts index e8d3fc4..cee122f 100644 --- a/test/oas/oas-json-schema.test.ts +++ b/test/oas/oas-json-schema.test.ts @@ -25,7 +25,8 @@ it('supports JSON Schema object', async () => { }, items: { type: 'array', - maxLength: 2, + minItems: 2, + maxItems: 2, items: { type: 'object', properties: { diff --git a/test/oas/petstore.test.ts b/test/oas/petstore.test.ts index 63b1762..fa95896 100644 --- a/test/oas/petstore.test.ts +++ b/test/oas/petstore.test.ts @@ -12,11 +12,11 @@ beforeAll(async () => { const entities = { pet: { - id: 10, - name: 'doggie', + id: expect.any(Number), + name: expect.any(String), category: { - id: 1, - name: 'Dogs', + id: expect.any(Number), + name: expect.any(String), }, photoUrls: expect.arrayContaining([expect.any(String)]), tags: expect.arrayContaining([ @@ -28,9 +28,9 @@ const entities = { status: expect.stringMatching(/^(available|pending|sold)$/), }, order: { - id: 10, - petId: 198772, - quantity: 7, + id: 42, + petId: 100, + quantity: 72, shipDate: expect.stringMatching( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+?Z$/, ), @@ -38,14 +38,14 @@ const entities = { complete: expect.any(Boolean), }, user: { - id: 10, - username: 'theUser', - firstName: 'John', - lastName: 'James', - email: 'john@email.com', - password: 12345, - phone: 12345, - userStatus: 1, + id: 42, + email: 'abaft', + firstName: 'fooey', + lastName: 'fully', + password: 'lined', + phone: 'waste', + userStatus: 81, + username: 'wetly', }, } diff --git a/vitest.setup.ts b/vitest.setup.ts index a6cfe7e..9449e9b 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,4 +1,9 @@ import { invariant } from 'outvariant' +import { faker } from '@yellow-ticket/seed-json-schema' + +beforeEach(() => { + faker.seed(1) +}) expect.extend({ toEqualBytes(actual: unknown, expected: unknown) {