test(manacore): add widget service tests for contacts, storage, todo (score 84→86)

- Contacts service: 10 tests (getDisplayName variants, favorites, recent sort)
- Storage service: 10 tests (formatSize units, getStats, getRecentFiles)
- Todo service: 7 tests (today, upcoming, inbox, projects)
- Total: 9 test files, 85 tests passing
- Updated audit: testing 55→65, score 84→86

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-19 21:36:29 +01:00
parent a5364392d7
commit 64c9d49254
4 changed files with 424 additions and 8 deletions

View file

@ -1,16 +1,16 @@
---
title: 'ManaCore: Production Readiness Audit'
description: 'Multi-App Ecosystem Dashboard mit 25 Web-Routes, 11 Dashboard-Widgets, 58 Tests, Error Boundary, Onboarding, 5 Sprachen, Mobile App'
description: 'Multi-App Ecosystem Dashboard mit 25 Web-Routes, 11 Dashboard-Widgets, 85 Tests, Error Boundary, Onboarding, 5 Sprachen, Mobile App'
date: 2026-03-19
app: 'manacore'
author: 'Till Schneider'
tags: ['audit', 'manacore', 'production-readiness', 'platform']
score: 84
score: 86
scores:
backend: 55
frontend: 90
database: 70
testing: 55
testing: 65
deployment: 90
documentation: 88
security: 80
@ -22,8 +22,8 @@ stats:
webRoutes: 25
components: 36
dbTables: 0
testFiles: 6
testCount: 58
testFiles: 9
testCount: 85
languages: 5
---
@ -82,26 +82,28 @@ ManaCore ist das **Herzstück des Monorepos** - das Multi-App Ecosystem Dashboar
- Kein lokaler Cache/Offline-Speicher
- Widget-Daten nicht persistiert
## Testing (55/100)
## Testing (65/100)
**Stärken:**
- Vitest konfiguriert mit Coverage (package.json)
- Playwright für E2E Tests eingerichtet
- @vitest/coverage-v8 und @vitest/ui installiert
- 6 Test-Dateien mit 58 Unit Tests:
- 9 Test-Dateien mit 85 Unit Tests:
- Dashboard Widget Registry Tests (14 Tests)
- Default Dashboard Config Tests (12 Tests)
- Base API Client mit Retry-Logik (15 Tests)
- Credits Service API Tests (7 Tests)
- API Keys Service Tests (4 Tests)
- Profile Service Tests (6 Tests)
- Contacts Widget Service Tests (10 Tests)
- Storage Widget Service Tests (10 Tests)
- Todo Widget Service Tests (7 Tests)
**Lücken:**
- Keine E2E Tests (Playwright Setup vorhanden)
- Keine Store-Tests (Auth, Dashboard State)
- Keine Komponenten-Tests
## Deployment (90/100)

View file

@ -0,0 +1,148 @@
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 { contactsService, type Contact } from './contacts';
const mockContact = (overrides: Partial<Contact> = {}): Contact => ({
id: 'c-1',
userId: 'u-1',
firstName: 'Max',
lastName: 'Mustermann',
email: 'max@example.com',
isFavorite: false,
isArchived: false,
createdAt: '2026-01-01',
updatedAt: '2026-03-01',
...overrides,
});
describe('contactsService', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('getDisplayName', () => {
it('should return displayName when set', () => {
const contact = mockContact({ displayName: 'Dr. Max' });
expect(contactsService.getDisplayName(contact)).toBe('Dr. Max');
});
it('should return full name from firstName + lastName', () => {
const contact = mockContact({ displayName: undefined });
expect(contactsService.getDisplayName(contact)).toBe('Max Mustermann');
});
it('should return firstName only when lastName is missing', () => {
const contact = mockContact({ displayName: undefined, lastName: undefined });
expect(contactsService.getDisplayName(contact)).toBe('Max');
});
it('should return lastName only when firstName is missing', () => {
const contact = mockContact({
displayName: undefined,
firstName: undefined,
lastName: 'Mustermann',
});
expect(contactsService.getDisplayName(contact)).toBe('Mustermann');
});
it('should return email when no name is available', () => {
const contact = mockContact({
displayName: undefined,
firstName: undefined,
lastName: undefined,
email: 'max@example.com',
});
expect(contactsService.getDisplayName(contact)).toBe('max@example.com');
});
it('should return Unknown when nothing is available', () => {
const contact = mockContact({
displayName: undefined,
firstName: undefined,
lastName: undefined,
email: undefined,
});
expect(contactsService.getDisplayName(contact)).toBe('Unknown');
});
});
describe('getFavoriteContacts', () => {
it('should fetch favorite contacts', async () => {
const favorites = [mockContact({ isFavorite: true })];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ contacts: favorites, total: 1 }),
});
const result = await contactsService.getFavoriteContacts();
expect(result.data).toEqual(favorites);
expect(result.error).toBeNull();
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/contacts?isFavorite=true&limit=5'),
expect.any(Object)
);
});
it('should respect custom limit', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ contacts: [], total: 0 }),
});
await contactsService.getFavoriteContacts(10);
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('limit=10'),
expect.any(Object)
);
});
it('should return empty array when no contacts', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ contacts: [], total: 0 }),
});
const result = await contactsService.getFavoriteContacts();
expect(result.data).toEqual([]);
});
});
describe('getRecentContacts', () => {
it('should sort by updatedAt and filter archived', async () => {
const contacts = [
mockContact({ id: 'c-1', updatedAt: '2026-01-01', isArchived: false }),
mockContact({ id: 'c-2', updatedAt: '2026-03-01', isArchived: false }),
mockContact({ id: 'c-3', updatedAt: '2026-02-01', isArchived: true }),
];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ contacts, total: 3 }),
});
const result = await contactsService.getRecentContacts(5);
expect(result.data).toHaveLength(2);
expect(result.data![0].id).toBe('c-2'); // Most recent first
expect(result.data![1].id).toBe('c-1');
});
});
});

