Athena — bdd-coverage-audit.md

BDD Coverage Audit — Athena

Date: 2026-09-02
Auditor: swarm agent athena-bdd-coverage-audit
Scope: All features listed in the task directive
Test runner: JUnit 5 + Spring MockMvc (backend); Jasmine/Karma (frontend)
BDD runner note: The project does not currently use a Cucumber/Gherkin runner. pom.xml contains no Cucumber dependency. Adding one would be a significant stack change mid-project (requires .feature files, step definitions, runner configuration, and parallel test infrastructure changes). This audit therefore uses given-when-then structured JUnit/MockMvc tests — the existing testing infrastructure — rather than introducing a Cucumber runner. All new tests in this audit follow this pattern.


Legend

| Symbol | Meaning | |--------|---------| | ✅ | Covered — real end-to-end test exists (no mocks on the production path) | | ⚠️ | Partial — only one side of a flow is tested, or only a unit test exists | | ❌ | Gap — no test at all, or only a trivially shallow create-and-destroy test |


1. XAF Upload + Population Analysis

1.1 Happy paths

| Scenario | Status | Test location | |----------|--------|---------------| | Upload valid XAF, get summary with correct totals (163 entries, correct debit/credit) | ✅ | AnalysisControllerTest#uploadReturnsPopulationSummaryAndDrillDownReturnsMatchingTransactions | | All 12 flag detectors execute and produce correct counts against the training fixture | ✅ | PopulationAnalysisServiceTest (14 tests, each grounded against fixture values) | | Benford distribution included in summary; chi-square computed | ✅ | PopulationAnalysisServiceTest#benfordDistributionIsIncludedInSummaryAndHasCorrectAnalyzableCount | | Population imbalance warning set when debit ≠ credit | ✅ | PopulationAnalysisServiceTest#setsImbalanceWarningWhenTotalDebitDiffersFromCredit | | Configurable round-amount divisor and cutoff window applied | ✅ | PopulationAnalysisServiceTest#usesConfiguredRoundAmountDivisorFromSettings, #usesConfiguredCutoffWindowFromSettings | | Credit-only filter on drill-down | ✅ | AnalysisControllerTest#creditOnlyFilterReturnsCreditSideTransactionsOnly | | Filtered summary for date range (Q1 only) | ✅ | AnalysisControllerTest#filteredSummaryReturnsSubsetOfTransactionsForDateRange |

1.2 Unhappy paths

| Scenario | Status | Test location | |----------|--------|---------------| | Oversized file → 413 Payload Too Large | ✅ | AnalysisControllerTest#uploadWithOversizedFileReturnsPayloadTooLarge | | Malformed XML → 400 with error message | ✅ | AnalysisControllerTest#uploadWithMalformedXmlReturnsBadRequestWithMessage | | Drill-down on unknown session → 404 | ✅ | AnalysisControllerTest#drillDownForUnknownAnalysisIdReturnsNotFound | | Empty transaction list XAF (valid XML, 0 transactions) | ❌ GAP | No test — added by this audit (see §8.5) |

1.3 Flag-category coverage detail

| Category | Happy path | Fixture grounding | Configurable threshold | |----------|-----------|-------------------|------------------------| | ROUND_AMOUNTS | ✅ | ✅ (3 exact entries) | ✅ | | WEEKEND_ENTRIES | ✅ | ✅ (46 entries) | — | | LARGE_ENTRIES | ✅ | ✅ (8 entries) | — | | CUTOFF_EXCEPTIONS | ✅ | ✅ (7 entries) | ✅ | | UNUSUAL_USERS | ✅ | ✅ (directie, 1 tx) | — | | POTENTIAL_DUPLICATES | ✅ | ✅ (0 in fixture; synthetic test) | — | | ADMIN_ENTRIES | ✅ | synthetic test | — | | LOW_ACTIVITY_ACCOUNTS | ✅ | synthetic test | — | | DUPLICATE_AMOUNT_DATE | ✅ | synthetic test | — | | FUZZY_DUPLICATES | ✅ | synthetic test | — | | BENFORD_LAW | ✅ | anomalous + normal datasets | — | | HIGH_RISK (composite) | ✅ | ✅ (multi-category BNK-9003) | — | | OUTSIDE_OFFICE_HOURS | ✅ (marked unavailable) | ✅ | — |


2. GL-to-Trial-Balance Reconciliation

2.1 Happy paths

| Scenario | Status | Test location | |----------|--------|---------------| | Real XAF + real trial balance → 0 matched, 3 mismatched, 2 not-in-ledger | ✅ | ReconciliationControllerTest#reconcilesAnAlreadyUploadedXafSessionAgainstAnUploadedTrialBalance | | Each account's net amount, difference, and status are correct | ✅ | ReconciliationServiceTest (4 detailed account tests) | | ACCOUNT_NOT_IN_LEDGER status for accounts absent from chart-of-accounts | ✅ | ReconciliationServiceTest#accountsAbsentFromTheImportedChartOfAccountsAreReportedAsNotInLedger |

