test(platform-service): add products, cache, votes repository tests — 362 tests, 32.6% stmts
This commit is contained in:
parent
747d89fe4d
commit
87f7d76915
117
services/platform-service/src/modules/products/cache.test.ts
Normal file
117
services/platform-service/src/modules/products/cache.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
100
services/platform-service/src/modules/votes/repository.test.ts
Normal file
100
services/platform-service/src/modules/votes/repository.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user