Files
cannamanage/docs/sprint-11/cannamanage-sprint11-plan.md
Patrick Plate 59b785b8ed
Deploy to Production / test (push) Failing after 1s
Deploy to Production / deploy (push) Has been skipped
test(sprint-11): centralize JaCoCo coverage rules and add bank import + finance test coverage
- pom.xml: introduce risk-tiered JaCoCo rules in parent POM
  - bundle: 80% line coverage
  - bankimport/finance packages: 90% (highest precision)
  - api.security: 85%
  - scheduler/notification: 70%
  - exclude entity/enums/dto/config from coverage measurement
  - add Surefire 3.5.2 plugin management
- cannamanage-service/pom.xml: remove obsolete module-local ComplianceService=100% rule
  (subsumed by parent package rules), add explicit jackson-databind dep so
  ByteBuddy can mock AuditService.METADATA_MAPPER
- Add AbstractServiceTest base class for service-layer tests
- Add FinanceServiceTest
- Add bankimport test suite:
  - Mt940ParserTest with malformed input fixtures
    (encoding, overflow, truncated, generic)
  - PaymentMatchingServiceTest with ParsedTransactionBuilder helper
  - CAMT.053 / Sparkasse MT940 sample fixtures
  - XXE attack fixtures (billion-laughs, SSRF, generic)
- docs/sprint-11/: analysis, plan, plan-review, testplan
2026-06-15 21:37:49 +02:00

