feat(mvp): phase 8 - integration, testing & deployment

Fix all build/type/lint errors (zod 3.25 compat wrapper, Svelte 5 fixes),
write 115 unit tests across 10 test files, expand seed script with demo
data, update Docker config with migration on startup.
This commit is contained in:
2026-03-24 22:09:17 +03:00
parent 0bd30c5e17
commit e6b50fb4f1
36 changed files with 1634 additions and 99 deletions
@@ -0,0 +1,165 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('../../prisma.js', () => ({
prisma: {
app: {
findMany: vi.fn(),
findUnique: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn()
},
appStatus: {
create: vi.fn(),
findFirst: vi.fn(),
findMany: vi.fn()
}
}
}));
import { prisma } from '../../prisma.js';
import * as appService from '../appService.js';
const mockApp = prisma.app as unknown as {
findMany: ReturnType<typeof vi.fn>;
findUnique: ReturnType<typeof vi.fn>;
create: ReturnType<typeof vi.fn>;
update: ReturnType<typeof vi.fn>;
delete: ReturnType<typeof vi.fn>;
};
const mockAppStatus = prisma.appStatus as unknown as {
create: ReturnType<typeof vi.fn>;
};
describe('appService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('findAll', () => {
it('returns all apps', async () => {
const apps = [
{ id: '1', name: 'App1', statuses: [] },
{ id: '2', name: 'App2', statuses: [] }
];
mockApp.findMany.mockResolvedValue(apps);
const result = await appService.findAll();
expect(result).toEqual(apps);
expect(mockApp.findMany).toHaveBeenCalledWith({
where: {},
orderBy: { name: 'asc' },
include: { statuses: { orderBy: { checkedAt: 'desc' }, take: 1 } }
});
});
it('filters by category', async () => {
mockApp.findMany.mockResolvedValue([]);
await appService.findAll({ category: 'media' });
expect(mockApp.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { category: 'media' }
})
);
});
it('filters by search term', async () => {
mockApp.findMany.mockResolvedValue([]);
await appService.findAll({ search: 'grafana' });
const call = mockApp.findMany.mock.calls[0][0];
expect(call.where.OR).toBeDefined();
expect(call.where.OR).toHaveLength(3);
});
});
describe('findById', () => {
it('returns app when found', async () => {
const app = { id: '1', name: 'App', statuses: [], createdBy: null };
mockApp.findUnique.mockResolvedValue(app);
const result = await appService.findById('1');
expect(result).toEqual(app);
});
it('throws when not found', async () => {
mockApp.findUnique.mockResolvedValue(null);
await expect(appService.findById('missing')).rejects.toThrow('App not found');
});
});
describe('create', () => {
it('creates app with required fields', async () => {
const input = { name: 'New App', url: 'https://app.local' };
const created = { id: '1', ...input };
mockApp.create.mockResolvedValue(created);
const result = await appService.create(input);
expect(result.id).toBe('1');
expect(mockApp.create).toHaveBeenCalledWith({
data: expect.objectContaining({
name: 'New App',
url: 'https://app.local',
healthcheckEnabled: false,
healthcheckInterval: 300
})
});
});
});
describe('update', () => {
it('updates specified fields', async () => {
mockApp.findUnique.mockResolvedValue({ id: '1' });
mockApp.update.mockResolvedValue({ id: '1', name: 'Updated' });
const result = await appService.update('1', { name: 'Updated' });
expect(mockApp.update).toHaveBeenCalledWith({
where: { id: '1' },
data: { name: 'Updated' }
});
expect(result.name).toBe('Updated');
});
});
describe('remove', () => {
it('deletes app', async () => {
mockApp.findUnique.mockResolvedValue({ id: '1' });
mockApp.delete.mockResolvedValue({});
await appService.remove('1');
expect(mockApp.delete).toHaveBeenCalledWith({ where: { id: '1' } });
});
});
describe('recordStatus', () => {
it('creates a status record', async () => {
const status = { id: 's1', appId: '1', status: 'online', responseTime: 150 };
mockAppStatus.create.mockResolvedValue(status);
const result = await appService.recordStatus('1', 'online', 150);
expect(result).toEqual(status);
});
});
describe('getCategories', () => {
it('returns unique categories', async () => {
mockApp.findMany.mockResolvedValue([
{ category: 'Media' },
{ category: 'Monitoring' }
]);
const result = await appService.getCategories();
expect(result).toEqual(['Media', 'Monitoring']);
});
});
});