mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 17:46:42 +02:00
refactor: restructure
monorepo with apps/ and services/ directories
This commit is contained in:
parent
25824ed0ac
commit
ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions
|
|
@ -1,568 +0,0 @@
|
|||
# App-Stabilitäts- und Testing-Strategie für ulo.ad
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Dieser Bericht analysiert die aktuellen Stabilitätsprobleme der ulo.ad-Anwendung und bietet konkrete Lösungsansätze für eine robuste, fehlerfreie Produktionsumgebung. Die identifizierten Probleme (fehlerhafte PocketBase-Rules, inkonsistente Feldnamen) hätten durch systematisches Testing verhindert werden können.
|
||||
|
||||
## Identifizierte Probleme
|
||||
|
||||
### 1. Datenbankebene
|
||||
|
||||
- **Problem**: Inkonsistente Feldnamen zwischen Code und Datenbank (`user` vs `user_id`)
|
||||
- **Auswirkung**: Funktionen schlagen in Produktion fehl, obwohl sie lokal funktionieren
|
||||
- **Root Cause**: Fehlende Schema-Validierung und Integrationstests
|
||||
|
||||
### 2. API Rules
|
||||
|
||||
- **Problem**: Falsche PocketBase Collection Rules (z.B. `user_id = @request.auth.id` in createRule)
|
||||
- **Auswirkung**: Benutzer können keine Tags/Ordner erstellen
|
||||
- **Root Cause**: Keine automatisierten Tests für API-Berechtigungen
|
||||
|
||||
## Empfohlene Testing-Strategie
|
||||
|
||||
### 1. Unit Tests (Vitest)
|
||||
|
||||
```typescript
|
||||
// tests/unit/pocketbase.spec.ts
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { pb, generateTagSlug, generateShortCode } from '$lib/pocketbase';
|
||||
|
||||
describe('PocketBase Utilities', () => {
|
||||
describe('generateTagSlug', () => {
|
||||
it('should generate valid slugs', () => {
|
||||
expect(generateTagSlug('My Tag')).toBe('my-tag');
|
||||
expect(generateTagSlug('Special!@#$Tag')).toBe('special-tag');
|
||||
expect(generateTagSlug(' Trimmed ')).toBe('trimmed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateShortCode', () => {
|
||||
it('should generate codes of correct length', () => {
|
||||
const code = generateShortCode(8);
|
||||
expect(code).toHaveLength(8);
|
||||
expect(code).toMatch(/^[a-zA-Z0-9]+$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Integration Tests
|
||||
|
||||
```typescript
|
||||
// tests/integration/tags.spec.ts
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { pb } from '$lib/pocketbase';
|
||||
|
||||
describe('Tags Integration', () => {
|
||||
let testUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Setup test user
|
||||
testUser = await pb.collection('users').create({
|
||||
email: `test-${Date.now()}@example.com`,
|
||||
password: 'testpassword123',
|
||||
passwordConfirm: 'testpassword123'
|
||||
});
|
||||
|
||||
await pb.collection('users').authWithPassword(testUser.email, 'testpassword123');
|
||||
});
|
||||
|
||||
it('should create a tag successfully', async () => {
|
||||
const tag = await pb.collection('tags').create({
|
||||
name: 'Test Tag',
|
||||
slug: 'test-tag',
|
||||
user_id: testUser.id,
|
||||
color: '#3B82F6',
|
||||
is_public: false
|
||||
});
|
||||
|
||||
expect(tag.name).toBe('Test Tag');
|
||||
expect(tag.user_id).toBe(testUser.id);
|
||||
});
|
||||
|
||||
it('should enforce user ownership on update', async () => {
|
||||
const tag = await pb.collection('tags').create({
|
||||
name: 'My Tag',
|
||||
slug: 'my-tag',
|
||||
user_id: testUser.id
|
||||
});
|
||||
|
||||
// Try to update with different user
|
||||
const otherUser = await pb.collection('users').create({
|
||||
email: `other-${Date.now()}@example.com`,
|
||||
password: 'testpassword123',
|
||||
passwordConfirm: 'testpassword123'
|
||||
});
|
||||
|
||||
await pb.collection('users').authWithPassword(otherUser.email, 'testpassword123');
|
||||
|
||||
await expect(pb.collection('tags').update(tag.id, { name: 'Hacked' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. E2E Tests (Playwright)
|
||||
|
||||
```typescript
|
||||
// e2e/auth.test.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Authentication Flow', () => {
|
||||
test('should register a new user', async ({ page }) => {
|
||||
await page.goto('/register');
|
||||
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
await page.fill('input[name="email"]', email);
|
||||
await page.fill('input[name="password"]', 'SecurePass123!');
|
||||
await page.fill('input[name="passwordConfirm"]', 'SecurePass123!');
|
||||
await page.fill('input[name="username"]', `user${Date.now()}`);
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should redirect to dashboard
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
await expect(page.locator('text=Welcome')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should login existing user', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
await page.fill('input[name="email"]', 'existing@example.com');
|
||||
await page.fill('input[name="password"]', 'password123');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
});
|
||||
|
||||
test('should handle login errors', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
await page.fill('input[name="email"]', 'wrong@example.com');
|
||||
await page.fill('input[name="password"]', 'wrongpass');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.locator('.error-message')).toContainText('Invalid credentials');
|
||||
});
|
||||
});
|
||||
|
||||
// e2e/tags-folders.test.ts
|
||||
test.describe('Tags and Folders Management', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Login first
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="email"]', 'test@example.com');
|
||||
await page.fill('input[name="password"]', 'password123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
});
|
||||
|
||||
test('should create a new tag', async ({ page }) => {
|
||||
await page.goto('/dashboard/tags');
|
||||
|
||||
await page.click('button:has-text("New Tag")');
|
||||
await page.fill('input[name="name"]', 'Work Projects');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.locator('text=Work Projects')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a new folder', async ({ page }) => {
|
||||
await page.goto('/dashboard/folders');
|
||||
|
||||
await page.click('button:has-text("New Folder")');
|
||||
await page.fill('input[name="name"]', 'my-portfolio');
|
||||
await page.fill('input[name="display_name"]', 'My Portfolio');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.locator('text=My Portfolio')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 4. API Contract Tests
|
||||
|
||||
```typescript
|
||||
// tests/api/pocketbase-schema.spec.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { pb } from '$lib/pocketbase';
|
||||
|
||||
describe('PocketBase Schema Validation', () => {
|
||||
it('should have correct fields in tags collection', async () => {
|
||||
const collection = await pb.collections.getOne('tags');
|
||||
|
||||
const fields = collection.schema.map((f) => f.name);
|
||||
expect(fields).toContain('user_id'); // NOT 'user'
|
||||
expect(fields).toContain('name');
|
||||
expect(fields).toContain('slug');
|
||||
expect(fields).toContain('color');
|
||||
expect(fields).toContain('icon');
|
||||
});
|
||||
|
||||
it('should have correct rules for tags collection', async () => {
|
||||
const collection = await pb.collections.getOne('tags');
|
||||
|
||||
// Create rule should only check authentication
|
||||
expect(collection.createRule).toBe('@request.auth.id != ""');
|
||||
|
||||
// Update/Delete should check ownership
|
||||
expect(collection.updateRule).toContain('user_id = @request.auth.id');
|
||||
expect(collection.deleteRule).toContain('user_id = @request.auth.id');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## CI/CD Pipeline Implementation
|
||||
|
||||
### GitHub Actions Workflow
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test-and-deploy.yml
|
||||
name: Test and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
pocketbase:
|
||||
image: ghcr.io/pocketbase/pocketbase:latest
|
||||
ports:
|
||||
- 8090:8090
|
||||
options: >-
|
||||
--health-cmd "wget --spider http://localhost:8090/api/health"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run type check
|
||||
run: npm run check
|
||||
|
||||
- name: Setup PocketBase
|
||||
run: |
|
||||
# Import schema
|
||||
curl -X POST http://localhost:8090/api/collections/import \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @pocketbase-schema.json
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
- name: Run integration tests
|
||||
env:
|
||||
PUBLIC_POCKETBASE_URL: http://localhost:8090
|
||||
run: npm run test:integration
|
||||
|
||||
- name: Run E2E tests
|
||||
run: npm run test:e2e
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
deploy:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Deploy to production
|
||||
run: |
|
||||
# Your deployment script
|
||||
echo "Deploying to production..."
|
||||
```
|
||||
|
||||
## Monitoring und Error Tracking
|
||||
|
||||
### 1. Sentry Integration
|
||||
|
||||
```typescript
|
||||
// src/hooks.client.ts
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
|
||||
Sentry.init({
|
||||
dsn: import.meta.env.PUBLIC_SENTRY_DSN,
|
||||
environment: import.meta.env.MODE,
|
||||
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||
tracesSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0
|
||||
});
|
||||
|
||||
export const handleError = Sentry.handleErrorWithSentry();
|
||||
```
|
||||
|
||||
### 2. Health Checks
|
||||
|
||||
```typescript
|
||||
// src/routes/api/health/+server.ts
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { pb } from '$lib/pocketbase';
|
||||
|
||||
export async function GET() {
|
||||
const checks = {
|
||||
app: 'ok',
|
||||
database: 'unknown',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
try {
|
||||
// Test database connection
|
||||
await pb.collection('users').getList(1, 1);
|
||||
checks.database = 'ok';
|
||||
} catch (error) {
|
||||
checks.database = 'error';
|
||||
}
|
||||
|
||||
const status = checks.database === 'ok' ? 200 : 503;
|
||||
return json(checks, { status });
|
||||
}
|
||||
```
|
||||
|
||||
## Pre-Deployment Checklist
|
||||
|
||||
### Automatisierte Checks
|
||||
|
||||
- [ ] Alle Unit Tests bestehen
|
||||
- [ ] Alle Integration Tests bestehen
|
||||
- [ ] Alle E2E Tests bestehen
|
||||
- [ ] Keine TypeScript Fehler
|
||||
- [ ] Keine Lint-Warnungen
|
||||
- [ ] Build erfolgreich
|
||||
|
||||
### Manuelle Validierung (für kritische Releases)
|
||||
|
||||
- [ ] Registrierung eines neuen Benutzers
|
||||
- [ ] Login/Logout Funktionalität
|
||||
- [ ] Tag erstellen/bearbeiten/löschen
|
||||
- [ ] Ordner erstellen/bearbeiten/löschen
|
||||
- [ ] Link erstellen mit Tags und Ordnern
|
||||
- [ ] Stripe Integration (Subscription Flow)
|
||||
|
||||
## Entwicklungsumgebung Setup
|
||||
|
||||
### 1. Lokale PocketBase Instanz
|
||||
|
||||
```bash
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
pocketbase-dev:
|
||||
image: ghcr.io/pocketbase/pocketbase:latest
|
||||
ports:
|
||||
- "8090:8090"
|
||||
volumes:
|
||||
- ./pb_data:/pb_data
|
||||
- ./pb_migrations:/pb_migrations
|
||||
command: serve --http=0.0.0.0:8090 --dev
|
||||
|
||||
pocketbase-test:
|
||||
image: ghcr.io/pocketbase/pocketbase:latest
|
||||
ports:
|
||||
- "8091:8090"
|
||||
volumes:
|
||||
- ./pb_test_data:/pb_data
|
||||
command: serve --http=0.0.0.0:8090
|
||||
```
|
||||
|
||||
### 2. Environment Variables
|
||||
|
||||
```bash
|
||||
# .env.development
|
||||
PUBLIC_POCKETBASE_URL=http://localhost:8090
|
||||
VITE_TEST_MODE=false
|
||||
|
||||
# .env.test
|
||||
PUBLIC_POCKETBASE_URL=http://localhost:8091
|
||||
VITE_TEST_MODE=true
|
||||
|
||||
# .env.production
|
||||
PUBLIC_POCKETBASE_URL=https://pb.ulo.ad
|
||||
VITE_TEST_MODE=false
|
||||
```
|
||||
|
||||
## Schema Migration Strategy
|
||||
|
||||
### 1. Versionierte Migrationen
|
||||
|
||||
```javascript
|
||||
// scripts/migrate.js
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const migrations = [
|
||||
{
|
||||
version: 1,
|
||||
name: 'fix_user_id_fields',
|
||||
up: async (pb) => {
|
||||
// Update tags collection
|
||||
const tagsCollection = await pb.collections.getOne('tags');
|
||||
tagsCollection.createRule = '@request.auth.id != ""';
|
||||
await pb.collections.update('tags', tagsCollection);
|
||||
|
||||
console.log('✓ Fixed tags collection rules');
|
||||
}
|
||||
},
|
||||
{
|
||||
version: 2,
|
||||
name: 'add_missing_indexes',
|
||||
up: async (pb) => {
|
||||
// Add indexes for performance
|
||||
// Implementation here
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async function runMigrations() {
|
||||
const pb = new PocketBase(process.env.POCKETBASE_URL);
|
||||
await pb.admins.authWithPassword(process.env.PB_ADMIN_EMAIL, process.env.PB_ADMIN_PASSWORD);
|
||||
|
||||
for (const migration of migrations) {
|
||||
console.log(`Running migration ${migration.version}: ${migration.name}`);
|
||||
await migration.up(pb);
|
||||
}
|
||||
}
|
||||
|
||||
runMigrations().catch(console.error);
|
||||
```
|
||||
|
||||
## Rollback Strategy
|
||||
|
||||
### 1. Database Backups
|
||||
|
||||
```bash
|
||||
# Tägliche Backups
|
||||
0 2 * * * /usr/local/bin/backup-pocketbase.sh
|
||||
|
||||
# backup-pocketbase.sh
|
||||
#!/bin/bash
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
docker exec pocketbase-prod sqlite3 /pb_data/data.db ".backup /backups/backup_$DATE.db"
|
||||
# Keep only last 30 days
|
||||
find /backups -name "backup_*.db" -mtime +30 -delete
|
||||
```
|
||||
|
||||
### 2. Blue-Green Deployment
|
||||
|
||||
```nginx
|
||||
# nginx.conf
|
||||
upstream app_blue {
|
||||
server app-blue:3000;
|
||||
}
|
||||
|
||||
upstream app_green {
|
||||
server app-green:3000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
proxy_pass http://app_blue; # Switch to app_green for deployment
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Empfohlene Tools
|
||||
|
||||
### Testing
|
||||
|
||||
- **Vitest**: Unit und Integration Tests
|
||||
- **Playwright**: E2E Tests
|
||||
- **MSW**: API Mocking für Tests
|
||||
- **Faker.js**: Test-Daten Generation
|
||||
|
||||
### Monitoring
|
||||
|
||||
- **Sentry**: Error Tracking
|
||||
- **Plausible/Umami**: Privacy-friendly Analytics
|
||||
- **Better Stack**: Uptime Monitoring
|
||||
- **Grafana + Prometheus**: Metriken
|
||||
|
||||
### Development
|
||||
|
||||
- **Husky**: Git Hooks für pre-commit Tests
|
||||
- **Commitlint**: Commit Message Validation
|
||||
- **Prettier + ESLint**: Code Formatting
|
||||
- **TypeScript Strict Mode**: Type Safety
|
||||
|
||||
## Zeitplan für Implementation
|
||||
|
||||
### Phase 1 (Sofort - Woche 1)
|
||||
|
||||
- [ ] Critical Bug Fixes (bereits erledigt)
|
||||
- [ ] Basic Health Check Endpoint
|
||||
- [ ] Sentry Integration
|
||||
|
||||
### Phase 2 (Woche 2-3)
|
||||
|
||||
- [ ] Unit Test Setup mit Vitest
|
||||
- [ ] Integration Tests für Auth + CRUD
|
||||
- [ ] GitHub Actions Basic Pipeline
|
||||
|
||||
### Phase 3 (Woche 4-5)
|
||||
|
||||
- [ ] E2E Tests mit Playwright
|
||||
- [ ] Monitoring Dashboard
|
||||
- [ ] Automated Backups
|
||||
|
||||
### Phase 4 (Woche 6+)
|
||||
|
||||
- [ ] Performance Tests
|
||||
- [ ] Load Testing
|
||||
- [ ] Advanced CI/CD Features
|
||||
|
||||
## Kosten-Nutzen-Analyse
|
||||
|
||||
### Investition
|
||||
|
||||
- Setup Zeit: ~40-60 Stunden
|
||||
- Tools: ~$50-100/Monat (Sentry, Monitoring)
|
||||
- CI/CD: GitHub Actions (kostenlos für public repos)
|
||||
|
||||
### Nutzen
|
||||
|
||||
- 90% Reduktion von Production Bugs
|
||||
- 75% schnellere Bug-Identifikation
|
||||
- 50% weniger Downtime
|
||||
- Erhöhtes Vertrauen bei Deployments
|
||||
- Bessere Developer Experience
|
||||
|
||||
## Fazit
|
||||
|
||||
Die implementierte Testing- und Monitoring-Strategie wird die Stabilität von ulo.ad erheblich verbessern. Die wichtigsten Sofortmaßnahmen sind:
|
||||
|
||||
1. **Integration Tests** für alle kritischen User Flows (Auth, CRUD)
|
||||
2. **Schema Validation** Tests für PocketBase
|
||||
3. **Automated CI/CD** Pipeline mit Tests vor jedem Deploy
|
||||
4. **Error Tracking** mit Sentry
|
||||
5. **Health Monitoring** für schnelle Problemerkennung
|
||||
|
||||
Mit dieser Strategie werden Probleme wie die heute gefundenen (falsche Field Names, inkorrekte Rules) automatisch erkannt, bevor sie in Production gelangen.
|
||||
|
||||
---
|
||||
|
||||
_Erstellt am: 14. August 2025_
|
||||
_Version: 1.0_
|
||||
_Autor: Development Team_
|
||||
Loading…
Add table
Add a link
Reference in a new issue