Files
cannamanage/docs/sprint-13/cannamanage-sprint13-testplan.md
T
2026-06-18 16:08:05 +02:00

17 KiB

Testplan: Sprint 13 — Production Hardening

Date: 2026-06-18 Author: Patrick Plate / Lumen (Planner) Status: v1 Basis: cannamanage-sprint13-plan.md


Test Overview

ID Description Type Class Status
T-01 Path traversal filename sanitization Unit DocumentServiceTest
T-02 Null filename fallback Unit DocumentServiceTest
T-03 Valid filename preserved Unit DocumentServiceTest
T-04 Download wrong tenant — forbidden Unit DocumentServiceTest
T-05 Download correct tenant — success Unit DocumentServiceTest
T-06 Delete wrong tenant — forbidden Unit DocumentServiceTest
T-07 Delete admin role — success Unit DocumentServiceTest
T-08 Download unauthenticated — 401 Integration DocumentControllerSecurityTest
T-09 Download wrong tenant — 403 Integration DocumentControllerSecurityTest
T-10 Download correct tenant — 200 Integration DocumentControllerSecurityTest
T-11 Delete as MEMBER — 403 Integration DocumentControllerSecurityTest
T-12 Delete as STAFF — 200 Integration DocumentControllerSecurityTest
T-13 Upload as MEMBER — 403 Integration DocumentControllerSecurityTest
T-14 Upload as STAFF — 200 Integration DocumentControllerSecurityTest
T-15 Login valid credentials — token pair Unit AuthServiceTest
T-16 Login invalid password — 401 Unit AuthServiceTest
T-17 Login non-existent user — 401 Unit AuthServiceTest
T-18 Refresh token valid — new access token Unit AuthServiceTest
T-19 Refresh token expired — 401 Unit AuthServiceTest
T-20 SHA-256 hashing deterministic Unit AuthServiceTest
T-21 Document endpoints require auth Integration SecurityConfigTest
T-22 Auth endpoints are public Integration SecurityConfigTest
T-23 Actuator health is public Integration SecurityConfigTest
T-24 CORS allows configured origin Integration SecurityConfigTest
T-25 CORS rejects unconfigured origin Integration SecurityConfigTest
T-26 Rate limit — 5 requests pass Integration LoginRateLimitFilterTest
T-27 Rate limit — 6th request returns 429 Integration LoginRateLimitFilterTest
T-28 Rate limit — different IPs independent Integration LoginRateLimitFilterTest
T-29 Rate limiter evicts stale entries (Caffeine TTL) Unit LoginRateLimitFilterTest
T-30 CI backend tests run on push Manual CI/CD verification
T-31 CI frontend lint runs on push Manual CI/CD verification
T-32 CI blocks deploy on test failure Manual CI/CD verification

Status: Open | Passed | Failed | ⏭️ Skipped


Test Cases

T-01: Path Traversal Filename Sanitization

Type: Unit Class: cannamanage-service/src/test/java/de/cannamanage/service/DocumentServiceTest.java Method: testUploadDocument_pathTraversalFilename_isSanitized()

Preconditions:

  • DocumentService instantiated with mocked dependencies
  • Mock file storage path configured

Scenarios:

# Input Expected Result
a "../../etc/passwd.pdf" Stored as passwd.pdf (path components stripped)
b "../../../tmp/evil.txt" Stored as evil.txt
c "..\\..\\windows\\system32\\bad.exe" Stored as bad.exe (backslash traversal)
d "normal-document.pdf" Stored as normal-document.pdf (unchanged)

Postconditions:

  • File is stored under {UPLOAD_BASE}/{clubId}/{docId}_{sanitizedFilename}
  • No path escapes the upload base directory

T-02: Null Filename Fallback

Type: Unit Class: DocumentServiceTest Method: testUploadDocument_nullFilename_usesFallback()

Scenarios:

# Input Expected Result
a null filename Stored as "document"
b Empty string "" Stored as "document"
c Whitespace only " " Stored as "document"

T-03: Valid Filename Preserved

Type: Unit Class: DocumentServiceTest Method: testUploadDocument_validFilename_preserved()

Scenarios:

# Input Expected Result
a "meeting-notes-2026.pdf" Stored as-is
b "Mitgliederversammlung Protokoll.docx" Stored as-is (spaces allowed)
c "report_v2.1_final.xlsx" Stored as-is (dots and underscores)

T-04: Download Wrong Tenant — Forbidden

Type: Unit Class: DocumentServiceTest Method: testDownloadDocument_wrongTenant_throwsForbidden()

Preconditions:

  • Document exists with clubId = "club-A"
  • Current user's tenant context = "club-B"

Scenarios:

# Input Expected Result
a Request document by UUID belonging to different club AccessDeniedException thrown

Postconditions:

  • No file content is returned
  • Audit log records the denied access attempt

T-05: Download Correct Tenant — Success

Type: Unit Class: DocumentServiceTest Method: testDownloadDocument_correctTenant_returnsContent()

Preconditions:

  • Document exists with clubId = "club-A"
  • Current user's tenant context = "club-A"

