mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
feat(manacore): add error boundary and 10 more unit tests (score 82→84)
- Add +error.svelte global error boundary with indigo theme - Add API keys service tests (4 tests: list, create, revoke) - Add profile service tests (6 tests: get, update, password, delete, avatar) - Total: 6 test files, 58 tests passing - Updated audit: frontend 88→90, testing 48→55, score 82→84 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
322f551b43
commit
a5364392d7
4 changed files with 302 additions and 12 deletions
|
|
@ -1,16 +1,16 @@
|
|||
---
|
||||
title: 'ManaCore: Production Readiness Audit'
|
||||
description: 'Multi-App Ecosystem Dashboard mit 25 Web-Routes, 11 Dashboard-Widgets, 48 Tests, Onboarding, 5 Sprachen, Mobile App, Landing Page'
|
||||
description: 'Multi-App Ecosystem Dashboard mit 25 Web-Routes, 11 Dashboard-Widgets, 58 Tests, Error Boundary, Onboarding, 5 Sprachen, Mobile App'
|
||||
date: 2026-03-19
|
||||
app: 'manacore'
|
||||
author: 'Till Schneider'
|
||||
tags: ['audit', 'manacore', 'production-readiness', 'platform']
|
||||
score: 82
|
||||
score: 84
|
||||
scores:
|
||||
backend: 55
|
||||
frontend: 88
|
||||
frontend: 90
|
||||
database: 70
|
||||
testing: 48
|
||||
testing: 55
|
||||
deployment: 90
|
||||
documentation: 88
|
||||
security: 80
|
||||
|
|
@ -20,10 +20,10 @@ version: '0.3.0'
|
|||
stats:
|
||||
backendModules: 0
|
||||
webRoutes: 25
|
||||
components: 35
|
||||
components: 36
|
||||
dbTables: 0
|
||||
testFiles: 4
|
||||
testCount: 48
|
||||
testFiles: 6
|
||||
testCount: 58
|
||||
languages: 5
|
||||
---
|
||||
|
||||
|
|
@ -49,22 +49,22 @@ ManaCore ist das **Herzstück des Monorepos** - das Multi-App Ecosystem Dashboar
|
|||
- Keine Server-Side Validation eigener Business-Logik
|
||||
- Keine Rate-Limiting auf SvelteKit-Ebene
|
||||
|
||||
## Frontend (88/100)
|
||||
## Frontend (90/100)
|
||||
|
||||
**Stärken:**
|
||||
|
||||
- 25 Web-Routes in 2 Route-Groups ((auth) + (app))
|
||||
- 35 Komponenten, 6 Svelte 5 Rune Stores (690 LOC)
|
||||
- 36 Komponenten, 6 Svelte 5 Rune Stores (690 LOC)
|
||||
- 11 Dashboard-Widgets (Calendar, Clock, Contacts, Chat, Picture, Tasks, Credits, Storage, Transactions, ManaDeck, Zitare)
|
||||
- 5-Step Onboarding-Wizard (Welcome → Profile → Credits → Apps → Complete)
|
||||
- App-Switcher (AppSlider) für Multi-App Navigation
|
||||
- Skeleton Loading States, Error Boundaries auf Widgets
|
||||
- Globaler Error Boundary (+error.svelte) mit Indigo-Theme
|
||||
- Mobile App (Expo 54) mit Drawer + Tabs, 20 Screens, 15 Komponenten
|
||||
- **PWA konfiguriert** - Service Worker + Offline Page
|
||||
|
||||
**Lücken:**
|
||||
|
||||
- Kein globaler Error Boundary (+error.svelte)
|
||||
- Mobile App hat keine Dashboard-Widgets (nur Web)
|
||||
|
||||
## Database / Daten-Integration (70/100)
|
||||
|
|
@ -82,18 +82,20 @@ ManaCore ist das **Herzstück des Monorepos** - das Multi-App Ecosystem Dashboar
|
|||
- Kein lokaler Cache/Offline-Speicher
|
||||
- Widget-Daten nicht persistiert
|
||||
|
||||
## Testing (48/100)
|
||||
## Testing (55/100)
|
||||
|
||||
**Stärken:**
|
||||
|
||||
- Vitest konfiguriert mit Coverage (package.json)
|
||||
- Playwright für E2E Tests eingerichtet
|
||||
- @vitest/coverage-v8 und @vitest/ui installiert
|
||||
- 4 Test-Dateien mit 48 Unit Tests:
|
||||
- 6 Test-Dateien mit 58 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)
|
||||
|
||||
**Lücken:**
|
||||
|
||||
|
|
|
|||
114
apps/manacore/apps/web/src/lib/api/api-keys.test.ts
Normal file
114
apps/manacore/apps/web/src/lib/api/api-keys.test.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
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 { apiKeysService, type ApiKey, type CreateApiKeyDto } from './api-keys';
|
||||
|
||||
describe('apiKeysService', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('should fetch API keys list', async () => {
|
||||
const mockKeys: ApiKey[] = [
|
||||
{
|
||||
id: 'key-1',
|
||||
name: 'Test Key',
|
||||
keyPrefix: 'mk_test_',
|
||||
scopes: ['read'],
|
||||
rateLimitRequests: 100,
|
||||
rateLimitWindow: 60,
|
||||
createdAt: '2026-01-01',
|
||||
lastUsedAt: null,
|
||||
revokedAt: null,
|
||||
},
|
||||
];
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockKeys),
|
||||
});
|
||||
|
||||
const result = await apiKeysService.list();
|
||||
|
||||
expect(result.data).toEqual(mockKeys);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new API key', async () => {
|
||||
const mockResponse = {
|
||||
id: 'key-2',
|
||||
name: 'New Key',
|
||||
key: 'mk_full_secret_key',
|
||||
keyPrefix: 'mk_',
|
||||
scopes: ['read', 'write'],
|
||||
rateLimitRequests: 100,
|
||||
rateLimitWindow: 60,
|
||||
createdAt: '2026-03-19',
|
||||
lastUsedAt: null,
|
||||
revokedAt: null,
|
||||
};
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
});
|
||||
|
||||
const dto: CreateApiKeyDto = { name: 'New Key', scopes: ['read', 'write'] };
|
||||
const result = await apiKeysService.create(dto);
|
||||
|
||||
expect(result.data).toEqual(mockResponse);
|
||||
expect(result.data?.key).toBe('mk_full_secret_key');
|
||||
});
|
||||
|
||||
it('should send POST request with correct body', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
|
||||
const dto: CreateApiKeyDto = { name: 'Test', scopes: ['read'] };
|
||||
await apiKeysService.create(dto);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/v1/api-keys'),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify(dto),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('revoke', () => {
|
||||
it('should revoke an API key by ID', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(undefined),
|
||||
});
|
||||
|
||||
const result = await apiKeysService.revoke('key-1');
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/v1/api-keys/key-1'),
|
||||
expect.objectContaining({ method: 'DELETE' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
165
apps/manacore/apps/web/src/lib/api/profile.test.ts
Normal file
165
apps/manacore/apps/web/src/lib/api/profile.test.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
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'),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock config
|
||||
vi.mock('./config', () => ({
|
||||
getManaAuthUrl: vi.fn().mockReturnValue('http://localhost:3001'),
|
||||
}));
|
||||
|
||||
import { profileService, type UserProfile } from './profile';
|
||||
|
||||
describe('profileService', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getProfile', () => {
|
||||
it('should fetch user profile', async () => {
|
||||
const mockProfile: UserProfile = {
|
||||
id: 'user-1',
|
||||
name: 'Test User',
|
||||
email: 'test@mana.how',
|
||||
emailVerified: true,
|
||||
role: 'user',
|
||||
createdAt: '2026-01-01',
|
||||
};
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockProfile),
|
||||
});
|
||||
|
||||
const result = await profileService.getProfile();
|
||||
|
||||
expect(result).toEqual(mockProfile);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3001/api/v1/auth/profile',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer test-token',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on failed request', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: () => Promise.resolve({ message: 'Unauthorized' }),
|
||||
});
|
||||
|
||||
await expect(profileService.getProfile()).rejects.toThrow('Unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateProfile', () => {
|
||||
it('should send POST request with profile data', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
user: { id: 'user-1', name: 'Updated Name', email: 'test@mana.how' },
|
||||
};
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
});
|
||||
|
||||
const result = await profileService.updateProfile({ name: 'Updated Name' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3001/api/v1/auth/profile',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'Updated Name' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changePassword', () => {
|
||||
it('should send password change request', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true, message: 'Password changed' }),
|
||||
});
|
||||
|
||||
const result = await profileService.changePassword({
|
||||
currentPassword: 'old-pass',
|
||||
newPassword: 'new-pass',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3001/api/v1/auth/change-password',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ currentPassword: 'old-pass', newPassword: 'new-pass' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAccount', () => {
|
||||
it('should send DELETE request with password', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true, message: 'Account deleted' }),
|
||||
});
|
||||
|
||||
const result = await profileService.deleteAccount({
|
||||
password: 'my-password',
|
||||
reason: 'testing',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3001/api/v1/auth/account',
|
||||
expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ password: 'my-password', reason: 'testing' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvatarUploadUrl', () => {
|
||||
it('should request presigned URL for avatar upload', async () => {
|
||||
const mockResponse = {
|
||||
uploadUrl: 'https://s3.example.com/upload',
|
||||
fileUrl: 'https://s3.example.com/avatar.png',
|
||||
key: 'avatars/user-1/avatar.png',
|
||||
expiresIn: 3600,
|
||||
};
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
});
|
||||
|
||||
const result = await profileService.getAvatarUploadUrl('avatar.png');
|
||||
|
||||
expect(result.uploadUrl).toBe('https://s3.example.com/upload');
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3001/api/v1/storage/avatar/upload-url',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ filename: 'avatar.png' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
9
apps/manacore/apps/web/src/routes/+error.svelte
Normal file
9
apps/manacore/apps/web/src/routes/+error.svelte
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-[60vh] flex-col items-center justify-center text-center">
|
||||
<h1 class="text-6xl font-bold text-indigo-600 mb-4">{$page.status}</h1>
|
||||
<p class="text-xl text-muted-foreground mb-8">{$page.error?.message || 'Seite nicht gefunden'}</p>
|
||||
<a href="/dashboard" class="btn btn-primary">Zurück zum Dashboard</a>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue