diff --git a/apps/manacore/apps/landing/src/content/audits/2026-03-19-manacore.md b/apps/manacore/apps/landing/src/content/audits/2026-03-19-manacore.md index c35e9ab67..75ee21af1 100644 --- a/apps/manacore/apps/landing/src/content/audits/2026-03-19-manacore.md +++ b/apps/manacore/apps/landing/src/content/audits/2026-03-19-manacore.md @@ -1,29 +1,29 @@ --- title: 'ManaCore: Production Readiness Audit' -description: 'Multi-App Ecosystem Dashboard mit 25 Web-Routes, 11 Dashboard-Widgets, 85 Tests, Error Boundary, Onboarding, 5 Sprachen, Mobile App' +description: 'Multi-App Ecosystem Dashboard mit 25 Web-Routes, 11 Dashboard-Widgets, 103 Tests, Error Boundary, Onboarding, 5 Sprachen, Mobile App' date: 2026-03-19 app: 'manacore' author: 'Till Schneider' tags: ['audit', 'manacore', 'production-readiness', 'platform'] -score: 86 +score: 88 scores: backend: 55 frontend: 90 database: 70 - testing: 65 + testing: 72 deployment: 90 documentation: 88 security: 80 ux: 92 -status: 'beta' +status: 'production' version: '0.3.0' stats: backendModules: 0 webRoutes: 25 components: 36 dbTables: 0 - testFiles: 9 - testCount: 85 + testFiles: 12 + testCount: 103 languages: 5 --- @@ -82,14 +82,14 @@ ManaCore ist das **Herzstück des Monorepos** - das Multi-App Ecosystem Dashboar - Kein lokaler Cache/Offline-Speicher - Widget-Daten nicht persistiert -## Testing (65/100) +## Testing (72/100) **Stärken:** - Vitest konfiguriert mit Coverage (package.json) - Playwright für E2E Tests eingerichtet - @vitest/coverage-v8 und @vitest/ui installiert -- 9 Test-Dateien mit 85 Unit Tests: +- 12 Test-Dateien mit 103 Unit Tests: - Dashboard Widget Registry Tests (14 Tests) - Default Dashboard Config Tests (12 Tests) - Base API Client mit Retry-Logik (15 Tests) @@ -99,6 +99,9 @@ ManaCore ist das **Herzstück des Monorepos** - das Multi-App Ecosystem Dashboar - Contacts Widget Service Tests (10 Tests) - Storage Widget Service Tests (10 Tests) - Todo Widget Service Tests (7 Tests) + - Calendar Widget Service Tests (6 Tests) + - Chat Widget Service Tests (6 Tests) + - Zitare Widget Service Tests (6 Tests) **Lücken:** diff --git a/apps/manacore/apps/web/src/lib/api/services/calendar.test.ts b/apps/manacore/apps/web/src/lib/api/services/calendar.test.ts new file mode 100644 index 000000000..e4cb8fc5d --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/calendar.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock $app/environment +vi.mock('$app/environment', () => ({ + browser: true, +})); + +// Mock auth store +vi.mock('$lib/stores/auth.svelte', () => ({ + authStore: { + getAccessToken: vi.fn().mockResolvedValue('test-token'), + }, +})); + +import { calendarService, type CalendarEvent, type Calendar } from './calendar'; + +describe('calendarService', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('getUpcomingEvents', () => { + it('should fetch upcoming events with date range params', async () => { + const events: CalendarEvent[] = [ + { + id: 'e-1', + calendarId: 'cal-1', + userId: 'u-1', + title: 'Meeting', + startTime: '2026-03-20T10:00:00Z', + endTime: '2026-03-20T11:00:00Z', + isAllDay: false, + timezone: 'Europe/Berlin', + status: 'confirmed', + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + }, + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ events }), + }); + + const result = await calendarService.getUpcomingEvents(7); + + expect(result.data).toEqual(events); + expect(result.error).toBeNull(); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringMatching(/\/events\?startDate=\d{4}-\d{2}-\d{2}&endDate=\d{4}-\d{2}-\d{2}/), + expect.any(Object) + ); + }); + + it('should return empty array when no events', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ events: [] }), + }); + + const result = await calendarService.getUpcomingEvents(); + + expect(result.data).toEqual([]); + }); + + it('should return error on network failure', async () => { + global.fetch = vi.fn().mockRejectedValue(new Error('Failed to fetch')); + + const result = await calendarService.getUpcomingEvents(); + + expect(result.data).toBeNull(); + expect(result.error).toBeTruthy(); + }); + }); + + describe('getTodayEvents', () => { + it('should fetch events for today', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ events: [{ id: 'e-1', title: 'Today Event' }] }), + }); + + const result = await calendarService.getTodayEvents(); + + expect(result.data).toHaveLength(1); + // Both startDate and endDate should be the same (today) + const url = (global.fetch as any).mock.calls[0][0]; + const params = new URL(url).searchParams; + expect(params.get('startDate')).toBe(params.get('endDate')); + }); + }); + + describe('getCalendars', () => { + it('should fetch all calendars', async () => { + const calendars: Calendar[] = [ + { + id: 'cal-1', + userId: 'u-1', + name: 'Work', + color: '#3B82F6', + isDefault: true, + isVisible: true, + timezone: 'Europe/Berlin', + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + }, + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ calendars }), + }); + + const result = await calendarService.getCalendars(); + + expect(result.data).toEqual(calendars); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/calendars'), + expect.any(Object) + ); + }); + }); + + describe('getCalendarEvents', () => { + it('should fetch events for specific calendar', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ events: [] }), + }); + + await calendarService.getCalendarEvents('cal-1', 14); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('calendarIds=cal-1'), + expect.any(Object) + ); + }); + }); +}); diff --git a/apps/manacore/apps/web/src/lib/api/services/chat.test.ts b/apps/manacore/apps/web/src/lib/api/services/chat.test.ts new file mode 100644 index 000000000..2a3f25ce8 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/chat.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock $app/environment +vi.mock('$app/environment', () => ({ + browser: true, +})); + +// Mock auth store +vi.mock('$lib/stores/auth.svelte', () => ({ + authStore: { + getAccessToken: vi.fn().mockResolvedValue('test-token'), + }, +})); + +import { chatService, type Conversation } from './chat'; + +const mockConversation = (overrides: Partial = {}): Conversation => ({ + id: 'conv-1', + userId: 'u-1', + title: 'Test Chat', + modelId: 'gpt-4', + conversationMode: 'free', + documentMode: false, + isArchived: false, + isPinned: false, + createdAt: '2026-01-01', + updatedAt: '2026-03-01', + ...overrides, +}); + +describe('chatService', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('getRecentConversations', () => { + it('should fetch and sort conversations by updatedAt', async () => { + const conversations = [ + mockConversation({ id: 'c-1', updatedAt: '2026-01-01' }), + mockConversation({ id: 'c-2', updatedAt: '2026-03-01' }), + mockConversation({ id: 'c-3', updatedAt: '2026-02-01' }), + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(conversations), + }); + + const result = await chatService.getRecentConversations(5); + + expect(result.data).toHaveLength(3); + expect(result.data![0].id).toBe('c-2'); // Most recent first + expect(result.data![1].id).toBe('c-3'); + expect(result.data![2].id).toBe('c-1'); + }); + + it('should filter out archived conversations', async () => { + const conversations = [ + mockConversation({ id: 'c-1', isArchived: false }), + mockConversation({ id: 'c-2', isArchived: true }), + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(conversations), + }); + + const result = await chatService.getRecentConversations(); + + expect(result.data).toHaveLength(1); + expect(result.data![0].id).toBe('c-1'); + }); + + it('should respect limit parameter', async () => { + const conversations = Array.from({ length: 10 }, (_, i) => + mockConversation({ id: `c-${i}`, updatedAt: `2026-03-${String(i + 1).padStart(2, '0')}` }) + ); + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(conversations), + }); + + const result = await chatService.getRecentConversations(3); + + expect(result.data).toHaveLength(3); + }); + }); + + describe('getPinnedConversations', () => { + it('should return only pinned non-archived conversations', async () => { + const conversations = [ + mockConversation({ id: 'c-1', isPinned: true, isArchived: false }), + mockConversation({ id: 'c-2', isPinned: false }), + mockConversation({ id: 'c-3', isPinned: true, isArchived: true }), + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(conversations), + }); + + const result = await chatService.getPinnedConversations(); + + expect(result.data).toHaveLength(1); + expect(result.data![0].id).toBe('c-1'); + }); + }); + + describe('getConversationCount', () => { + it('should count active and pinned conversations', async () => { + const conversations = [ + mockConversation({ isPinned: true, isArchived: false }), + mockConversation({ isPinned: false, isArchived: false }), + mockConversation({ isPinned: true, isArchived: true }), + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(conversations), + }); + + const result = await chatService.getConversationCount(); + + expect(result.data).toEqual({ total: 2, pinned: 1 }); + }); + }); + + describe('getModels', () => { + it('should fetch AI models', async () => { + const models = [{ id: 'm-1', name: 'GPT-4', description: 'Advanced model' }]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(models), + }); + + const result = await chatService.getModels(); + + expect(result.data).toEqual(models); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/chat/models'), + expect.any(Object) + ); + }); + }); +}); diff --git a/apps/manacore/apps/web/src/lib/api/services/zitare.test.ts b/apps/manacore/apps/web/src/lib/api/services/zitare.test.ts new file mode 100644 index 000000000..2b4217491 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/zitare.test.ts @@ -0,0 +1,132 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock $app/environment +vi.mock('$app/environment', () => ({ + browser: true, +})); + +// Mock auth store +vi.mock('$lib/stores/auth.svelte', () => ({ + authStore: { + getAccessToken: vi.fn().mockResolvedValue('test-token'), + }, +})); + +import { zitareService, type Favorite } from './zitare'; + +describe('zitareService', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('getFavorites', () => { + it('should fetch favorites', async () => { + const favorites: Favorite[] = [ + { id: 'f-1', userId: 'u-1', quoteId: 'q-1', createdAt: '2026-01-01' }, + { id: 'f-2', userId: 'u-1', quoteId: 'q-2', createdAt: '2026-02-01' }, + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(favorites), + }); + + const result = await zitareService.getFavorites(); + + expect(result.data).toEqual(favorites); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/favorites'), + expect.any(Object) + ); + }); + }); + + describe('getRandomFavorite', () => { + it('should return a random favorite from the list', async () => { + const favorites: Favorite[] = [ + { id: 'f-1', userId: 'u-1', quoteId: 'q-1', createdAt: '2026-01-01' }, + { id: 'f-2', userId: 'u-1', quoteId: 'q-2', createdAt: '2026-02-01' }, + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(favorites), + }); + + const result = await zitareService.getRandomFavorite(); + + expect(result.data).toBeTruthy(); + expect(result.error).toBeNull(); + expect(favorites.map((f) => f.id)).toContain(result.data!.id); + }); + + it('should return error when no favorites exist', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve([]), + }); + + const result = await zitareService.getRandomFavorite(); + + expect(result.data).toBeNull(); + expect(result.error).toBe('No favorites found'); + }); + }); + + describe('getFavoriteCount', () => { + it('should return the count of favorites', async () => { + const favorites = [ + { id: 'f-1', userId: 'u-1', quoteId: 'q-1', createdAt: '2026-01-01' }, + { id: 'f-2', userId: 'u-1', quoteId: 'q-2', createdAt: '2026-02-01' }, + { id: 'f-3', userId: 'u-1', quoteId: 'q-3', createdAt: '2026-03-01' }, + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(favorites), + }); + + const result = await zitareService.getFavoriteCount(); + + expect(result.data).toBe(3); + expect(result.error).toBeNull(); + }); + + it('should return error on network failure', async () => { + global.fetch = vi.fn().mockRejectedValue(new Error('Failed to fetch')); + + const result = await zitareService.getFavoriteCount(); + + expect(result.data).toBeNull(); + expect(result.error).toBeTruthy(); + }); + }); + + describe('getLists', () => { + it('should fetch quote lists', async () => { + const lists = [ + { + id: 'l-1', + userId: 'u-1', + name: 'Motivation', + quoteIds: ['q-1', 'q-2'], + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + }, + ]; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(lists), + }); + + const result = await zitareService.getLists(); + + expect(result.data).toEqual(lists); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/lists'), + expect.any(Object) + ); + }); + }); +});