feat: Sprint 14 — Marketing & Monetization
Deploy to TrueNAS / deploy (push) Failing after 33s

- Landing page with hero, feature grid, trust signals
- Split-layout login redesign (admin + portal)
- Pricing page with storage tiers (5GB/50GB/unlimited)
- StorageQuotaService backend (V36 migration, 402 on exceeded)
- Frontend storage integration + 402 error handling
- StorageController uses TenantContext for tenant isolation
- onTierChange() hook for subscription tier updates
This commit is contained in:
Patrick Plate
2026-06-18 20:27:54 +02:00
parent 52d23053e7
commit dad798a904
24 changed files with 2485 additions and 212 deletions
@@ -0,0 +1,596 @@
# Testplan: Sprint 14 — Marketing & Monetization
**Date:** 2026-06-18
**Author:** Patrick Plate / Lumen (Planner)
**Status:** v2
**Basis:** cannamanage-sprint14-plan.md
---
## Test Overview
| ID | Description | Type | Class/Tool | Status |
|----|-------------|------|------------|--------|
| T-01 | Landing page renders all sections | E2E | Playwright | ⬜ |
| T-02 | Landing page responsive (mobile) | E2E | Playwright | ⬜ |
| T-03 | Landing page dark/light mode | E2E | Playwright | ⬜ |
| T-04 | Landing page CTA links work | E2E | Playwright | ⬜ |
| T-05 | Marketing nav shows Features link | E2E | Playwright | ⬜ |
| T-06 | Admin login — split layout on desktop | E2E | Playwright | ⬜ |
| T-07 | Admin login — full-width on mobile | E2E | Playwright | ⬜ |
| T-08 | Admin login — form still functional | E2E | Playwright | ⬜ |
| T-09 | Portal login — member-themed split | E2E | Playwright | ⬜ |
| T-10 | Portal login — form functional | E2E | Playwright | ⬜ |
| T-11 | Pricing — storage tiers displayed | E2E | Playwright | ⬜ |
| T-12 | Pricing — comparison table with storage row | E2E | Playwright | ⬜ |
| T-13 | Pricing — FAQ storage entry visible | E2E | Playwright | ⬜ |
| T-14 | Storage usage — correct calculation | Unit | `StorageQuotaServiceTest` | ⬜ |
| T-15 | Storage quota — allows upload under limit | Unit | `StorageQuotaServiceTest` | ⬜ |
| T-16 | Storage quota — rejects upload over limit | Unit | `StorageQuotaServiceTest` | ⬜ |
| T-17 | Storage quota — increment on upload | Unit | `StorageQuotaServiceTest` | ⬜ |
| T-18 | Storage quota — decrement on delete | Unit | `StorageQuotaServiceTest` | ⬜ |
| T-19 | Storage quota — decrement floors at zero | Unit | `StorageQuotaServiceTest` | ⬜ |
| T-20 | Storage quota — tier limit mapping | Unit | `StorageQuotaServiceTest` | ⬜ |
| T-21 | Storage quota — near-limit detection (80%) | Unit | `StorageQuotaServiceTest` | ⬜ |
| T-22 | Storage quota — near-limit detection (95%) | Unit | `StorageQuotaServiceTest` | ⬜ |
| T-23 | GET /api/v1/storage/usage — authenticated | Integration | `StorageControllerTest` | ⬜ |
| T-24 | GET /api/v1/storage/usage — unauthenticated 401 | Integration | `StorageControllerTest` | ⬜ |
| T-25 | GET /api/v1/storage/usage — correct DTO shape | Integration | `StorageControllerTest` | ⬜ |
| T-26 | Document upload — quota check integrated | Integration | `DocumentServiceTest` | ⬜ |
| T-27 | Document upload — 402 on quota exceeded | Integration | `DocumentControllerTest` | ⬜ |
| T-28 | Document delete — usage decremented | Integration | `DocumentServiceTest` | ⬜ |
| T-29 | Flyway V36 — migration applies cleanly | Integration | Flyway boot test | ⬜ |
| T-30 | Flyway V36 — backfill calculates correctly | Integration | SQL verification | ⬜ |
| T-31 | StorageQuotaExceededException — 402 response format | Unit | `GlobalExceptionHandlerTest` | ⬜ |
| T-32 | i18n — all marketing.home keys resolve (de + en) | Unit | Lint / next-intl | ⬜ |
Status: ⬜ Open | ✅ Passed | ❌ Failed | ⏭️ Skipped
---
## Test Cases
### T-01: Landing Page Renders All Sections
**Type:** E2E
**Tool:** Playwright
**Script:** `e2e/marketing/landing-page.spec.ts`
**Preconditions:**
- App running at localhost:3000
- No authentication required (public page)
**Scenarios:**
| # | Action | Expected Result |
|---|--------|-----------------|
| a | Navigate to `/` | Page loads without errors |
| b | Check hero section | Headline text visible, CTA buttons present |
| c | Check feature grid | 6 feature cards visible with titles and descriptions |
| d | Check trust signals | At least 4 trust badges visible |
| e | Check final CTA | "Kostenlos testen" button visible |
**Postconditions:**
- All i18n keys resolve (no raw key strings visible)
- No console errors
---
### T-02: Landing Page Responsive (Mobile)
**Type:** E2E
**Tool:** Playwright (viewport: 375×812)
**Scenarios:**
| # | Viewport | Expected Result |
|---|----------|-----------------|
| a | 375×812 (iPhone) | Feature grid stacks to single column |
| b | 375×812 | Hero section full-width, text wraps cleanly |
| c | 768×1024 (iPad) | Feature grid shows 2 columns |
| d | 1280×720 (desktop) | Feature grid shows 3 columns |
---
### T-03: Landing Page Dark/Light Mode
**Type:** E2E
**Tool:** Playwright (`colorScheme: 'dark'` / `'light'`)
**Scenarios:**
| # | Mode | Expected Result |
|---|------|-----------------|
| a | Dark | Background is dark, text is light, no contrast issues |
| b | Light | Background is light, text is dark, cards have proper borders |
| c | Switch | Toggle theme mid-page — re-renders correctly |
---
### T-04: Landing Page CTA Links
**Type:** E2E
**Tool:** Playwright
**Scenarios:**
| # | Element | Expected Navigation |
|---|---------|-------------------|
| a | Primary CTA ("Preise ansehen") | Navigates to `/pricing` |
| b | Secondary CTA ("Jetzt anmelden") | Navigates to `/login` |
| c | Final CTA ("Kostenlos testen") | Navigates to `/pricing` |
| d | Header "Features" link | Scrolls to `#features` section or navigates to `/#features` |
---
### T-05: Marketing Nav Features Link
**Type:** E2E
**Tool:** Playwright
**Scenarios:**
| # | Page | Expected |
|---|------|----------|
| a | `/` | Header shows "Features" link |
| b | `/pricing` | Header shows "Features" link |
| c | Click "Features" from `/pricing` | Navigates to homepage features section |
---
### T-06: Admin Login — Split Layout Desktop
**Type:** E2E
**Tool:** Playwright (viewport: 1280×720)
**Scenarios:**
| # | Check | Expected |
|---|-------|----------|
| a | Left panel visible | Branding panel with logo, tagline, feature bullets visible |
| b | Right panel visible | Login form visible |
| c | Layout proportions | Left panel ~55%, right panel ~45% |
| d | Left panel content | "CannaManage" text, tagline, 3 feature highlights with icons |
---
### T-07: Admin Login — Full-Width Mobile
**Type:** E2E
**Tool:** Playwright (viewport: 375×812)
**Scenarios:**
| # | Check | Expected |
|---|-------|----------|
| a | Left panel | Hidden (`hidden md:flex`) |
| b | Form panel | Full width, centered vertically |
| c | Form usability | All fields accessible, submit button tappable |
---
### T-08: Admin Login — Form Still Functional
**Type:** E2E
**Tool:** Playwright
**Preconditions:**
- Backend running with test credentials available
**Scenarios:**
| # | Input | Expected |
|---|-------|----------|
| a | Valid credentials | Redirects to `/dashboard` |
| b | Invalid password | Error message displayed |
| c | Empty fields | Validation errors shown |
---
### T-09: Portal Login — Member-Themed Split
**Type:** E2E
**Tool:** Playwright (viewport: 1280×720)
**Scenarios:**
| # | Check | Expected |
|---|-------|----------|
| a | Left panel | Member-specific messaging ("Willkommen zurück") |
| b | Visual theme | Different gradient than admin login (teal/emerald vs. primary green) |
| c | Feature bullets | Member-relevant: Abgabehistorie, Profil, Dokumente |
---
### T-10: Portal Login — Form Functional
**Type:** E2E
**Tool:** Playwright
**Scenarios:**
| # | Input | Expected |
|---|-------|----------|
| a | Submit form | Redirects to `/portal/dashboard` |
| b | Invalid input | Error message shown |
---
### T-11: Pricing — Storage Tiers Displayed
**Type:** E2E
**Tool:** Playwright
**Scenarios:**
| # | Plan | Expected Storage Display |
|---|------|------------------------|
| a | Starter card | "5 GB Speicher" visible |
| b | Pro card | "50 GB Speicher" visible + overage note |
| c | Enterprise card | "Individueller Speicher" visible |
---
### T-12: Pricing — Comparison Table
**Type:** E2E
**Tool:** Playwright
**Scenarios:**
| # | Check | Expected |
|---|-------|----------|
| a | Table exists | Comparison table rendered below plan cards |
| b | Storage row | "Speicher" row shows 5 GB / 50 GB / Individuell |
| c | Overage row | "Überschreitung" row shows values per plan |
| d | Responsive | Table scrollable on mobile |
---
### T-13: Pricing — FAQ Storage Entry
**Type:** E2E
**Tool:** Playwright
**Scenarios:**
| # | Check | Expected |
|---|-------|----------|
| a | FAQ section | Contains storage question |
| b | Click expand | Answer mentions Starter upgrade + Pro overage pricing |
---
### T-14: Storage Usage Calculation
**Type:** Unit
**Class:** `cannamanage-service/src/test/java/de/cannamanage/service/StorageQuotaServiceTest.java`
**Method:** `testGetUsage_calculatesCorrectly()`
**Scenarios:**
| # | Setup | Expected |
|---|-------|----------|
| a | Club with 1 GB used, 5 GB limit | `{ usedBytes: 1073741824, limitBytes: 5368709120, percentage: 20.0 }` |
| b | Club with 0 bytes used | `{ usedBytes: 0, percentage: 0.0 }` |
| c | Club with exactly limit used | `{ percentage: 100.0 }` |
---
### T-15: Quota Allows Upload Under Limit
**Type:** Unit
**Class:** `StorageQuotaServiceTest`
**Method:** `testCheckQuota_underLimit_noException()`
**Scenarios:**
| # | Current Usage | Limit | Upload Size | Expected |
|---|---------------|-------|-------------|----------|
| a | 1 GB | 5 GB | 100 MB | No exception |
| b | 4.9 GB | 5 GB | 50 MB | No exception (4.95 GB < 5 GB) |
| c | 0 | 5 GB | 5 GB | No exception (exactly at limit) |
---
### T-16: Quota Rejects Upload Over Limit
**Type:** Unit
**Class:** `StorageQuotaServiceTest`
**Method:** `testCheckQuota_overLimit_throwsQuotaExceeded()`
**Scenarios:**
| # | Current Usage | Limit | Upload Size | Expected |
|---|---------------|-------|-------------|----------|
| a | 4.9 GB | 5 GB | 200 MB | `StorageQuotaExceededException` thrown |
| b | 5 GB | 5 GB | 1 byte | `StorageQuotaExceededException` thrown |
| c | 50 GB | 50 GB | 1 KB | `StorageQuotaExceededException` thrown |
---
### T-17: Increment On Upload
**Type:** Unit
**Class:** `StorageQuotaServiceTest`
**Method:** `testIncrementUsage_addsBytes()`
**Scenarios:**
| # | Initial | Increment | Expected |
|---|---------|-----------|----------|
| a | 0 | 1048576 (1 MB) | 1048576 |
| b | 1000000 | 500000 | 1500000 |
---
### T-18: Decrement On Delete
**Type:** Unit
**Class:** `StorageQuotaServiceTest`
**Method:** `testDecrementUsage_subtractsBytes()`
**Scenarios:**
| # | Initial | Decrement | Expected |
|---|---------|-----------|----------|
| a | 5000000 | 1000000 | 4000000 |
| b | 1048576 | 1048576 | 0 |
---
### T-19: Decrement Floors at Zero
**Type:** Unit
**Class:** `StorageQuotaServiceTest`
**Method:** `testDecrementUsage_floorsAtZero()`
**Scenarios:**
| # | Initial | Decrement | Expected |
|---|---------|-----------|----------|
| a | 100 | 200 | 0 (not negative) |
| b | 0 | 1000 | 0 |
---
### T-20: Tier Limit Mapping
**Type:** Unit
**Class:** `StorageQuotaServiceTest`
**Method:** `testGetLimitForTier()`
**Scenarios:**
| # | Tier | Expected Limit |
|---|------|---------------|
| a | TRIAL | 5 GB (5368709120) |
| b | STARTER | 5 GB (5368709120) |
| c | PRO | 50 GB (53687091200) |
| d | ENTERPRISE | Long.MAX_VALUE |
---
### T-21: Near-Limit Detection 80%
**Type:** Unit
**Class:** `StorageQuotaServiceTest`
**Method:** `testIsNearLimit_at80Percent()`
**Scenarios:**
| # | Usage | Limit | Threshold | Expected |
|---|-------|-------|-----------|----------|
| a | 4.0 GB | 5 GB | 80% | true (80%) |
| b | 3.9 GB | 5 GB | 80% | false (78%) |
| c | 4.1 GB | 5 GB | 80% | true (82%) |
---
### T-22: Near-Limit Detection 95%
**Type:** Unit
**Class:** `StorageQuotaServiceTest`
**Method:** `testIsNearLimit_at95Percent()`
**Scenarios:**
| # | Usage | Limit | Threshold | Expected |
|---|-------|-------|-----------|----------|
| a | 4.75 GB | 5 GB | 95% | true (95%) |
| b | 4.7 GB | 5 GB | 95% | false (94%) |
---
### T-23: Storage Endpoint — Authenticated
**Type:** Integration
**Class:** `cannamanage-api/src/test/java/de/cannamanage/api/controller/StorageControllerTest.java`
**Method:** `testGetUsage_authenticated_returns200()`
**Preconditions:**
- Test user with ADMIN role and known clubId
- Club has pre-set `storageUsedBytes` and `storageLimitBytes`
**Scenarios:**
| # | Auth | Expected |
|---|------|----------|
| a | Valid ADMIN JWT | 200 with `{ usedBytes, limitBytes, percentage }` |
| b | Valid STAFF JWT | 200 with correct response |
---
### T-24: Storage Endpoint — Unauthenticated
**Type:** Integration
**Class:** `StorageControllerTest`
**Method:** `testGetUsage_unauthenticated_returns401()`
**Scenarios:**
| # | Auth | Expected |
|---|------|----------|
| a | No token | 401 Unauthorized |
| b | Expired token | 401 Unauthorized |
| c | MEMBER role | 403 Forbidden |
---
### T-25: Storage Endpoint — DTO Shape
**Type:** Integration
**Class:** `StorageControllerTest`
**Method:** `testGetUsage_responseShape()`
**Scenarios:**
| # | Check | Expected |
|---|-------|----------|
| a | JSON keys | Response contains `usedBytes`, `limitBytes`, `percentage` |
| b | Types | `usedBytes` and `limitBytes` are numbers, `percentage` is double |
| c | Percentage calculation | Matches `usedBytes / limitBytes * 100` |
---
### T-26: Document Upload — Quota Check Integrated
**Type:** Integration
**Class:** `cannamanage-service/src/test/java/de/cannamanage/service/DocumentServiceTest.java`
**Method:** `testUploadDocument_checksQuotaBeforeWrite()`
**Scenarios:**
| # | Setup | Expected |
|---|-------|----------|
| a | Club under quota, upload 1 MB | Upload succeeds, `storageUsedBytes` incremented by file size |
| b | Club at quota limit | Upload rejected with `StorageQuotaExceededException` before file write |
---
### T-27: Document Upload — 402 Response
**Type:** Integration
**Class:** `cannamanage-api/src/test/java/de/cannamanage/api/controller/DocumentControllerTest.java`
**Method:** `testUploadDocument_quotaExceeded_returns402()`
**Preconditions:**
- Club with `storageUsedBytes = storageLimitBytes` (fully used)
**Scenarios:**
| # | Action | Expected |
|---|--------|----------|
| a | POST multipart upload (1 byte file) | 402 Payment Required |
| b | Response body | RFC 9457 ProblemDetail with `currentUsage`, `limit`, `requestedBytes` |
---
### T-28: Document Delete — Usage Decremented
**Type:** Integration
**Class:** `DocumentServiceTest`
**Method:** `testDeleteDocument_decrementsUsage()`
**Scenarios:**
| # | Setup | Expected |
|---|-------|----------|
| a | Club with 5 MB used, delete 2 MB document | `storageUsedBytes` = 3 MB |
| b | Club with 1 MB used, delete 1 MB document | `storageUsedBytes` = 0 |
---
### T-29: Flyway V36 — Migration Applies
**Type:** Integration
**Tool:** Spring Boot test context startup
**Scenarios:**
| # | Check | Expected |
|---|-------|----------|
| a | Application starts | V36 migration applies without error |
| b | Column exists | `SELECT storage_used_bytes FROM clubs LIMIT 1` succeeds |
| c | Default value | New clubs get `storage_used_bytes = 0` and `storage_limit_bytes = 5368709120` |
---
### T-30: Flyway V36 — Backfill
**Type:** Integration
**Tool:** SQL verification after migration
**Preconditions:**
- Club exists with 3 documents (sizes: 1MB, 2MB, 3MB)
**Scenarios:**
| # | Check | Expected |
|---|-------|----------|
| a | After migration | `storage_used_bytes` = 6291456 (6 MB = sum of document sizes) |
| b | Club with no docs | `storage_used_bytes` = 0 |
---
### T-31: StorageQuotaExceededException — 402 Format
**Type:** Unit
**Class:** `cannamanage-api/src/test/java/de/cannamanage/api/exception/GlobalExceptionHandlerTest.java`
**Method:** `testStorageQuotaExceeded_returns402WithProblemDetail()`
**Scenarios:**
| # | Input | Expected |
|---|-------|----------|
| a | `StorageQuotaExceededException(1GB, 5GB, 200MB)` | HTTP 402, title="Storage Quota Exceeded" |
| b | Response properties | Contains `currentUsage`, `limit`, `requestedBytes` numeric fields |
---
### T-32: i18n Keys Resolve
**Type:** Unit / Lint
**Tool:** next-intl compile check or custom script
**Scenarios:**
| # | Namespace | Expected |
|---|-----------|----------|
| a | `marketing.home.*` | All 20+ keys resolve in `de.json` |
| b | `marketing.home.*` | All 20+ keys resolve in `en.json` (English equivalents) |
| c | `marketing.pricing.faq.storage.*` | question + answer keys present in both locales |
| d | `marketing.pricing.plans.*.storage` | Storage labels present for each plan in both locales |
---
## Test Data
### Backend
- **Test club:** UUID `00000000-0000-0000-0000-000000000001`, `storageUsedBytes = 1073741824` (1 GB), `storageLimitBytes = 5368709120` (5 GB)
- **Test documents:** 3 documents with `fileSize` = 100MB, 200MB, 773741824 bytes (total = 1 GB)
- **Full quota club:** UUID `00000000-0000-0000-0000-000000000002`, `storageUsedBytes = storageLimitBytes = 5368709120`
### Frontend E2E
- Landing page, pricing, login pages are public — no auth setup needed for T-01 through T-13
- Login form tests (T-08, T-10) require running backend with test user `admin@gruener-daumen.de` / `TestAdmin123!`
---
## Test Coverage
| Component | Unit | Integration | E2E | Total |
|-----------|------|-------------|-----|-------|
| Landing page | 0 | 0 | 5 | 5 |
| Login redesign | 0 | 0 | 5 | 5 |
| Pricing update | 0 | 0 | 3 | 3 |
| StorageQuotaService | 9 | 0 | 0 | 9 |
| StorageController | 0 | 3 | 0 | 3 |
| DocumentService (quota) | 0 | 2 | 0 | 2 |
| DocumentController (402) | 0 | 1 | 0 | 1 |
| Flyway migration | 0 | 2 | 0 | 2 |
| Exception handling | 1 | 0 | 0 | 1 |
| i18n verification | 1 | 0 | 0 | 1 |
| **Total** | **11** | **8** | **13** | **32** |