3232d2f7fd
- V6 migration: consents table with audit columns - Consent entity, repository, service (grant/revoke/check) - ConsentController: GET/POST/DELETE consent endpoints - DSGVO export (Art. 15): full personal data JSON download - DSGVO deletion (Art. 17): anonymization + account deactivation - Frontend: consent banner (modal, cannot dismiss), privacy settings page - React Query hooks for consent + DSGVO operations - Full i18n (de/en) for consent and DSGVO namespaces
882 lines
29 KiB
Markdown
882 lines
29 KiB
Markdown
# Testplan: CannaManage Sprint 6 — Production Readiness
|
|
|
|
**Datum:** 2026-06-12
|
|
**Modul:** cannamanage (all modules)
|
|
**Autor:** Patrick Plate
|
|
**Status:** Entwurf v1
|
|
**Basis:** cannamanage-sprint6-plan.md (v1)
|
|
|
|
---
|
|
|
|
## Testübersicht
|
|
|
|
| ID | Beschreibung | Typ | Klasse/Tool | Status |
|
|
|----|-------------|-----|-------------|--------|
|
|
| T-01 | ConsentService: accept consent | Unit | `ConsentServiceTest` | ⬜ |
|
|
| T-02 | ConsentService: revoke consent | Unit | `ConsentServiceTest` | ⬜ |
|
|
| T-03 | ConsentService: check consent status | Unit | `ConsentServiceTest` | ⬜ |
|
|
| T-04 | ConsentService: re-prompt on new version | Unit | `ConsentServiceTest` | ⬜ |
|
|
| T-05 | DsgvoService: export user data (ZIP) | Unit | `DsgvoServiceTest` | ⬜ |
|
|
| T-06 | DsgvoService: erasure request (anonymize) | Unit | `DsgvoServiceTest` | ⬜ |
|
|
| T-07 | DsgvoService: retain legally required data | Unit | `DsgvoServiceTest` | ⬜ |
|
|
| T-08 | StripeService: create customer | Unit | `StripeServiceTest` | ⬜ |
|
|
| T-09 | StripeService: create checkout session (SEPA) | Unit | `StripeServiceTest` | ⬜ |
|
|
| T-10 | StripeService: create billing portal session | Unit | `StripeServiceTest` | ⬜ |
|
|
| T-11 | StripeService: handle webhook invoice.paid | Unit | `StripeServiceTest` | ⬜ |
|
|
| T-12 | StripeService: handle webhook payment_failed | Unit | `StripeServiceTest` | ⬜ |
|
|
| T-13 | StripeService: handle webhook subscription.deleted | Unit | `StripeServiceTest` | ⬜ |
|
|
| T-14 | StripeWebhookController: reject invalid signature | Unit | `StripeWebhookControllerTest` | ⬜ |
|
|
| T-15 | SubscriptionFilter: block expired subscription | Unit | `SubscriptionFilterTest` | ⬜ |
|
|
| T-16 | SubscriptionFilter: allow active subscription | Unit | `SubscriptionFilterTest` | ⬜ |
|
|
| T-17 | SubscriptionFilter: allow trial period | Unit | `SubscriptionFilterTest` | ⬜ |
|
|
| T-18 | AuditService: log event | Unit | `AuditServiceTest` | ⬜ |
|
|
| T-19 | AuditService: immutability (no update/delete) | Integration | `AuditServiceIntegrationTest` | ⬜ |
|
|
| T-20 | AuditService: JSON details serialization | Unit | `AuditServiceTest` | ⬜ |
|
|
| T-21 | AuditService: server-generated timestamp (Europe/Berlin) | Unit | `AuditServiceTest` | ⬜ |
|
|
| T-22 | AuditController: paginated list with filters | Integration | `AuditControllerIntegrationTest` | ⬜ |
|
|
| T-23 | AuditController: PDF export | Integration | `AuditControllerIntegrationTest` | ⬜ |
|
|
| T-24 | GrowCalendarService: create grow entry | Unit | `GrowCalendarServiceTest` | ⬜ |
|
|
| T-25 | GrowCalendarService: update stage transition | Unit | `GrowCalendarServiceTest` | ⬜ |
|
|
| T-26 | GrowCalendarService: link harvest to batch | Unit | `GrowCalendarServiceTest` | ⬜ |
|
|
| T-27 | GrowCalendarService: reject invalid stage progression | Unit | `GrowCalendarServiceTest` | ⬜ |
|
|
| T-28 | NotificationService: quota warning at 80% | Unit | `NotificationServiceTest` | ⬜ |
|
|
| T-29 | NotificationService: batch recall notification | Unit | `NotificationServiceTest` | ⬜ |
|
|
| T-30 | NotificationService: new distribution notification | Unit | `NotificationServiceTest` | ⬜ |
|
|
| T-31 | Flyway V6: consent + audit_event tables created | Integration | `FlywayMigrationTest` | ⬜ |
|
|
| T-32 | Flyway V7: stripe columns added to club | Integration | `FlywayMigrationTest` | ⬜ |
|
|
| T-33 | Flyway V8: grow_entry table created | Integration | `FlywayMigrationTest` | ⬜ |
|
|
| T-34 | Flyway V9: notification table created | Integration | `FlywayMigrationTest` | ⬜ |
|
|
| T-35 | Consent flow E2E: login → banner → accept → app access | E2E | Playwright | ⬜ |
|
|
| T-36 | Consent flow E2E: login → banner → reject → logout | E2E | Playwright | ⬜ |
|
|
| T-37 | Billing page E2E: display plan + upgrade CTA | E2E | Playwright | ⬜ |
|
|
| T-38 | Audit log page E2E: filter + pagination + PDF download | E2E | Playwright | ⬜ |
|
|
| T-39 | Grow calendar E2E: create entry + drag resize | E2E | Playwright | ⬜ |
|
|
| T-40 | Notification bell E2E: receive + display + mark read | E2E | Playwright | ⬜ |
|
|
| T-41 | PWA manifest: installable, correct icons | E2E | Lighthouse | ⬜ |
|
|
| T-42 | Production smoke test: full user journey | Manual | Production | ⬜ |
|
|
| T-43 | Backup/restore: pg_dump + restore verification | Manual | Production | ⬜ |
|
|
| T-44 | TLS verification: SSL Labs A+ rating | Manual | SSL Labs | ⬜ |
|
|
| T-45 | Load test: 100 concurrent users, p95 < 500ms | Performance | k6 | ⬜ |
|
|
|
|
Status: ⬜ Offen | ✅ Bestanden | ❌ Fehlgeschlagen | ⏭️ Übersprungen
|
|
|
|
---
|
|
|
|
## Testfälle
|
|
|
|
### T-01: ConsentService — accept consent
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.ConsentServiceTest`
|
|
**Methode:** `testAcceptConsent()`
|
|
|
|
**Vorbedingungen:**
|
|
- User existiert in der DB
|
|
- Kein vorhandener Consent-Eintrag für diesen User + Typ + Version
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Gültiger User, PRIVACY_POLICY, Version "2026-06-v1" | Consent-Entity gespeichert mit IP, User-Agent, Timestamp |
|
|
| b | User mit bereits akzeptiertem Consent (gleiche Version) | Exception: ConsentAlreadyAcceptedException |
|
|
| c | User mit revoked Consent (gleiche Version) | Neuer Consent-Eintrag erstellt (re-accept möglich) |
|
|
|
|
**Nachbedingungen:**
|
|
- Audit-Log-Eintrag CONSENT_ACCEPTED erstellt
|
|
- Consent in DB mit korrektem Timestamp
|
|
|
|
---
|
|
|
|
### T-02: ConsentService — revoke consent
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.ConsentServiceTest`
|
|
**Methode:** `testRevokeConsent()`
|
|
|
|
**Vorbedingungen:**
|
|
- User hat aktiven Consent (acceptedAt != null, revokedAt == null)
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | User mit aktivem Consent | revokedAt gesetzt, Audit-Log CONSENT_REVOKED |
|
|
| b | User ohne aktiven Consent | Exception: NoActiveConsentException |
|
|
|
|
**Nachbedingungen:**
|
|
- Nachfolgende API-Aufrufe blockiert (Consent-Check schlägt fehl)
|
|
|
|
---
|
|
|
|
### T-03: ConsentService — check consent status
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.ConsentServiceTest`
|
|
**Methode:** `testCheckConsentStatus()`
|
|
|
|
**Vorbedingungen:**
|
|
- Verschiedene Consent-Zustände in DB
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | User mit aktivem Consent (aktuelle Version) | `true` |
|
|
| b | User ohne Consent | `false` |
|
|
| c | User mit revoked Consent | `false` |
|
|
| d | User mit Consent alter Version (neue Version verfügbar) | `false` (re-prompt nötig) |
|
|
|
|
---
|
|
|
|
### T-04: ConsentService — re-prompt on new version
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.ConsentServiceTest`
|
|
**Methode:** `testRePromptOnNewVersion()`
|
|
|
|
**Vorbedingungen:**
|
|
- User hat Consent für Version "2026-06-v1"
|
|
- System-Consent-Version ist jetzt "2026-09-v2"
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Check mit aktueller Version "2026-09-v2" | `false` — user muss neue Version akzeptieren |
|
|
| b | Nach Accept der neuen Version | `true` |
|
|
|
|
---
|
|
|
|
### T-05: DsgvoService — export user data (ZIP)
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.DsgvoServiceTest`
|
|
**Methode:** `testExportUserData()`
|
|
|
|
**Vorbedingungen:**
|
|
- User mit Profildaten, Distributions, Quota-History, Consent-Einträgen
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | User mit vollständigen Daten | ZIP enthält: profile.json, distributions.json, quota-history.json, consents.json |
|
|
| b | User ohne Distributions | ZIP enthält leere distributions.json (leeres Array) |
|
|
| c | User mit gelöschtem Account | Exception: UserNotFoundException |
|
|
|
|
**Nachbedingungen:**
|
|
- Audit-Log-Eintrag DATA_EXPORT_REQUESTED
|
|
- ZIP-Datei ist valides ZIP-Format
|
|
|
|
---
|
|
|
|
### T-06: DsgvoService — erasure request
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.DsgvoServiceTest`
|
|
**Methode:** `testErasureRequest()`
|
|
|
|
**Vorbedingungen:**
|
|
- User mit vollständigen Daten, aktiven Sessions
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Gültiger User | Name → "GELÖSCHT", Email → hash, Distributions anonymisiert (Mengen erhalten, Personenbezug entfernt) |
|
|
| b | User mit aktivem Consent | Alle Consents revoked |
|
|
| c | User mit aktiver Session | Alle Sessions invalidiert |
|
|
|
|
**Nachbedingungen:**
|
|
- Audit-Log-Eintrag DATA_ERASURE_REQUESTED
|
|
- Aggregat-Daten für Compliance erhalten (Gesamtmengen pro Monat)
|
|
- Kein Personenbezug mehr herstellbar
|
|
|
|
---
|
|
|
|
### T-07: DsgvoService — retain legally required data
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.DsgvoServiceTest`
|
|
**Methode:** `testRetainLegallyRequiredData()`
|
|
|
|
**Vorbedingungen:**
|
|
- User mit Distributions (steuerrelevant, 10-Jahres-Aufbewahrungspflicht)
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Erasure-Request für User mit Distributions | Distributions anonymisiert (member_id → null oder "ANON"), aber Zeilen + Mengen erhalten |
|
|
| b | Erasure-Request für User ohne Distributions | Vollständige Löschung aller Daten |
|
|
|
|
---
|
|
|
|
### T-08: StripeService — create customer
|
|
|
|
**Typ:** Unit (mit Stripe Mock)
|
|
**Klasse:** `de.cannamanage.service.StripeServiceTest`
|
|
**Methode:** `testCreateCustomer()`
|
|
|
|
**Vorbedingungen:**
|
|
- Club und Owner existieren
|
|
- Stripe API gemockt
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Club "Grüner Daumen", Owner email "owner@test.de" | Stripe Customer erstellt mit email + metadata (club_id, user_id) |
|
|
| b | Club bereits mit stripe_customer_id | Exception: CustomerAlreadyExistsException |
|
|
|
|
**Nachbedingungen:**
|
|
- club.stripeCustomerId gesetzt
|
|
|
|
---
|
|
|
|
### T-09: StripeService — create checkout session
|
|
|
|
**Typ:** Unit (mit Stripe Mock)
|
|
**Klasse:** `de.cannamanage.service.StripeServiceTest`
|
|
**Methode:** `testCreateCheckoutSession()`
|
|
|
|
**Vorbedingungen:**
|
|
- Club hat stripe_customer_id
|
|
- Price ID existiert
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Starter plan, SEPA | Session mit mode=subscription, payment_methods=[sepa_debit,card,paypal], trial=14 days |
|
|
| b | Pro plan | Session mit korrektem price_id |
|
|
| c | Club ohne stripe_customer_id | Exception: NoStripeCustomerException |
|
|
|
|
---
|
|
|
|
### T-10: StripeService — billing portal session
|
|
|
|
**Typ:** Unit (mit Stripe Mock)
|
|
**Klasse:** `de.cannamanage.service.StripeServiceTest`
|
|
**Methode:** `testCreateBillingPortalSession()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Club mit activem Subscription | Portal-Session-URL zurückgegeben |
|
|
| b | Club ohne Subscription | Exception oder leere Portal-Session |
|
|
|
|
---
|
|
|
|
### T-11: StripeService — webhook invoice.paid
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.StripeServiceTest`
|
|
**Methode:** `testHandleInvoicePaid()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Gültiges invoice.paid Event | Club subscription_status → ACTIVE, Audit-Log PAYMENT_RECEIVED |
|
|
| b | Event für unbekannten Customer | Logged + ignoriert (kein Fehler) |
|
|
|
|
---
|
|
|
|
### T-12: StripeService — webhook payment_failed
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.StripeServiceTest`
|
|
**Methode:** `testHandlePaymentFailed()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Gültiges invoice.payment_failed Event | Club subscription_status → PAST_DUE, Audit-Log PAYMENT_FAILED |
|
|
| b | Wiederholter Fehlschlag | Status bleibt PAST_DUE (idempotent) |
|
|
|
|
---
|
|
|
|
### T-13: StripeService — webhook subscription.deleted
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.StripeServiceTest`
|
|
**Methode:** `testHandleSubscriptionDeleted()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Gültiges customer.subscription.deleted Event | Club subscription_status → CANCELED, Audit-Log SUBSCRIPTION_CANCELED |
|
|
|
|
---
|
|
|
|
### T-14: StripeWebhookController — reject invalid signature
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.api.controller.StripeWebhookControllerTest`
|
|
**Methode:** `testRejectInvalidSignature()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Request ohne Stripe-Signature Header | 400 Bad Request |
|
|
| b | Request mit ungültiger Signatur | 400 Bad Request |
|
|
| c | Request mit gültiger Signatur | 200 OK, Event verarbeitet |
|
|
|
|
---
|
|
|
|
### T-15: SubscriptionFilter — block expired subscription
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.api.security.SubscriptionFilterTest`
|
|
**Methode:** `testBlockExpiredSubscription()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Club mit status CANCELED | 402 Payment Required |
|
|
| b | Club mit status PAST_DUE (>grace period) | 402 Payment Required |
|
|
| c | Actuator/health endpoint | Kein Filter (immer erlaubt) |
|
|
| d | Webhook endpoint | Kein Filter (immer erlaubt) |
|
|
|
|
---
|
|
|
|
### T-16: SubscriptionFilter — allow active subscription
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.api.security.SubscriptionFilterTest`
|
|
**Methode:** `testAllowActiveSubscription()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Club mit status ACTIVE | Request durchgelassen |
|
|
| b | Club mit status PAST_DUE (innerhalb Grace Period) | Request durchgelassen (mit Warning-Header) |
|
|
|
|
---
|
|
|
|
### T-17: SubscriptionFilter — allow trial period
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.api.security.SubscriptionFilterTest`
|
|
**Methode:** `testAllowTrialPeriod()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Club mit status TRIALING, trial_ends_at > now | Request durchgelassen |
|
|
| b | Club mit status TRIALING, trial_ends_at < now | 402 Payment Required |
|
|
|
|
---
|
|
|
|
### T-18: AuditService — log event
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.AuditServiceTest`
|
|
**Methode:** `testLogEvent()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | DISTRIBUTION_CREATED, actor, entity, description | AuditEvent in DB gespeichert mit allen Feldern |
|
|
| b | System-Event (kein Actor) | actorId=null, actorName="SYSTEM" |
|
|
| c | Event mit Details (JSON) | details-Feld enthält serialisiertes JSON |
|
|
|
|
**Nachbedingungen:**
|
|
- createdAt server-generiert (nicht aus Request)
|
|
|
|
---
|
|
|
|
### T-19: AuditService — immutability
|
|
|
|
**Typ:** Integration
|
|
**Klasse:** `de.cannamanage.service.AuditServiceIntegrationTest`
|
|
**Methode:** `testImmutability()`
|
|
|
|
**Vorbedingungen:**
|
|
- AuditEvent in DB gespeichert
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | UPDATE auf audit_event Zeile via EntityManager | Exception (Hibernate @Immutable verhindert Update) |
|
|
| b | DELETE auf audit_event Zeile via EntityManager | Exception oder ignoriert |
|
|
| c | Native SQL UPDATE | DB-Trigger verhindert Änderung (wenn implementiert) |
|
|
|
|
---
|
|
|
|
### T-20: AuditService — JSON details serialization
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.AuditServiceTest`
|
|
**Methode:** `testJsonDetailsSerialization()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Map mit before/after Werten | Valides JSON in details-Feld |
|
|
| b | null details | details-Feld ist null (nicht "null" String) |
|
|
| c | Komplexes Objekt mit verschachtelten Feldern | Korrekt serialisiert, deserialisierbar |
|
|
|
|
---
|
|
|
|
### T-21: AuditService — server-generated timestamp
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.AuditServiceTest`
|
|
**Methode:** `testServerGeneratedTimestamp()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Event ohne explizites Timestamp | createdAt = Instant.now() (±1s Toleranz) |
|
|
| b | Event mit manipuliertem createdAt im Request | Ignoriert — Server überschreibt via @PrePersist |
|
|
|
|
---
|
|
|
|
### T-22: AuditController — paginated list with filters
|
|
|
|
**Typ:** Integration
|
|
**Klasse:** `de.cannamanage.api.controller.AuditControllerIntegrationTest`
|
|
**Methode:** `testPaginatedListWithFilters()`
|
|
|
|
**Vorbedingungen:**
|
|
- 100+ Audit-Events in DB (verschiedene Typen, Zeiträume, Akteure)
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | GET /audit?page=0&size=50 | 50 Events, Pagination-Metadaten korrekt |
|
|
| b | GET /audit?type=DISTRIBUTION_CREATED | Nur Distribution-Events |
|
|
| c | GET /audit?from=2026-06-01&to=2026-06-30 | Nur Events im Juni |
|
|
| d | GET /audit?actor=Patrick | Nur Events von Patrick |
|
|
| e | GET /audit?search=Northern+Lights | Full-text Suche in description |
|
|
|
|
---
|
|
|
|
### T-23: AuditController — PDF export
|
|
|
|
**Typ:** Integration
|
|
**Klasse:** `de.cannamanage.api.controller.AuditControllerIntegrationTest`
|
|
**Methode:** `testPdfExport()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | GET /audit/export?format=pdf&from=2026-06-01&to=2026-06-30 | Valides PDF, Content-Type application/pdf |
|
|
| b | Export mit 0 Events | PDF mit "Keine Einträge im Zeitraum" Meldung |
|
|
| c | Export mit 1000+ Events | PDF generiert ohne OutOfMemory (streamed) |
|
|
|
|
---
|
|
|
|
### T-24: GrowCalendarService — create grow entry
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.GrowCalendarServiceTest`
|
|
**Methode:** `testCreateGrowEntry()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Strain "Northern Lights", SEEDLING, startDate=today, plantCount=10 | GrowEntry gespeichert, Audit-Log |
|
|
| b | Ohne strain_id | Validation-Fehler |
|
|
| c | endDate vor startDate | Validation-Fehler |
|
|
|
|
---
|
|
|
|
### T-25: GrowCalendarService — stage transition
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.GrowCalendarServiceTest`
|
|
**Methode:** `testStageTransition()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | SEEDLING → VEGETATIVE | Erfolgreich, stage aktualisiert |
|
|
| b | SEEDLING → FLOWERING (Stufe übersprungen) | Erlaubt (flexible Progression) |
|
|
| c | HARVEST → SEEDLING (Rückwärts) | Erlaubt (Korrektur möglich) |
|
|
|
|
---
|
|
|
|
### T-26: GrowCalendarService — link harvest to batch
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.GrowCalendarServiceTest`
|
|
**Methode:** `testLinkHarvestToBatch()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | GrowEntry im HARVEST-Stage + existierende Batch | linkedBatch gesetzt, Audit-Log HARVEST_LINKED_TO_BATCH |
|
|
| b | GrowEntry nicht im HARVEST-Stage | Nur im HARVEST/DRYING/CURING-Stage erlaubt |
|
|
| c | Batch bereits mit anderem GrowEntry verknüpft | Warnung (aber erlaubt — ein Batch kann mehrere Grows haben) |
|
|
|
|
---
|
|
|
|
### T-27: GrowCalendarService — reject invalid stage
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.GrowCalendarServiceTest`
|
|
**Methode:** `testRejectInvalidStage()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Stage = null | Validation-Fehler |
|
|
| b | Stage = ungültiger String (API-Ebene) | 400 Bad Request |
|
|
|
|
---
|
|
|
|
### T-28: NotificationService — quota warning
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.NotificationServiceTest`
|
|
**Methode:** `testQuotaWarning()`
|
|
|
|
**Vorbedingungen:**
|
|
- SimpMessagingTemplate gemockt
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | userId=42, percentUsed=80 | Message an /user/42/queue/notifications mit type=QUOTA_WARNING |
|
|
| b | userId=42, percentUsed=95 | Gleiche Nachricht (Schwellwert-Logik liegt im Aufrufer) |
|
|
|
|
---
|
|
|
|
### T-29: NotificationService — batch recall
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.NotificationServiceTest`
|
|
**Methode:** `testBatchRecallNotification()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | clubId=1, batchName="Northern Lights #7" | Message an /topic/club/1/notifications mit type=BATCH_RECALL |
|
|
|
|
---
|
|
|
|
### T-30: NotificationService — new distribution
|
|
|
|
**Typ:** Unit
|
|
**Klasse:** `de.cannamanage.service.NotificationServiceTest`
|
|
**Methode:** `testNewDistributionNotification()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | memberId=7, "Northern Lights", 5.0g | Message an /user/7/queue/notifications mit type=DISTRIBUTION |
|
|
|
|
---
|
|
|
|
### T-31: Flyway V6 — consent + audit_event tables
|
|
|
|
**Typ:** Integration
|
|
**Klasse:** `de.cannamanage.FlywayMigrationTest`
|
|
**Methode:** `testV6ConsentAndAuditTables()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Migration V6 ausführen | Tabellen `consent` und `audit_event` existieren |
|
|
| b | consent: unique constraint (user_id, consent_type, version) | Duplikat-Insert schlägt fehl |
|
|
| c | audit_event: JSONB column für details | INSERT mit JSON-Wert funktioniert |
|
|
|
|
---
|
|
|
|
### T-32: Flyway V7 — stripe columns
|
|
|
|
**Typ:** Integration
|
|
**Klasse:** `de.cannamanage.FlywayMigrationTest`
|
|
**Methode:** `testV7StripeColumns()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Migration V7 ausführen | Spalten stripe_customer_id, subscription_status, subscription_plan, trial_ends_at in club |
|
|
| b | Existierende Clubs | NULL-Werte für neue Spalten (keine Default-Pflicht) |
|
|
|
|
---
|
|
|
|
### T-33: Flyway V8 — grow_entry table
|
|
|
|
**Typ:** Integration
|
|
**Klasse:** `de.cannamanage.FlywayMigrationTest`
|
|
**Methode:** `testV8GrowEntryTable()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Migration V8 ausführen | Tabelle `grow_entry` mit FK zu strain + batch |
|
|
| b | INSERT mit gültigem strain_id | Erfolgreich |
|
|
| c | INSERT mit ungültigem strain_id | FK-Violation |
|
|
|
|
---
|
|
|
|
### T-34: Flyway V9 — notification table
|
|
|
|
**Typ:** Integration
|
|
**Klasse:** `de.cannamanage.FlywayMigrationTest`
|
|
**Methode:** `testV9NotificationTable()`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Migration V9 ausführen | Tabelle `notification` existiert |
|
|
| b | Index auf (user_id, read_at) | Performante Abfrage ungelesener Notifications |
|
|
|
|
---
|
|
|
|
### T-35: Consent Flow E2E — accept
|
|
|
|
**Typ:** E2E (Playwright)
|
|
**Klasse:** `e2e/consent-flow.spec.ts`
|
|
|
|
**Vorbedingungen:**
|
|
- Neuer User (kein Consent in DB)
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Login → Consent-Banner erscheint | Modal sichtbar, "Akzeptieren" + "Ablehnen" Buttons |
|
|
| b | Klick "Akzeptieren" | Modal verschwindet, Dashboard wird geladen |
|
|
| c | Erneuter Login (gleicher User) | Kein Banner (Consent bereits akzeptiert) |
|
|
|
|
---
|
|
|
|
### T-36: Consent Flow E2E — reject
|
|
|
|
**Typ:** E2E (Playwright)
|
|
**Klasse:** `e2e/consent-flow.spec.ts`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Login → Klick "Ablehnen" | Redirect zu Login-Seite, Session invalidiert |
|
|
| b | Versuch direkter Navigation zu /dashboard (ohne Consent) | Redirect zu Consent-Banner |
|
|
|
|
---
|
|
|
|
### T-37: Billing Page E2E
|
|
|
|
**Typ:** E2E (Playwright)
|
|
**Klasse:** `e2e/billing.spec.ts`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Navigate zu /settings/billing (Trial-User) | Pricing Cards sichtbar (Starter + Pro) |
|
|
| b | Navigate zu /settings/billing (Active Subscriber) | Aktueller Plan + "Verwalten" Button sichtbar |
|
|
| c | Klick "Verwalten" | Redirect zu Stripe Billing Portal |
|
|
|
|
---
|
|
|
|
### T-38: Audit Log Page E2E
|
|
|
|
**Typ:** E2E (Playwright)
|
|
**Klasse:** `e2e/audit-log.spec.ts`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Navigate zu /admin/audit-log | Tabelle mit Events sichtbar |
|
|
| b | Filter nach Typ "Distribution" | Nur Distribution-Events angezeigt |
|
|
| c | Klick "PDF Export" | PDF-Download startet |
|
|
| d | Pagination (Seite 2) | Neue Events geladen |
|
|
|
|
---
|
|
|
|
### T-39: Grow Calendar E2E
|
|
|
|
**Typ:** E2E (Playwright)
|
|
**Klasse:** `e2e/grow-calendar.spec.ts`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Navigate zu /grow | FullCalendar sichtbar mit Monatsansicht |
|
|
| b | Klick auf Datum | Create-Dialog öffnet sich |
|
|
| c | Formular ausfüllen + speichern | Neuer farbiger Block im Kalender |
|
|
| d | Klick auf existierenden Block | Detail-Panel mit Edit/Delete |
|
|
|
|
---
|
|
|
|
### T-40: Notification Bell E2E
|
|
|
|
**Typ:** E2E (Playwright)
|
|
**Klasse:** `e2e/notifications.spec.ts`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Notification empfangen (via WebSocket) | Bell-Icon zeigt Badge mit Anzahl |
|
|
| b | Klick auf Bell | Dropdown mit Notification-Liste |
|
|
| c | Klick auf einzelne Notification | Als gelesen markiert, Badge decremented |
|
|
|
|
---
|
|
|
|
### T-41: PWA Manifest
|
|
|
|
**Typ:** E2E (Lighthouse)
|
|
**Tool:** Lighthouse PWA Audit
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Eingabe | Erwartetes Ergebnis |
|
|
|---|---------|-------------------|
|
|
| a | Lighthouse PWA Audit | installable=true, manifest valid, service worker registered |
|
|
| b | Icons vorhanden | 192px + 512px PNG Icons |
|
|
| c | Offline-Seite | Netzwerk trennen → offline.html angezeigt (statt Browser-Fehler) |
|
|
|
|
---
|
|
|
|
### T-42: Production Smoke Test
|
|
|
|
**Typ:** Manual (Production)
|
|
**Tool:** Browser auf cannamanage.de
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Aktion | Erwartetes Ergebnis |
|
|
|---|--------|-------------------|
|
|
| a | Registrierung neuer Club | Account erstellt, Consent-Banner erscheint |
|
|
| b | Consent akzeptieren | Zugang zur App |
|
|
| c | Subscription (Stripe Test-Karte) | Checkout → Subscription aktiv |
|
|
| d | Mitglied anlegen | Mitglied in Liste sichtbar |
|
|
| e | Ausgabe aufzeichnen | Distribution erstellt, Quota aktualisiert |
|
|
| f | Audit-Log prüfen | Alle Aktionen geloggt |
|
|
| g | PDF Export | Valides PDF heruntergeladen |
|
|
| h | Grow-Eintrag erstellen | Kalender zeigt Eintrag |
|
|
| i | Notification empfangen | Bell-Icon zeigt neue Notification |
|
|
|
|
---
|
|
|
|
### T-43: Backup/Restore Verification
|
|
|
|
**Typ:** Manual (Production)
|
|
**Tool:** SSH + pg_dump/pg_restore
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Aktion | Erwartetes Ergebnis |
|
|
|---|--------|-------------------|
|
|
| a | pg_dump auf Production | Komprimiertes SQL-File erstellt |
|
|
| b | Restore in leere DB | Alle Tabellen + Daten wiederhergestellt |
|
|
| c | Cron-Job Ausführung prüfen | Backup-Datei < 24h alt in /backup/ |
|
|
| d | Rotation prüfen | Keine Dateien > 7 Tage in /backup/ |
|
|
|
|
---
|
|
|
|
### T-44: TLS Verification
|
|
|
|
**Typ:** Manual
|
|
**Tool:** SSL Labs (ssllabs.com/ssltest)
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Aktion | Erwartetes Ergebnis |
|
|
|---|--------|-------------------|
|
|
| a | SSL Labs Test auf cannamanage.de | Rating: A+ |
|
|
| b | HSTS Header prüfen | Strict-Transport-Security present, max-age ≥ 63072000 |
|
|
| c | Mixed Content prüfen | Keine HTTP-Ressourcen auf HTTPS-Seite |
|
|
|
|
---
|
|
|
|
### T-45: Load Test
|
|
|
|
**Typ:** Performance
|
|
**Tool:** k6
|
|
|
|
**Script:** `scripts/load-test.js`
|
|
|
|
**Szenarien:**
|
|
|
|
| # | Konfiguration | Erwartetes Ergebnis |
|
|
|---|--------------|-------------------|
|
|
| a | 100 VUs, 5 min, Ramp-up 30s | p95 < 500ms, 0 errors |
|
|
| b | API endpoints: /dashboard, /members, /distributions | Alle < 500ms p95 |
|
|
| c | WebSocket connections: 50 concurrent | Stable, kein Disconnect |
|
|
|
|
---
|
|
|
|
## Testdaten
|
|
|
|
### Seed-Daten (für Unit/Integration Tests)
|
|
|
|
- **Club:** "Testverein Grüner Daumen" (id=1)
|
|
- **Users:** admin@test.de (ADMIN), staff@test.de (STAFF), member@test.de (MEMBER)
|
|
- **Members:** 5 Testmitglieder mit verschiedenen Quota-Ständen (0%, 50%, 80%, 95%, 100%)
|
|
- **Batches:** 3 Batches (Northern Lights, Amnesia Haze, White Widow)
|
|
- **Distributions:** 20 historische Distributionen
|
|
- **Strains:** 5 Sorten mit THC/CBD-Werten
|
|
- **Grow Entries:** 3 in verschiedenen Stages
|
|
|
|
### Stripe Test-Daten
|
|
|
|
- Stripe Test-Karte: `4242424242424242` (Erfolg)
|
|
- Stripe Test-SEPA: `DE89370400440532013000` (Erfolg)
|
|
- Stripe Test-Karte: `4000000000000002` (Ablehnung)
|
|
- Webhook Events: via Stripe CLI (`stripe listen --forward-to localhost:8080/api/v1/webhooks/stripe`)
|
|
|
|
---
|
|
|
|
## Testabdeckung
|
|
|
|
| Komponente | Unit | Integration | E2E | Manual | Gesamt |
|
|
|-----------|------|-------------|-----|--------|--------|
|
|
| ConsentService | 4 | 0 | 2 | 0 | 6 |
|
|
| DsgvoService | 3 | 0 | 0 | 0 | 3 |
|
|
| StripeService | 6 | 0 | 1 | 0 | 7 |
|
|
| StripeWebhookController | 1 | 0 | 0 | 0 | 1 |
|
|
| SubscriptionFilter | 3 | 0 | 0 | 0 | 3 |
|
|
| AuditService | 4 | 2 | 1 | 0 | 7 |
|
|
| GrowCalendarService | 4 | 0 | 1 | 0 | 5 |
|
|
| NotificationService | 3 | 0 | 1 | 0 | 4 |
|
|
| Flyway Migrations | 0 | 4 | 0 | 0 | 4 |
|
|
| PWA/Production | 0 | 0 | 1 | 3 | 4 |
|
|
| Performance | 0 | 0 | 0 | 1 | 1 |
|
|
| **Summe** | **28** | **6** | **7** | **4** | **45** |
|
|
|
|
---
|
|
|
|
## Hinweise
|
|
|
|
- **Stripe-Tests:** Verwenden gemockten Stripe-Client (keine echten API-Calls in Unit-Tests). Integration mit Stripe CLI für Webhook-Testing.
|
|
- **WebSocket-Tests:** SimpMessagingTemplate wird gemockt. E2E-Test prüft echte WS-Verbindung.
|
|
- **Immutability:** Zusätzlich zum Hibernate @Immutable kann ein DB-Trigger empfohlen werden — Test T-19c prüft dies nur wenn implementiert.
|
|
- **DSGVO-Tests:** Besonderes Augenmerk auf Datenminimierung — Tests verifizieren, dass Löschung vollständig ist UND dass legal notwendige Daten erhalten bleiben.
|
|
- **Performance-Test:** k6 Script wird in Phase 7 erstellt. Baseline-Werte nach Phase 1 Deployment messen.
|