803 lines
48 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sprint 11 Implementation Plan — Backend Test Coverage (v3 — Easy Targets Added)
**Date:** 2026-06-15
**Sprint Theme:** Quality Foundation — Backend Test Coverage
**Author:** Patrick Plate / Roo (Architect)
**Status:** Draft v2 (Expanded targets: 80% coverage, ~250 new tests)
**Basis:** cannamanage-sprint11-analysis.md
---
## Phase 1: Test Infrastructure Setup
### 1.1 Add JaCoCo Maven Plugin
**File:** `pom.xml` (parent)
Add the JaCoCo plugin to the `<build><plugins>` section of the parent POM:
```xml
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
<execution>
<id>check</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
<configuration>
<excludes>
<exclude>**/entity/**</exclude>
<exclude>**/enums/**</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/config/**</exclude>
<exclude>**/CannaManageApplication.*</exclude>
</excludes>
</configuration>
</plugin>
```
### 1.2 Add Maven Surefire Parallelization
**File:** `pom.xml` (parent)
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>2</forkCount>
<reuseForks>true</reuseForks>
</configuration>
</plugin>
```
### 1.3 Create Abstract Unit Test Base
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/AbstractServiceTest.java`
```java
package de.cannamanage.service;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.UUID;
@ExtendWith(MockitoExtension.class)
public abstract class AbstractServiceTest {
protected static final UUID TEST_CLUB_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
protected static final UUID TEST_MEMBER_ID = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
protected static final UUID TEST_USER_ID = UUID.fromString("cccccccc-cccc-cccc-cccc-cccccccccccc");
protected static final UUID TEST_STAFF_ID = UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd");
}
```
### 1.4 Create Test Data Fixtures
**Files:**
- `cannamanage-service/src/test/resources/bankimport/sample.mt940` — realistic MT940 file (Sparkasse format)
- `cannamanage-service/src/test/resources/bankimport/sample-real-sparkasse.mt940` — anonymized real Sparkasse statement
- `cannamanage-service/src/test/resources/bankimport/sample-camt053.xml` — CAMT.053 XML
- `cannamanage-service/src/test/resources/bankimport/sample.csv` — CSV bank statement
- `cannamanage-service/src/test/resources/bankimport/malformed.mt940` — broken MT940 for negative tests
- `cannamanage-service/src/test/resources/bankimport/malformed-truncated.mt940` — truncated mid-statement
- `cannamanage-service/src/test/resources/bankimport/malformed-encoding.mt940` — wrong encoding (UTF-8 instead of ISO-8859-1)
- `cannamanage-service/src/test/resources/bankimport/malformed-overflow.mt940` — amounts exceeding Long.MAX
- `cannamanage-service/src/test/resources/bankimport/xxe-attack.xml` — XXE payload for security test
- `cannamanage-service/src/test/resources/bankimport/xxe-billion-laughs.xml` — billion laughs DoS attack
- `cannamanage-service/src/test/resources/bankimport/xxe-ssrf.xml` — SSRF via external entity
---
## Phase 2: P1 — Financial/Compliance Tests (Priority Critical) — ~120 tests
### 2.1 FinanceServiceTest (~18 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/FinanceServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testCreateFeeSchedule_ValidInput_ReturnsSchedule` | Standard fee creation | Schedule persisted with correct fields |
| 2 | `testCreateFeeSchedule_DefaultFlag_ClearsOtherDefaults` | Creating default schedule | Previous default becomes non-default |
| 3 | `testCreateFeeSchedule_NullName_ThrowsException` | Missing required field | IllegalArgumentException |
| 4 | `testCreateFeeSchedule_NegativeAmount_ThrowsException` | Negative fee | IllegalArgumentException |
| 5 | `testCreateFeeSchedule_ZeroAmount_Allowed` | Free tier fee schedule | Schedule with 0 amount persisted |
| 6 | `testRecordPayment_ValidAmount_CreatesPaymentAndLedger` | Normal payment | Payment CONFIRMED, LedgerEntry INCOME created |
| 7 | `testRecordPayment_ZeroAmount_ThrowsException` | Edge case | IllegalArgumentException |
| 8 | `testRecordPayment_MaxIntAmount_Succeeds` | Boundary: Integer.MAX_VALUE cents | Payment created without overflow |
| 9 | `testRecordPayment_NullMemberId_ThrowsException` | Missing member | IllegalArgumentException |
| 10 | `testVoidPayment_ExistingPayment_MarksVoidedAndCreatesReversalLedger` | Voiding | Payment VOIDED, reversal LedgerEntry created |
| 11 | `testVoidPayment_AlreadyVoided_ThrowsException` | Double-void | IllegalStateException |
| 12 | `testVoidPayment_NonExistentId_ThrowsException` | Unknown payment | NotFoundException |
| 13 | `testGetMemberBalance_WithPaymentsAndFees_CalculatesCorrectly` | Balance calculation | Sum of payments - sum of assigned fees |
| 14 | `testGetMemberBalance_NoPayments_ReturnsNegative` | Unpaid fees | Negative balance |
| 15 | `testGetMemberBalance_OddCentSplit_NoRoundingLoss` | 1/3 split: 100 cents ÷ 3 | No cent lost (33+33+34 or similar) |
| 16 | `testGetOutstandingMembers_MixedBalances_ReturnsOnlyNegative` | Outstanding detection | Only members with negative balance |
| 17 | `testRecordExpense_ValidInput_CreatesLedgerEntry` | Expense recording | LedgerEntry EXPENSE type |
| 18 | `testGetLedgerEntries_Pagination_ReturnsPage` | Pagination | Correct page size and order |
### 2.2 PaymentMatchingServiceTest (~22 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/PaymentMatchingServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testMatch_ExactMemberNumber_ScoresAbove90` | Member# in Verwendungszweck | MATCHED status, confidence ≥90 |
| 2 | `testMatch_AmountAndName_ScoresAbove60` | Amount matches + name fuzzy | SUGGESTED status |
| 3 | `testMatch_NoMatch_ScoresBelow60` | Random transaction | UNMATCHED status |
| 4 | `testMatch_IbanExactMatch_AddsPoints` | IBAN field matches member | Higher confidence than without |
| 5 | `testMatch_AmountTolerance20Percent_Matches` | Amount within ±20% | Still scores amount points |
| 6 | `testMatch_AmountExceeds20Percent_NoAmountScore` | Amount off by >20% | Zero amount score component |
| 7 | `testMatch_DoublePaymentSafety_DowngradesToSuggested` | Same member best for 2 txns | Both downgraded to SUGGESTED |
| 8 | `testMatch_GermanUmlauts_NormalizedComparison` | Name "Müller" vs "Mueller" | Recognized as same name |
| 9 | `testMatch_EmptyTransactionList_ReturnsEmpty` | No transactions | Empty result list |
| 10 | `testMatch_NoActiveMembers_AllUnmatched` | Club has no active members | All UNMATCHED |
| 11 | `testMatch_MemberNumberInReference_CaseInsensitive` | "M-001" vs "m-001" | Case-insensitive match |
| 12 | `testMatch_MultipleFeesForMember_UsesClosestAmount` | Member has 2 fee schedules | Closest amount used for scoring |
| 13 | `testMatch_BookingDateContext_UsesCorrectPeriod` | December txns imported in Jan | Fee schedule for December used |
| 14 | `testMatch_EarlyExit_SkipsExpensiveChecks` | Amount off + no member# | Name/IBAN comparison skipped |
| 15 | `testMatch_PartialMemberNumber_NoMatch` | "M-00" (partial) in reference | Not matched as member# |
| 16 | `testMatch_ConcurrentMatching_ThreadSafe` | 10 threads matching simultaneously | No race conditions, consistent results |
| 17 | `testMatch_100Transactions_CompletesUnder1Second` | Performance boundary | <1000ms execution |
| 18 | `testMatch_AmountExactlyAt20PercentBoundary_Included` | Boundary: exactly 20% off | Still scores (inclusive boundary) |
| 19 | `testMatch_AmountJustOver20PercentBoundary_Excluded` | Boundary: 20.01% off | Zero amount score |
| 20 | `testMatch_MemberNumberWithSpaces_Normalized` | "M - 001" in reference | Whitespace stripped before matching |
| 21 | `testMatch_NullReference_NoNpe` | Null Verwendungszweck | UNMATCHED without NPE |
| 22 | `testMatch_EmptyName_NoNameScore` | Blank debtor name | Name component scores 0 |
### 2.3 Mt940ParserTest (~16 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/Mt940ParserTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testParse_ValidFile_ExtractsTransactions` | Standard MT940 | Correct count and fields |
| 2 | `testParse_CenturyBoundary_HandlesYear70Plus` | Year 70 → 1970 | Correct century assignment |
| 3 | `testParse_CenturyBoundary_HandlesYear69Below` | Year 69 → 2069 | Correct century assignment |
| 4 | `testParse_ProprietaryHeaders_SkipsGracefully` | Extra headers before :20: | Parses without error |
| 5 | `testParse_MalformedAmount_ThrowsParseException` | "ABC" as amount | BankStatementParseException |
| 6 | `testParse_MissingMandatoryField_ThrowsParseException` | No :60F: field | BankStatementParseException |
| 7 | `testParse_MultipleStatements_ParsesAll` | File with 3 statements | 3 × n transactions |
| 8 | `testParse_GermanDecimalFormat_HandlesComma` | "1.234,56" | 123456 cents |
| 9 | `testParse_CreditAndDebit_CorrectSign` | C and D indicators | Positive/negative amounts |
| 10 | `testParse_EmptyFile_ReturnsEmptyResult` | Empty input | ParseResult with 0 transactions |
| 11 | `testParse_TruncatedFile_ThrowsParseException` | File cut mid-statement | BankStatementParseException with context |
| 12 | `testParse_WrongEncoding_HandlesGracefully` | UTF-8 with BOM instead of ISO-8859-1 | Either parses or throws meaningful error |
| 13 | `testParse_AmountOverflow_ThrowsParseException` | Amount > Long.MAX_VALUE | BankStatementParseException |
| 14 | `testParse_RealSparkasseFormat_ExtractsCorrectly` | Real anonymized Sparkasse file | All fields populated |
| 15 | `testParse_DateFeb29LeapYear_Parses` | Leap year date 240229 | Valid date |
| 16 | `testParse_DateFeb29NonLeapYear_ThrowsException` | Non-leap year 250229 | BankStatementParseException |
### 2.4 Camt053ParserTest (~14 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/Camt053ParserTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testParse_ValidCamt053_ExtractsEntries` | Standard XML | Correct entries extracted |
| 2 | `testParse_XxeAttack_Rejected` | XXE entity in XML | BankStatementParseException (not file disclosure) |
| 3 | `testParse_XxeBillionLaughs_Rejected` | Billion laughs DoS | BankStatementParseException within 1s |
| 4 | `testParse_XxeSsrf_Rejected` | SSRF via external entity | No HTTP request made |
| 5 | `testParse_WrongNamespace_ThrowsException` | Unknown namespace | BankStatementParseException |
| 6 | `testParse_MultipleNtry_ParsesAll` | 5 Ntry elements | 5 transactions |
| 7 | `testParse_DebitCredit_CorrectSign` | DBIT/CRDT indicators | Correct amount signs |
| 8 | `testParse_MissingOptionalFields_Succeeds` | No RmtInf element | Transaction with null reference |
| 9 | `testParse_LargeFile_PerformanceOk` | 1000 entries | Completes under 2 seconds |
| 10 | `testParse_InvalidXml_ThrowsException` | Broken XML structure | BankStatementParseException |
| 11 | `testParse_EmptyDocument_ThrowsException` | Valid XML but no entries | BankStatementParseException |
| 12 | `testParse_DuplicateEntryId_AcceptsBoth` | Same NtryRef twice | Both transactions returned |
| 13 | `testParse_NegativeAmount_ThrowsException` | Negative Amt value | BankStatementParseException |
| 14 | `testParse_CurrencyNotEur_IncludesCurrency` | USD transactions | Currency field populated |
### 2.5 CsvBankParserTest (~10 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/CsvBankParserTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testParse_StandardCsv_ExtractsRows` | Normal CSV | Correct transaction count |
| 2 | `testParse_GermanNumberFormat_ParsesComma` | "1.234,56" | 123456 cents |
| 3 | `testParse_Iso88591Encoding_HandlesUmlauts` | Müller, Straße | Names preserved correctly |
| 4 | `testParse_CustomColumnMapping_RespectsConfig` | Non-default column order | Fields mapped correctly |
| 5 | `testParse_EmptyCsv_ReturnsEmpty` | Headers only, no data | 0 transactions |
| 6 | `testParse_MissingRequiredColumn_ThrowsException` | No amount column | BankStatementParseException |
| 7 | `testParse_QuotedFieldsWithCommas_ParsesCorrectly` | "Müller, Hans" as name | Name includes comma |
| 8 | `testParse_TrailingNewlines_IgnoresBlankRows` | Extra blank lines | Only valid rows parsed |
| 9 | `testParse_TabSeparated_DetectsDelimiter` | TSV instead of CSV | Auto-detects tab separator |
| 10 | `testParse_BomPrefix_StripsUtf8Bom` | UTF-8 BOM in first byte | First column name parsed correctly |
### 2.6 BankImportServiceTest (~14 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/BankImportServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testUpload_NewFile_CreatesSession` | Valid MT940 upload | Session UPLOADED, SHA-256 stored |
| 2 | `testUpload_DuplicateFile_RejectsDuplicate` | Same SHA-256 twice | Exception (GoBD) |
| 3 | `testParse_UploadedSession_TransitionsToMatched` | Parse after upload | Status → MATCHED |
| 4 | `testConfirm_MatchedTransaction_CreatesPayment` | Admin confirms match | Payment created in FinanceService |
| 5 | `testConfirm_CompletedSession_ThrowsImmutable` | Modify after complete | IllegalStateException (GoBD) |
| 6 | `testComplete_AllConfirmed_SessionCompletes` | All txns resolved | Status → COMPLETED |
| 7 | `testComplete_UnresolvedTransactions_ThrowsException` | Some still UNMATCHED | IllegalStateException |
| 8 | `testReassign_SuggestedTransaction_UpdatesMatch` | Admin picks different member | Match updated, still in session |
| 9 | `testGetSessionHistory_MultipleClubs_OnlyOwn` | Cross-club query | Only own club's sessions |
| 10 | `testSplitTransaction_SingleTxn_CreatesTwo` | Split payment | Two txns summing to original |
| 11 | `testUpload_ConcurrentSameFile_OnlyOneSucceeds` | Race condition: same file | One session, one DuplicateException |
| 12 | `testConfirm_ThenUnconfirm_ReversesPayment` | Undo confirmation | Payment voided, status reverts |
| 13 | `testSessionExpiry_OldUploadedSession_CanStillBeProcessed` | Session created 30 days ago | No timeout on processing |
| 14 | `testUpload_EmptyFile_ThrowsException` | Zero-byte file | BankStatementParseException |
### 2.7 RetentionServiceTest (~10 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/RetentionServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testAnonymize_ExpiredMember_AnonymizesFields` | Retention period passed | Name/email/phone = "ANONYMIZED" |
| 2 | `testAnonymize_ActiveMember_Skipped` | Still active | No changes |
| 3 | `testAnonymize_RecentlyLeft_NotYetEligible` | Left 6 months ago (< retention) | No changes |
| 4 | `testDryRun_ReturnsCountWithoutChanging` | Dry-run mode | Count returned, DB unchanged |
| 5 | `testRetentionRules_DifferentCategories_DifferentPeriods` | Financial vs personal | 10y vs 3y retention |
| 6 | `testAnonymize_PreservesAuditLog` | After anonymization | AuditEvent still references member UUID |
| 7 | `testAnonymize_DistributionRecords_AmountsPreserved` | Financial data | Amounts kept, personal data removed |
| 8 | `testScheduledRun_ProcessesAllClubs` | Scheduler trigger | All eligible clubs processed |
| 9 | `testAnonymize_MemberLeftExactlyAtBoundary_NotEligible` | Left exactly 3 years ago today | Not yet eligible (must be >3y) |
| 10 | `testAnonymize_ConcurrentRun_Idempotent` | Two triggers at same time | No double-anonymization |
### 2.8 ReportGeneratorServiceTest (~8 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/ReportGeneratorServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testDispatch_ValidType_CallsCorrectGenerator` | MEMBER_LIST type | MemberListRegistryGenerator invoked |
| 2 | `testDispatch_UnknownType_ThrowsException` | Invalid type | IllegalArgumentException |
| 3 | `testDispatch_PdfFormat_ReturnsPdfBytes` | PDF format | Non-empty byte array, PDF magic bytes |
| 4 | `testDispatch_CsvFormat_ReturnsCsvString` | CSV format | Valid CSV content |
| 5 | `testDispatch_Concurrent_RateLimited` | 10 simultaneous requests | Only N proceed, rest rejected |
| 6 | `testDispatch_StoresGeneratedReport` | Any successful generation | GeneratedReport entity persisted |
| 7 | `testDispatch_NullClubId_ThrowsException` | Missing club context | IllegalArgumentException |
| 8 | `testDispatch_EveryReportType_HasGenerator` | Each enum value in ReportType | No IllegalArgumentException for any type |
### 2.9 EurReportGeneratorTest (~8 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/report/EurReportGeneratorTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testGenerate_WithIncomeAndExpenses_CorrectSums` | Mixed ledger | Income - Expenses = Profit/Loss |
| 2 | `testGenerate_EmptyLedger_ZeroSums` | No entries in period | All zeros |
| 3 | `testGenerate_CrossYearEntries_OnlyRequestedYear` | Multi-year data | Only specified year included |
| 4 | `testGenerate_CategorizedExpenses_GroupedCorrectly` | Multiple expense categories | Grouped by ExpenseCategory |
| 5 | `testGenerate_VoidedPayments_ExcludeFromSums` | Voided entries | Not counted in totals |
| 6 | `testGenerate_PdfOutput_ValidPdf` | PDF format | Starts with %PDF- magic bytes |
| 7 | `testGenerate_LargeDataset_1000Entries_CompletesUnder3s` | Performance | <3000ms |
| 8 | `testGenerate_SingleCentEntry_NoRoundingError` | 1 cent income | Shows as 0,01 € |
### 2.10 AnnualAuthorityReportGeneratorTest (~8 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/report/AnnualAuthorityReportGeneratorTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testGenerate_FullYear_IncludesAllDistributions` | Year of data | All distributions in report |
| 2 | `testGenerate_PerMemberTotals_CorrectAggregation` | Multiple distributions | Summed per member correctly |
| 3 | `testGenerate_ThcCbdBreakdown_Present` | THC/CBD data | THC and CBD columns populated |
| 4 | `testGenerate_InactiveMembersIncluded` | Left during year | Still in annual report |
| 5 | `testGenerate_EmptyYear_ProducesHeaderOnly` | No distributions | Report with headers but no data rows |
| 6 | `testGenerate_U21MembersMarked` | Members under 21 | U21 flag present in report |
| 7 | `testGenerate_QuotaBreachHighlighted` | Member exceeds 25g/day | Highlighted in report |
| 8 | `testGenerate_DecemberDistributionInCorrectYear` | Dec 31 distribution | Included in correct reporting year |
### 2.11 BankStatementParserServiceTest (~6 test methods) — NEW
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/BankStatementParserServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testDetectFormat_Mt940Extension_UsesMt940Parser` | file.mt940 | MT940 parser selected |
| 2 | `testDetectFormat_XmlExtension_UsesCamt053Parser` | file.xml | CAMT053 parser selected |
| 3 | `testDetectFormat_CsvExtension_UsesCsvParser` | file.csv | CSV parser selected |
| 4 | `testDetectFormat_UnknownExtension_ThrowsException` | file.pdf | UnsupportedFormatException |
| 5 | `testParse_DelegatesToCorrectParser_Mt940` | MT940 content | Mt940Parser.parse() called |
| 6 | `testParse_DelegatesToCorrectParser_Camt053` | CAMT XML content | Camt053Parser.parse() called |
### 2.12 ComplianceServiceTest Extensions (~10 test methods) — NEW
**File:** Extend existing `cannamanage-service/src/test/java/de/cannamanage/service/ComplianceServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testQuota_ExactlyAt25g_Allowed` | Boundary: exactly 25g daily | No violation |
| 2 | `testQuota_25g01_Rejected` | Boundary: 25.01g | QuotaExceededException |
| 3 | `testQuota_Monthly50g_ExactBoundary_Allowed` | Exactly 50g monthly | No violation |
| 4 | `testQuota_Monthly50g01_Rejected` | 50.01g monthly | QuotaExceededException |
| 5 | `testQuota_U21_10gDaily_Boundary` | U21 member at 10g | No violation |
| 6 | `testQuota_U21_10g01_Rejected` | U21 member at 10.01g | QuotaExceededException |
| 7 | `testQuota_U21_ThcLimit15Pct_Boundary` | THC exactly 15% for U21 | No violation |
| 8 | `testQuota_U21_ThcLimit15Pct01_Rejected` | THC 15.01% for U21 | QuotaExceededException |
| 9 | `testQuota_MultipleSameDayDistributions_Cumulative` | 3 distributions same day | Cumulative check |
| 10 | `testQuota_CrossMidnight_ResetsDailyCounter` | 23:59 + 00:01 | Independent days |
### 2.13 Report Generators Batch (~30 test methods) — NEW
Easy-win coverage for 14 report generator classes in `cannamanage-service/src/main/java/de/cannamanage/service/report/`. Each generator follows the same pattern: `ReportGenerator<P extends ReportParameters>` with a `generate(Club, P)` method producing PDF/CSV bytes. Tests verify (a) successful generation with valid inputs, (b) handling of empty data sets, (c) PDF/CSV header integrity where applicable.
**Files:** New test classes under `cannamanage-service/src/test/java/de/cannamanage/service/report/`
| # | Test Class | # Tests | Key Scenarios |
|---|------------|---------|---------------|
| 1 | `KassenbuchExportGeneratorTest` | 3 | Valid year, empty year, GoBD column order |
| 2 | `DistributionLogGeneratorTest` | 2 | Valid range, empty range |
| 3 | `DestructionProtocolGeneratorTest` | 2 | Valid records, no records → empty report |
| 4 | `TransportCertificateGeneratorTest` | 2 | Valid transport, INVALID status rejected |
| 5 | `BeitragsbescheinigungGeneratorTest` | 2 | Member with payments, member without payments |
| 6 | `BestandsfuehrungGeneratorTest` | 2 | Valid stock snapshot, empty stock |
| 7 | `BoardChangeGeneratorTest` | 2 | Term change captured, no changes → empty report |
| 8 | `PreventionActivityReportGeneratorTest` | 2 | Activity present, no activities → empty PDF |
| 9 | `DsfaReportGeneratorTest` | 2 | DSFA template renders, custom risks list |
| 10 | `TomReportGeneratorTest` | 2 | TOM categories rendered, control measures section present |
| 11 | `VvtReportGeneratorTest` | 2 | Processing activities listed, legal basis column populated |
| 12 | `LoeschkonzeptGeneratorTest` | 2 | Retention categories rendered, retention period column correct |
| 13 | `BreachNotificationGeneratorTest` | 2 | Severity HIGH renders, severity LOW renders |
| 14 | `AuthorityExportServiceTest` | 3 | Format=CSV, format=XML, invalid format rejected |
All tests share a common pattern using `AbstractReportGeneratorTest` base class (new in Phase 1) which provides a stub `Club`, helper `assertPdfHasHeader(byte[], String)` and `assertCsvHasColumns(byte[], String...)`.
### 2.14 Simple CRUD Service Tests (~18 test methods) — NEW
Coverage for thin CRUD-style services with minimal logic. Each test covers basic CRUD operations + tenant isolation + happy-path validation.
**Files:** New test classes under `cannamanage-service/src/test/java/de/cannamanage/service/`
| # | Test Class | # Tests | Key Scenarios |
|---|------------|---------|---------------|
| 1 | `ConsentServiceTest` | 4 | record consent, revoke consent, query active consents, tenant isolation |
| 2 | `AuditServiceTest` | 4 | log entry created, query by entity, query by date range, immutability check |
| 3 | `NotificationPreferenceServiceTest` | 3 | default prefs created, update prefs, query prefs |
| 4 | `DsgvoServiceTest` | 4 | export personal data, anonymize member, delete data subject, generate DSGVO report |
| 5 | `ComplianceDashboardServiceTest` | 3 | aggregate metrics, upcoming deadlines, overdue items |
---
## Phase 3: P2 — Core Business Logic Tests — ~82 tests
### 3.1 AssemblyServiceTest (~20 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/AssemblyServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testCreate_ValidInput_ReturnsAssembly` | Standard creation | Assembly PLANNED status |
| 2 | `testQuorumCheck_50PctPresent_HasQuorum` | Majority present | quorumReached = true |
| 3 | `testQuorumCheck_LessThan50Pct_NoQuorum` | Minority present | quorumReached = false |
| 4 | `testQuorumCheck_ExactlyHalf_NoQuorum` | 50% exactly (not majority) | quorumReached = false |
| 5 | `testVote_SimpleMajority_Passes` | >50% YES (VoteType.SIMPLE) | VoteResult.PASSED |
| 6 | `testVote_SimpleMajority_Fails` | ≤50% YES (VoteType.SIMPLE) | VoteResult.FAILED |
| 7 | `testVote_TwoThirdsMajority_RequiresHigherBar` | VoteType.TWO_THIRDS, 66% | VoteResult.FAILED (need 67%+) |
| 8 | `testVote_TwoThirdsMajority_ExactThreshold_Passes` | VoteType.TWO_THIRDS, exactly 67% | VoteResult.PASSED |
| 9 | `testVote_Unanimous_AllYes_Passes` | VoteType.UNANIMOUS, 100% | VoteResult.PASSED |
| 10 | `testVote_Unanimous_OneNo_Fails` | VoteType.UNANIMOUS, 99% yes | VoteResult.FAILED |
| 11 | `testVote_SecretBallot_NoVoterTracking` | VoteType with secret flag | No voter→choice link stored |
| 12 | `testVote_MemberNotAttending_CannotVote` | Non-attendee votes | Exception |
| 13 | `testVote_AlreadyVoted_CannotVoteAgain` | Double vote | Exception |
| 14 | `testVote_Abstention_DoesNotCountTowardsTotal` | ABSTAIN vote | Not counted in yes/no ratio |
| 15 | `testBoardTermTracking_TermExpired_FlaggedForElection` | Past end date | Term status EXPIRED |
| 16 | `testAddAgendaItem_DuringAssembly_Allowed` | Runtime addition | Item added with sequence# |
| 17 | `testComplete_WithProtocol_GeneratesPdf` | Assembly completion | PDF protocol generated |
| 18 | `testAttend_MaxCapacity_Rejected` | Optional max limit | CapacityExceededException |
| 19 | `testVote_AfterAssemblyCompleted_Rejected` | Vote on completed assembly | IllegalStateException |
| 20 | `testCancel_PlannedAssembly_NotifiesInvitees` | Cancel event | All invitees notified |
### 3.2 EventServiceTest (~14 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/EventServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testCreate_ValidEvent_Persists` | Standard creation | Event saved |
| 2 | `testRsvp_UnderMaxAttendees_Accepted` | Space available | RSVP CONFIRMED |
| 3 | `testRsvp_AtMaxAttendees_Waitlisted` | Full event | RSVP WAITLISTED |
| 4 | `testRsvp_CancelFreesSeat_WaitlistPromoted` | Cancel then promote | Next waitlisted → CONFIRMED |
| 5 | `testRecurring_Weekly_ExpandsCorrectly` | RecurrenceRule.WEEKLY, 4 weeks | 4 event instances |
| 6 | `testRecurring_Monthly_LastDay_HandlesShortMonths` | Monthly on 31st | Feb gets 28th/29th |
| 7 | `testRecurring_Biweekly_CorrectDates` | RecurrenceRule.BIWEEKLY | Every 14 days |
| 8 | `testRecurring_DstTransition_CorrectTime` | Summer→Winter time | Event stays at local time |
| 9 | `testIcalGeneration_ValidEvent_ReturnsIcalString` | iCal export | Valid VCALENDAR/VEVENT |
| 10 | `testDelete_WithRsvps_NotifiesAttendees` | Delete booked event | Notification sent |
| 11 | `testRsvp_SameMemberTwice_Idempotent` | Double RSVP | Only one confirmation |
| 12 | `testRecurring_CancelSingle_OthersPersist` | Cancel one instance | Other instances unchanged |
| 13 | `testRecurring_EditSeries_UpdatesAllFuture` | Edit series | Future instances updated |
| 14 | `testCreate_PastDate_ThrowsException` | Start date in past | IllegalArgumentException |
### 3.3 ForumServiceTest (~14 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/ForumServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testCreateTopic_ValidInput_Persists` | New topic | Topic saved with createdAt |
| 2 | `testEditTopic_WithinEditWindow_Succeeds` | Edit within 15min | Content updated |
| 3 | `testEditTopic_AfterEditWindow_Rejected` | Edit after 15min | Exception |
| 4 | `testEditTopic_ExactlyAtBoundary_Succeeds` | Edit at 14:59 | Still allowed |
| 5 | `testEditTopic_OneSecondAfterBoundary_Rejected` | Edit at 15:01 | Exception |
| 6 | `testReaction_Toggle_AddsThenRemoves` | React twice | First adds, second removes |
| 7 | `testReaction_DifferentTypes_Coexist` | LIKE + HELPFUL on same post | Both stored independently |
| 8 | `testReaction_SameTypeTwice_Idempotent` | LIKE → LIKE → LIKE | Toggle: add, remove, add |
| 9 | `testReport_NewTopic_CreatesReport` | Report content | ForumReport PENDING |
| 10 | `testReport_SameMemberTwice_OnlyOneReport` | Duplicate report | Idempotent |
| 11 | `testReply_ToExistingTopic_IncrementsCount` | Add reply | Topic replyCount + 1 |
| 12 | `testDelete_ByAuthor_SoftDeletes` | Author deletes | deletedAt set, content hidden |
| 13 | `testDelete_ByModerator_SoftDeletes` | Moderator removes | deletedAt set with moderator flag |
| 14 | `testReply_ToDeletedTopic_ThrowsException` | Reply to soft-deleted | Exception |
### 3.4 InfoBoardServiceTest (~8 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/InfoBoardServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testCreate_ValidPost_Persists` | New post | Post saved |
| 2 | `testPin_Post_SetsFlag` | Pin action | isPinned = true |
| 3 | `testArchive_Post_SetsArchivedAt` | Archive action | archivedAt set |
| 4 | `testMarkRead_NewPost_TracksReading` | Read tracking | PostReadStatus created |
| 5 | `testMarkRead_AlreadyRead_Idempotent` | Double read | No duplicate entry |
| 6 | `testGetUnread_ReturnsOnlyNew` | Mixed read/unread | Only unread posts returned |
| 7 | `testPin_AlreadyPinned_Idempotent` | Double pin | No error, still pinned |
| 8 | `testGetAll_OrderedByPinnedFirst_ThenDate` | Mixed pinned/unpinned | Pinned first, then by createdAt desc |
### 3.5 NotificationDispatchServiceTest (~10 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/NotificationDispatchServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testDispatch_EmailEnabled_SendsEmail` | User prefers email | EmailService called |
| 2 | `testDispatch_PushEnabled_SendsPush` | User prefers push | PushSender called |
| 3 | `testDispatch_AllChannelsDisabled_OnlyInApp` | Opt-out all | Only in-app notification |
| 4 | `testDispatch_MultipleRecipients_FansOut` | 10 recipients | 10 notifications created |
| 5 | `testDispatch_PreferenceFiltering_RespectsType` | User disables EVENT type | No notification for events |
| 6 | `testDispatch_WebSocketPublish_AlwaysFires` | Any notification | WebSocket event published |
| 7 | `testDispatch_EmailFailure_DoesNotBlockPush` | Email throws | Push still sent, in-app still created |
| 8 | `testDispatch_NullRecipientList_ThrowsException` | No recipients | IllegalArgumentException |
| 9 | `testDispatch_DeduplicatesSameRecipient` | Same user listed twice | Only one notification |
| 10 | `testDispatch_Concurrent100Recipients_NoDeadlock` | Mass notify | All 100 created without timeout |
### 3.6 GrowCalendarServiceTest (~4 test methods) — NEW
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/GrowCalendarServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testScheduleHarvest_FutureDates_Persists` | Valid harvest window | Entry saved |
| 2 | `testScheduleHarvest_PastDate_Rejected` | Date already passed | IllegalArgumentException |
| 3 | `testGetUpcoming_OnlyFuture_Returned` | Mix past/future | Only future entries |
| 4 | `testDelete_OwnEntry_Succeeds` | Creator deletes | Entry removed |
### 3.7 Schedulers Batch (~12 test methods) — NEW
Coverage for 4 `@Scheduled` services. Tests verify (a) scheduler triggers correctly with deterministic fixed clock, (b) handles empty input gracefully, (c) idempotency on re-execution.
**Files:** New test classes under `cannamanage-service/src/test/java/de/cannamanage/service/`
| # | Test Class | # Tests | Key Scenarios |
|---|------------|---------|---------------|
| 1 | `BoardTermSchedulerTest` | 3 | Expired terms flagged, future terms ignored, no terms → no-op |
| 2 | `EventReminderSchedulerTest` | 3 | Events tomorrow → reminder sent, events today not duplicated, no events → no-op |
| 3 | `PaymentReminderSchedulerTest` | 3 | Overdue payments → reminder, not-yet-due ignored, already-reminded skipped |
| 4 | `TokenCleanupSchedulerTest` (extensions) | 3 | Expired tokens removed, active tokens preserved, idempotent re-run |
All scheduler tests use `Clock.fixed(...)` injection to make `LocalDate.now()` deterministic.
---
## Phase 4: P3 — Security & Infrastructure Tests — ~46 tests
### 4.1 JwtServiceTest (~12 test methods)
**File:** `cannamanage-api/src/test/java/de/cannamanage/api/security/JwtServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testGenerateToken_ValidUser_ReturnsJwt` | Normal user | Non-null JWT string |
| 2 | `testValidateToken_ValidToken_ReturnsTrue` | Fresh token | true |
| 3 | `testValidateToken_ExpiredToken_ReturnsFalse` | Token past expiry | false |
| 4 | `testValidateToken_TamperedSignature_ReturnsFalse` | Modified token | false |
| 5 | `testValidateToken_TamperedPayload_ReturnsFalse` | Payload modified, signature untouched | false |
| 6 | `testValidateToken_NoneAlgorithm_ReturnsFalse` | alg:none attack | false |
| 7 | `testValidateToken_WrongSigningKey_ReturnsFalse` | Signed with different key | false |
| 8 | `testExtractUsername_ValidToken_ReturnsSubject` | Token with subject | Correct username |
| 9 | `testRefreshToken_ValidRefresh_ReturnsNewAccess` | Valid refresh token | New access token |
| 10 | `testRefreshToken_ExpiredRefresh_ThrowsException` | Expired refresh | Exception |
| 11 | `testGenerateToken_IncludesRoleClaim` | User with role | Role in token claims |
| 12 | `testValidateToken_EmptyString_ReturnsFalse` | Empty/null token | false without exception |
### 4.2 LoginRateLimiterTest (~8 test methods)
**File:** `cannamanage-api/src/test/java/de/cannamanage/api/security/LoginRateLimiterTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testAllow_UnderLimit_Permits` | 1st attempt | true |
| 2 | `testAllow_AtLimit_Blocks` | 6th attempt (limit=5) | false |
| 3 | `testAllow_ExactlyAtLimit_StillAllowed` | 5th attempt (limit=5) | true (boundary) |
| 4 | `testAllow_OneOverLimit_Blocked` | 6th attempt | false (boundary) |
| 5 | `testReset_AfterBlock_AllowsAgain` | Successful login resets | true again |
| 6 | `testAllow_DifferentIps_IndependentCounters` | IP-A blocked, IP-B ok | IP-B still allowed |
| 7 | `testAllow_AfterWindowExpires_AllowsAgain` | Wait for window reset | true |
| 8 | `testAllow_IpV6_HandledCorrectly` | IPv6 address as key | Independent counter |
### 4.3 TenantFilterAspectTest (~8 test methods)
**File:** `cannamanage-api/src/test/java/de/cannamanage/api/security/TenantFilterAspectTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testFilter_MatchingClubId_Passes` | User's own club data | Data returned |
| 2 | `testFilter_DifferentClubId_Blocked` | Other club's data | AccessDeniedException |
| 3 | `testFilter_SystemAdmin_BypassesFilter` | SYSTEM_ADMIN role | Data returned regardless |
| 4 | `testFilter_NullClubId_Blocked` | No club context | AccessDeniedException |
| 5 | `testFilter_FinanceController_EnforcedOnAllEndpoints` | /api/finance/* cross-club | AccessDeniedException |
| 6 | `testFilter_MemberController_EnforcedOnAllEndpoints` | /api/members/* cross-club | AccessDeniedException |
| 7 | `testFilter_DocumentController_EnforcedOnAllEndpoints` | /api/documents/* cross-club | AccessDeniedException |
| 8 | `testFilter_ReportController_EnforcedOnAllEndpoints` | /api/reports/* cross-club | AccessDeniedException |
### 4.4 DocumentServiceTest (~12 test methods)
**File:** `cannamanage-service/src/test/java/de/cannamanage/service/DocumentServiceTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testUpload_ValidFile_Persists` | Normal PDF upload | Document entity created |
| 2 | `testUpload_FilenameSanitization_RemovesPathTraversal` | "../../../etc/passwd" | Sanitized to "etc_passwd" |
| 3 | `testUpload_FilenameSanitization_RemovesBackslash` | "..\\..\\windows\\system" | Sanitized |
| 4 | `testUpload_FilenameSanitization_NullBytes` | "file%00.pdf" | Null bytes stripped |
| 5 | `testUpload_FilenameSanitization_UnicodeSlash` | Unicode path separators | Stripped |
| 6 | `testUpload_ExceedsSizeLimit_Rejected` | 51MB file (limit=50MB) | FileTooLargeException |
| 7 | `testUpload_ExactlyAtSizeLimit_Allowed` | 50MB file (boundary) | Upload succeeds |
| 8 | `testUpload_ZeroByteFile_Rejected` | Empty file | IllegalArgumentException |
| 9 | `testDownload_OwnClub_Allowed` | Tenant match | File bytes returned |
| 10 | `testDownload_OtherClub_Denied` | Tenant mismatch | AccessDeniedException |
| 11 | `testDelete_NonOwner_Denied` | Different user | AccessDeniedException |
| 12 | `testUpload_DangerousExtension_Rejected` | .exe, .sh, .bat | InvalidFileTypeException |
### 4.5 GlobalExceptionHandlerTest (~6 test methods) — NEW
**File:** `cannamanage-api/src/test/java/de/cannamanage/api/exception/GlobalExceptionHandlerTest.java`
| # | Method | Scenario | Expected |
|---|--------|----------|----------|
| 1 | `testHandleAccessDenied_Returns403_WithSafeBody` | AccessDeniedException thrown | 403, no stack trace in body |
| 2 | `testHandleValidation_Returns400_WithFieldErrors` | MethodArgumentNotValidException | 400, field error map |
| 3 | `testHandleQuotaExceeded_Returns422_WithCode` | QuotaExceededException | 422, error code in body |
| 4 | `testHandleEntityNotFound_Returns404` | EntityNotFoundException | 404, generic message |
| 5 | `testHandleGenericException_Returns500_NoInternalDetails` | RuntimeException | 500, no stack trace leaked |
| 6 | `testHandleMaxUploadSize_Returns413` | MaxUploadSizeExceededException | 413 Payload Too Large |
Tests use `MockMvc` standalone setup with `@ControllerAdvice` registered.
---
## Phase 5: P4 — Integration Tests — ~29 tests
### 5.1 BankImportIntegrationTest (~7 test methods)
**File:** `cannamanage-api/src/test/java/de/cannamanage/api/integration/BankImportIntegrationTest.java`
Extends `AbstractIntegrationTest`. Full HTTP flow with real PostgreSQL.
| # | Method | Scenario |
|---|--------|----------|
| 1 | `testFullFlow_UploadMt940_MatchConfirmComplete` | Upload → parse → confirm → complete |
| 2 | `testDuplicateUpload_SameFile_Rejected` | SHA-256 duplicate detection |
| 3 | `testImmutability_CompleteSessionCannotBeModified` | GoBD enforcement |
| 4 | `testReassign_ChangeMatch_UpdatesTransaction` | Manual match correction |
| 5 | `testConcurrentUpload_SameFile_OnlyOneSucceeds` | Race condition handling |
| 6 | `testFullFlow_Camt053_MatchConfirmComplete` | CAMT053 variant of flow 1 |
| 7 | `testSplit_ThenConfirmBoth_SessionCompletes` | Split + confirm lifecycle |
### 5.2 FinanceIntegrationTest (~6 test methods)
**File:** `cannamanage-api/src/test/java/de/cannamanage/api/integration/FinanceIntegrationTest.java`
| # | Method | Scenario |
|---|--------|----------|
| 1 | `testFullFlow_CreateFee_AssignMember_RecordPayment_CheckBalance` | Fee → Assign → Pay → Balance |
| 2 | `testVoidPayment_ReversesLedgerEntry` | Record + Void + verify Kassenbuch |
| 3 | `testOutstandingMembers_CorrectFiltering` | Multiple members, mixed status |
| 4 | `testConcurrentPayments_SameMember_CorrectBalance` | 5 simultaneous payments | Balance reflects all 5 |
| 5 | `testLedgerImmutability_CannotDeleteEntry` | Direct DELETE attempt | Rejected/forbidden |
| 6 | `testEurReport_AfterPayments_ReflectsAll` | Full financial → EÜR | Report totals match |
### 5.3 AssemblyIntegrationTest (~5 test methods)
**File:** `cannamanage-api/src/test/java/de/cannamanage/api/integration/AssemblyIntegrationTest.java`
| # | Method | Scenario |
|---|--------|----------|
| 1 | `testFullFlow_Create_Invite_Attend_Vote_Complete` | Full assembly lifecycle |
| 2 | `testQuorum_InsufficientAttendees_VoteBlocked` | Quorum enforcement via API |
| 3 | `testProtocolPdf_AfterComplete_Downloadable` | PDF generation + download |
| 4 | `testConcurrentVotes_AllCounted` | 20 members vote simultaneously | All votes registered |
| 5 | `testComplete_GeneratesProtocol_ThenImmutable` | Complete → no more edits |
### 5.4 ReportIntegrationTest Extensions (~4 test methods)
**File:** Extend existing `cannamanage-api/src/test/java/de/cannamanage/api/integration/ReportIntegrationTest.java`
| # | Method | Scenario |
|---|--------|----------|
| 1 | `testEurReport_GenerateAndDownload_ValidPdf` | EÜR PDF download |
| 2 | `testAnnualAuthority_GenerateAndDownload_ValidPdf` | Authority report PDF |
| 3 | `testMemberList_GenerateAndDownload_ValidCsv` | Member list CSV |
| 4 | `testConcurrentReportGeneration_NoMixup` | 3 reports simultaneously | Each correct |
### 5.5 MigrationIntegrationTest (~3 test methods) — NEW
**File:** `cannamanage-api/src/test/java/de/cannamanage/api/integration/MigrationIntegrationTest.java`
| # | Method | Scenario |
|---|--------|----------|
| 1 | `testFlywayMigration_AllMigrationsApply_NoErrors` | Fresh DB + all migrations | Schema valid |
| 2 | `testFlywayMigration_Idempotent_SecondRunNoOp` | Double-run | No failures |
| 3 | `testFlywayMigration_ForeignKeys_AllValid` | Constraint validation | No orphan FKs |
### 5.6 SecurityConfigIntegrationTest (~4 test methods) — NEW
**File:** `cannamanage-api/src/test/java/de/cannamanage/api/integration/SecurityConfigIntegrationTest.java`
Extends `AbstractIntegrationTest`. Verifies Spring Security filter chain end-to-end.
| # | Method | Scenario |
|---|--------|----------|
| 1 | `testUnauthenticated_PublicEndpoint_Allowed` | GET /actuator/health | 200 OK |
| 2 | `testUnauthenticated_ProtectedEndpoint_Returns401` | GET /api/members without JWT | 401 |
| 3 | `testAuthenticated_ProtectedEndpoint_Returns200` | GET /api/members with valid JWT | 200 |
| 4 | `testCorsHeaders_PresentOnOptions` | OPTIONS /api/members with Origin | CORS headers present |
---
## Phase 6: Coverage Report & CI Enforcement
### 6.1 JaCoCo Report Verification
After all tests pass, run:
```bash
mvn verify -pl cannamanage-service,cannamanage-api
```
Expected output: HTML report at `target/site/jacoco/index.html`
### 6.2 Coverage Enforcement Configuration
The JaCoCo `check` goal (configured in Phase 1) will fail the build if:
- Overall line coverage < **80%**
Per-package enforcement:
- `de.cannamanage.service.bankimport`**90% minimum**
- `de.cannamanage.service` (finance-related) — **90% minimum**
- `de.cannamanage.api.security`**80% minimum**
- `de.cannamanage.service.*` (business logic) — **75% minimum**
### 6.3 Gitea Actions CI Integration (Stretch Goal)
**File:** `.gitea/workflows/test.yml`
```yaml
name: Test & Coverage
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: cannamanage_test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- run: mvn verify --batch-mode
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: '**/target/site/jacoco/'
```
---
## Summary: Test Count by Phase (v3 — Easy Targets Added)
| Phase | New Unit Tests | New Integration Tests | Total |
|-------|---------------|----------------------|-------|
| Phase 1 (Infrastructure) | 0 | 0 | 0 |
| Phase 2 (Financial/Compliance + Reports + CRUD) | ~168 | 0 | 168 |
| Phase 3 (Core Business + Schedulers) | ~82 | 0 | 82 |
| Phase 4 (Security/Infra + GlobalExceptionHandler) | ~46 | 0 | 46 |
| Phase 5 (Integration + SecurityConfig) | 0 | ~29 | 29 |
| Phase 6 (CI) | 0 | 0 | 0 |
| **Total New** | **~296** | **~29** | **~325** |
Combined with existing 9 unit tests + 11 integration tests = **~345 backend tests total** (from current ~20).
### v2 → v3 Delta (+70 easy-win tests)
| Section | Count | Rationale |
|---------|------:|-----------|
| 2.13 Report Generators Batch (14 classes) | +30 | Each generator is small and follows identical pattern — cheap coverage |
| 2.14 Simple CRUD Services (5 services) | +18 | Thin services with little logic — easy to mock and verify |
| 3.7 Schedulers Batch (4 schedulers) | +12 | Pure `@Scheduled` methods — deterministic with `Clock.fixed()` |
| 4.5 GlobalExceptionHandler | +6 | Critical for security (no stack-trace leakage) but trivial to test |
| 5.6 SecurityConfigIntegrationTest | +4 | Single end-to-end happy/sad path verification |
| **Total Delta** | **+70** | |
---
## Coverage Targets (v3)
| Category | Target | v2 | v1 |
|----------|--------|----|----|
| Overall line coverage | **≥80%** (now realistically achievable with +70 easy wins) | 80% | 60% |
| Financial (FinanceService, BankImport, Parsers, Matching) | **≥90%** | 90% | 80% |
| Compliance (Retention, ComplianceService, Reports) | **≥90%** (boosted by 14 generator tests) | 90% | 80% |
| Security (JWT, RateLimiter, Tenant, Document, GlobalExceptionHandler) | **≥85%** | 80% | 75% |
| Core Business (Assembly, Events, Forum, InfoBoard) | **≥75%** | 75% | 60% |
| Infrastructure (Notifications, Schedulers) | **≥70%** (boosted by 4 scheduler tests) | 60% | 50% |
---
## Key Expansions from v1 → v2
| Area | v1 | v2 | What was added |
|------|----|----|----------------|
| PaymentMatchingServiceTest | 15 tests | 22 tests | Concurrent, boundary conditions (20% tolerance edge), null handling |
| Mt940ParserTest | 10 tests | 16 tests | Truncated file, wrong encoding, overflow, real Sparkasse, leap year |
| Camt053ParserTest | 8 tests | 14 tests | Billion laughs, SSRF, empty doc, duplicate ID, negative amount, currency |
| CsvBankParserTest | 6 tests | 10 tests | Quoted fields, trailing newlines, tab-separated, BOM |
| BankImportServiceTest | 10 tests | 14 tests | Concurrent upload, undo confirm, session expiry, empty file |
| AssemblyServiceTest | 12 tests | 20 tests | Every VoteType, abstention, secret ballot, cancel, boundary quorum |
| EventServiceTest | 8 tests | 14 tests | DST transition, biweekly, cancel single recurring, idempotent RSVP |
| ForumServiceTest | 8 tests | 14 tests | Edit window boundary (exact second), reaction types, moderator delete |
| JwtServiceTest | 8 tests | 12 tests | None algorithm, wrong key, tampered payload, empty string |
| LoginRateLimiterTest | 5 tests | 8 tests | Boundary (exactly at/over limit), IPv6 |
| TenantFilterAspectTest | 4 tests | 8 tests | Cross-tenant on every controller type |
| DocumentServiceTest | 6 tests | 12 tests | Every filename attack vector (backslash, null byte, unicode, extension) |
| Integration tests | 12 tests | 25 tests | Concurrent ops, migration verification, full lifecycles |
| **NEW: ComplianceServiceTest extensions** | 0 | 10 | Every quota boundary (daily, monthly, U21, THC%) |
| **NEW: BankStatementParserServiceTest** | 0 | 6 | Format detection and delegation |
| **NEW: MigrationIntegrationTest** | 0 | 3 | Flyway migration verification |