test(platform-service): add products, cache, votes repository tests — 362 tests, 32.6% stmts

This commit is contained in:
saravanakumardb1 2026-02-16 12:08:40 -08:00
parent 747d89fe4d
commit 87f7d76915
3 changed files with 325 additions and 0 deletions

View File

@ -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);
});
});
});

View File

@ -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();
});
});
});

View File

@ -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);
});
});
});