From 87f7d76915522dc5204b34c9ef307c0011850c59 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Mon, 16 Feb 2026 12:08:40 -0800 Subject: [PATCH] =?UTF-8?q?test(platform-service):=20add=20products,=20cac?= =?UTF-8?q?he,=20votes=20repository=20tests=20=E2=80=94=20362=20tests,=203?= =?UTF-8?q?2.6%=20stmts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/modules/products/cache.test.ts | 117 ++++++++++++++++++ .../src/modules/products/repository.test.ts | 108 ++++++++++++++++ .../src/modules/votes/repository.test.ts | 100 +++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 services/platform-service/src/modules/products/cache.test.ts create mode 100644 services/platform-service/src/modules/products/repository.test.ts create mode 100644 services/platform-service/src/modules/votes/repository.test.ts diff --git a/services/platform-service/src/modules/products/cache.test.ts b/services/platform-service/src/modules/products/cache.test.ts new file mode 100644 index 00000000..d16b7264 --- /dev/null +++ b/services/platform-service/src/modules/products/cache.test.ts @@ -0,0 +1,117 @@ +/** + * Tests for products in-memory cache. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('./repository.js', () => ({ + getAll: vi.fn(), +})); + +import { getAll } from './repository.js'; +import { loadProductCache, getProduct, isValidProduct, getAllProducts, cacheSize } from './cache.js'; + +const mockGetAll = vi.mocked(getAll); + +const products = [ + { + id: 'lysnrai', + productId: 'lysnrai', + displayName: 'LysnrAI', + licensePrefix: 'LYSNR', + packageName: 'com.bytelyst.lysnrai', + defaultPlan: 'free' as const, + trialDays: 14, + deviceLimits: { free: 1, pro: 3, enterprise: 10 }, + websiteUrl: 'https://lysnr.ai', + status: 'active' as const, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', + }, + { + id: 'mindlyst', + productId: 'mindlyst', + displayName: 'MindLyst', + licensePrefix: 'MIND', + packageName: 'com.bytelyst.mindlyst', + defaultPlan: 'free' as const, + trialDays: 7, + deviceLimits: { free: 1, pro: 3, enterprise: 10 }, + websiteUrl: 'https://mindlyst.com', + status: 'active' as const, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', + }, +]; + +describe('product cache', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('loadProductCache', () => { + it('loads products into cache', async () => { + mockGetAll.mockResolvedValue(products); + await loadProductCache(); + expect(cacheSize()).toBe(2); + }); + + it('clears old cache on reload', async () => { + mockGetAll.mockResolvedValue(products); + await loadProductCache(); + expect(cacheSize()).toBe(2); + + mockGetAll.mockResolvedValue([products[0]]); + await loadProductCache(); + expect(cacheSize()).toBe(1); + }); + }); + + describe('getProduct', () => { + it('returns product from cache', async () => { + mockGetAll.mockResolvedValue(products); + await loadProductCache(); + const p = getProduct('lysnrai'); + expect(p?.displayName).toBe('LysnrAI'); + }); + + it('returns undefined for unknown product', async () => { + mockGetAll.mockResolvedValue(products); + await loadProductCache(); + expect(getProduct('unknown')).toBeUndefined(); + }); + }); + + describe('isValidProduct', () => { + it('returns true for cached product', async () => { + mockGetAll.mockResolvedValue(products); + await loadProductCache(); + expect(isValidProduct('lysnrai')).toBe(true); + }); + + it('returns false for unknown product', async () => { + mockGetAll.mockResolvedValue(products); + await loadProductCache(); + expect(isValidProduct('unknown')).toBe(false); + }); + }); + + describe('getAllProducts', () => { + it('returns all cached products', async () => { + mockGetAll.mockResolvedValue(products); + await loadProductCache(); + const all = getAllProducts(); + expect(all).toHaveLength(2); + expect(all.map(p => p.id)).toContain('lysnrai'); + expect(all.map(p => p.id)).toContain('mindlyst'); + }); + }); + + describe('cacheSize', () => { + it('returns 0 before loading', async () => { + mockGetAll.mockResolvedValue([]); + await loadProductCache(); + expect(cacheSize()).toBe(0); + }); + }); +}); diff --git a/services/platform-service/src/modules/products/repository.test.ts b/services/platform-service/src/modules/products/repository.test.ts new file mode 100644 index 00000000..1c632e67 --- /dev/null +++ b/services/platform-service/src/modules/products/repository.test.ts @@ -0,0 +1,108 @@ +/** + * Repository tests for products module — mocked Cosmos DB. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const mockFetchAll = vi.fn(); +const mockCreate = vi.fn(); +const mockRead = vi.fn(); +const mockReplace = vi.fn(); + +vi.mock('../../lib/cosmos.js', () => ({ + getContainer: vi.fn(() => ({ + items: { + query: () => ({ fetchAll: mockFetchAll }), + create: mockCreate, + }, + item: () => ({ read: mockRead, replace: mockReplace }), + })), +})); + +import { getAll, getById, create, update } from './repository.js'; +import type { ProductDoc } from './types.js'; + +const baseProduct: ProductDoc = { + id: 'lysnrai', + productId: 'lysnrai', + displayName: 'LysnrAI', + licensePrefix: 'LYSNR', + packageName: 'com.bytelyst.lysnrai', + defaultPlan: 'free', + trialDays: 14, + deviceLimits: { free: 1, pro: 3, enterprise: 10 }, + websiteUrl: 'https://lysnr.ai', + status: 'active', + createdAt: '2026-02-16T00:00:00Z', + updatedAt: '2026-02-16T00:00:00Z', +}; + +describe('products repository', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('getAll', () => { + it('returns all products', async () => { + mockFetchAll.mockResolvedValue({ resources: [baseProduct] }); + const result = await getAll(); + expect(result).toEqual([baseProduct]); + }); + + it('returns empty array when no products', async () => { + mockFetchAll.mockResolvedValue({ resources: [] }); + const result = await getAll(); + expect(result).toEqual([]); + }); + }); + + describe('getById', () => { + it('returns product when found', async () => { + mockRead.mockResolvedValue({ resource: baseProduct }); + const result = await getById('lysnrai'); + expect(result).toEqual(baseProduct); + }); + + it('returns null when not found', async () => { + mockRead.mockRejectedValue(new Error('Not found')); + const result = await getById('nonexistent'); + expect(result).toBeNull(); + }); + + it('returns null when resource is undefined', async () => { + mockRead.mockResolvedValue({ resource: undefined }); + const result = await getById('lysnrai'); + expect(result).toBeNull(); + }); + }); + + describe('create', () => { + it('creates and returns product', async () => { + mockCreate.mockResolvedValue({ resource: baseProduct }); + const result = await create(baseProduct); + expect(result).toEqual(baseProduct); + }); + }); + + describe('update', () => { + it('merges updates and returns product', async () => { + mockRead.mockResolvedValue({ resource: baseProduct }); + const updated = { ...baseProduct, displayName: 'LysnrAI Pro' }; + mockReplace.mockResolvedValue({ resource: updated }); + const result = await update('lysnrai', { displayName: 'LysnrAI Pro' }); + expect(result).toEqual(updated); + }); + + it('returns null when not found', async () => { + mockRead.mockResolvedValue({ resource: undefined }); + const result = await update('nonexistent', { displayName: 'Test' }); + expect(result).toBeNull(); + }); + + it('returns null on error', async () => { + mockRead.mockRejectedValue(new Error('Not found')); + const result = await update('nonexistent', { displayName: 'Test' }); + expect(result).toBeNull(); + }); + }); +}); diff --git a/services/platform-service/src/modules/votes/repository.test.ts b/services/platform-service/src/modules/votes/repository.test.ts new file mode 100644 index 00000000..fb351d73 --- /dev/null +++ b/services/platform-service/src/modules/votes/repository.test.ts @@ -0,0 +1,100 @@ +/** + * Repository tests for votes module — mocked Cosmos DB. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const mockFetchAll = vi.fn(); +const mockCreate = vi.fn(); +const mockDelete = vi.fn(); + +vi.mock('../../lib/cosmos.js', () => ({ + getContainer: vi.fn(() => ({ + items: { + query: () => ({ fetchAll: mockFetchAll }), + create: mockCreate, + }, + item: () => ({ delete: mockDelete }), + })), +})); + +import { getByItemAndUser, countByItem, listByItem, create, remove } from './repository.js'; +import type { VoteDoc } from './types.js'; + +const baseVote: VoteDoc = { + id: 'vote_1', + productId: 'lysnrai', + itemId: 'item_1', + userId: 'user_1', + createdAt: '2026-02-16T00:00:00Z', +}; + +describe('votes repository', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('getByItemAndUser', () => { + it('returns vote when found', async () => { + mockFetchAll.mockResolvedValue({ resources: [baseVote] }); + const result = await getByItemAndUser('item_1', 'user_1'); + expect(result).toEqual(baseVote); + }); + + it('returns null when not found', async () => { + mockFetchAll.mockResolvedValue({ resources: [] }); + const result = await getByItemAndUser('item_1', 'user_1'); + expect(result).toBeNull(); + }); + }); + + describe('countByItem', () => { + it('returns count', async () => { + mockFetchAll.mockResolvedValue({ resources: [5] }); + const result = await countByItem('item_1'); + expect(result).toBe(5); + }); + + it('returns 0 when no votes', async () => { + mockFetchAll.mockResolvedValue({ resources: [] }); + const result = await countByItem('item_1'); + expect(result).toBe(0); + }); + }); + + describe('listByItem', () => { + it('returns votes for item', async () => { + mockFetchAll.mockResolvedValue({ resources: [baseVote] }); + const result = await listByItem('item_1'); + expect(result).toEqual([baseVote]); + }); + + it('returns empty array when no votes', async () => { + mockFetchAll.mockResolvedValue({ resources: [] }); + const result = await listByItem('item_1'); + expect(result).toEqual([]); + }); + }); + + describe('create', () => { + it('creates and returns vote', async () => { + mockCreate.mockResolvedValue({ resource: baseVote }); + const result = await create(baseVote); + expect(result).toEqual(baseVote); + }); + }); + + describe('remove', () => { + it('deletes and returns true', async () => { + mockDelete.mockResolvedValue(undefined); + const result = await remove('vote_1'); + expect(result).toBe(true); + }); + + it('returns false on error', async () => { + mockDelete.mockRejectedValue(new Error('Not found')); + const result = await remove('vote_1'); + expect(result).toBe(false); + }); + }); +});