2.2 Unhappy paths

| Scenario | Status | Test location | |----------|--------|---------------| | Unknown analysis ID → 404 | ✅ | ReconciliationControllerTest#reconciliationForUnknownAnalysisIdReturnsNotFound | | Malformed/non-Excel trial balance file → 400 | ❌ GAP | No test — added by this audit (see §8.2) |


3. Report Pages

All report endpoints are tested end-to-end via AnalysisControllerTest using the real training fixture:

| Report | Endpoint | Happy path | 404 | |--------|----------|-----------|-----| | Grootboek (accounts list) | /accounts | ✅ | ✅ | | Grootboek (account postings) | /accounts/{id}/postings | ✅ | ✅ | | Kolommenbalans | /kolommenbalans | ✅ | ✅ | | Stamgegevens | /stamgegevens | ✅ | ✅ | | Transactions browser | /transactions (with filters) | ✅ | ✅ | | Periodebalans (MONTH) | /periodebalans?granularity=MONTH | ✅ | ✅ | | Periodebalans (QUARTER) | /periodebalans?granularity=QUARTER | ✅ | — | | Journaalbalans | /journaalbalans | ✅ | ✅ | | Kengetallen | /kengetallen | ✅ (null ratios for non-RGS XAF) | ✅ | | RGS-categorieën | /rgs-categorieen | ✅ (empty for non-RGS XAF) | ✅ | | Top-accounts | /top-accounts | ✅ | ✅ | | Trend | /trend?granularity=MONTH | ✅ | ✅ | | Memoriaal | /memorial | ✅ | ✅ | | Financial KPIs | /financial-kpis | ✅ | ✅ | | PDF report | /report/pdf | ✅ | ✅ | | Excel export | /categories/{cat}/export | ✅ | — |

Frontend coverage: Each report page has a corresponding *.component.spec.ts that tests HTTP wiring, filtering, pagination, and error states using HttpTestingController. These are substantive tests, not just should create stubs.


