- 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
48 KiB
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:
<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)
<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
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 statementcannamanage-service/src/test/resources/bankimport/sample-camt053.xml— CAMT.053 XMLcannamanage-service/src/test/resources/bankimport/sample.csv— CSV bank statementcannamanage-service/src/test/resources/bankimport/malformed.mt940— broken MT940 for negative testscannamanage-service/src/test/resources/bankimport/malformed-truncated.mt940— truncated mid-statementcannamanage-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.MAXcannamanage-service/src/test/resources/bankimport/xxe-attack.xml— XXE payload for security testcannamanage-service/src/test/resources/bankimport/xxe-billion-laughs.xml— billion laughs DoS attackcannamanage-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 |
| 5 | testLedgerImmutability_CannotDeleteEntry |
Direct DELETE attempt |
| 6 | testEurReport_AfterPayments_ReflectsAll |
Full financial → EÜR |
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 |
| 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 |
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 |
| 2 | testFlywayMigration_Idempotent_SecondRunNoOp |
Double-run |
| 3 | testFlywayMigration_ForeignKeys_AllValid |
Constraint validation |
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 |
| 2 | testUnauthenticated_ProtectedEndpoint_Returns401 |
GET /api/members without JWT |
| 3 | testAuthenticated_ProtectedEndpoint_Returns200 |
GET /api/members with valid JWT |
| 4 | testCorsHeaders_PresentOnOptions |
OPTIONS /api/members with Origin |
Phase 6: Coverage Report & CI Enforcement
6.1 JaCoCo Report Verification
After all tests pass, run:
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% minimumde.cannamanage.service(finance-related) — 90% minimumde.cannamanage.api.security— 80% minimumde.cannamanage.service.*(business logic) — 75% minimum
6.3 Gitea Actions CI Integration (Stretch Goal)
File: .gitea/workflows/test.yml
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 |