Scenarios:

# Input Expected Result
a Request document by valid UUID, same tenant File bytes returned successfully

T-06: Delete Wrong Tenant — Forbidden

Type: Unit Class: DocumentServiceTest Method: testDeleteDocument_wrongTenant_throwsForbidden()

Preconditions:

  • Document exists with clubId = "club-A"
  • Current user's tenant context = "club-B"

Scenarios:

# Input Expected Result
a Delete request for document in different club AccessDeniedException thrown

Postconditions:

  • Document is NOT deleted
  • File remains on disk

T-07: Delete Admin Role — Success

Type: Unit Class: DocumentServiceTest Method: testDeleteDocument_adminRole_succeeds()

Preconditions:

  • Document exists with clubId = "club-A"
  • Current user: role ADMIN, tenant "club-A"

Scenarios:

# Input Expected Result
a Admin deletes own tenant's document Document removed from DB + file system

T-08: Download Unauthenticated — 401

Type: Integration Class: cannamanage-api/src/test/java/de/cannamanage/api/controller/DocumentControllerSecurityTest.java Method: testDownload_unauthenticated_returns401()

Scenarios:

# Input Expected Result
a GET /api/v1/documents/{id}/download with no auth header HTTP 401

T-09: Download Wrong Tenant — 403

Type: Integration Class: DocumentControllerSecurityTest Method: testDownload_wrongTenant_returns403()

Preconditions:

  • @WithMockUser configured for club-B
  • Document belongs to club-A

Scenarios:

# Input Expected Result
a Authenticated user requests document from different tenant HTTP 403

T-10: Download Correct Tenant — 200

Type: Integration Class: DocumentControllerSecurityTest Method: testDownload_correctTenant_returns200()

Scenarios:

# Input Expected Result
a Authenticated user downloads own tenant's document HTTP 200 + file content

T-11: Delete as MEMBER — 403

Type: Integration Class: DocumentControllerSecurityTest Method: testDelete_memberRole_returns403()

Scenarios:

# Input Expected Result
a @WithMockUser(roles="MEMBER") → DELETE /api/v1/documents/{id} HTTP 403

T-12: Delete as STAFF — 200

Type: Integration Class: DocumentControllerSecurityTest Method: testDelete_staffRole_returns200()

Scenarios:

# Input Expected Result
a @WithMockUser(roles="STAFF") → DELETE /api/v1/documents/{id} HTTP 200 or 204

T-13: Upload as MEMBER — 403

Type: Integration Class: DocumentControllerSecurityTest Method: testUpload_memberRole_returns403()

Scenarios:

# Input Expected Result
a @WithMockUser(roles="MEMBER") → POST /api/v1/documents HTTP 403

T-14: Upload as STAFF — 200

Type: Integration Class: DocumentControllerSecurityTest Method: testUpload_staffRole_returns200()

Scenarios:

# Input Expected Result
a @WithMockUser(roles="STAFF") → POST /api/v1/documents with multipart file HTTP 200 or 201

T-15: Login Valid Credentials — Token Pair

Type: Unit Class: cannamanage-api/src/test/java/de/cannamanage/api/service/AuthServiceTest.java Method: testLogin_validCredentials_returnsTokenPair()

Scenarios:

# Input Expected Result
a Valid email + matching BCrypt password Response contains accessToken (non-null, non-empty) + refreshToken

T-16: Login Invalid Password — 401

Type: Unit Class: AuthServiceTest Method: testLogin_invalidPassword_throws401()

Scenarios:

# Input Expected Result
a Valid email + wrong password Exception with 401 semantics
b Valid email + empty password Exception with 401 semantics

T-17: Login Non-Existent User — 401

Type: Unit Class: AuthServiceTest Method: testLogin_nonExistentUser_throws401()

Scenarios:

# Input Expected Result
a unknown@example.com + any password Exception with 401 semantics

Postconditions:

  • Timing is consistent with valid-user path (prevent user enumeration)

T-18: Refresh Token Valid — New Access Token

Type: Unit Class: AuthServiceTest Method: testRefreshToken_validToken_returnsNewAccess()

Scenarios:

# Input Expected Result
a Valid, non-expired refresh token New access token returned, refresh token rotated

T-19: Refresh Token Expired — 401

Type: Unit Class: AuthServiceTest Method: testRefreshToken_expired_throws401()

Scenarios:

# Input Expected Result
a Refresh token past expiry date Exception with 401 semantics
b Token hash doesn't match stored hash Exception with 401 semantics

T-20: SHA-256 Hashing Deterministic

Type: Unit Class: AuthServiceTest Method: testSha256_sameInput_sameOutput()

Scenarios:

# Input Expected Result
a "test-token-123" hashed twice Both results are identical
b "different-input" Different hash than input (a)

T-21: Document Endpoints Require Auth

Type: Integration Class: cannamanage-api/src/test/java/de/cannamanage/api/security/SecurityConfigTest.java Method: testDocumentEndpoints_requireAuthentication()

Scenarios:

