# 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 `` section of the parent POM: ```xml org.jacoco jacoco-maven-plugin 0.8.12 prepare-agent prepare-agent report verify report check verify check BUNDLE LINE COVEREDRATIO 0.80 **/entity/** **/enums/** **/dto/** **/config/** **/CannaManageApplication.* ``` ### 1.2 Add Maven Surefire Parallelization **File:** `pom.xml` (parent) ```xml org.apache.maven.plugins maven-surefire-plugin 2 true ``` ### 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

` 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 |