From 0fee7e9ee75b3eb56da4f0b4033b15f428703c96 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 15 Feb 2026 14:36:33 -0800 Subject: [PATCH] test(platform-service): add products module tests and fix product schema defaults - Added products.test.ts covering CreateProductSchema and UpdateProductSchema - Added route export smoke test for productRoutes - Fixed CreateProductSchema packageName default validation (allow empty default) - Verified: tsc --noEmit clean, 19 test files / 178 tests passing --- .../src/modules/products/products.test.ts | 125 ++++++++++++++++++ .../src/modules/products/types.ts | 2 +- 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 services/platform-service/src/modules/products/products.test.ts diff --git a/services/platform-service/src/modules/products/products.test.ts b/services/platform-service/src/modules/products/products.test.ts new file mode 100644 index 00000000..15e5c9e0 --- /dev/null +++ b/services/platform-service/src/modules/products/products.test.ts @@ -0,0 +1,125 @@ +/** + * Unit tests for products module — types, schemas, cache. + */ + +import { describe, it, expect } from 'vitest'; +import { CreateProductSchema, UpdateProductSchema } from './types.js'; + +describe('CreateProductSchema', () => { + it('accepts valid product input', () => { + const result = CreateProductSchema.safeParse({ + productId: 'lysnrai', + displayName: 'LysnrAI', + licensePrefix: 'LYSNR', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.productId).toBe('lysnrai'); + expect(result.data.defaultPlan).toBe('free'); + expect(result.data.trialDays).toBe(14); + expect(result.data.deviceLimits).toEqual({ free: 1, pro: 3, enterprise: 10 }); + expect(result.data.status).toBe('active'); + } + }); + + it('rejects productId with uppercase', () => { + const result = CreateProductSchema.safeParse({ + productId: 'LysnrAI', + displayName: 'LysnrAI', + licensePrefix: 'LYSNR', + }); + expect(result.success).toBe(false); + }); + + it('rejects productId with spaces', () => { + const result = CreateProductSchema.safeParse({ + productId: 'lysnr ai', + displayName: 'LysnrAI', + licensePrefix: 'LYSNR', + }); + expect(result.success).toBe(false); + }); + + it('rejects licensePrefix with lowercase', () => { + const result = CreateProductSchema.safeParse({ + productId: 'lysnrai', + displayName: 'LysnrAI', + licensePrefix: 'lysnr', + }); + expect(result.success).toBe(false); + }); + + it('accepts full product input with all fields', () => { + const result = CreateProductSchema.safeParse({ + productId: 'mindlyst', + displayName: 'MindLyst', + licensePrefix: 'MIND', + packageName: 'com.mindlyst.app', + defaultPlan: 'pro', + trialDays: 30, + deviceLimits: { free: 2, pro: 5, enterprise: 20 }, + websiteUrl: 'https://mindlyst.com', + status: 'active', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.trialDays).toBe(30); + expect(result.data.deviceLimits.pro).toBe(5); + } + }); + + it('rejects empty productId', () => { + const result = CreateProductSchema.safeParse({ + productId: '', + displayName: 'Test', + licensePrefix: 'TEST', + }); + expect(result.success).toBe(false); + }); +}); + +describe('UpdateProductSchema', () => { + it('accepts partial update', () => { + const result = UpdateProductSchema.safeParse({ + displayName: 'Updated Name', + }); + expect(result.success).toBe(true); + }); + + it('accepts empty object', () => { + const result = UpdateProductSchema.safeParse({}); + expect(result.success).toBe(true); + }); + + it('accepts partial deviceLimits', () => { + const result = UpdateProductSchema.safeParse({ + deviceLimits: { pro: 10 }, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.deviceLimits?.pro).toBe(10); + expect(result.data.deviceLimits?.free).toBeUndefined(); + } + }); + + it('accepts status change', () => { + const result = UpdateProductSchema.safeParse({ + status: 'disabled', + }); + expect(result.success).toBe(true); + }); + + it('rejects invalid status', () => { + const result = UpdateProductSchema.safeParse({ + status: 'deleted', + }); + expect(result.success).toBe(false); + }); +}); + +describe('productRoutes export', () => { + it('exports productRoutes function', async () => { + const mod = await import('./routes.js'); + expect(typeof mod.productRoutes).toBe('function'); + }); +}); diff --git a/services/platform-service/src/modules/products/types.ts b/services/platform-service/src/modules/products/types.ts index bf7afd5a..74031ba6 100644 --- a/services/platform-service/src/modules/products/types.ts +++ b/services/platform-service/src/modules/products/types.ts @@ -42,7 +42,7 @@ export const CreateProductSchema = z.object({ .min(1) .max(16) .regex(/^[A-Z]+$/, 'licensePrefix must be uppercase letters'), - packageName: z.string().min(1).max(256).default(''), + packageName: z.string().max(256).default(''), defaultPlan: z.enum(['free', 'pro']).default('free'), trialDays: z.number().int().min(0).max(365).default(14), deviceLimits: DeviceLimitsSchema.default({ free: 1, pro: 3, enterprise: 10 }),