UNPKG

hey-api-builders

Version:

A custom plugin for @hey-api/openapi-ts that generates mock data builders with a lightweight custom runtime, Zod integration, or static mock generation.

663 lines (573 loc) 25.8 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { generateZodSchema, generateMockFromZodSchema } from './zod-mock-generator'; import type { Schema } from '../types'; describe('Zod Mock Generator', () => { describe('generateZodSchema', () => { it('generates z.string() for string type', () => { const schema: Schema = { type: 'string' }; const result = generateZodSchema(schema); expect(result).toBe('z.string()'); }); it('generates z.number() for number type', () => { const schema: Schema = { type: 'number' }; const result = generateZodSchema(schema); expect(result).toBe('z.number()'); }); it('generates z.number().int() for integer type', () => { const schema: Schema = { type: 'integer' }; const result = generateZodSchema(schema); expect(result).toContain('z.number().int()'); }); it('generates z.boolean() for boolean type', () => { const schema: Schema = { type: 'boolean' }; const result = generateZodSchema(schema); expect(result).toBe('z.boolean()'); }); it('generates z.null() for null type', () => { const schema: Schema = { type: 'null' }; const result = generateZodSchema(schema); expect(result).toBe('z.null()'); }); it('handles string with uuid format', () => { const schema: Schema = { type: 'string', format: 'uuid' }; const result = generateZodSchema(schema); expect(result).toBe('z.string().uuid()'); }); it('handles string with email format', () => { const schema: Schema = { type: 'string', format: 'email' }; const result = generateZodSchema(schema); expect(result).toBe('z.string().email()'); }); it('handles string with url format', () => { const schema: Schema = { type: 'string', format: 'url' }; const result = generateZodSchema(schema); expect(result).toBe('z.string().url()'); }); it('handles string with date format', () => { const schema: Schema = { type: 'string', format: 'date' }; const result = generateZodSchema(schema); expect(result).toBe('z.string().date()'); }); it('handles string with date-time format', () => { const schema: Schema = { type: 'string', format: 'date-time' }; const result = generateZodSchema(schema); expect(result).toBe('z.string().datetime()'); }); it('handles string with phone format', () => { const schema: Schema = { type: 'string', format: 'phone' }; const result = generateZodSchema(schema); expect(result).toContain('z.string().regex'); expect(result).toContain('\\+?[1-9]\\d{1,14}'); }); it('handles string with minLength', () => { const schema: Schema = { type: 'string', minLength: 5 }; const result = generateZodSchema(schema); expect(result).toBe('z.string().min(5)'); }); it('handles string with maxLength', () => { const schema: Schema = { type: 'string', maxLength: 100 }; const result = generateZodSchema(schema); expect(result).toBe('z.string().max(100)'); }); it('handles string with pattern', () => { const schema: Schema = { type: 'string', pattern: '^[A-Z]+$' }; const result = generateZodSchema(schema); expect(result).toContain('.regex(/^[A-Z]+$/)'); }); it('handles number with minimum', () => { const schema: Schema = { type: 'number', minimum: 0 }; const result = generateZodSchema(schema); expect(result).toBe('z.number().min(0)'); }); it('handles number with maximum', () => { const schema: Schema = { type: 'number', maximum: 100 }; const result = generateZodSchema(schema); expect(result).toBe('z.number().max(100)'); }); it('handles number with exclusive minimum', () => { const schema: Schema = { type: 'number', exclusiveMinimum: 0 }; const result = generateZodSchema(schema); expect(result).toBe('z.number().gt(0)'); }); it('handles number with exclusive maximum', () => { const schema: Schema = { type: 'number', exclusiveMaximum: 100 }; const result = generateZodSchema(schema); expect(result).toBe('z.number().lt(100)'); }); it('handles object with properties', () => { const schema: Schema = { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' }, }, required: ['name'], }; const result = generateZodSchema(schema); expect(result).toContain('z.object({'); expect(result).toContain('name: z.string()'); expect(result).toContain('age: z.number().optional()'); }); it('handles array type', () => { const schema: Schema = { type: 'array', items: { type: 'string' }, }; const result = generateZodSchema(schema); expect(result).toBe('z.array(z.string())'); }); it('handles array with minItems', () => { const schema: Schema = { type: 'array', items: { type: 'string' }, minItems: 1, }; const result = generateZodSchema(schema); expect(result).toContain('.min(1)'); }); it('handles array with maxItems', () => { const schema: Schema = { type: 'array', items: { type: 'string' }, maxItems: 10, }; const result = generateZodSchema(schema); expect(result).toContain('.max(10)'); }); }); describe('generateMockFromZodSchema', () => { beforeEach(() => { vi.spyOn(Math, 'random').mockReturnValue(0.5); }); describe('primitive types', () => { it('generates mock for z.string()', () => { const zodSchema = 'z.string()'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect(result).toBeTruthy(); }); it('generates mock for z.number()', () => { const zodSchema = 'z.number()'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); }); it('generates mock for z.boolean()', () => { const zodSchema = 'z.boolean()'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('boolean'); }); it('generates null for z.null()', () => { const zodSchema = 'z.null()'; const result = generateMockFromZodSchema(zodSchema); expect(result).toBe(null); }); it('omits null when omitNulls option is true', () => { const zodSchema = 'z.null()'; const result = generateMockFromZodSchema(zodSchema, {}, { omitNulls: true }); expect(result).toBe(undefined); }); }); describe('string validations', () => { it('generates email for z.string().email()', () => { const zodSchema = 'z.string().email()'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect(result).toMatch(/^[\w.-]+@[\w.-]+\.\w+$/); }); it('generates UUID for z.string().uuid()', () => { const zodSchema = 'z.string().uuid()'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); }); it('generates URL for z.string().url()', () => { const zodSchema = 'z.string().url()'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect(result).toMatch(/^https?:\/\/.+/); }); it('generates date for z.string().date()', () => { const zodSchema = 'z.string().date()'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/); }); it('generates datetime for z.string().datetime()', () => { const zodSchema = 'z.string().datetime()'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); }); it('generates string with min length for z.string().min()', () => { const zodSchema = 'z.string().min(10)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect((result as string).length).toBeGreaterThanOrEqual(10); }); it('generates string with max length for z.string().max()', () => { const zodSchema = 'z.string().max(5)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect((result as string).length).toBeLessThanOrEqual(5); }); }); describe('number validations', () => { it('generates number with min for z.number().min()', () => { const zodSchema = 'z.number().min(10)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); expect(result as number).toBeGreaterThanOrEqual(10); }); it('generates number with max for z.number().max()', () => { const zodSchema = 'z.number().max(100)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); expect(result as number).toBeLessThanOrEqual(100); }); it('generates integer for z.number().int()', () => { const zodSchema = 'z.number().int()'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); expect(Number.isInteger(result as number)).toBe(true); }); it('generates number greater than for z.number().gt()', () => { const zodSchema = 'z.number().gt(5)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); expect(result as number).toBeGreaterThan(5); }); it('generates number less than for z.number().lt()', () => { const zodSchema = 'z.number().lt(10)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); expect(result as number).toBeLessThan(10); }); }); describe('enum types', () => { it('generates mock for z.enum()', () => { const zodSchema = 'z.enum(["active", "inactive", "pending"])'; const result = generateMockFromZodSchema(zodSchema); expect(['active', 'inactive', 'pending']).toContain(result); }); it('generates mock for single value enum', () => { const zodSchema = 'z.enum(["single"])'; const result = generateMockFromZodSchema(zodSchema); expect(result).toBe('single'); }); }); describe('array types', () => { it('generates empty array when items schema cannot be parsed', () => { const zodSchema = 'z.array(z.string())'; const result = generateMockFromZodSchema(zodSchema); expect(Array.isArray(result)).toBe(true); }); it('generates array with overrides using items property', () => { const zodSchema = 'z.array(z.string())'; const result = generateMockFromZodSchema(zodSchema, { items: ['a', 'b', 'c'] }); expect(Array.isArray(result)).toBe(true); expect(result).toEqual(['a', 'b', 'c']); }); it('respects maxItems constraint when specified', () => { const zodSchema = 'z.array(z.string()).max(2)'; const result = generateMockFromZodSchema(zodSchema); expect(Array.isArray(result)).toBe(true); expect((result as unknown[]).length).toBeLessThanOrEqual(2); }); it('generates array with default length between min and max', () => { const zodSchema = 'z.array(z.number())'; const result = generateMockFromZodSchema(zodSchema); expect(Array.isArray(result)).toBe(true); expect((result as unknown[]).length).toBeGreaterThanOrEqual(1); expect((result as unknown[]).length).toBeLessThanOrEqual(3); }); }); describe('object types', () => { it('generates mock for z.object()', () => { const zodSchema = 'z.object({ name: z.string(), age: z.number() })'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('object'); expect(result).toHaveProperty('name'); expect(result).toHaveProperty('age'); expect(typeof (result as Record<string, unknown>).name).toBe('string'); expect(typeof (result as Record<string, unknown>).age).toBe('number'); }); it('handles optional fields', () => { const zodSchema = 'z.object({ name: z.string(), age: z.number().optional() })'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('object'); expect(result).toHaveProperty('name'); }); it('always includes optionals when alwaysIncludeOptionals is true', () => { const zodSchema = 'z.object({ name: z.string(), age: z.number().optional() })'; const result = generateMockFromZodSchema(zodSchema, {}, { alwaysIncludeOptionals: true }); expect(typeof result).toBe('object'); expect(result).toHaveProperty('name'); expect(result).toHaveProperty('age'); }); it('handles nested objects', () => { const zodSchema = 'z.object({ user: z.object({ name: z.string(), email: z.string() }) })'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('object'); expect(result).toHaveProperty('user'); expect(typeof (result as Record<string, unknown>).user).toBe('object'); expect((result as Record<string, Record<string, unknown>>).user).toHaveProperty('name'); expect((result as Record<string, Record<string, unknown>>).user).toHaveProperty('email'); }); it('handles properties with special characters', () => { const zodSchema = 'z.object({ "user-name": z.string(), "my.email": z.string() })'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('object'); expect(result).toHaveProperty('user-name'); expect(result).toHaveProperty('my.email'); }); it('applies overrides to object properties', () => { const zodSchema = 'z.object({ name: z.string(), age: z.number() })'; const result = generateMockFromZodSchema(zodSchema, { name: 'John', age: 30 }); expect((result as Record<string, unknown>).name).toBe('John'); expect((result as Record<string, unknown>).age).toBe(30); }); it('applies partial overrides', () => { const zodSchema = 'z.object({ name: z.string(), age: z.number() })'; const result = generateMockFromZodSchema(zodSchema, { name: 'John' }); expect((result as Record<string, unknown>).name).toBe('John'); expect(result).toHaveProperty('age'); expect(typeof (result as Record<string, unknown>).age).toBe('number'); }); }); describe('union types', () => { it('generates mock for z.union()', () => { const zodSchema = 'z.union([z.string(), z.number()])'; const result = generateMockFromZodSchema(zodSchema); expect(['string', 'number']).toContain(typeof result); }); it('handles nullable unions', () => { const zodSchema = 'z.union([z.string(), z.null()])'; const result = generateMockFromZodSchema(zodSchema); expect(['string', 'object']).toContain(typeof result); }); it('omits null in nullable unions when omitNulls is true', () => { const zodSchema = 'z.union([z.string(), z.null()])'; const result = generateMockFromZodSchema(zodSchema, {}, { omitNulls: true }); if (result === null) { expect(result).toBe(undefined); } else { expect(typeof result).toBe('string'); } }); }); describe('complex nested schemas', () => { it('generates mock for complex object structure', () => { const zodSchema = `z.object({ id: z.string().uuid(), user: z.object({ name: z.string(), email: z.string().email(), age: z.number().int().min(0).max(120) }), tags: z.array(z.string()), status: z.enum(["active", "inactive"]), metadata: z.object({ createdAt: z.string().datetime(), updatedAt: z.string().datetime() }).optional() })`; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('object'); expect(result).toHaveProperty('id'); expect(result).toHaveProperty('user'); expect(result).toHaveProperty('tags'); expect(result).toHaveProperty('status'); const obj = result as Record<string, unknown>; expect(typeof obj.id).toBe('string'); expect(obj.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); expect(Array.isArray(obj.tags)).toBe(true); expect(['active', 'inactive']).toContain(obj.status); }); it('generates array using overrides for simple types', () => { const zodSchema = `z.array(z.number())`; const result = generateMockFromZodSchema(zodSchema, { items: [1, 2, 3], }); expect(Array.isArray(result)).toBe(true); expect((result as unknown[]).length).toBe(3); expect(result).toEqual([1, 2, 3]); }); }); describe('options handling', () => { it('respects optionalsProbability option', () => { const zodSchema = 'z.object({ optional1: z.string().optional(), optional2: z.string().optional(), optional3: z.string().optional() })'; vi.spyOn(Math, 'random').mockReturnValue(0.9); const _result1 = generateMockFromZodSchema(zodSchema, {}, { optionalsProbability: 0 }); vi.spyOn(Math, 'random').mockReturnValue(0.5); const _result2 = generateMockFromZodSchema(zodSchema, {}, { optionalsProbability: 1 }); }); it('handles optionalsProbability as false', () => { const zodSchema = 'z.object({ optional: z.string().optional() })'; vi.spyOn(Math, 'random').mockReturnValue(0.5); const result = generateMockFromZodSchema(zodSchema, {}, { optionalsProbability: false }); expect(typeof result).toBe('object'); }); }); describe('edge cases', () => { it('handles empty object schema', () => { const zodSchema = 'z.object({})'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('object'); expect(Object.keys(result as Record<string, unknown>).length).toBe(0); }); it('handles unknown schema types', () => { const zodSchema = 'z.unknown()'; const result = generateMockFromZodSchema(zodSchema); expect(result).toBe(null); }); it('handles malformed schema strings gracefully', () => { const zodSchema = 'not a valid schema'; const result = generateMockFromZodSchema(zodSchema); expect(result).toBe(null); }); it('handles deeply nested property paths', () => { const zodSchema = `z.object({ level1: z.object({ level2: z.object({ level3: z.object({ value: z.string() }) }) }) })`; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('object'); const obj = result as Record<string, unknown>; expect(obj).toHaveProperty('level1'); }); it('generates consistent mock values with overrides', () => { const zodSchema = 'z.object({ a: z.string(), b: z.string(), c: z.string() })'; const overrides = { b: 'fixed-value' }; const result = generateMockFromZodSchema(zodSchema, overrides); expect((result as Record<string, unknown>).b).toBe('fixed-value'); expect(typeof (result as Record<string, unknown>).a).toBe('string'); expect(typeof (result as Record<string, unknown>).c).toBe('string'); }); it('handles empty string schema', () => { const zodSchema = ''; const result = generateMockFromZodSchema(zodSchema); expect(result).toBe(null); }); it('handles string with only whitespace', () => { const zodSchema = ' '; const result = generateMockFromZodSchema(zodSchema); expect(result).toBe(null); }); it('handles numeric enum values', () => { const zodSchema = 'z.enum([1, 2, 3])'; const result = generateMockFromZodSchema(zodSchema); expect([1, 2, 3]).toContain(result); }); it('handles boolean enum values', () => { const zodSchema = 'z.enum([true, false])'; const result = generateMockFromZodSchema(zodSchema); expect([true, false]).toContain(result); }); it('handles mixed type enum values', () => { const zodSchema = 'z.enum(["active", 1, true])'; const result = generateMockFromZodSchema(zodSchema); expect(['active', 1, true]).toContain(result); }); }); describe('string regex patterns', () => { it('generates phone number for phone regex pattern', () => { const zodSchema = 'z.string().regex(/^\\+?[1-9]\\d{1,14}$/)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect(result).toMatch(/^\+1\d{10}$/); }); it('generates random string for custom regex pattern', () => { const zodSchema = 'z.string().regex(/^[A-Z]{5}$/)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect((result as string).length).toBe(10); }); }); describe('string length constraints', () => { it('generates string respecting both min and max length', () => { const zodSchema = 'z.string().min(8).max(12)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); const len = (result as string).length; expect(len).toBeGreaterThanOrEqual(8); expect(len).toBeLessThanOrEqual(12); }); it('generates string with exact length when min equals max', () => { const zodSchema = 'z.string().min(10).max(10)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('string'); expect((result as string).length).toBe(10); }); }); describe('number constraints', () => { it('handles number with both min and max', () => { const zodSchema = 'z.number().min(10).max(20)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); expect(result as number).toBeGreaterThanOrEqual(10); expect(result as number).toBeLessThanOrEqual(20); }); it('handles number with both gt and lt', () => { const zodSchema = 'z.number().gt(5).lt(10)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); expect(result as number).toBeGreaterThan(5); expect(result as number).toBeLessThan(10); }); it('handles integer with constraints', () => { const zodSchema = 'z.number().int().min(1).max(10)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); expect(Number.isInteger(result as number)).toBe(true); expect(result as number).toBeGreaterThanOrEqual(1); expect(result as number).toBeLessThanOrEqual(10); }); it('generates negative numbers when range is negative', () => { const zodSchema = 'z.number().min(-100).max(-50)'; const result = generateMockFromZodSchema(zodSchema); expect(typeof result).toBe('number'); expect(result as number).toBeGreaterThanOrEqual(-100); expect(result as number).toBeLessThanOrEqual(-50); }); }); describe('union type variations', () => { it('handles union with multiple types', () => { const zodSchema = 'z.union([z.string(), z.number(), z.boolean()])'; const result = generateMockFromZodSchema(zodSchema); expect(['string', 'number', 'boolean']).toContain(typeof result); }); it('handles union with complex types', () => { const zodSchema = 'z.union([z.string().email(), z.number().int()])'; const result = generateMockFromZodSchema(zodSchema); if (typeof result === 'string') { expect(result).toMatch(/^[\w.-]+@[\w.-]+\.\w+$/); } else { expect(Number.isInteger(result as number)).toBe(true); } }); it('handles union with empty array returns null', () => { const zodSchema = 'z.union([])'; const result = generateMockFromZodSchema(zodSchema); expect(result).toBe(null); }); }); describe('nullable types', () => { it('handles nullable string', () => { const zodSchema = 'z.string().nullable()'; const result = generateMockFromZodSchema(zodSchema); expect(['string', 'object']).toContain(typeof result); }); it('handles nullable number', () => { const zodSchema = 'z.number().nullable()'; const result = generateMockFromZodSchema(zodSchema); expect(['number', 'object']).toContain(typeof result); }); }); }); });