View file

@ -0,0 +1,125 @@
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 { storageService, type StorageStats } from './storage';
describe('storageService', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('formatSize', () => {
it('should format 0 bytes', () => {
expect(storageService.formatSize(0)).toBe('0 B');
});
it('should format bytes', () => {
expect(storageService.formatSize(500)).toBe('500 B');
});
it('should format kilobytes', () => {
expect(storageService.formatSize(1024)).toBe('1 KB');
expect(storageService.formatSize(1536)).toBe('1.5 KB');
});
it('should format megabytes', () => {
expect(storageService.formatSize(1048576)).toBe('1 MB');
expect(storageService.formatSize(5242880)).toBe('5 MB');
});
it('should format gigabytes', () => {
expect(storageService.formatSize(1073741824)).toBe('1 GB');
});
it('should format terabytes', () => {
expect(storageService.formatSize(1099511627776)).toBe('1 TB');
});
it('should format with decimal precision', () => {
expect(storageService.formatSize(1500000)).toBe('1.43 MB');
});
});
describe('getStats', () => {
it('should fetch storage statistics', async () => {
const mockStats: StorageStats = {
totalFiles: 42,
totalSize: 104857600,
favoriteCount: 5,
recentFiles: [],
};
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockStats),
});
const result = await storageService.getStats();
expect(result.data).toEqual(mockStats);
expect(result.error).toBeNull();
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/files/stats'),
expect.any(Object)
);
});
it('should return error on failure', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 401,
});
const result = await storageService.getStats();
expect(result.data).toBeNull();
expect(result.error).toBeTruthy();
});
});
describe('getRecentFiles', () => {
it('should return limited recent files from stats', async () => {
const files = Array.from({ length: 10 }, (_, i) => ({
id: `f-${i}`,
name: `file-${i}.txt`,
originalName: `file-${i}.txt`,
mimeType: 'text/plain',
size: 1024,
storagePath: `/files/f-${i}`,
storageKey: `f-${i}`,
isFavorite: false,
currentVersion: 1,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
}));
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
totalFiles: 10,
totalSize: 10240,
favoriteCount: 0,
recentFiles: files,
}),
});
const result = await storageService.getRecentFiles(3);
expect(result.data).toHaveLength(3);
});
});
});

View file

@ -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 { todoService, type Task } from './todo';
const mockTask = (overrides: Partial<Task> = {}): Task => ({
id: 't-1',
title: 'Test Task',
priority: 'medium',
isCompleted: false,
status: 'pending',
labelIds: [],
createdAt: '2026-01-01',
updatedAt: '2026-03-01',
...overrides,
});
describe('todoService', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('getTodayTasks', () => {
it('should fetch today tasks', async () => {
const tasks = [mockTask(), mockTask({ id: 't-2', title: 'Task 2' })];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ tasks }),
});
const result = await todoService.getTodayTasks();
expect(result.data).toEqual(tasks);
expect(result.error).toBeNull();
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/tasks/today'),
expect.any(Object)
);
});
it('should return empty array when no tasks', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ tasks: [] }),
});
const result = await todoService.getTodayTasks();
expect(result.data).toEqual([]);
});
it('should return error on failure', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 401,
});
const result = await todoService.getTodayTasks();
expect(result.data).toBeNull();
expect(result.error).toBeTruthy();
});
});
describe('getUpcomingTasks', () => {
it('should fetch upcoming tasks with default 7 days', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ tasks: [mockTask()] }),
});
const result = await todoService.getUpcomingTasks();
expect(result.data).toHaveLength(1);
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/tasks/upcoming?days=7'),
expect.any(Object)
);
});
it('should pass custom days parameter', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ tasks: [] }),
});
await todoService.getUpcomingTasks(14);
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('days=14'),
expect.any(Object)
);
});
});
describe('getInboxTasks', () => {
it('should fetch inbox tasks', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ tasks: [mockTask({ projectId: null })] }),
});
const result = await todoService.getInboxTasks();
expect(result.data).toHaveLength(1);
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/tasks/inbox'),
expect.any(Object)
);
});
});
describe('getProjects', () => {
it('should fetch projects', async () => {
const projects = [{ id: 'p-1', name: 'Work', color: '#3B82F6', order: 0, isArchived: false }];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ projects }),
});
const result = await todoService.getProjects();
expect(result.data).toEqual(projects);
});
});
});