4. Supporting-Document Checklist (PRs #83/#84)

4.1 Happy paths

| Scenario | Status | Test location | |----------|--------|---------------| | Initial checklist shows 4 types, all missing | ✅ | SupportingDocumentControllerTest#getStatusReturnsAllFourDocumentTypesAsMissingAfterXafUpload | | Upload marks that document type as present | ✅ | SupportingDocumentControllerTest#postUploadMarksThatDocumentTypeAsPresent | | Batch classify: bank statement by filename pattern | ✅ | SupportingDocumentControllerTest#classifyBatchMarksBankStatementPresentByFilenamePattern | | Batch classify: multiple files in one POST | ✅ | SupportingDocumentControllerTest#classifyBatchClassifiesMultipleFilesInOnePOST | | Batch classify extracts and classifies files from ZIP | ✅ | SupportingDocumentControllerTest#classifyBatchExtractsAndClassifiesFilesFromZipArchive | | Bankverklaring content validation: all checks pass | ✅ | SupportingDocumentControllerTest#uploadBankverklaringRunsContentValidationAndReturnsCheckResult | | Bankverklaring: wrong company name fails check | ✅ | SupportingDocumentControllerTest#uploadBankverklaringWithWrongCompanyNameFailsCompanyNameCheck | | Non-bankverklaring has null checkResult | ✅ | SupportingDocumentControllerTest#nonBankverklaringUploadHasNullCheckResult |

4.2 Unhappy paths (zip abuse)

| Scenario | Status | Test location | |----------|--------|---------------| | Nested ZIP rejected → 400 | ✅ | SupportingDocumentControllerTest#classifyBatchRejectsZipWithNestedZip | | Oversized uncompressed ZIP rejected → 400 | ✅ | SupportingDocumentControllerTest#classifyBatchRejectsZipExceedingUncompressedSizeLimit | | Too many ZIP entries rejected → 400 | ✅ | ZipExtractionServiceTest#rejectsTooManyEntries | | Non-ZIP file in ZIP field → classified as file | — | Service strips non-ZIP content and delegates to classifier |

4.3 Checklist configuration (PR #84)

| Scenario | Status | Test location | |----------|--------|---------------| | Default config shows all 4 types required | ✅ | ChecklistConfigControllerTest#getChecklistConfigReturnsAllFourTypesRequiredByDefault | | Update config to subset → only those types required | ✅ | ChecklistConfigControllerTest#updateChecklistConfigSetsOnlySelectedTypesAsRequired | | Status endpoint reflects config change | ✅ | ChecklistConfigControllerTest#getStatusAfterChecklistConfigChangeReturnsOnlyConfiguredTypes | | Unknown type in update → 400 | ✅ | ChecklistConfigControllerTest#updateChecklistConfigWithUnknownTypeReturnsBadRequest |


5. External Client Upload Link

5.1 Public upload endpoint (ExternalUploadController)

| Scenario | Status | Test location | |----------|--------|---------------| | Valid token + matching file type → 200 | ✅ | ExternalUploadControllerTest#validTokenAndMatchingFileTypeReturnsOk | | Unknown token → 401 Unauthorized | ✅ | ExternalUploadControllerTest#unknownTokenReturnsUnauthorized | | Wrong file extension for document type → 415 | ✅ | ExternalUploadControllerTest#wrongFileExtensionForDocumentTypeReturnsUnsupportedMediaType | | XML file rejected regardless of declared type → 415 | ✅ | ExternalUploadControllerTest#xmlFileIsRejectedRegardlessOfDeclaredDocumentType | | Unknown document type → 400 | ✅ | ExternalUploadControllerTest#unknownDocumentTypeReturnsBadRequest | | Token scoped to its own session only | ✅ | ExternalUploadControllerTest#tokenIsLockedToItsOwnAnalysisIdAndNotAnotherSession | | Rate limit exceeded → 429 | ✅ | ExternalUploadControllerTest#rateLimitBlocksExcessiveRequestsFromSameToken | | Checklist reflects upload | ✅ | ExternalUploadControllerTest#getChecklistReflectsUploadedDocumentAfterSuccessfulUpload | | Checklist for unknown token → 401 | ✅ | ExternalUploadControllerTest#getChecklistForUnknownTokenReturnsUnauthorized | | Expired token → 410 Gone (controller level) | ❌ GAP | Service-only; no controller test — added by this audit (see §8.4) |

5.2 Authenticated link generation endpoint (UploadLinkController)

| Scenario | Status | Test location | |----------|--------|---------------| | POST generate link for valid session → token + metadata | ❌ GAP | Zero tests on this controller — added by this audit (see §8.1) | | POST with createdBy param → auditor name stored | ❌ GAP | Added by this audit | | POST for unknown session → 404 | ❌ GAP | Added by this audit | | GET list tokens for session → active tokens only | ❌ GAP | Added by this audit | | GET list for unknown session → 404 | ❌ GAP | Added by this audit | | Generated token usable on public endpoint (end-to-end) | ❌ GAP | Added by this audit | | Token hash not exposed (raw token only returned once) | ✅ | UploadTokenServiceTest#tokenHashIsNeverEqualToRawToken | | Each generated token is unique | ✅ | UploadTokenServiceTest#eachGeneratedTokenIsUnique | | Token expiry validated, 410 returned | ✅ (service only) | UploadTokenServiceTest#expiredTokenReturnsGone | | Rate limit enforced | ✅ | UploadTokenServiceTest#rateLimitBlocksAfterTenRequestsInOneMinute | | Active tokens filtered by session | ✅ | UploadTokenServiceTest#listActiveTokensReturnsOnlyTokensForTheRequestedSession |


6. Customer/Dossier Persistence Layer (PRs #70/#74)

| Scenario | Status | Test location | |----------|--------|---------------| | Create customer → 201 with ID and timestamps | ✅ | CustomerControllerIntegrationTest#createCustomer_returnsCreatedWithId | | Create customer with missing name → 400 | ✅ | CustomerControllerIntegrationTest#createCustomer_withMissingName_returnsBadRequest | | List customers → all created | ✅ | CustomerControllerIntegrationTest#listCustomers_returnsAllCreated | | Get customer → 200 with correct fields | ✅ | CustomerControllerIntegrationTest#getCustomer_returnsCorrectCustomer | | Get unknown customer → 404 | ✅ | CustomerControllerIntegrationTest#getCustomer_notFound_returns404 | | Create dossier for valid customer → 201 | ✅ | DossierControllerIntegrationTest#createDossier_withValidCustomer_returnsCreated | | Create dossier defaults status to OPEN | ✅ | DossierControllerIntegrationTest#createDossier_defaultsStatusToOpen | | Create dossier for non-existent customer → 404 | ✅ | DossierControllerIntegrationTest#createDossier_withNonExistentCustomer_returns404 | | List dossiers filtered by customerId | ✅ | DossierControllerIntegrationTest#listDossiers_filteredByCustomerId_returnsOnlyMatching | | Get dossier → 200 with customerName | ✅ | DossierControllerIntegrationTest#getDossier_returnsCorrectDossier | | Get unknown dossier → 404 | ✅ | DossierControllerIntegrationTest#getDossier_notFound_returns404 | | List files for empty dossier → empty list | ✅ | DossierControllerIntegrationTest#listFiles_emptyDossier_returnsEmptyList | | List files for non-existent dossier → 404 | ✅ | DossierControllerIntegrationTest#listFiles_nonExistentDossier_returns404 | | DB-unavailable (no datasource) → controllers disabled via @ConditionalOnProperty | ✅ (structural) | Spring conditional ensures controllers absent when no DB |


7. Admin / Wipe-All

| Scenario | Status | Test location | |----------|--------|---------------| | DELETE /api/admin/wipe-all → 204 No Content | ✅ | AdminControllerTest#wipeAllReturnsNoContent | | Wipe-all clears analysis sessions | ✅ | AdminControllerTest#wipeAllClearsAnalysisSessionsAfterUpload | | Wipe-all is idempotent | ✅ | AdminControllerTest#wipeAllIsIdempotentWhenStoresAreAlreadyEmpty | | Wipe-all also clears upload tokens (token becomes invalid after wipe) | ❌ GAP | The code clears tokens but the test only verifies session clearing — added by this audit (see §8.3) |


8. New Tests Added by This Audit

The five highest-value gaps — ranked by criticality to a real audit engagement — are addressed here with actual given-when-then-structured tests using the existing JUnit/MockMvc infrastructure.

8.1 UploadLinkController (authenticated token generation) — HIGHEST VALUE

Why high value: This controller is the entry point for the entire external-client upload workflow. Without tests, regressions (e.g., allowing token generation for non-existent sessions, not returning the token in the response, wrong HTTP method wiring) go completely undetected. The public-side tests (ExternalUploadControllerTest) only work if the link-generation side is correct.

New file: backend/src/test/java/nl/glorylabs/athena/uploadlink/controller/UploadLinkControllerTest.java

Scenarios covered (6):

  • Given a valid analysis session, when POST /upload-link, then 200 with token, analysisId, createdAt, expiresAt
  • Given a valid session and createdBy param, when POST /upload-link?createdBy=janssen, then token record has createdBy=janssen
  • Given no matching session, when POST /upload-link, then 404
  • Given a valid session with 2 generated tokens, when GET /upload-link, then 200 with both token summaries
  • Given no matching session, when GET /upload-link, then 404
  • Given a token generated via POST, when that token is used on /api/public/upload/{token}, then 200 (end-to-end)

8.2 Reconciliation with malformed trial balance — HIGH VALUE

Why high value: Clients routinely send the wrong format (PDF, plain text, corrupt binary) when uploading the trial balance. The controller maps TrialBalanceParsingException to 400, but this path has never been exercised in an integration test.

Added to: ReconciliationControllerTest

Scenarios covered (1):

  • Given a valid XAF session, when POST reconciliation with a PDF file as the trial balance, then 400 Bad Request

8.3 Admin wipe-all clears upload tokens — MEDIUM-HIGH VALUE

Why high value: After a wipe-all, any outstanding external upload links must become invalid. If this breaks (e.g., uploadTokenService.clearAll() is accidentally removed from wipeAll()), clients could continue uploading to a cleared session with no way to detect the inconsistency. The current test only verifies session clearing.

Added to: AdminControllerTest

Scenarios covered (1):

  • Given a generated upload token, when DELETE /admin/wipe-all, then the token is no longer valid (401 on use)

8.4 Expired upload token returns 410 Gone at controller level — MEDIUM VALUE

Why high value: The 410 Gone behavior is currently only tested at the service level via reflection. A controller-level regression (e.g., the exception not being propagated, HTTP status remapped) would not be caught. Production clients need a clear signal that the link has expired (not just "unknown").

Added to: ExternalUploadControllerTest

Scenarios covered (1):

  • Given an upload token whose expiry is in the past, when POST /api/public/upload/{expiredToken}, then 410 Gone

8.5 XAF upload with empty transactions — MEDIUM VALUE

Why high value: An auditor might upload an XAF that contains the header and chart-of-accounts but no transaction lines (e.g., the wrong fiscal period or a newly-started company). The system must handle this gracefully (summary with 0 entries, empty categories, no crash) rather than throwing an NPE or division-by-zero in the population analysis.

Added to: AnalysisControllerTest

Scenarios covered (1):

  • Given a valid XAF with 0 transactions, when POST /api/analysis/upload, then 200 with totalEntries=0 and all categories at count=0

Summary of Gaps Before This Audit

| Area | Gap severity | Description | |------|-------------|-------------| | UploadLinkController | Critical | ZERO test coverage on authenticated token-generation endpoint | | Reconciliation malformed input | High | Controller's 400 error path never triggered in integration test | | Admin wipe-all token clearing | Medium | Code exists; test only verifies sessions cleared, not tokens | | Expired token at controller level | Medium | Service-only coverage via reflection; no HTTP-level 410 test | | Empty-transactions XAF | Medium | Edge case with no test; division-by-zero risk in analysis |

All five gaps have been addressed by new tests in this PR.

Reacties

Nog geen reacties