# Input Expected Result
a GET /api/v1/documents without auth HTTP 401
b POST /api/v1/documents without auth HTTP 401
c DELETE /api/v1/documents/{id} without auth HTTP 401

T-22: Auth Endpoints Are Public

Type: Integration Class: SecurityConfigTest Method: testAuthEndpoints_arePublic()

Scenarios:

# Input Expected Result
a POST /api/v1/auth/login without auth HTTP 200 or 400 (not 401)
b POST /api/v1/auth/register without auth HTTP 200 or 400 (not 401)

T-23: Actuator Health Is Public

Type: Integration Class: SecurityConfigTest Method: testActuatorHealth_isPublic()

Scenarios:

# Input Expected Result
a GET /actuator/health without auth HTTP 200

T-24: CORS Allows Configured Origin

Type: Integration Class: SecurityConfigTest Method: testCors_allowedOrigin_returns200()

Preconditions:

  • cannamanage.cors.allowed-origins=http://localhost:3000,https://app.cannamanage.de

Scenarios:

# Input Expected Result
a Origin: http://localhost:3000 Access-Control-Allow-Origin: http://localhost:3000
b Origin: https://app.cannamanage.de Access-Control-Allow-Origin: https://app.cannamanage.de

T-25: CORS Rejects Unconfigured Origin

Type: Integration Class: SecurityConfigTest Method: testCors_unconfiguredOrigin_noHeader()

Scenarios:

# Input Expected Result
a Origin: https://evil.com No Access-Control-Allow-Origin header in response

T-26: Rate Limit — 5 Requests Pass

Type: Integration Class: cannamanage-api/src/test/java/de/cannamanage/api/security/LoginRateLimitFilterTest.java Method: testRateLimit_5requests_allPass()

Scenarios:

# Input Expected Result
a 5 POST requests to /api/v1/auth/login from same IP within 1 minute All return normal response (200 or 401)

T-27: Rate Limit — 6th Request Returns 429

Type: Integration Class: LoginRateLimitFilterTest Method: testRateLimit_6thRequest_returns429()

Scenarios:

# Input Expected Result
a 6th POST to /api/v1/auth/login from same IP within 1 minute HTTP 429 + Retry-After header

T-28: Rate Limit — Different IPs Independent

Type: Integration Class: LoginRateLimitFilterTest Method: testRateLimit_differentIPs_independent()

Scenarios:

# Input Expected Result
a 5 requests from IP-A, then 1 request from IP-B IP-B request passes normally

T-29: Rate Limiter Evicts Stale Entries (Caffeine TTL)

Type: Unit Class: cannamanage-api/src/test/java/de/cannamanage/api/security/LoginRateLimitFilterTest.java Method: testRateLimiter_evictsStaleEntries()

Preconditions:

  • Caffeine cache configured with short TTL for testing (override via @TestPropertySource or direct instantiation)

Scenarios:

# Input Expected Result
a Exhaust 5 attempts from IP-A, wait for TTL to expire, then attempt again Request passes (bucket evicted and recreated)
b Verify cache size does not grow unbounded after many unique IPs Cache respects maximumSize(10_000) — old entries evicted

Postconditions:

  • No memory leak under simulated DDoS (many unique IPs)
  • Stale rate limit buckets are automatically cleaned up

T-30: CI Backend Tests Run on Push

Type: Manual Verification: Push a commit to main, verify Gitea Actions log shows Maven test step executing.


T-31: CI Frontend Lint Runs on Push

Type: Manual Verification: Push a commit to main, verify Gitea Actions log shows pnpm lint + type-check step executing.


T-32: CI Blocks Deploy on Test Failure

Type: Manual Verification: Introduce a deliberately failing test, push to main, verify deployment does NOT proceed and the workflow fails at the test step.


Test Data

Documents Test Fixtures

  • Club A: UUID "11111111-1111-1111-1111-111111111111", document with known UUID
  • Club B: UUID "22222222-2222-2222-2222-222222222222", separate document

Auth Test Fixtures

  • Valid user: "test@example.com", BCrypt password hash of "TestPass123!"
  • Non-existent user: "ghost@example.com"

Rate Limit Test Setup

  • Use MockHttpServletRequest with different remoteAddr values to simulate multiple IPs

Test Coverage

Component Unit Integration Manual Total
DocumentService 7 0 0 7
DocumentController 0 7 0 7
AuthService 6 0 0 6
SecurityConfig 0 5 0 5
LoginRateLimitFilter 1 3 0 4
CI/CD Pipeline 0 0 3 3
Total 14 15 3 32

Execution Order

  1. Run unit tests first (T-01 through T-07, T-15 through T-20, T-29) — fast, no Spring context
  2. Run integration tests (T-08 through T-14, T-21 through T-28) — require @WebMvcTest / @SpringBootTest
  3. Verify CI/CD manually (T-30 through T-32) — after all code is merged

Pass Criteria

  • All 29 automated tests (T-01 through T-29) must pass before merging
  • All 3 manual tests (T-30 through T-32) must pass after CI changes are deployed
  • Zero tolerance on security tests (T-01 through T-14) — any failure is a blocker