- Overview: Full-stack contact, task, appointment, and project tracker built with Spring Boot 4.0.0 and React 19.
- Architecture highlights: PostgreSQL + JPA with 13 Flyway migrations (V1-V13), JWT authentication with RBAC, per-user data isolation, token-bucket rate limiting, structured logging with PII masking, Prometheus metrics, Docker packaging, Kubernetes-ready health probes, and 6 GitHub Actions workflows (CI/CD, CodeQL, ZAP DAST, API fuzzing, Dependabot).
- My role: Designed the schema (13 migrations), built 6 domain aggregates with services and controllers, wired the JWT security and observability stack, created a React 19 SPA using TanStack Query, packaged everything with Docker + Compose, and automated the stack via a Makefile (55 targets) and CI/CD pipelines.
- Quality bar: 930 test executions on the full Linux matrix run, ~90% line coverage (JaCoCo), ~84% mutation score (PITest), and 5 CI quality gates (JaCoCo, PITest, SpotBugs, Checkstyle, OWASP Dependency-Check).
- Linux CI runs the full suite with Testcontainers/Postgres and enforces coverage/mutation gates; Windows CI uses the
skip-testcontainersprofile on H2 with a reduced JaCoCo gate that excludes container-only code paths while still running PITest. LegacygetInstance()suites are taggedlegacy-singletonand can be run separately viamvn test -Plegacy-singletonwithout touching the main pipeline.
- Linux CI runs the full suite with Testcontainers/Postgres and enforces coverage/mutation gates; Windows CI uses the
Started as the simple CS320 contact-service milestone and grew into a multi-entity suite (Contact, Task, Appointment, Project) with full persistence, Spring Boot 4.0.0, a React 19 SPA, and backward-compatible singletons. The work breaks down into:
- Built the
ContactandContactServiceclasses exactly as described in the original requirements. - Proved every rule with unit tests (length limits, null checks, unique IDs, add/update/delete behavior) using a shared
Validationhelper so exceptions surface clear messages. - Mirrored the same patterns for the
Taskentity/service (ID, name, description) andAppointment(ID, date, description) so all domains share validation, atomic updates, and singleton storage. - Persisted all aggregates through Spring Data JPA repositories and Flyway migrations (Postgres in dev/prod, Postgres via Testcontainers for SpringBootTests, H2 for targeted slices) while keeping the legacy
getInstance()singletons alive for backward compatibility. - Added comprehensive security (JWT auth, per-user data isolation, rate limiting, CSRF protection) and observability (structured logging, Prometheus metrics, PII masking).
- Built a production-ready React 19 SPA with TanStack Query, search/pagination/sorting, an admin dashboard, and full accessibility support.
- Implemented Project/Task Tracker Evolution (ADR-0045 Phases 1–5): Project entity with CRUD operations and status tracking, task enhancements (status and due dates), task–project linking, appointment–task/project linking, and team collaboration via task assignment with access control.
Everything is packaged under contactapp with layered sub-packages (domain, service, api, persistence, security, config); production classes live in src/main/java and the JUnit tests in src/test/java. Spring Boot 4.0.0 provides the runtime with actuator health/info endpoints and Prometheus metrics.
- Getting Started
- Phase Roadmap & Highlights
- Branches & History
- Folder Highlights
- Design Decisions & Highlights
- Architecture Overview
- Backend Domain & Services
- Security Infrastructure
- Observability Infrastructure
- REST API Layer
- React UI Layer
- Spring Boot Infrastructure
- Static Analysis & Quality Gates
- Backlog
- CI/CD Pipeline
- QA Summary
- Self-Hosted Mutation Runner Setup
| Tool | Version | Notes |
|---|---|---|
| Java | 17+ | Temurin JDK 17 recommended |
| Apache Maven | 3.9+ | For building and testing |
| Node.js | 22+ | For frontend development (matches pom.xml) |
| npm | 8+ | Comes with Node.js |
| Python | 3.8+ | For development scripts (dev_stack.py) |
| Docker | 20.10+ | For Postgres dev database (optional) |
| Docker Compose | 2.0+ | For multi-container setup (optional) |
Hardware: 2GB RAM minimum, 500MB free disk space.
Operating Systems: Linux, macOS, Windows (WSL2 recommended for Windows).
-
Install Java 17 and Apache Maven (3.9+).
-
Run
mvn verifyfrom the project root to compile everything, execute the JUnit suite, and run Checkstyle/SpotBugs/JaCoCo quality gates. (Testcontainers-based integration tests are skipped locally by default; runmvn verify -DskipITs=falsewith Docker running to include them.) -
Development mode (backend only):
mvn spring-boot:run(base profile uses an in-memory H2 database in PostgreSQL compatibility mode so you can try the API without installing Postgres; use--spring.profiles.active=devorprodto connect to a real Postgres instance that persists data between restarts)- Health/info actuator endpoints at
http://localhost:8080/actuator/health - Swagger UI at
http://localhost:8080/swagger-ui.html - OpenAPI spec at
http://localhost:8080/v3/api-docs - REST APIs at
/api/v1/contacts,/api/v1/tasks,/api/v1/appointments,/api/v1/projects
- Health/info actuator endpoints at
-
Frontend development (hot reload):
cd ui/contact-app npm install npm run dev # Starts Vite at http://localhost:5173
The Vite dev server proxies
/api/v1requests tolocalhost:8080(run Spring Boot in a separate terminal).Development URLs (when running both servers):
URL Purpose http://localhost:5173 React UI (use this for the web app) http://localhost:8080/api/v1/* REST API (JSON responses only) http://localhost:8080/swagger-ui.html API documentation http://localhost:8080/actuator/health Health check endpoint One-command option:
python scripts/dev_stack.pystartsmvn spring-boot:run, waits forhttp://localhost:8080/actuator/health, installs UI deps if needed, and launchesnpm run dev -- --port 5173. Press Ctrl+C once to shut down both services. Add--database postgresto have the helper start Docker Compose, set thedevprofile, and inject datasource env vars automatically.
The default profile uses in-memory H2, so data disappears when the backend stops. To keep data between restarts, let the launcher handle the Postgres database setup:
python scripts/dev_stack.py --database postgresThis command runs docker compose -f docker-compose.dev.yml up -d (or docker-compose if needed), wires datasource env vars/credentials, activates the dev profile, and launches both servers.
Prefer manual control? Follow these steps instead:
- Start Postgres via Docker Compose (ships with default
contactapp/contactappcreds):docker compose -f docker-compose.dev.yml up -d
- Export datasource credentials (or add them to your shell profile):
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/contactapp export SPRING_DATASOURCE_USERNAME=contactapp export SPRING_DATASOURCE_PASSWORD=contactapp
- Run the stack with the
devprofile so Spring Boot connects to Postgres:mvn spring-boot:run -Dspring-boot.run.profiles=dev npm run dev
Flyway automatically creates the schema on first run. Stop the database with docker compose -f docker-compose.dev.yml down when you are done (data stays in the pgdata volume). 5. Production build (single JAR with UI):
mvn package -DskipTests java -jar target/cs320-contact-service-junit-1.0.0-SNAPSHOT.jarOpen http://localhost:8080 — Spring Boot serves both the React UI and REST API from the same origin. 8. Test profiles:
- Default CI/local run:
mvn test(Linux) exercises the full suite with Testcontainers/Postgres. - Windows/
-DskipTestcontainersTests=true: runs the same service/controller suites against in-memory H2 (no Docker) while still reporting JaCoCo. - Legacy singleton coverage:
mvn test -Plegacy-singletonruns only the legacygetInstance()suites taggedlegacy-singletonagainst H2 to avoid interfering with the main pipeline.
- Open the folder in IntelliJ/VS Code if you want IDE assistance—the Maven project model is auto-detected.
- Planning note: Phases 0-7 complete (Spring Boot scaffold, REST API + DTOs, API fuzzing, persistence layer, React UI, security & observability, DAST, packaging/CI, UX polish). 930 tests (Linux full suite) cover the JPA path, legacy singleton fallbacks, JWT auth components, User entity validation, Project CRUD, and the validation helpers including the new
validateNotNullenum helper (PIT mutation coverage ~84% with ~90% line coverage on the full suite). +84 mutation-focused tests added targeting boundary conditions, comparison operators, copy semantics, and helper adapters. ADR-0014..0046 capture the selected stack plus validation/Project evolution decisions. See Phase Roadmap & Highlights for the consolidated deliverables list.
The phased plan in docs/REQUIREMENTS.md governs scope. This snapshot keeps the status and headline deliverables handy while future phases (8+) stay visible.
| Phase | Status | Focus | Highlights |
|---|---|---|---|
| 0 | Complete | Domain guardrails | Defensive copies in Contact/Task services; appointment date-not-past fix |
| 1 | Complete | Spring Boot foundation | Services promoted to @Service beans; actuator health/info endpoints |
| 2 | Complete | REST API + DTOs | CRUD controllers at /api/v1/**; Bean Validation DTOs; 137 controller tests |
| 2.5 | Complete | OpenAPI fuzzing | Schemathesis workflows with spec validation/hardening |
| 3 | Complete | Persistence & storage | Spring Data JPA + Flyway; Postgres dev/prod; H2 slices + Testcontainers-backed SpringBootTests |
| 4 | Complete | React SPA | CRUD UI with TanStack Query + Vite/Tailwind stack; Vitest + Playwright suites |
| 5 | Complete | Security & observability | JWT auth, per-user tenancy, rate limiting, sanitized logging, Prometheus |
| 5.5 | Complete | CI security gates | ZAP DAST scans, password strength validation, CSP/Permissions-Policy headers |
| 6 | Complete | Packaging + CI | Makefile (30+ targets), CI docker-build job, GHCR push, health checks |
| 7 | Complete | UX polish + Project Tracker (ADR-0045 Phases 1-5) | Search/pagination/sorting, toasts, empty states, admin dashboard; Project CRUD with status tracking, task status/due dates/project linking, appointment linking, task assignment with access control |
- Security & Observability (Phase 5): JWT auth, per-user data isolation, rate limiting, CSRF protection, structured logging with correlation IDs, Prometheus metrics. See Security Infrastructure and Observability Infrastructure sections below.
- Project/Task Tracker (ADR-0045 Phases 1-5): Project CRUD with status tracking, task status/due dates/assignment, task-project linking, appointment-task/project linking. See Backend Domain & Services and ADR-0045 for details.
master(this branch) - the Spring Boot + React suite with persistence, CI, and the full UI.original-cs320- the unmodified CS320 milestone (HashMap-backed services + the original requirements/tests). Browse it atoriginal-cs320to compare the baseline against this modern implementation.
We tag releases from both branches so GitHub’s “Releases” view exposes the original assignment snapshot alongside the current platform.
| Path | Description |
|---|---|
src/main/java/contactapp/Application.java | Spring Boot entrypoint (@SpringBootApplication). |
src/main/java/contactapp/domain/Contact.java | Contact entity enforcing the ID/name/phone/address constraints. |
src/main/java/contactapp/domain/Task.java | Task entity (ID/name/description/status/dueDate) with status enum and optional due date validation. |
src/main/java/contactapp/domain/Appointment.java | Appointment entity (ID/date/description) with date-not-past enforcement. |
src/main/java/contactapp/domain/Validation.java | Centralized validation helpers (not blank, length, numeric, date-not-past, enum not-null checks). |
src/main/java/contactapp/service/ContactService.java | @Service bean backed by a ContactStore abstraction (Spring Data or legacy fallback). |
src/main/java/contactapp/service/TaskService.java | Task service wired the same way via TaskStore, still exposing getInstance() for legacy callers. |
src/main/java/contactapp/service/AppointmentService.java | Appointment service using AppointmentStore with transactional CRUD + defensive copies. |
src/main/java/contactapp/persistence/entity | JPA entities mirroring the domain objects without validation logic. |
src/main/java/contactapp/persistence/mapper | Bidirectional mappers that re-validate persisted data via domain constructors. |
src/main/java/contactapp/persistence/repository | Spring Data repositories plus in-memory fallbacks for legacy getInstance() callers. |
src/main/java/contactapp/persistence/store | DomainDataStore abstraction + JPA-backed implementations injected into services. |
src/main/resources/application.yml | Multi-document profile configuration (dev/test/integration/prod + Flyway/JPA settings). |
src/main/resources/db/migration | Flyway migrations (V1-V13) split by database (common/h2/postgresql): contacts (V1), tasks (V2), appointments (V3), users (V4), user FKs (V5), surrogate keys (V6), version columns (V7), projects (V8), task status/dueDate (V9), task-project FK (V10), appointment-task/project FKs (V11), task assignment FK (V12), project-contacts junction table (V13). |
src/test/java/contactapp/ApplicationTest.java | Spring Boot context load smoke test. |
src/test/java/contactapp/ActuatorEndpointsTest.java | Verifies actuator endpoint security (health/info exposed, others blocked). |
src/test/java/contactapp/ServiceBeanTest.java | Verifies service beans are injectable and singletons. |
src/test/java/contactapp/domain/ContactTest.java | Unit tests for Contact class (+16 mutation tests: ID/name/address/phone boundaries, copy independence, max length validation). |
src/test/java/contactapp/domain/TaskTest.java | Unit tests for Task class (+13 mutation tests: ID/name/description boundaries, atomic updates, copy semantics). |
src/test/java/contactapp/domain/AppointmentTest.java | Unit tests for Appointment entity (ID/date/description validation). |
src/test/java/contactapp/domain/ProjectTest.java | Unit tests for Project class (+19 mutation tests: boundaries, empty description handling, status enum, trimming logic). |
src/test/java/contactapp/domain/ProjectStatusTest.java | Unit tests for ProjectStatus enum (all status values, display names). |
src/test/java/contactapp/domain/ValidationTest.java | Validation helpers (+14 mutation tests: length boundaries, digit validation, date millisecond precision, email max length). |
src/test/java/contactapp/service/ContactServiceTest.java | SpringBootTest on the integration profile using Postgres via Testcontainers (Flyway applied). |
src/test/java/contactapp/service/TaskServiceTest.java | SpringBootTest coverage for TaskService persistence flows on Postgres via Testcontainers. |
src/test/java/contactapp/service/AppointmentServiceTest.java | SpringBootTest coverage for AppointmentService persistence flows on Postgres via Testcontainers. |
src/test/java/contactapp/service/ProjectServiceTest.java | SpringBootTest coverage for ProjectService (28 tests: CRUD, status filtering, contact linking, per-user isolation) on Postgres via Testcontainers. |
src/test/java/contactapp/service/*LegacyTest.java | Verifies the legacy getInstance() singletons still work outside Spring. |
src/test/java/contactapp/persistence/entity | JPA entity tests (protected constructor/setter coverage for Hibernate proxies). |
src/test/java/contactapp/persistence/mapper | Mapper unit tests (Contact/Task/Appointment conversions + validation). |
src/test/java/contactapp/persistence/repository | @DataJpaTest slices for Contact/Task/Appointment repositories (H2 + Flyway). |
src/test/java/contactapp/persistence/store | Store tests (JPA + InMemory implementations). |
src/test/java/contactapp/persistence/store/JpaContactStoreTest.java | Tests JpaContactStore repository delegations (existsById, findById, deleteById branches). |
src/test/java/contactapp/persistence/store/JpaTaskStoreTest.java | Tests JpaTaskStore repository delegations for mutation testing coverage. |
src/test/java/contactapp/persistence/store/JpaAppointmentStoreTest.java | Tests JpaAppointmentStore repository delegations and return code handling. |
src/test/java/contactapp/persistence/store/JpaProjectStoreTest.java | Tests JpaProjectStore repository delegations (24 tests: CRUD, status filtering, per-user isolation). |
src/test/java/contactapp/persistence/store/InMemoryContactStoreTest.java | Tests legacy InMemoryContactStore defensive copy semantics. |
src/test/java/contactapp/persistence/store/InMemoryProjectStoreTest.java | Tests legacy InMemoryProjectStore for non-Spring fallback. |
src/test/java/contactapp/persistence/store/InMemoryTaskStoreTest.java | Tests legacy InMemoryTaskStore defensive copy semantics. |
src/test/java/contactapp/persistence/store/InMemoryAppointmentStoreTest.java | Tests legacy InMemoryAppointmentStore defensive copy semantics. |
src/main/java/contactapp/api/ContactController.java | REST controller for Contact CRUD operations at /api/v1/contacts. |
src/main/java/contactapp/api/TaskController.java | REST controller for Task CRUD operations at /api/v1/tasks. |
src/main/java/contactapp/api/ProjectController.java | REST controller for Project CRUD + contact linking at /api/v1/projects. |
src/main/java/contactapp/api/AppointmentController.java | REST controller for Appointment CRUD operations at /api/v1/appointments. |
src/main/java/contactapp/api/AuthController.java | REST controller for authentication (login/register) at /api/auth (see ADR-0038). |
src/main/java/contactapp/api/GlobalExceptionHandler.java | Maps exceptions to HTTP responses (400, 401, 403, 404, 409 including optimistic locking conflicts). The Contact/Task/Appointment entities each use a JPA @Version column, so stale PUT/DELETE requests surface ObjectOptimisticLockingFailureException errors that the handler converts to HTTP 409 responses with “refresh and retry” guidance. |
src/main/java/contactapp/api/CustomErrorController.java | Ensures ALL errors return JSON (including Tomcat-level errors). |
src/main/java/contactapp/config/JsonErrorReportValve.java | Tomcat valve ensuring container-level errors return JSON (see ADR-0022). |
src/main/java/contactapp/config/TomcatConfig.java | Registers JsonErrorReportValve with embedded Tomcat. |
src/main/java/contactapp/config/JacksonConfig.java | Disables Jackson type coercion for strict schema compliance (see ADR-0023). |
src/main/java/contactapp/config/ApplicationContextProvider.java | Captures the Spring context so getInstance() can return the DI-managed service even after instrumentation resets static fields. |
src/main/java/contactapp/config/RateLimitConfig.java | Configuration properties for API rate limiting (login/register/api limits). |
src/main/java/contactapp/config/RateLimitingFilter.java | Servlet filter enforcing rate limits via Bucket4j token buckets. |
src/main/java/contactapp/config/CorrelationIdFilter.java | Servlet filter generating/propagating X-Correlation-ID for request tracing. |
src/main/java/contactapp/config/RequestLoggingFilter.java | Servlet filter logging request/response details with MDC correlation. |
src/main/java/contactapp/config/PiiMaskingConverter.java | Logback converter that masks PII patterns in log output. |
src/main/java/contactapp/config/RequestUtils.java | Utility class for client IP extraction supporting X-Forwarded-For and X-Real-IP proxy headers. |
src/main/java/contactapp/security/User.java | User entity implementing UserDetails for Spring Security (see ADR-0037). |
src/main/java/contactapp/security/Role.java | Role enum (USER, ADMIN) for authorization. |
src/main/java/contactapp/security/UserRepository.java | Spring Data repository for User persistence with lookup methods. |
src/main/java/contactapp/security/JwtService.java | JWT token generation and validation service (HMAC-SHA256). |
src/main/java/contactapp/security/JwtAuthenticationFilter.java | Filter that validates JWT tokens from HttpOnly cookies first, then Authorization header fallback (see ADR-0043). |
src/main/java/contactapp/security/SpaCsrfTokenRequestHandler.java | CSRF token handler for SPA double-submit cookie pattern (sets XSRF-TOKEN cookie with SameSite=Lax). |
src/main/java/contactapp/security/SecurityConfig.java | Spring Security configuration (JWT auth, CSRF protection, CORS, security headers, endpoint protection). |
src/main/java/contactapp/security/CustomUserDetailsService.java | UserDetailsService implementation loading users from repository. |
src/test/java/contactapp/security/UserTest.java | Unit tests for User entity validation (boundary, null/blank, role tests). |
src/test/java/contactapp/security/JwtServiceTest.java | Unit tests for JWT token lifecycle (+9 mutation tests: expiration boundaries, refresh window, case-sensitive username). |
src/test/java/contactapp/security/JwtAuthenticationFilterTest.java | Unit tests for JWT filter (missing cookie/header, invalid token, valid token scenarios). |
src/test/java/contactapp/security/SpaCsrfTokenRequestHandlerTest.java | Unit tests for SPA CSRF handler (cookie persistence, SameSite attribute, parameter resolution). |
src/test/java/contactapp/security/CustomUserDetailsServiceTest.java | Unit tests for user lookup and UsernameNotFoundException handling. |
src/main/java/contactapp/api/dto/ | Request/Response DTOs with Bean Validation (Contact/Task/Appointment + LoginRequest, RegisterRequest, AuthResponse). |
src/main/java/contactapp/api/exception/ | Custom exceptions (ResourceNotFoundException, DuplicateResourceException). |
src/test/java/contactapp/ContactControllerTest.java | MockMvc integration tests for Contact API (32 tests). |
src/test/java/contactapp/TaskControllerTest.java | MockMvc integration tests for Task API (41 tests). |
src/test/java/contactapp/AppointmentControllerTest.java | MockMvc integration tests for Appointment API (23 tests). |
src/test/java/contactapp/ProjectControllerTest.java | MockMvc integration tests for Project API (41 tests). |
src/test/java/contactapp/AuthControllerTest.java | MockMvc integration tests for Auth API (login, register, validation errors). |
src/test/java/contactapp/ContactControllerUnitTest.java | Unit tests for Contact controller getAll() branch coverage (mutation testing). |
src/test/java/contactapp/TaskControllerUnitTest.java | Unit tests for Task controller getAll() branch coverage (mutation testing). |
src/test/java/contactapp/AppointmentControllerUnitTest.java | Unit tests for Appointment controller getAll() branch coverage (mutation testing). |
src/test/java/contactapp/AuthControllerUnitTest.java | Unit tests for AuthController login branch coverage (mutation testing). |
src/test/java/contactapp/SecurityConfigIntegrationTest.java | Tests security filter chain order, CORS config, and BCrypt encoder setup. |
src/test/java/contactapp/service/ContactServiceIT.java | Testcontainers-backed ContactService integration tests (Postgres). |
src/test/java/contactapp/service/TaskServiceIT.java | Testcontainers-backed TaskService integration tests. |
src/test/java/contactapp/service/AppointmentServiceIT.java | Testcontainers-backed AppointmentService integration tests. |
src/test/java/contactapp/service/ContactServiceMutationTest.java | Additional tests targeting surviving mutants in ContactService. |
src/test/java/contactapp/service/TaskServiceMutationTest.java | Additional tests targeting surviving mutants in TaskService. |
src/test/java/contactapp/service/AppointmentServiceMutationTest.java | Additional tests targeting surviving mutants in AppointmentService. |
src/test/java/contactapp/GlobalExceptionHandlerTest.java | Unit tests for GlobalExceptionHandler methods (5 tests). |
src/test/java/contactapp/CustomErrorControllerTest.java | Unit tests for CustomErrorController (17 tests). |
src/test/java/contactapp/config | Config tests (Jackson, Tomcat, JsonErrorReportValve, RateLimitingFilter). |
src/test/java/contactapp/config/CorrelationIdFilterTest.java | Tests correlation ID generation, header propagation, MDC lifecycle, boundary validation (64 vs 65 chars). |
src/test/java/contactapp/config/RequestLoggingFilterTest.java | Tests request/response logging, query string/user-agent sanitization, IP masking, and duration math (15 tests). |
src/test/java/contactapp/config/RateLimitingFilterTest.java | Tests rate limiting per IP and per-user, retry-after headers, bucket counting, log sanitization, and bucket reset paths (24 tests). |
src/test/java/contactapp/config/PiiMaskingConverterTest.java | Tests PII masking for phone numbers and addresses in log output. |
src/test/java/contactapp/config/RequestUtilsTest.java | Tests client IP extraction with header precedence (X-Forwarded-For, X-Real-IP, RemoteAddr). |
src/test/java/contactapp/config/JacksonConfigTest.java | Tests Jackson ObjectMapper strict type coercion (rejects boolean/numeric coercion). |
src/test/java/contactapp/service/*BridgeTest.java | Singleton bridge tests (verifies DI-managed and legacy singletons share state). |
src/test/java/contactapp/LegacySingletonUsageTest.java | End-to-end legacy singleton usage patterns (pre-Spring callers). |
docs/cs320-requirements/ | Original CS320 assignment requirements (contact, task, appointment specs). |
docs/showcase/ | Project showcase materials for recruiters and portfolio. |
docs/architecture/2025-11-19-task-entity-and-service.md | Task entity/service design plan with Definition of Done and phased approach. |
docs/architecture/2025-11-24-appointment-entity-and-service.md | Appointment entity/service implementation record. |
docs/adrs/README.md | Architecture Decision Record index: CS320 Foundation (ADR-0001-0049) + Jira Evolution (ADR-0050+). |
Dockerfile | Multi-stage production Docker image (Eclipse Temurin 17, non-root user, layered JAR). |
docker-compose.yml | Production-like stack (Postgres + App + optional pgAdmin) with health checks. |
docs/operations/ | Operations docs: Docker setup guide, Actuator endpoints reference, deployment guides. |
docs/ci-cd/ | CI/CD design notes (pipeline plan + badge automation). |
docs/design-notes/ | Informal design notes hub; individual write-ups live under docs/design-notes/notes/. |
docs/REQUIREMENTS.md | Master document: scope, architecture, phased plan, checklist, and code examples. |
docs/roadmaps/ROADMAP.md | Quick phase overview (points to REQUIREMENTS.md for details). |
docs/INDEX.md | Full file/folder navigation for the repository. |
pom.xml | Maven build file (dependencies, plugins, compiler config). |
config/checkstyle | Checkstyle configuration used by Maven/CI quality gates. |
config/owasp-suppressions.xml | Placeholder suppression list for OWASP Dependency-Check. |
scripts/ci_metrics_summary.py | Helper that parses JaCoCo/PITest/Dependency-Check reports and posts the QA summary table in CI. |
scripts/serve_quality_dashboard.py | Tiny HTTP server that opens target/site/qa-dashboard locally after downloading CI artifacts. |
scripts/api_fuzzing.py | API fuzzing helper for local Schemathesis runs (starts app, fuzzes, exports OpenAPI spec). |
scripts/dev_stack.py | Runs the Spring Boot API and Vite UI together with health checks and dependency bootstrapping. |
docker-compose.dev.yml | One-command Postgres stack for local persistence (docker compose -f docker-compose.dev.yml up). |
.github/workflows | CI/CD pipelines (tests, quality gates, release packaging, CodeQL, API fuzzing). |
- Immutable identifiers -
contactIdis set once in the constructor and never mutates, which keeps map keys stable and mirrors real-world record identifiers. - Centralized validation - Every constructor/setter call funnels through
Validation.validateNotBlank,validateLength,validateDigits, and (for appointments)validateDateNotPast, so IDs, names, phones, addresses, and dates all share one enforcement pipeline. - Fail-fast IllegalArgumentException - Invalid input is a caller bug, so we throw standard JDK exceptions with precise messages and assert on them in tests.
- DomainDataStore persistence strategy - Services depend on store interfaces implemented by Spring Data JPA (contacts/tasks/appointments live in Postgres via Flyway migrations) while legacy
getInstance()callers automatically fall back to in-memory stores. New code always goes through Spring DI so repositories/mappers enforce the same validation. - Legacy singleton compatibility -
getInstance()now simply returns the Spring-managed proxy once the context is available (or the in-memory fallback before boot) so we no longer unwrap proxies manually. Tests assert shared behavior/state across both access paths instead of brittle reference equality checks. When every caller uses DI we will delete the static entry points and in-memory stores (see backlog). - Why the map based version disappeared - The original CS320 version stored everything in a static
ConcurrentHashMap, which was fine for an in memory demo but lost data on restart, could not support multiple app instances, and had no way to evolve the schema. In Phase 3 the source of truth moved into Spring Data JPA repositories and Flyway managed Postgres tables, and the in memory map now exists only as a compatibility layer for legacygetInstance()tests while normal application code always uses the database backed stores.
Previous design (static ConcurrentHashMap):
- All data lived in the heap of one JVM so restarts dropped every record.
- No way to share data across multiple instances, which breaks load-balanced deployments.
- No real database meant no SQL queries, migrations, or schema validation.
- Cannot enforce constraints at the storage layer or evolve the data model over time.
- Ignored the stack decisions already captured in ADR-0014/0015 (Postgres, Flyway, Testcontainers).
- Integration tests only exercised map behavior and proved nothing about persistence.
Current design (JPA + Flyway + stores):
- Postgres is the single source of truth (Testcontainers for SpringBootTests; H2 only for targeted slice/unit tests).
- Schema versions are tracked by Flyway and validated by Hibernate on startup.
- Services run inside transactions instead of ad hoc map updates.
- Tests hit the same persistence stack used in development and production.
- Static
getInstance()and the in-memory stores exist only to keep old callers working; real code paths use the injected stores. - Defensive copies - Each entity provides a
copy()method, andgetDatabase()returns defensive copies so external callers cannot mutate internal state; safe to surface over APIs. - Boolean service API - Every service's
add/delete/updatemethods returnbooleanso callers know immediately whether the operation succeeded (true) or why it failed (falsefor duplicate IDs, missing IDs, etc.). That keeps the milestone interfaces lightweight while still letting JUnit assertions check the outcome without extra exception types. - Security posture - Input validation in the domain + service layers acts as the first defense layer; nothing reaches the in-memory store unless it passes the guards.
- Testing depth - Parameterized JUnit 5 tests, AssertJ assertions, JaCoCo coverage, and PITest mutation scores combine to prove the validation logic rather than just executing it.
- Minimal state (ID, first/last name, phone, address) with the
contactIdlocked after construction. - Constructor delegates to setters so validation logic fires in one place.
- Address validation uses the same length helper as IDs/names, ensuring the 30-character maximum cannot drift.
validateNotBlank(input, label)- rejects null, empty, and whitespace-only fields with label-specific messages.validateLength(input, label, min, max)- enforces 1-10 char IDs/names and 1-30 char addresses (bounds are parameters, so future changes touch one file).validateTrimmedLength(...)/validateTrimmedLengthAllowBlank(...)- new helpers that return the trimmed value after enforcing length rules so Contact/Task/Project no longer carry their ownvalidateAndTrimhelpers.validateDigits(input, label, requiredLength)- requires digits-only phone numbers with exact length (10 in this project).validateDateNotPast(date, label)- rejects null dates and any timestamp strictly before now (dates equal to "now" are accepted to avoid millisecond-boundary flakiness), powering the Appointment rules. Note: The default usesClock.systemUTC(), so "now" is evaluated in UTC; callers in significantly different timezones should construct dates with UTC awareness. An overload accepting aClockparameter enables deterministic testing of boundary conditions.validateDateNotPast(LocalDate, ...)+validateOptionalDateNotPast(...)- new overloads so LocalDate-based due dates reuse the same source of truth (Task set/update + TaskService getters now funnel through these helpers).validateNotNull(enumValue, label)- generic enum validation that throwsIllegalArgumentExceptionif null, used by Project.setStatus() and User roles to consolidate duplicate null checks.- These helpers double as both correctness logic and security filtering.
- Depends on
ContactStore, an interface implemented byJpaContactStore(Spring Data repository + mapper) in normal operation andInMemoryContactStorefor the legacygetInstance()path. - Constructor registers the first Spring-managed bean as the singleton instance so controllers/tests can inject it while
getInstance()callers transparently share the same backing store. @Transactionalensures multi-step operations (exists check + save, read + update) remain atomic at the database level; read-only queries opt into@Transactional(readOnly = true)for better performance.- Defensive copies (
getDatabase(),getAllContacts(),getContactById()) still return new domain objects so calling code cannot mutate persistent state.
-
Primary path: Spring Data repositories (
JpaContactStore,JpaTaskStore,JpaAppointmentStore) backed by Flyway-managed Postgres tables (or H2 in tests). Mappers convert between entities and domain objects, reusingValidation. -
Legacy fallback:
InMemory*Storeclasses used only whengetInstance()is called before Spring initializes; as soon as the@Servicebean registers, data migrates into the JPA-backed store. -
The
DomainDataStoreabstraction keeps the services agnostic to storage details, so future optimizations (caching, outbox, etc.) localize to the persistence package without touching controllers or domain tests.
Contactacts as the immutable ID holder with mutable first/last name, phone, and address fields.- Constructor delegates to setters so validation stays centralized and consistent for both creation and updates.
- Validation trims IDs/names/addresses before storing them; phone numbers are stored as provided and must already be 10 digits (whitespace fails the digit check instead of being silently removed).
copy()creates a defensive copy by validating the source state and reusing the public constructor, keeping defensive copies aligned with validation rules.
graph TD A[input] C{Field type} D["validateLength(input): validateNotBlank + measure trimmed length"] E[trim & assign id/name/address] F["validateDigits(input): validateNotBlank + digits-only + exact length"] G[assign phone as provided] X[IllegalArgumentException] A --> C C -->|id/name/address| D C -->|phone| F D -->|pass| E D -->|fail| X F -->|pass| G F -->|fail| X - Text fields (
contactId,firstName,lastName,address):validateLengthfirst callsvalidateNotBlankon the original input, then measuresinput.trim().length()against the bounds. If valid, the caller trims and stores. - Phone numbers:
validateDigitscallsvalidateNotBlankon the original input, then checks for digits-only and exact length. No trimming; whitespace fails the digit check. - Because the constructor routes through the setters, the exact same pipeline applies whether the object is being created or updated.
// Bad throw new IllegalArgumentException("Invalid input"); // Good throw new IllegalArgumentException("firstName length must be between 1 and 10");- Specific, label-driven messages make debugging easier and double as documentation. Tests assert on the message text so regressions are caught immediately.
| Exception Type | Use Case | Recovery? | Our Choice |
|---|---|---|---|
| Checked | Recoverable issues | Maybe | ❌ |
| Unchecked | Programming errors | Fix code | ✅ |
- We throw
IllegalArgumentException(unchecked) because invalid input is a caller bug and should crash fast.
graph TD A[Client request] --> B[ContactService] B --> C[Validation] C --> D{valid?} D -->|no| E[IllegalArgumentException] E --> F[Client handles/fails fast] D -->|yes| G[State assignment] - Fail-fast means invalid state never reaches persistence/logs, and callers/tests can react immediately.
- Each validator rule started as a failing test, then the implementation was written until the suite passed.
ContactTestserves as the living specification covering both the success path and every invalid scenario.TaskTestandTaskServiceTestmirror the same workflow for the Task domain/service, reusing the sharedValidationhelper and singleton patterns, and include invalid update cases to prove atomicity.
@ParameterizedTest+@CsvSourceenumerate the invalid IDs, names, phones, and addresses so we don’t duplicate boilerplate tests.
@ParameterizedTest @CsvSource({ "'', 'contactId must not be null or blank'", "' ', 'contactId must not be null or blank'", "'12345678901', 'contactId length must be between 1 and 10'" }) void testInvalidContactId(String id, String expectedMessage) { assertThatThrownBy(() -> new Contact(id, "first", "last", "1234567890", "123 Main St")) .isInstanceOf(IllegalArgumentException.class) .hasMessage(expectedMessage); }- AssertJ’s
hasFieldOrPropertyWithValuevalidates the happy path in one fluent statement. assertThatThrownBy().isInstanceOf(...).hasMessage(...)proves exactly which validation rule triggered.
testSuccessfulCreationvalidates the positive constructor path (all fields stored).testValidSettersensures setters update fields when inputs pass validation.testConstructorTrimsStoredValuesconfirms IDs, names, and addresses are normalized viatrim().testFailedCreation(@ParameterizedTest) enumerates every invalid ID/name/phone/address combination and asserts the corresponding message.testFailedSetFirstName(@ParameterizedTest) exercises the setter's invalid inputs (blank/long/null).testUpdateRejectsInvalidValuesAtomically(@MethodSource) proves invalid updates throw and leave the existing Contact state unchanged.testCopyRejectsNullInternalState(@ParameterizedTest) uses reflection to corrupt each internal field (contactId, firstName, lastName, phone, address), proving thevalidateCopySource()guard triggers for all null branches.ValidationTest.validateLengthAcceptsBoundaryValuesproves 1/10-char names and 30-char addresses remain valid.ValidationTest.validateLengthRejectsBlankStringsandValidationTest.validateLengthRejectsNullensure blanks/nulls fail before length math is evaluated.ValidationTest.validateLengthRejectsTooLonghits the max-length branch to keep upper-bound validation covered.ValidationTest.validateLengthRejectsTooShortcovers the min-length branch so both ends of the range are exercised.ValidationTest.validateDigitsRejectsBlankStringsandValidationTest.validateDigitsRejectsNullensure the phone validator raises the expected messages before regex/length checks.ValidationTest.validateDigitsAcceptsValidPhoneNumberproves valid 10-digit inputs pass without exception.ValidationTest.validateDigitsRejectsNonDigitCharactersasserts non-digit input triggers the "must only contain digits 0-9" message.ValidationTest.validateDigitsRejectsWrongLengthasserts wrong-length input triggers the "must be exactly 10 digits" message.ValidationTest.validateDateNotPastAcceptsFutureDate,validateDateNotPastRejectsNull, andvalidateDateNotPastRejectsPastDateassert the appointment date guard enforces the non-null/not-in-the-past contract before any Appointment state mutates.ValidationTest.validateDateNotPastAcceptsDateExactlyEqualToNow(added for PITest) usesClock.fixed()to deterministically test the exact boundary wheredate.getTime() == clock.millis(), killing the boundary mutant (<vs<=).ValidationTest.privateConstructorIsNotAccessible(added for line coverage) exercises the private constructor via reflection to cover the utility class pattern.RateLimitingFilterTestasserts bucket invalidation, log sanitization, and[unsafe-value]placeholders for client IPs/usernames so inline guards (logSafeValue,sanitizeForWarningLog) can't be removed.RequestLoggingFilterTestnow coverssanitizeForLog,sanitizeLogValue, query-string inclusion/exclusion, and duration math to stop PIT from flipping boundaries or stripping sanitizers.- Spring Boot tests use Mockito's subclass mock-maker (
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker) to avoid agent attach issues on newer JDKs while still enabling MockMvc/context testing. ContactControllerUnitTestandProjectControllerUnitTestmock the service to prove the controller-level ADMIN guard (?all=true) blocks non-admin callers before falling back to the service guard, killing the survivingisAdminmutants.AuthControllerUnitTestcovers the cookie parsing helper used during logout/token refresh so PIT can't rewrite it to always returnnull.
Note (JDK 25+): When running tests on JDK 25 or later, you may see a warning like
Mockito is currently self-attaching to enable the inline-mock-maker. This is expected and harmless; Mockito's subclass mock-maker handles mocking without requiring the Java agent, so the warning does not affect test correctness.
Legacy singleton safety: All service-layer suites that exercise the
ContactService,TaskService, andAppointmentServicesingletons (including the legacy fallback and bridge tests) are annotated with JUnit’s@Isolated. Surefire otherwise executes those classes in parallel, which races the shared staticgetInstance()store and can trigger duplicate ID inserts. Keep the annotation on any new suites that mutate the singleton to avoid reintroducing the flaky DuplicateResourceException bursts.
Security hardening introduced specialized test utilities to simplify authenticated testing:
-
@WithMockAppUser(WithMockAppUser.java) - Custom annotation that populates the SecurityContext with a realUserentity (not just a generic Spring Security stub). Use@WithMockAppUseron test methods or classes to authenticate as a default c, or customize with@WithMockAppUser(username = "admin", role = Role.ADMIN)for role-specific tests. The factory creates a detached user instance so tests don't need database setup. -
TestUserSetup(TestUserSetup.java) - Spring@Componentthat persists a test user to the database and configures the SecurityContext, satisfying foreign key constraints when creating contacts/tasks/appointments. Inject via@Autowiredand callsetupTestUser()in@BeforeEach. This utility is essential for integration tests that exercise full service-to-repository flows with real database persistence. -
@AutoConfigureObservability- Spring Boot Test annotation enabling actuator metrics endpoints in test contexts. Used in ActuatorEndpointsTest.java to verify Prometheus metrics exposure and endpoint security without starting a full production server.
- DomainDataStore abstraction -
ContactServicedepends onContactStore, injected with the JPA-backed implementation during normal operation. The legacy singleton path lazily spins up anInMemoryContactStore, then migrates data into the JPA store when Spring finishes wiring beans. - Transactional guarantees - Methods run inside Spring transactions so the
existsById+saveorfindById+updatesequences remain atomic. Read methods opt into@Transactional(readOnly = true)for SQL efficiency. - Validation + normalization - Service methods validate/trims IDs via
Validation.validateNotBlank, but all field-level rules still live insideContact(update()and setters). That keeps controller/service logic shallow. - Defensive copies -
getAllContacts(),getDatabase(), andgetContactById()return freshContact.copy()instances so external callers cannot mutate persistent state. - Package-private reset hooks -
clearAllContacts()sticks around exclusively for tests in the same package; production code never calls it directly.
graph TD A[Controller/Service call] --> B{Operation} B -->|add| C["validate contact != null"] C -->|fail| X[IllegalArgumentException] C -->|pass| D["store.existsById(contactId)?"] D -->|true| E[return false] D -->|false| F["store.save(mapper.toEntity(contact))"] B -->|delete| G["validateNotBlank(contactId)"] G --> H["store.deleteById(trimmedId)"] B -->|update| I["validateNotBlank(contactId)"] I --> J["store.findById(trimmedId)"] J -->|empty| E J -->|present| K["Contact.update(...)"] K --> L["store.save(updated contact)"] store.savehandles both inserts and updates; the mapper converts domain objects into JPA entities while preserving validation.- Delete/update operations trim IDs before handing them to the store so whitespace inputs remain consistent.
- Duplicate IDs or missing rows return
false, letting controllers return 409/404 without extra exception types.
ContactServiceTestis now a@SpringBootTestrunning against theintegrationprofile (Postgres + Flyway via Testcontainers) so every service method hits the real repositories/mappers.ContactServiceLegacyTestcold-starts the singleton without Spring, proving the fallback still works for older callers (and that state remains isolated between tests via reflection resets).- Slice tests live next door for mappers (
ContactMapperTest) and repositories (ContactRepositoryTest), catching schema or mapping regressions without booting the full application context. - Boolean outcomes are asserted explicitly (
isTrue()/isFalse()) so duplicate and missing-ID branches stay verified.
testGetInstanceensures the singleton accessor always returns a concrete service before any CRUD logic runs.testGetInstanceReturnsSameReferenceproves repeated invocations return the same singleton instance.testAddContactproves the happy path and that the map contains the stored entry with correct field values.testAddDuplicateContactFailsconfirms the boolean contract for duplicates and that the original data remains untouched.testAddContactNullThrowshits the defensive null guard so callers see a clearIllegalArgumentExceptioninstead of an NPE.testDeleteContactexercises removal plus assertion that the key disappears.testDeleteMissingContactReturnsFalsecovers the branch where no contact exists for the given id.testDeleteContactBlankIdThrowsshows ID validation runs even on deletes, surfacing the standard "contactId must not be null or blank" message.testUpdateContactverifies every mutable field changes via setter delegation.testUpdateContactTrimsIdconfirms IDs are trimmed before lookups during updates.testUpdateContactBlankIdThrowsensures update throws when the ID is blank so validation mirrors delete().testUpdateMissingContactReturnsFalsecovers the "not found" branch so callers can rely on the boolean result.testGetDatabaseReturnsDefensiveCopiesproves callers cannot mutate internal state through the returned snapshot.testGetInstanceColdStartuses reflection to reset the static instance, then verifiesgetInstance()creates a new instance when none exists—ensuring full branch coverage of the lazy initialization pattern.testGetContactByIdReturnsContactverifies getContactById returns the contact when it exists.testGetContactByIdReturnsEmptyWhenNotFoundverifies getContactById returns empty when contact doesn't exist.testGetContactByIdBlankIdThrowsverifies getContactById throws when ID is blank.testGetContactByIdTrimsIdverifies getContactById trims the ID before lookup.testGetContactByIdReturnsDefensiveCopyverifies getContactById returns a defensive copy.testGetAllContactsReturnsEmptyListverifies getAllContacts returns empty list when no contacts exist.testGetAllContactsReturnsAllContactsverifies getAllContacts returns all contacts.
getAllContactsAllUsers_requiresAdminRoleproves non-ADMIN users getAccessDeniedExceptionwhen attempting to fetch all users' contacts.getAllContactsAllUsers_returnsDataForAdminsproves ADMIN users can fetch contacts from multiple users.getContactById_onlyReturnsCurrentUsersRecordsproves users cannot see contacts owned by other users.addContact_duplicateIdsReturnFalseForSameUserproves duplicate ID detection scopes to the current user.deleteContact_doesNotRemoveOtherUsersDataproves users cannot delete contacts owned by other users.updateContact_doesNotAffectOtherUserRecordsproves users cannot update contacts owned by other users.
- Mapper tests (
ContactMapperTest,TaskMapperTest,AppointmentMapperTest) now assert the null-input short-circuit paths so PIT can mutate those guards without leaving uncovered lines. - New JPA entity tests (
ContactEntityTest,TaskEntityTest,AppointmentEntityTest) exercise the protected constructors and setters to prove Hibernate proxies can hydrate every column even when instantiated via reflection. - Legacy
InMemory*Storesuites assert theOptional.emptybranch offindByIdso both success and miss paths copy data defensively. - Combined with the existing controller/service suites and the security additions above, this brings the repo to 930 tests on the full suite with ~84% mutation kills and ~90% line coverage (higher on stores/mappers; container-dependent coverage enforced on Linux only).
The following tests were added specifically to catch surviving mutants by targeting boundary conditions, comparison operators, and edge cases. Each test includes Javadoc explaining why it exists and what mutation it prevents:
| Test File | +Tests | Focus Areas |
|---|---|---|
ValidationTest.java | +14 | Length boundaries (< vs <=), digit validation (!= operator), date millisecond precision, email max length (100 vs 101 chars), blank vs empty string handling |
ContactTest.java | +16 | ID max length (10 vs 11), name/address boundaries, phone digit count (9/10/11), copy independence verification, atomic update semantics |
TaskTest.java | +13 | ID/name/description boundary values, min/max length validation, copy-doesn't-affect-original assertions |
ProjectTest.java | +19 | Special minLength=0 for description, empty string after trimming, all ProjectStatus enum values, whitespace trimming edge cases |
JwtServiceTest.java | +9 | Token expiration at exact boundary (1ms), refresh window <= operator, case-sensitive username comparison, equals() vs == verification |
Mutation patterns targeted:
- Comparison operators:
<↔<=,>↔>=,==↔!= - Boundary off-by-one: testing at
max,max+1,min,min-1 - Boolean inversions: null checks, empty string checks
- Return value mutations: ensuring correct object returns from
copy() - Conditional removals: verifying validation is actually called
See ADR-0046 for the full rationale and methodology.
- Task IDs are required, trimmed, and immutable after construction (length 1-10).
- Name (≤20 chars) and description (≤50 chars) share one helper so constructor, setters, and
update(...)all enforce identical rules. statusis a requiredTaskStatusenum (TODO, IN_PROGRESS, DONE) that defaults to TODO if null; validated viaValidation.validateNotNull.- Optional
dueDateflows throughValidation.validateOptionalDateNotPast, so null values remain allowed but past dates throw immediately. - Optional
projectIdlinks tasks to projects (nullable, max 10 chars); setting it updatesupdatedAttimestamp. createdAtandupdatedAttimestamps (Instant) are set automatically on construction and update.- Three
update(...)overloads support different update scenarios:update(name, description)- backward-compatible, preserves existing status/dueDateupdate(name, description, status, dueDate)- full mutable field updateupdate(name, description, status, dueDate, projectId)- includes project linking
- All update methods validate atomically; invalid inputs leave the object untouched.
copy()creates a defensive copy preserving timestamps and projectId.- Tests mirror Contact coverage: constructor trimming, happy-path setters/update, status/dueDate validation, and every invalid-path exception message.
graph TD A[Constructor input] --> B["validateLength(taskId, 1-10)"] B -->|ok| C[trim & store taskId] B -->|fail| X[IllegalArgumentException] C --> D["validateLength(name, 1-20)"] D -->|ok| E[trim & store name] D -->|fail| X E --> F["validateLength(description, 1-50)"] F -->|ok| G[trim & store description] F -->|fail| X G --> H["validateNotNull(status) or default TODO"] H -->|ok| I[store status enum] H -->|fail| X I --> J["validateOptionalDateNotPast(dueDate)"] J -->|ok| K[store dueDate or null] J -->|fail| X - Constructor validates taskId, then delegates to setters for name/description.
validateLengthmeasuresinput.trim().length()on the original input, then the caller trims before storing.- Setters (
setName,setDescription) use the samevalidateLength+ trim pattern for updates. update(...)validates both fields first, then assigns both atomically if they pass.
- All strings come from
Validation.validateLength, so failures always say<label> must not be null or blankor<label> length must be between X and Y. - Tests assert the exact messages so a wording change immediately fails the suite.
graph TD A[Client] --> B[Task constructor/setter/update] B --> C{Validation} C -->|pass| D[State updated] C -->|fail| E[IllegalArgumentException] - No silent coercion; invalid data throws fast so tests/users fix the source input.
- Started with constructor trimming tests, then added invalid cases before writing setters/update so every branch had a failing test first.
- AssertJ
hasFieldOrPropertyWithValuekeeps success assertions short. assertThatThrownBy(...).hasMessage(...)locks in the Validation wording for each failure mode.
-
Constructor stores trimmed values and rejects null/blank/too-long IDs, names, and descriptions.
-
Setters accept valid updates and reject invalid ones with the same helper-generated messages.
-
update(...)replaces both mutable fields atomically and never mutates on invalid input. -
testUpdateRejectsInvalidValuesAtomically(@MethodSource) enumerates invalid name/description pairs (blank/empty/null/over-length) and asserts the Task remains unchanged when validation fails. -
testCopyRejectsNullInternalState(@ParameterizedTest) uses reflection to corrupt each internal field (taskId, name, description), proving thevalidateCopySource()guard triggers for all null branches. -
TaskServiceTestmirrors the entity atomicity: invalid updates (blank name) throw and leave the stored task unchanged.
- Depends on
TaskStore, which is implemented byJpaTaskStore(Spring Data repository + mapper) for normal operation andInMemoryTaskStorefor legacygetInstance()callers. As soon as the Spring bean initializes it registers itself as the singleton instance. - Transactions wrap add/delete/update operations; read-only queries opt into
@Transactional(readOnly = true)for efficient SQL on H2/Postgres. - Service-level guards cover null task inputs and blank IDs.
Task.update(...)enforces field rules so error messages stay aligned with the domain tests. getDatabase()andgetAllTasks()return defensive copies, andclearAllTasks()stays package-private for test resets.
graph TD A[TaskService call] --> B{Operation} B -->|add| C["task != null?"] C -->|no| X[IllegalArgumentException] C -->|yes| D["store.existsById(taskId)?"] D -->|true| Y[return false] D -->|false| E["store.save(task)"] B -->|delete| F["validateNotBlank(taskId)"] F --> G["store.deleteById(trimmedId)"] B -->|update| H["validateNotBlank(taskId)"] H --> I["store.findById(trimmedId)"] I -->|empty| Y I -->|present| J["Task.update(newName, description, status, dueDate)"] J --> K["store.save(updated task)"] - Duplicate IDs or missing rows simply return
falseso controllers can produce 409/404 responses without inspecting exceptions. - Mapper conversions ensure persisted data always flows back through the domain constructor/update path for validation.
TaskServiceTestis a Spring Boot test running on theintegrationprofile (Postgres + Flyway via Testcontainers), so every operation exercises the real repositories/mappers instead of in-memory maps.TaskServiceLegacyTestcold-startsgetInstance()outside Spring, proving the fallback still works and that legacy callers stay isolated.TaskServiceexposes a package-privatesetClock(Clock)hook used exclusively by tests so overdue calculations can be validated deterministically without mutating the domain validation rules.- Mapper/repository tests sit alongside the service tests for faster feedback on schema or conversion issues.
-
getAllTasksAllUsers_requiresAdminRoleproves non-ADMIN users getAccessDeniedExceptionwhen attempting to fetch all users' tasks. -
getAllTasksAllUsers_returnsDataForAdminsproves ADMIN users can fetch tasks from multiple users. -
getTaskById_onlyReturnsCurrentUsersTasksproves users cannot see tasks owned by other users. -
deleteTask_doesNotAllowOtherUsersToDeleteproves users cannot delete tasks owned by other users. -
updateTask_doesNotAllowCrossUserModificationproves users cannot update tasks owned by other users.
- Appointment IDs are required, trimmed, and immutable after construction (length 1-10).
appointmentDateusesjava.util.Date, must not be null or in the past, is stored/returned via defensive copies, and is serialized/deserialized as ISO 8601 with millis + offset (yyyy-MM-dd'T'HH:mm:ss.SSSXXX, UTC).- Descriptions are required, trimmed, and capped at 50 characters; constructor and update share the same validation path.
flowchart TD X[IllegalArgumentException] C1[Constructor inputs] --> C2["validateLength(appointmentId, 1-10)"] C2 -->|ok| C3[trim & store id] C2 -->|fail| X C3 --> C4[validateDateNotPast] C4 -->|ok| C5[defensive copy of date stored] C4 -->|fail| X C5 --> C6["validateLength(description, 1-50)"] C6 -->|ok| C7[trim & store description] C6 -->|fail| X U1[update newDate, newDescription] --> U2[validateDateNotPast] U2 -->|fail| X U2 -->|ok| U3["validateLength(description, 1-50)"] U3 -->|fail| X U3 -->|ok| U4[copy date + trim & store description] - Constructor:
validateLengthvalidates/measures trimmed ID, then trim & store. Then delegates to setters for date + description. validateLengthmeasuresinput.trim().length()on the original input, then the caller trims before storing (matches Contact/Task pattern).- Dates are validated via
Validation.validateDateNotPastand copied on set/get to prevent external mutation. update(...)validates both inputs before mutating, keeping updates atomic.
AppointmentTestcovers trimmed creation with defensive date copies, description setter happy path, invalid constructor cases (null/blank/over-length id/description, null/past dates), invalid description setters, invalid updates (null/past dates, bad descriptions) that leave state unchanged, and defensive getters.- AssertJ getters/field checks verify stored values; future/past dates are relative to “now” to avoid flakiness.
testSuccessfulCreationTrimsAndCopiesDatevalidates trim/defensive copy on construction.testUpdateReplacesValuesAtomicallyconfirms date/description updates and defensive date copy.testSetDescriptionAcceptsValidValuecovers setter happy path.testGetAppointmentDateReturnsDefensiveCopyensures callers can't mutate stored dates.testConstructorValidationenumerates invalid id/description/null/past date cases.testSetDescriptionValidationcovers invalid description setter inputs.testUpdateRejectsInvalidValuesAtomicallyenumerates invalid update inputs and asserts state remains unchanged.testCopyProducesIndependentInstanceverifies copy() produces an independent instance with identical values.testCopyRejectsNullInternalState(@ParameterizedTest) uses reflection to corrupt each internal field (appointmentId, appointmentDate, description), proving thevalidateCopySource()guard triggers for all null branches.
- Uses
AppointmentStore(JPA-backed) under Spring andInMemoryAppointmentStorewhengetInstance()is called before the context loads. As soon as the Spring bean initializes it registers itself as the canonical singleton. - Transactions wrap add/delete/update operations; read-only queries use
@Transactional(readOnly = true)like the other services. - Service methods validate/trim IDs via
Validation.validateNotBlank, butAppointment.update(...)enforces the date-not-past rule and description length so error messages match the domain layer. getDatabase(),getAllAppointments(), andgetAppointmentById()return defensive copies so external code never mutates persistent state.
graph TD A[AppointmentService call] --> B{Operation} B -->|add| C["appointment != null?"] C -->|no| X[IllegalArgumentException] C -->|yes| D["store.existsById(appointmentId)?"] D -->|true| Y[return false] D -->|false| E["store.save(appointment)"] B -->|delete| F["validateNotBlank(appointmentId)"] F --> G["store.deleteById(trimmedId)"] B -->|update| H["validateNotBlank(appointmentId)"] H --> I["store.findById(trimmedId)"] I -->|empty| Y I -->|present| J["Appointment.update(date, description)"] J --> K["store.save(updated appointment)"] - Duplicate IDs/missing rows return
false; invalid fields bubble up asIllegalArgumentException, keeping the fail-fast philosophy intact. - Mapper conversions preserve the Instant/Date handling so persisted timestamps match domain expectations.
AppointmentServiceTestruns against Postgres + Flyway (integrationprofile via Testcontainers) and covers add/delete/update/defensive-copy behaviors end-to-end.AppointmentServiceLegacyTestvalidates the non-Spring fallback and uses reflection resets to isolate state between runs.- Mapper/repository tests verify Instant↔Date conversion plus schema-level guarantees (e.g., NOT NULL/length constraints).
getAllAppointmentsAllUsers_requiresAdminRoleproves non-ADMIN users getAccessDeniedExceptionwhen attempting to fetch all users' appointments.getAllAppointmentsAllUsers_returnsDataForAdminsproves ADMIN users can fetch appointments from multiple users.getAppointmentById_onlyReturnsCurrentUsersAppointmentsproves users cannot see appointments owned by other users.deleteAppointment_doesNotAllowCrossUserDeletionproves users cannot delete appointments owned by other users.updateAppointment_doesNotAllowCrossUserModificationproves users cannot update appointments owned by other users.
- Project IDs are required, trimmed, and immutable after construction (length 1-10).
- Name (≤50 chars) is required and trimmed via
Validation.validateTrimmedLength. - Description (0-100 chars) allows blank values via
Validation.validateTrimmedLengthAllowBlank. statusis a requiredProjectStatusenum (ACTIVE, ON_HOLD, COMPLETED, ARCHIVED); validated viaValidation.validateNotNull.update(name, description, status)validates all fields atomically before mutation; invalid inputs leave the object untouched.copy()creates a defensive copy by validating source state and reusing the public constructor.
graph TD A[Constructor input] --> B["validateTrimmedLength(projectId, 1-10)"] B -->|ok| C[store projectId] B -->|fail| X[IllegalArgumentException] C --> D["validateTrimmedLength(name, 1-50)"] D -->|ok| E[store name] D -->|fail| X E --> F["validateTrimmedLengthAllowBlank(description, 0-100)"] F -->|ok| G[store description] F -->|fail| X G --> H["validateNotNull(status)"] H -->|ok| I[store status enum] H -->|fail| X - Constructor validates projectId, then delegates to setters for name/description/status.
validateTrimmedLengthtrims input before length check, returning the trimmed value.validateTrimmedLengthAllowBlankpermits empty strings when minLength=0 (description only).validateNotNullensures enum status is never null.
ProjectTest(51 tests) covers constructor trimming, setter/update happy paths, invalid constructor/setter/update cases (null/blank/over-length), atomic update rejection, and defensive copy verification.ProjectStatusTest(12 tests) validates all enum values and their string representations.
- Constructor stores trimmed values and rejects null/blank/too-long IDs and names.
- Description allows empty strings but rejects over-length values.
- Status rejects null with
IllegalArgumentException. update(...)replaces all mutable fields atomically and never mutates on invalid input.testCopyRejectsNullInternalStateuses reflection to corrupt each internal field, provingvalidateCopySource()guard triggers.
- Depends on
ProjectStore, which is implemented byJpaProjectStore(Spring Data repository + mapper) for normal operation andInMemoryProjectStorefor legacygetInstance()callers. - Transactions wrap add/delete/update operations; read-only queries use
@Transactional(readOnly = true). - Per-user data isolation enforces that users only see their own projects.
- CRUD methods:
addProject(),deleteProject(),updateProject(id, name, description, status). - Query methods:
getAllProjects(),getAllProjectsAllUsers()(ADMIN only),getProjectById(),getProjectsByStatus(). - Contact linking:
addContactToProject(),removeContactFromProject(),getProjectContacts(),getContactProjects(). getDatabase()andgetAllProjects()return defensive copies;clearAllProjects()stays package-private for test resets.
graph TD A[ProjectService call] --> B{Operation} B -->|add| C["project != null?"] C -->|no| X[IllegalArgumentException] C -->|yes| D["store.existsById(projectId)?"] D -->|true| Y[DuplicateResourceException] D -->|false| E["store.insert(project, user)"] B -->|delete| F["validateNotBlank(projectId)"] F --> G["store.deleteById(trimmedId, user)"] B -->|update| H["validateNotBlank(projectId)"] H --> I["store.findById(trimmedId, user)"] I -->|empty| Y2[return false] I -->|present| J["Project.update(name, description, status)"] J --> K["store.save(updated project, user)"] B -->|getByStatus| L["store.findByStatus(user, status)"] - Duplicate IDs throw
DuplicateResourceExceptionfor 409 Conflict responses. - Missing rows return
falseso controllers can produce 404 responses. - All operations scope to the authenticated user via
getCurrentUser().
ProjectServiceTest(28 tests) is a Spring Boot test running on theintegrationprofile (Postgres + Flyway via Testcontainers).InMemoryProjectStoreTestvalidates the non-Spring fallback store.- Mapper/repository tests sit alongside for faster feedback on schema or conversion issues.
getAllProjectsAllUsers_requiresAdminRoleproves non-ADMIN users getAccessDeniedException.getAllProjectsAllUsers_returnsDataForAdminsproves ADMIN users can fetch projects from all users.getProjectById_onlyReturnsCurrentUsersProjectsproves users cannot see projects owned by others.deleteProject_doesNotAllowCrossUserDeletionproves users cannot delete projects owned by others.updateProject_doesNotAllowCrossUserModificationproves users cannot update projects owned by others.
Phase 5 delivered the following foundational pieces and they remain the backbone of the security stack:
Password Hashing (ADR-0018): BCrypt (Spring Security's BCryptPasswordEncoder) selected for portability across CI/dev/prod environments and existing FIPS-validated implementations. Argon2 remains the long-term goal for memory-hard hashing; migration strategy includes algorithm column for dual-algorithm support with transparent rehashing on login.
Authentication Evolution (ADR-0043): Migrated from localStorage (XSS vulnerability FRONTEND-SEC-01) to HttpOnly auth_token cookies. Migration timeline: dual support opened 2025-12-05, enforcement date 2026-01-05 when Authorization header will be rejected. Rollback available via LEGACY_TOKEN_ALLOWED=true feature flag.
CSRF Protection: Double-submit cookie pattern via SpaCsrfTokenRequestHandler sets XSRF-TOKEN cookie with SameSite=Lax attribute. Frontend reads cookie value and sends it in X-XSRF-TOKEN header for state-changing requests.
- User entity implements Spring Security's
UserDetailsinterface for seamless authentication integration. - Field constraints defined in
Validation.java: username (1-50 chars), email (1-100 chars, valid format), password (1-255 chars, BCrypt hash required). - Constructor validates all fields, trims username/email, and rejects raw passwords (must start with
$2a$,$2b$, or$2y$). - Role enum (
USER,ADMIN) stored as string; default isUSER. - JPA lifecycle callbacks (
@PrePersist,@PreUpdate) managecreatedAtandupdatedAttimestamps.
graph TD A[Constructor input] --> B["validateTrimmedLength(username, 1-50)"] B -->|ok| C[store trimmed username] B -->|fail| X[IllegalArgumentException] C --> D["validateEmail(email)"] D -->|ok| E[trim & store email] D -->|fail| X E --> F["validateLength(password, 1-255)"] F -->|ok| G[BCrypt pattern check] F -->|fail| X G -->|ok| H[store password] G -->|fail| X H --> I["validateNotNull(role)"] I -->|ok| J[store role] I -->|fail| X validateTrimmedLength()validates and returns trimmed username in one call.validateEmail()checks both length (1-100) and format via regex pattern.validateNotNull()uses the new enum helper for consistent null checking.- BCrypt pattern
^\\$2[aby]\\$.+ensures passwords are pre-hashed; raw passwords rejected immediately.
UserTestcovers successful creation with valid BCrypt hash, trimming behavior, UserDetails interface methods (getAuthorities, account status), and all validation edge cases.- Parameterized tests exercise null/blank/over-length inputs for username, email, and password.
- Invalid email format tests cover common malformed patterns (missing @, missing domain, double dots).
- BCrypt requirement test confirms plain-text passwords are rejected.
testSuccessfulCreationvalidates all fields stored correctly with valid BCrypt hash.testConstructorTrimsUsernameAndEmailproves normalization on both fields.testGetAuthoritiesReturnsRoleWithPrefixverifies Spring SecurityROLE_prefix.testEmailInvalidFormatThrowsenumerates malformed email patterns.testPasswordMustBeBcryptHashconfirms raw password rejection.- Boundary tests use
emailOfLength()helper to generate valid emails at max length.
- Spring Data JPA repository with
findByUsername,findByEmail,existsByUsername,existsByEmailmethods. - Used by
CustomUserDetailsServicefor authentication lookups.
- JWT token generation and validation using HMAC-SHA256.
- Configurable via
jwt.secret(Base64-encoded, UTF-8 fallback) andjwt.expiration(default 30 minutes). - Methods:
generateToken(),extractUsername(),isTokenValid(),extractClaim().
OncePerRequestFilterthat extracts JWTs from the HttpOnlyauth_tokencookie (preferred) withAuthorization: Bearer <token>header fallback for API clients.- Extracts username from JWT, loads user via
UserDetailsService, validates token, and setsSecurityContextHolder. - Invalid/expired tokens silently continue without authentication (endpoints handle 401).
- Configures Spring Security filter chain with stateless session management (JWT).
- CSRF tokens issued via
CookieCsrfTokenRepositoryfor browser routes while/api/**relies on JWT; BCrypt password encoder bean. - Public endpoints:
/api/auth/**,/actuator/health,/actuator/info,/swagger-ui/**,/error, static SPA files. - Protected endpoints:
/api/v1/**require authenticated users with valid JWT. - CORS configured via
cors.allowed-originsproperty for SPA development (default:localhost:5173,localhost:8080). - Security headers:
Content-Security-Policy(script/style/img/font/connect/frame-ancestors/form-action/base-uri/object-src),Permissions-Policy(disables geolocation, camera, microphone, etc.),X-Content-Type-Options,X-Frame-Options(SAMEORIGIN),Referrer-Policy(strict-origin-when-cross-origin).
- Implements
UserDetailsServicefor Spring Security authentication. - Loads users from
UserRepositoryby username; throwsUsernameNotFoundExceptionif not found.
- REST controller for authentication operations at
/api/auth(see ADR-0038). - DTOs:
LoginRequest(username, password),RegisterRequest(username, email, password),AuthResponse(token, username, email, role, expiresIn). - Uses
AuthenticationManagerfor credential verification andJwtServicefor token generation. - Duplicate username/email checks via
UserRepositorymethods. - Exposes
GET /api/auth/csrf-tokenso the SPA can fetch theXSRF-TOKENvalue for double-submit protection now that JWTs live in HttpOnly cookies. - Uses a dedicated
app.auth.cookie.secureconfiguration property (overridable viaAPP_AUTH_COOKIE_SECURE/COOKIE_SECURE) so the auth cookie can be forced to Secure=true in production without conflating it with the servlet session cookie settings.
| Endpoint | Method | Description | Success | Errors |
|---|---|---|---|---|
/api/auth/login | POST | Authenticate user | 200 + HttpOnly JWT cookie (token omitted from body) | 400 (validation), 401 (invalid credentials) |
/api/auth/register | POST | Register new user | 201 + HttpOnly JWT cookie (token omitted from body) | 400 (validation), 409 (duplicate username/email) |
/api/auth/logout | POST | Invalidate session | 204 No Content | - |
| Status | Meaning | When Used |
|---|---|---|
| 200 | OK | Login successful |
| 201 | Created | Registration successful |
| 204 | No Content | Logout successful |
| 400 | Bad Request | Validation error (blank/invalid fields) |
| 401 | Unauthorized | Invalid credentials (login only) |
| 409 | Conflict | Username or email already exists (register only) |
- Controllers annotated with
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")require authenticated users. @SecurityRequirement(name = "bearerAuth")documents JWT requirement in OpenAPI spec.- SPA relies on HttpOnly cookies for tokens while caching profile metadata in
sessionStorageviatokenStorage/useProfile, so no JS-accessible token is stored (profile cache clears when the browser ends the session—subject to browser restore/bfcache behavior).
JwtServiceTestunit tests the JWT token lifecycle (generation, extraction, validation) using reflection to inject test secrets.JwtAuthenticationFilterTestmocks the filter chain and verifies SecurityContext population for valid/invalid tokens.CustomUserDetailsServiceTestmocks the repository to verify user lookup and exception handling.AuthControllerTestintegration tests for login/register endpoints (validation, success, conflicts, auth failures).
JwtServiceTest.java Scenario Coverage
generateTokenContainsUsernameClaimproves tokens include the subject claim with the username.isTokenValidReturnsTrueForValidTokenverifies validation passes for freshly generated tokens.isTokenValidReturnsFalseForDifferentUserensures tokens are rejected when the user doesn't match.extractUsernameThrowsWhenTokenExpiredconfirmsExpiredJwtExceptionis thrown for expired tokens.extractClaimReturnsCustomClaimverifies custom claims (e.g., role) can be extracted from tokens.fallbackToUtf8SecretWhenBase64DecodingFailsproves the UTF-8 fallback works when Base64 decoding fails.
JwtAuthenticationFilterTest.java Scenario Coverage
skipsFilterWhenAuthorizationHeaderMissingensures requests without a JWT cookie/header proceed without authentication.proceedsWhenTokenInvalidconfirms invalid tokens don't block the filter chain and leave SecurityContext empty.setsAuthenticationWhenTokenValidverifies valid tokens populate SecurityContext with the authenticated user.
CustomUserDetailsServiceTest.java Scenario Coverage
loadUserReturnsUserDetailsWhenFoundproves successful user lookup returns the UserDetails.loadUserThrowsWhenMissingconfirmsUsernameNotFoundExceptionis thrown when user doesn't exist.
AuthControllerTest.java Scenario Coverage
register_validRequest_returns201WithTokenproves successful registration returns JWT and user info.register_validRequest_userPersistedInDatabaseverifies user is actually saved to repository.login_validCredentials_returns200WithTokenproves successful login returns JWT with user details.login_adminUser_returnsAdminRoleconfirms admin users get ADMIN role in response.login_wrongPassword_returns401verifies invalid password returns 401 Unauthorized.login_nonexistentUser_returns401confirms missing user returns generic 401 (prevents enumeration).register_duplicateUsername_returns409proves duplicate username triggers 409 Conflict.register_duplicateEmail_returns409proves duplicate email triggers 409 Conflict.register_invalidInput_returns400(parameterized) covers blank/long username, invalid email format, short password.login_invalidInput_returns400(parameterized) covers blank username/password validation.register_malformedJson_returns400/login_malformedJson_returns400verify malformed JSON handling.register_usernameAtMaxLength_accepted/register_passwordAtMinLength_acceptedboundary tests.
Each service enforces per-user data isolation via user_id foreign keys. Tests use a runAs(username, role, action) helper to simulate different authenticated users and verify cross-user access is blocked:
| Service | Test Class | Key Scenarios |
|---|---|---|
| ContactService | ContactServiceTest | getAllContactsAllUsers_requiresAdminRole, getContactById_onlyReturnsCurrentUsersRecords, deleteContact_doesNotRemoveOtherUsersData, updateContact_doesNotAffectOtherUserRecords |
| TaskService | TaskServiceTest | getAllTasksAllUsers_requiresAdminRole, getTaskById_onlyReturnsCurrentUsersTasks, deleteTask_doesNotAllowOtherUsersToDelete, updateTask_doesNotAllowCrossUserModification |
| AppointmentService | AppointmentServiceTest | getAllAppointmentsAllUsers_requiresAdminRole, getAppointmentById_onlyReturnsCurrentUsersAppointments, deleteAppointment_doesNotAllowCrossUserDeletion, updateAppointment_doesNotAllowCrossUserModification |
See detailed scenario descriptions in each service's "Security Tests (Per-User Isolation)" subsection above.
- Mutation tests target specific PIT mutants that survive standard coverage.
- Each test exercises the
DataIntegrityViolationExceptioncatch block inadd*()methods. - Mocks simulate race conditions where
existsById()returns false butsave()throws due to a concurrent insert.
ContactServiceMutationTest.addContactReturnsFalseWhenStoreThrowsIntegrityViolationkills the mutant that blindly returnstruefromaddContact()by verifying the catch block returnsfalse.TaskServiceMutationTest.addTaskReturnsFalseWhenStoreThrowsIntegrityViolationsame pattern for TaskService.AppointmentServiceMutationTest.addAppointmentReturnsFalseWhenStoreThrowsIntegrityViolationsame pattern for AppointmentService.
sequenceDiagram participant C as Client participant F as JwtAuthFilter participant J as JwtService participant U as UserDetailsService participant S as SecurityContext C->>F: Request with Bearer token F->>J: extractUsername(token) J-->>F: username F->>U: loadUserByUsername(username) U-->>F: UserDetails F->>J: isTokenValid(token, userDetails) J-->>F: true F->>S: setAuthentication(authToken) F-->>C: Continue to controller The same security/observability pass introduced the following request tracing, logging, and rate-limiting building blocks. See ADR-0040, ADR-0041, and ADR-0042.
Request → CorrelationIdFilter(1) → RateLimitingFilter(5) → RequestLoggingFilter(10) → JwtAuthFilter → Controllers - Extracts
X-Correlation-IDheader or generates UUID if missing/invalid. - Stores in SLF4J MDC for automatic inclusion in all log entries.
- Sanitizes IDs: max 64 chars, alphanumeric + hyphens/underscores only (prevents log injection).
- Logs HTTP method, URI, sanitized query string, masked client IP, and user agent.
- Configurable via
logging.request.enabledproperty. - Sensitive query parameters masked: token, password, api_key, secret, auth.
- IPv4 last octet masked (e.g.,
192.168.1.***), IPv6 fully masked.
- Token bucket rate limiting via Bucket4j with Caffeine bounded caches.
- Login: 5 req/min per IP, Register: 3 req/min per IP, API: 100 req/min per user.
- Returns 429 with
Retry-Afterheader and JSON error body.
- Custom Logback converter that masks PII in log messages.
- Phone numbers: shows last 4 digits (
***-***-1234). - Addresses: preserves city/state, masks street/zip (
*** Cambridge, MA ***).
- Utility for client IP extraction supporting reverse proxies.
- Header priority:
X-Forwarded-For→X-Real-IP→RemoteAddr.
CorrelationIdFilterTest.java Scenario Coverage
doFilterInternal_preservesValidHeaderverifies existing correlation IDs are propagated.doFilterInternal_generatesIdWhenHeaderMissingconfirms UUID generation for missing headers.doFilterInternal_generatesIdWhenHeaderInvalidproves invalid characters trigger regeneration.doFilterInternal_rejectsOverlyLongIdstests the max length boundary rejection.doFilterInternal_acceptsIdAtMaxLengthconfirms 64-char IDs are accepted.doFilterInternal_rejectsIdJustOverMaxLengthconfirms 65-char IDs are rejected (boundary test).doFilterInternal_trimsValidHeaderBeforePropagatingverifies whitespace trimming.
RequestLoggingFilterTest.java Scenario Coverage
doFilterInternal_logsRequestAndResponseWhenEnabledverifies logging output format.doFilterInternal_skipsLoggingWhenDisabledconfirms disabled filter passes through silently.doFilterInternal_includesQueryStringInLogWhenPresenttests query string inclusion.doFilterInternal_logsRequestWithoutQueryStringconfirms clean output without query params.doFilterInternal_logsDurationInResponseverifies duration is captured in milliseconds.maskClientIp_masksIpv4LastOctetproves IPv4 masking preserves first 3 octets.maskClientIp_masksIpv6Addressesconfirms IPv6 addresses are fully masked.sanitizeQueryString_redactsSensitiveParameterstests sensitive param masking (token, password, etc.).sanitizeQueryString_returnsNullForBlankInputconfirms null handling.sanitizeUserAgent_stripsControlCharactersprevents log injection via user agent.
RateLimitingFilterTest.java Scenario Coverage
testLoginEndpoint_allowsRequestsWithinLimitverifies requests under limit pass.testLoginEndpoint_blocksRequestsExceedingLimitconfirms 429 after limit exceeded.testLoginEndpoint_separateLimitsPerIpproves each IP has independent bucket.testRegisterEndpoint_enforcesRateLimittests registration endpoint limits.testApiEndpoint_enforcesRateLimitPerUserverifies per-user API limiting.testApiEndpoint_separateLimitsPerUserproves user isolation.testApiEndpoint_passesThoughWhenNotAuthenticatedconfirms unauthenticated passthrough.testNonRateLimitedPath_alwaysPassesThroughtests excluded paths.testXForwardedForHeader_usedForIpExtractionverifies proxy header support.testXForwardedForHeader_handlesMultipleIpstests comma-separated IP handling.testRetryAfterHeader_setCorrectlyconfirms Retry-After header format.testClearBuckets_resetsAllLimitstests bucket reset functionality.testJsonResponseFormatverifies 429 response body structure.calculateWaitTimeUsesProbeEstimatetests wait time calculation.calculateWaitTime_returnsAtLeastOneSecondconfirms minimum 1s retry window.clearBuckets_resetsBucketsAndAllowsNewRequestsverifies reset allows new requests.calculateWaitTimeReturnsOneWhenTokensAvailableedge case coverage.testBucketCountingverifies bucket token consumption.
PiiMaskingConverterTest.java Scenario Coverage
convert_masksPhonesAndAddressestests combined phone + address masking.convert_masksSevenDigitPhoneFallbackverifies non-10-digit phone handling.convert_handlesNullMessagesGracefullyconfirms null safety.
RequestUtilsTest.java Scenario Coverage
getClientIp_prefersFirstXForwardedForEntrytests header priority.getClientIp_usesXRealIpWhenForwardedForMissingconfirms fallback chain.getClientIp_fallsBackToRemoteAddresstests final fallback.getClientIp_returnsUnknownWhenNoSourcesPresentconfirms default handling.
JacksonConfigTest.java Scenario Coverage
objectMapperRejectsBooleanCoercionconfirms strict schema compliance.objectMapperRejectsNumericCoercionverifies string field type enforcement.
- Three REST controllers expose CRUD endpoints under
/api/v1/{contacts,tasks,appointments}. - Service-level lookup methods - Controllers use
getAllXxx()andgetXxxById(id)service methods for better encapsulation instead of accessinggetDatabase()directly. - DTOs with Bean Validation (
ContactRequest,TaskRequest,AppointmentRequest) validate input at the HTTP boundary before reaching domain logic. - Global exception handling via
@RestControllerAdvicemaps exceptions to consistent JSON error responses. - Custom error controller ensures ALL errors return JSON, including container-level errors (malformed requests, invalid paths) that bypass Spring MVC exception handling.
- OpenAPI/Swagger UI available at
/swagger-ui.htmland/v3/api-docs(powered by springdoc-openapi). - Enhanced OpenAPI spec: Controllers use
@Tag,@Operation, and@ApiResponsesannotations to produce a production-quality spec withapplication/jsoncontent types and documented error responses (400/404/409).
| Resource | Create (POST) | Read (GET) | Update (PUT) | Delete (DELETE) |
|---|---|---|---|---|
| Contacts | /api/v1/contacts | /api/v1/contacts, /api/v1/contacts/{id} | /api/v1/contacts/{id} | /api/v1/contacts/{id} |
| Tasks | /api/v1/tasks | /api/v1/tasks, /api/v1/tasks/{id} | /api/v1/tasks/{id} | /api/v1/tasks/{id} |
| Appointments | /api/v1/appointments | /api/v1/appointments, /api/v1/appointments/{id} | /api/v1/appointments/{id} | /api/v1/appointments/{id} |
| Projects | /api/v1/projects | /api/v1/projects, /api/v1/projects/{id} | /api/v1/projects/{id} | /api/v1/projects/{id} |
| Endpoint | Parameter | Description | Example |
|---|---|---|---|
GET /api/v1/tasks | ?projectId={id} | Filter tasks by project | /api/v1/tasks?projectId=PROJ001 |
GET /api/v1/tasks | ?projectId=none | Get unassigned tasks | /api/v1/tasks?projectId=none |
GET /api/v1/tasks | ?assigneeId={userId} | Filter tasks by assignee | /api/v1/tasks?assigneeId=123 |
GET /api/v1/tasks | ?status={status} | Filter tasks by status | /api/v1/tasks?status=TODO |
GET /api/v1/appointments | ?taskId={id} | Filter appointments by task | /api/v1/appointments?taskId=TASK001 |
GET /api/v1/appointments | ?projectId={id} | Filter appointments by project | /api/v1/appointments?projectId=PROJ001 |
GET /api/v1/projects | ?status={status} | Filter projects by status | /api/v1/projects?status=ACTIVE |
| Status | Meaning | When Used |
|---|---|---|
| 200 | OK | GET by ID, PUT update success |
| 201 | Created | POST create success |
| 204 | No Content | DELETE success |
| 400 | Bad Request | Validation failure, malformed JSON |
| 401 | Unauthorized | Missing/expired JWT or no auth_token cookie |
| 403 | Forbidden | Authenticated user lacks access (per-user isolation, admin override checks on POST /api/v1/admin/query / legacy ?all=true) |
| 404 | Not Found | Resource with given ID does not exist |
| 409 | Conflict | Duplicate ID on create or optimistic locking version mismatch |
GlobalExceptionHandler and the Spring Security filter chain surface the 401/403 rows even for controller methods that do not reference them directly, so API clients always see consistent JSON errors for authentication failures and per-user access violations.
flowchart TD A[HTTP Request] --> B[Bean Validation on DTO] B -->|fail| C[400 Bad Request] B -->|pass| D[Domain Constructor/Update] D -->|fail| C D -->|pass| E[Service Layer] E --> F[200/201/204 Response] - Bean Validation (
@NotBlank,@Size,@Pattern,@FutureOrPresent) catches invalid input early with user-friendly error messages. DTOs use@Schema(pattern = ".*\\S.*")to document non-whitespace requirement in OpenAPI. - Path variable validation:
@NotBlank @Size(min=1, max=10)on{id}path parameters enforces ID constraints (no whitespace-only, max 10 chars). Controllers are annotated with@Validated(required for Spring to enforce method-level constraints on@PathVariable). OpenAPI spec documents this via@Parameter(schema=@Schema(minLength=1, maxLength=10, pattern=".*\\S.*")). - Domain validation (
Validation.validateLength,validateDigits,validateDateNotPast) acts as a backup layer—same rules, same constants. - DTO constraints use static imports from
Validation.MAX_*constants to stay in sync with domain rules.
- ContactControllerTest (32 tests): 21 @Test + 11-case @ParameterizedTest covering CRUD, validation errors, boundary tests, 404/409 scenarios.
- TaskControllerTest (41 tests): 35 @Test + 6-case @ParameterizedTest covering CRUD, validation errors, status/dueDate/projectId fields, boundary tests, 404/409 scenarios.
- AppointmentControllerTest (23 tests): 19 @Test + 4-case @ParameterizedTest covering date validation, past-date rejection, ISO 8601 format handling.
- GlobalExceptionHandlerTest (6 tests): Direct unit tests for exception handler methods (
handleIllegalArgument,handleNotFound,handleDuplicate,handleConstraintViolation,handleAccessDenied). - CustomErrorControllerTest (17 tests): 12 @Test + 5-row CSV @ParameterizedTest for container-level error handling (status codes, JSON content type, message mapping).
- JsonErrorReportValveTest (17 tests): Unit tests for Tomcat valve JSON error handling (Content-Length, buffer reset, committed response guards).
Controller tests use reflection to access package-private clearAll*() methods on the autowired service:
@BeforeEach void setUp() throws Exception { final Method clearMethod = ContactService.class.getDeclaredMethod("clearAllContacts"); clearMethod.setAccessible(true); clearMethod.invoke(contactService); // Use autowired service, not getInstance() }report_successfulResponse_doesNotWriteBody- Verifies successful responses (status < 400) are not processed.report_committedResponse_doesNotWriteBody- Verifies already-committed responses are skipped to avoid corruption.report_badRequest_writesJsonBody- Verifies 400 errors write JSON with correct Content-Type, Content-Length, and message.report_notFound_writesCorrectMessage- Verifies 404 errors return "Resource not found" message.report_resetBufferThrowsException_returnsEarly- Verifies valve handlesIllegalStateExceptiongracefully.report_ioException_handledGracefully- Verifies IOException during write doesn't throw.report_statusCodeMapping(parameterized) - Verifies all HTTP status codes map to correct error messages (400→Bad request, 401→Unauthorized, 403→Forbidden, 404→Resource not found, 405→Method not allowed, 415→Unsupported media type, 500→Internal server error, unknown→Error).report_withThrowable_stillWritesJson- Verifies throwable parameter doesn't affect JSON output.report_statusBelowThreshold_doesNotWriteBody- Verifies 399 status (below threshold) is not processed.report_exactlyAtThreshold_writesBody- Verifies 400 status (exactly at threshold) is processed.
- React 19 + Vite + TypeScript powers the frontend with fast HMR and type safety.
- Tailwind CSS v4 provides utility-first styling via the
@themedirective for CSS-native design tokens. - shadcn/ui components (Button, Card, Table, Sheet, Dialog, etc.) offer accessible, copy-paste primitives built on Radix UI.
- TanStack Query handles server state with automatic caching, refetching, and error handling; auth helpers sync profile data with the backend and clear cached queries on logout.
- React Router v7 manages client-side navigation with a nested route structure and authentication-aware routing (
/login,RequireAuth,PublicOnlyRoute). - Zod schemas mirror backend
Validation.javaconstants for consistent client-side validation. - Settings page (profile + appearance) and Help page (getting started steps, resource links, keyboard shortcuts) provide user customization.
flowchart TD A[React 19] --> B[Vite Dev Server] B --> C{Request Path} C -->|/api/*| D[Proxy to Spring Boot :8080] C -->|Static| E[Serve from dist/] F[TanStack Query] --> G[lib/api.ts] G --> D H[React Hook Form] --> I[Zod Schemas] I --> J[lib/schemas.ts] J --> K[Mirrors Validation.java] - Use
python scripts/dev_stack.pyto launch Spring Boot (mvn spring-boot:run) and the Vite UI (npm run dev -- --port 5173) in one terminal; add--database postgresto have it start Docker Compose (auto-detectsdocker composevsdocker-compose), enable thedevprofile, and wire datasource env vars automatically. - The helper polls
http://localhost:8080/actuator/healthbefore starting the frontend, installsui/contact-appdependencies ifnode_modulesis missing, and shuts everything down on Ctrl+C. - Flags:
--frontend-port 4000,--backend-goal "spring-boot:run -Dspring-boot.run.profiles=dev",--skip-frontend-install,--database postgres,--docker-compose-file ./docker-compose.dev.yml,--postgres-url jdbc:postgresql://localhost:5432/contactapp,--postgres-username contactapp,--postgres-password contactapp, and--postgres-profile devkeep it flexible for custom setups.
┌─────────────────────────────────────────────────────────────────┐ │ ┌──────────┐ ┌─────────────────────────────────────────────────┐│ │ │ Logo │ │ TopBar: [Title] [🔍] [🌙] [👤] ││ │ ├──────────┤ └─────────────────────────────────────────────────┘│ │ │ Overview │ ┌──────────────────────────────────┬──────────────┐│ │ │ Contacts │ │ │ ││ │ │ Tasks │ │ Content Area │ Sheet ││ │ │ Appts │ │ (list/table/cards) │ (details) ││ │ │ │ │ │ ││ │ ├──────────┤ │ │ ││ │ │ Settings │ │ │ ││ │ │ Help │ │ │ ││ │ └──────────┘ └──────────────────────────────────┴──────────────┘│ └─────────────────────────────────────────────────────────────────┘ - Sidebar (desktop/tablet): Navigation with icons and labels, collapsible on tablet.
- TopBar: Page title, search trigger (Ctrl+K), dark mode toggle, theme switcher, user avatar.
- Content: Route-specific views (Overview dashboard, entity tables).
- Sheet: Right-hand drawer for viewing/editing entity details without losing list context.
| Breakpoint | Sidebar | Navigation | Drawer |
|---|---|---|---|
| Desktop (≥1024px) | Full width with labels | Left sidebar | Right sheet |
| Tablet (768-1023px) | Icons only | Left sidebar (narrow) | Right sheet |
| Mobile (<768px) | Hidden | Bottom nav (future) | Full-screen sheet |
- 5 professional themes: Slate (default), Ocean (fintech), Forest (productivity), Violet (startup), Zinc (developer tools).
- Light/dark variants controlled via
.darkclass on<html>. - CSS variable architecture with Tailwind v4
@themedirective for semantic tokens. - WCAG 2.1 AA compliant contrast ratios verified for all theme combinations.
flowchart LR A[User selects theme] --> B[useTheme hook] B --> C[Remove old theme-* classes] C --> D[Add new theme-* class] B --> E[Toggle dark class] D --> F[CSS variables update] E --> F F --> G[UI re-renders with new colors] ui/contact-app/ ├── src/ │ ├── components/ │ │ ├── layout/ # AppShell, Sidebar, TopBar │ │ └── ui/ # shadcn/ui components │ ├── hooks/ # useTheme, useMediaQuery │ ├── lib/ # api.ts, schemas.ts, utils.ts │ ├── pages/ # OverviewPage, ContactsPage, etc. │ ├── App.tsx # Router + QueryClient setup │ ├── index.css # Tailwind + theme CSS variables │ └── main.tsx # React DOM entry ├── components.json # shadcn/ui configuration ├── package.json # Dependencies (React 19, Tailwind v4) ├── tsconfig.app.json # TypeScript config with @/* alias └── vite.config.ts # Vite + Tailwind plugin + API proxy const METHODS_REQUIRING_CSRF = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); async function ensureCsrfToken(): Promise<string | null> { // Reads XSRF-TOKEN cookie or hits /api/auth/csrf-token if missing } async function fetchWithCsrf(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> { const headers = new Headers(init.headers ?? {}); const method = (init.method ?? 'GET').toUpperCase(); if (METHODS_REQUIRING_CSRF.has(method)) { const token = await ensureCsrfToken(); if (token) headers.set('X-XSRF-TOKEN', token); } return fetch(input, { credentials: 'include', ...init, headers }); } export const contactsApi = { create: (data: ContactRequest) => fetchWithCsrf('/api/v1/contacts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }).then(handleResponse<Contact>), // ... get/update/delete re-use fetchWithCsrf };CORS reminder: Every environment needs explicit origins (no
*) plusAccess-Control-Allow-Credentials: true,Access-Control-Allow-Origin: https://your-spa.example,Access-Control-Allow-Headers: Authorization, Content-Type, X-XSRF-TOKEN, X-Request-ID, andAccess-Control-Expose-Headers: X-Request-ID, X-Trace-ID. Keep the origin list in source control (nginx/ingress + SpringCorsRegistry) so UI deployments and backend configs stay synchronized.
// lib/schemas.ts - Zod schemas matching Validation.java constants export const ValidationLimits = { MAX_ID_LENGTH: 10, // Validation.MAX_ID_LENGTH MAX_NAME_LENGTH: 10, // Validation.MAX_NAME_LENGTH MAX_ADDRESS_LENGTH: 30, // Validation.MAX_ADDRESS_LENGTH MAX_TASK_NAME_LENGTH: 20, // Validation.MAX_TASK_NAME_LENGTH MAX_DESCRIPTION_LENGTH: 50, // Validation.MAX_DESCRIPTION_LENGTH PHONE_LENGTH: 10, // Validation.PHONE_LENGTH } as const; export const contactSchema = z.object({ id: z.string().min(1).max(ValidationLimits.MAX_ID_LENGTH), firstName: z.string().min(1).max(ValidationLimits.MAX_NAME_LENGTH), // ... });- Development:
npm run devstarts Vite with API proxy tolocalhost:8080. - Production: Maven's
frontend-maven-pluginrunsnpm ci && npm run buildduringprepare-packagephase. - Single JAR: Built UI assets copy to
target/classes/static/so Spring Boot serves them at/. - Fast feedback:
mvn testruns backend tests only;mvn packageincludes full UI build. - Docker required for backend tests: SpringBootTest/MockMvc/service suites now use Postgres via Testcontainers (
integrationprofile). Start Docker before runningmvn test; without Docker, limit to unit/slice suites or use-DskipITs=true -Dspring.profiles.active=testfor H2-only slices.
- Vitest + React Testing Library - 22 component tests (schemas, forms, pages)
- Playwright E2E - 5 tests covering CRUD happy path (list/create/edit/delete)
cd ui/contact-app npm run test:run # Vitest unit/component tests (22 tests) npm run test:e2e # Playwright E2E tests (5 tests, starts dev server automatically) npm run test:coverage # Vitest with coverage report| ADR | Title | Summary |
|---|---|---|
| ADR-0025 | UI Component Library | shadcn/ui + Tailwind v4 selection rationale |
| ADR-0026 | Theme System | CSS variable architecture, 5 themes, WCAG compliance |
| ADR-0027 | App Shell Layout | Sidebar + TopBar + Sheet pattern, responsive breakpoints |
| ADR-0028 | Build Integration | Maven plugin config, phase binding, single JAR output |
Application.java / Spring Boot Infrastructure
- Spring Boot 4.0.0 provides the runtime foundation with embedded Tomcat, auto-configuration, and actuator endpoints.
@SpringBootApplicationcombines@Configuration,@EnableAutoConfiguration, and@ComponentScanto wire everything together.- Component scanning discovers
@Servicebeans incontactapp.servicepackage automatically. - Services retain their singleton
getInstance()pattern for backward compatibility while also supporting Spring DI via@Autowired.
contactapp/ ├── Application.java # Spring Boot entrypoint ├── domain/ # Domain entities (Contact, Task, Appointment, Validation) ├── service/ # @Service beans (ContactService, TaskService, AppointmentService) ├── api/ # REST controllers + DTOs + error handling │ ├── ContactController.java # Contact CRUD endpoints │ ├── TaskController.java # Task CRUD endpoints │ ├── AppointmentController.java # Appointment CRUD endpoints │ ├── GlobalExceptionHandler.java # @RestControllerAdvice error mapping │ ├── CustomErrorController.java # JSON error responses for container-level errors │ ├── dto/ # Request/Response DTOs with Bean Validation │ └── exception/ # ResourceNotFoundException, DuplicateResourceException ├── config/ # Tomcat/Spring configuration │ ├── JsonErrorReportValve.java # Tomcat valve for JSON error responses (ADR-0022) │ └── TomcatConfig.java # Registers JsonErrorReportValve with embedded Tomcat └── persistence/ # Repository interfaces (Phase 3 - empty) - Profile-based settings in
application.yml:dev: Debug logging, health details always showntest: Minimal logging for fast CI runsprod: Restricted health details, warn-level logging
- Actuator lockdown: Only
/actuator/healthand/actuator/infoare exposed; all other endpoints (env, beans, metrics) return 404 per OWASP guidelines.
management: endpoints: web: exposure: include: health,info # Only health and info exposed endpoint: health: show-details: when-authorized probes: enabled: true # Kubernetes liveness/readiness support- Security: Actuator endpoints can expose sensitive information (environment variables, bean definitions, metrics). Locking them down by default follows defense-in-depth.
- Observability: Health and info endpoints are essential for orchestrators (Kubernetes probes, load balancer health checks) and operational dashboards.
- Profiles: Environment-specific behavior without code changes;
spring.profiles.active=devunlocks more verbose settings locally.
jwt.secretis mandatory in production and pairs withjwt.expiration=1800000(30 minutes) to keep cookie-backed sessions short-lived by default.app.auth.cookie.secure(env:APP_AUTH_COOKIE_SECURE) governs the customauth_tokencookie separately from the servlet session cookie; dev/test default tofalse, but the prod profile requiresCOOKIE_SECURE/APP_AUTH_COOKIE_SECUREwith no fallback so insecure cookies never ship accidentally.- Reverse proxies and Spring CORS config must define explicit origins +
Access-Control-Allow-Credentials: trueto satisfy the SPA’scredentials: 'include'fetch calls.
- Smoke test verifying the Spring application context loads without errors.
- Empty test body is intentional:
@SpringBootTesttriggers context loading before any test runs. If wiring fails, the test fails with detailed error messages.
contextLoads- Verifies the application context loads without exceptions.mainMethodCoverage- CallsApplication.main()directly to ensure the entrypoint is exercised for JaCoCo line coverage.
- Catches configuration errors early: missing beans, circular dependencies, invalid property bindings, component scanning failures.
- Fast feedback loop: fails in seconds rather than waiting for full integration tests to discover wiring issues.
- Integration tests verifying actuator endpoint security configuration using MockMvc.
- Confirms security posture matches
application.ymlsettings.
healthEndpointReturnsUp- Verifies/actuator/healthreturns 200 OK with{"status":"UP"}. Critical for Kubernetes probes and load balancer health checks.infoEndpointReturnsOk- Verifies/actuator/inforeturns 200 OK. Provides build metadata for operational dashboards.envEndpointIsNotExposed- Verifies/actuator/envreturns 404. Prevents exposure of environment variables that may contain secrets.beansEndpointIsNotExposed- Verifies/actuator/beansreturns 404. Prevents exposure of internal architecture details.metricsEndpointRequiresAuth- Verifies/actuator/metricsrequires authentication. Exposes JVM/application metrics for monitoring.prometheusEndpointRequiresAuth- Verifies/actuator/prometheusrequires authentication. Prometheus-format metrics for scraping.
| Endpoint | Status | Reason |
|---|---|---|
/actuator/health | ✅ Exposed | Required for orchestrator probes |
/actuator/info | ✅ Exposed | Build metadata for ops |
/actuator/metrics | ✅ Exposed | JVM/app metrics (auth required) |
/actuator/prometheus | ✅ Exposed | Prometheus scraping (auth required) |
/actuator/env | ❌ Blocked | May contain secrets |
/actuator/beans | ❌ Blocked | Reveals architecture |
- Integration tests verifying service beans are properly registered and injectable.
- Proves component scanning discovers all
@Serviceclasses.
contactServiceBeanExists- VerifiesContactServiceis injectable via@Autowiredand retrievable fromApplicationContext.taskServiceBeanExists- VerifiesTaskServiceis injectable and present in context.appointmentServiceBeanExists- VerifiesAppointmentServiceis injectable and present in context.serviceBeansAreSingletons- Verifies all three services are singletons (same instance across injection points), matching the expected behavior for stateful in-memory services.
- Catches
NoSuchBeanDefinitionExceptionerrors before they surface in controller tests (Phase 2). - Documents the expected wiring behavior: services should be singletons.
- Verifies backward compatibility: Spring-managed beans should behave identically to
getInstance()pattern.
Tests use a single static PostgreSQL container for the entire test suite (not per-class containers). Avoids the "48x slowdown" bug where @Testcontainers/@Container annotations create NEW containers for EACH test class, but Spring Boot's test context caching REUSES the same ApplicationContext (and HikariCP connection pool) across test classes, causing port mismatches between containers and cached connection pools.
Pattern (PostgresContainerSupport.java):
@ServiceConnection protected static final PostgreSQLContainer<?> postgres; static { postgres = new PostgreSQLContainer<>("postgres:16-alpine"); postgres.start(); }Result: Tests complete in ~15 seconds instead of 12+ minutes (48x speedup). Aligns with Spring Boot test context caching for consistent performance.
| Layer | Tool | Focus |
|---|---|---|
| Coverage | JaCoCo | Line/branch coverage enforcement during mvn verify. |
| Mutation | PITest | Ensures assertions catch injected faults; threshold currently 70%. |
| Style & complexity | Checkstyle | Formatting, naming, indentation, import ordering, and boolean simplification. |
| Bug patterns | SpotBugs | Bytecode bug-pattern scanning via spotbugs-maven-plugin (fails build on findings). |
| Dependency security | OWASP Dependency-Check | CVE scanning backed by NVD_API_KEY with optional skip fallback. |
| Semantic security | CodeQL | Detects SQLi/XSS/path-traversal patterns in a separate workflow. |
Each layer runs automatically in CI, so local mvn verify mirrors the hosted pipelines.
- Dependabot runs every Monday at 15:30 ET against the Maven ecosystem and automatically opens PRs for available dependency upgrades.
Why mutation testing matters: Line coverage (JaCoCo) shows what code executes, not whether tests catch bugs. A test that runs code but never asserts anything still counts as "covered". Mutation testing proves tests actually work by injecting faults and verifying tests fail.
Enforcement: PITest runs in mvn verify with 70% threshold. Mutations that survive indicate missing or weak assertions.
- PITest runs inside
mvn verify, so the new service tests contribute directly to the enforced mutation score. - The GitHub Actions matrix uses the same suite, ensuring duplicate/add/delete/update scenarios stay green across OS/JDK combinations.
- GitHub Actions still executes
{ubuntu-latest, windows-latest} × {Java 17, Java 21}withMAVEN_OPTS="--enable-native-access=ALL-UNNAMED -Djdk.attach.allowAttachSelf=true", so mutation coverage is enforced everywhere. - The optional self-hosted lane remains available for long mutation sessions or extra capacity; see the dedicated section below.
graph TD A[Static analysis] --> B[Unit tests] B --> C[Service tests] C --> D[Integration tests] D --> E[Mutation tests] | Check Group | Focus |
|---|---|
ImportOrder, AvoidStarImport, RedundantImport | Enforce ordered/separated imports, no wildcards, and no duplicates. |
NeedBraces, LeftCurly, RightCurly, EmptyBlock | Require braces and consistent brace placement; flag empty blocks. |
WhitespaceAround, WhitespaceAfter, NoWhitespaceBefore, NoWhitespaceAfter, SingleSpaceSeparator, ParenPad | Enforce consistent spacing around tokens and parentheses. |
Indentation, LineLength, FileTabCharacter, NewlineAtEndOfFile | Align indentation, cap lines at 120 chars, disallow tabs, ensure trailing newline. |
ModifierOrder, RedundantModifier | Keep modifier order canonical and drop redundant keywords. |
MethodLength, MethodParamPad, MethodName, ParameterName, LocalVariableName, MemberName | Bound method size and enforce naming/padding conventions. |
HiddenField | Prevent locals/parameters from shadowing fields (except constructors/setters). |
MagicNumber | Flags unwanted literals (excluding -1, 0, 1) to encourage constants. |
SimplifyBooleanExpression, SimplifyBooleanReturn, OneStatementPerLine | Reduce complex boolean logic and keep one statement per line. |
FinalParameters, FinalLocalVariable | Encourage immutability for parameters and locals when possible. |
# Run SpotBugs as part of the normal build mvn -Ddependency.check.skip=true verify # Fail fast on SpotBugs findings during local iterations mvn spotbugs:check # Generate the HTML report at target/spotbugs.html mvn spotbugs:spotbugs # Open the SpotBugs GUI to inspect findings interactively mvn spotbugs:guiCI already runs SpotBugs inside
mvn verify; these commands help when iterating locally.
Dependency-Check also pings the Sonatype OSS Index service. When requests are anonymous the analyzer often rate-limits, which is why CI prints warnings like “An error occurred while analyzing … (Sonatype OSS Index Analyzer)”. To receive full results:
- Create a free account at ossindex.sonatype.org and generate an API token.
- Add the credentials to your Maven
settings.xml:<settings> <servers> <server> <id>ossindex</id> <username>YOUR_OSS_INDEX_USERNAME</username> <password>YOUR_OSS_INDEX_API_TOKEN</password> </server> </servers> </settings>
- Run Maven with
-DossIndexServerId=ossindex(or set the property permanently withexport MAVEN_OPTS="$MAVEN_OPTS -DossIndexServerId=ossindex"). GitHub Actions can do the same by storing the username/token as repository secrets and writing the snippet above beforemvn verify.
If you skip these steps, the OSS Index analyzer simply logs warnings while the rest of Dependency-Check continues to rely on the NVD feed.
- Full backlog lives in
docs/logs/backlog.mdso the README stays concise, and includes future ideas for reporting, observability, and domain enhancements.
| Job | Trigger | What it does | Notes |
|---|---|---|---|
build-test | Push/PR to main/master, release, manual dispatch | Matrix {ubuntu, windows} × {JDK 17, 21} running mvn verify (tests + Checkstyle + SpotBugs + JaCoCo + PITest + Dependency-Check), builds QA dashboard, posts QA summary, uploads reports, Codecov upload. Note: Ubuntu runners explicitly run -DskipITs=false while Windows runners skip Testcontainers integration tests (-DskipITs=true) because GitHub-hosted Windows runners cannot run Linux containers. | Retries mvn verify with Dependency-Check/PITest skipped if the first attempt fails due to feed/timeouts. |
api-fuzz | Push/PR to main/master, manual dispatch | Starts Spring Boot app, runs Schemathesis against /v3/api-docs, exports OpenAPI spec, publishes JUnit XML results. Fails on 5xx errors or schema violations. | 20-minute timeout; exports spec as artifact for ZAP. |
container-test | Always (needs build-test) | Re-runs mvn verify inside maven:3.9.9-eclipse-temurin-17 using the H2/-DskipTestcontainersTests=true profile (no Docker socket available); retries with Dependency-Check/PITest skipped on failure. | Uses same MAVEN_OPTS for PIT attach; skips Testcontainers suites by design. |
mutation-test | Only when repo var RUN_SELF_HOSTED == 'true' and a self-hosted runner is online | Runs mvn verify on the self-hosted runner with PITest enabled; retries with Dependency-Check/PITest skipped on failure. | Optional lane; skipped otherwise. |
release-artifacts | Release event (published) | Packages the JAR and uploads it as an artifact; generates release notes. | Not run on normal pushes/PRs. |
| Command | Purpose |
|---|---|
mvn verify | Full build: compile, unit tests, Checkstyle, SpotBugs, JaCoCo, PITest, Dependency-Check. |
mvn -Ddependency.check.skip=true -Dpit.skip=true verify | Fast local build when Dependency-Check feed is slow/unavailable. |
mvn spotbugs:check | Run only SpotBugs and fail on findings. |
mvn -DossIndexServerId=ossindex verify | Opt-in authenticated OSS Index for Dependency-Check (see Sonatype section). |
cd ui/qa-dashboard && npm ci && npm run build | Build the React QA dashboard locally (already built in CI). |
pip install schemathesis && python scripts/api_fuzzing.py --start-app | Run API fuzzing locally (starts app, fuzzes, exports spec). |
.github/workflows/java-ci.ymlrunsmvn -B verifyacross{ubuntu-latest, windows-latest} × {Java 17, Java 21}to surface OS and JDK differences early.- Ubuntu runners execute the full test suite including Testcontainers integration tests (Postgres via Docker).
- Windows runners skip integration tests (
-DskipITs=true) because GitHub-hosted Windows runners only support Windows containers, not the Linux containers required by Testcontainers/Postgres. Unit tests, MockMvc tests, and all quality gates still run. - Local developers also skip the Testcontainers suite by default (Maven property
skipITs=true). Opt in withmvn verify -DskipITs=falseonce Docker Desktop/Colima is running to mirror the CI configuration. container-testrerunsmvn verifyinsidemaven:3.9.9-eclipse-temurin-17with-DskipTestcontainersTests=true -Pskip-testcontainers(H2 only; no Docker socket); if quality gates fail, it retries with Dependency-Check and PITest skipped.
Sequential execution (forkCount=1 in pom.xml): Required because singleton services share state across test classes. Spring Boot test context caching reuses ApplicationContext instances, and @Isolated tests need sequential execution to avoid collisions when clearing shared state (SecurityContext, seed users, singleton instances).
Profile strategy: Linux CI runs full suite with Testcontainers/Postgres; Windows CI uses skip-testcontainers profile on H2 (no Docker) with reduced JaCoCo threshold (75% vs 80%); legacy getInstance() suites tagged legacy-singleton run separately via mvn test -Plegacy-singleton.
- Each matrix job executes the quality gate suite (unit tests, JaCoCo, Checkstyle, SpotBugs, Dependency-Check, PITest). Integration tests run only on Ubuntu (see Matrix Verification above).
- Checkstyle enforces formatting/import/indentation rules while SpotBugs scans bytecode for bug patterns and fails the build on findings.
- SpotBugs runs as part of every
mvn verifyrun on the supported JDKs (currently 17 and 21 in CI) and fails the build on findings. - Dependency-Check throttling is tuned via
nvdApiDelay(defaults to 3500ms when an NVD API key is configured, 8000ms without a key) and honors-Ddependency.check.skip=trueif the NVD feed is unreachable; PITest has a similar-Dpit.skip=trueretry path so contributors stay unblocked but warnings remain visible. - Python 3.12 is provisioned via
actions/setup-python@v5soscripts/ci_metrics_summary.pyruns consistently on both Ubuntu and Windows runners. - Node.js 22 is provisioned via
actions/setup-node@v4and the React dashboard underui/qa-dashboard/is built every run so the artifacts contain the interactive QA console. - Mutation coverage now relies on GitHub-hosted runners by default; the self-hosted lane is opt-in and only fires when the repository variable
RUN_SELF_HOSTEDis set. - Dependabot checks run every Monday at 15:30 ET so Maven updates appear as automated PRs without waiting for the rest of the week.
- After every matrix job,
scripts/ci_metrics_summary.pyposts a table to the GitHub Actions run summary showing tests, JaCoCo coverage, PITest mutation score, and Dependency-Check counts (with ASCII bars for quick scanning). - The same summary script emits a dark-mode HTML dashboard (
target/site/qa-dashboard/index.html) with quick stats and links to the JaCoCo, SpotBugs, Dependency-Check, and PITest HTML reports (packaged inside thequality-reports-*artifact for download) and dropsserve_quality_dashboard.pynext to the reports for easy local previewing.
- Maven artifacts are cached via
actions/cache@v4(~/.m2/repository) to keep builds fast. - Dependency-Check data is intentionally purged every run (see the “Purge Dependency-Check database cache” step) to avoid stale or corrupted NVD downloads. If feed reliability improves we can re-enable caching in the workflow, but for now the clean slate proved more stable.
- The standard matrix already executes PITest, but some contributors keep a self-hosted runner handy for long mutation sessions, experiments, or when GitHub-hosted capacity is saturated.
- Toggling the repository variable
RUN_SELF_HOSTEDtotrueenables themutation-testjob, which mirrors the hosted command line but runs on your own hardware withMAVEN_OPTS="--enable-native-access=ALL-UNNAMED -Djdk.attach.allowAttachSelf=true".
- Successful workflows publish build artifacts, and the release workflow packages release notes so we can trace which commit delivered which binary.
- The
release-artifactsjob is intentionally gated withif: github.event_name == 'release' && github.event.action == 'published', so you will see it marked as “skipped” on normal pushes or pull requests. It only runs when a GitHub release/tag is published.
- After JaCoCo generates
target/site/jacoco/jacoco.xml, the workflow uploads it to Codecov so the coverage badge stays current. - Setup steps (once per repository):
- Sign in to Codecov with GitHub and add this repo.
- Generate a repository token in Codecov and save it as the GitHub secret
CODECOV_TOKEN. - Re-run the workflow; each matrix job uploads coverage with a
flagslabel (os-jdk).
- The badge at the top of this README pulls from the default
masterbranch; adjust the URL if you maintain long-lived release branches.
.github/workflows/codeql.ymlruns independently of the matrix job to keep static analysis focused.- The workflow pins Temurin JDK 17 via
actions/setup-java@v4, caches Maven dependencies, and enables the+security-and-qualityquery pack for broader coverage. - GitHub's CodeQL
autobuildruns the Maven build automatically; a commentedmvnfallback is available if the repo ever needs a custom command. - Concurrency guards prevent overlapping scans on the same ref, and
paths-ignoreensures doc-only/image-only changes do not queue CodeQL unnecessarily. - Triggers: pushes/PRs to
mainormaster(respecting the filters), a weekly scheduled scan (cron: 0 3 * * 0), and optional manual dispatch.
Log injection vulnerabilities (CWE-117) are prevented through inline validation in logging methods. Both RateLimitingFilter and RequestLoggingFilter use dedicated sanitization methods that:
- Strip CR/LF and control characters before logging
- Validate against safe character patterns (
^[A-Za-z0-9 .:@/_-]+$) - Truncate overly long values (max 120 chars)
- Return safe placeholder values (
[null],[empty],[unsafe-value]) for invalid input
This inline validation approach ensures CodeQL can trace data flow and verify that only sanitized values reach log statements.
.github/workflows/api-fuzzing.ymlruns Schemathesis against the live OpenAPI spec to detect 5xx errors, schema violations, and edge cases.- Schemathesis v4+ compatibility: The workflow uses updated options after v4 removed
--base-url,--hypothesis-*, and--junit-xmlflags. - Two-layer JSON error handling:
JsonErrorReportValveintercepts errors at the Tomcat container level, whileCustomErrorControllerhandles Spring-level errors. This ensures most error responses returnapplication/json. Note: Extremely malformed URLs (invalid Unicode) fail at Tomcat's connector level before the valve, socontent_type_conformancecheck is not used (see ADR-0022). - Content-Length fix for chunked encoding:
JsonErrorReportValvenow sets explicitContent-Lengthto avoid "invalid chunk" errors during fuzzing. The valve implements five safeguards:isCommitted()check, buffer reset,IllegalStateExceptionbailout, explicitContent-Length, and binary write viaOutputStream. This is the standard Tomcat pattern: guard → reset → set headers → write bytes → flush. - All Schemathesis phases pass: Coverage, Fuzzing, and Stateful phases all pass (30,668 test cases generated, 0 failures).
- Workflow steps:
- Build the JAR with
mvn -DskipTests package. - Start Spring Boot app in background, wait for
/actuator/healthto returnUP(usesjqfor robust JSON parsing). - Export OpenAPI spec to
target/openapi/openapi.json(artifact for ZAP/other tools). - Run
schemathesis runwith--checks not_a_server_error --checks response_schema_conformance(all phases enabled). - Stop app, publish summary to GitHub Actions.
- Build the JAR with
- Artifacts produced:
openapi-spec: JSON/YAML OpenAPI specification for ZAP and other security tools.api-fuzzing-results: Schemathesis output for debugging.
- Local testing:
pip install schemathesis && python scripts/api_fuzzing.py --start-appruns the same fuzzing locally. - Failure criteria: Any 5xx response or response schema violation fails the workflow. Expected 400s from validation (e.g., past dates) are not flagged.
graph TD A[Push or PR or Release] B[build-test: Matrix verify Ubuntu+Windows JDK17+21] C{Quality gate fail?} D[Retry step with DepCheck/PITest skipped] E[QA summary & artifacts & Codecov] F[container-test: Temurin17 Maven3.9.9] G{RUN_SELF_HOSTED set?} H[mutation-test: Self-hosted runner] I[release-artifacts: Package JAR] J[api-fuzz: Schemathesis] K[OpenAPI spec artifact] L{Release event?} A --> B A --> J --> K B --> C C -->|yes| D --> E C -->|no| E B --> F B --> G G -->|yes| H G -->|no| L H --> L F --> L L -->|yes| I L -->|no| M[Done] Each GitHub Actions matrix job writes a QA table (tests, coverage, mutation score, Dependency-Check status) to the run summary. The table now includes colored icons, ASCII bars, and severity breakdowns so drift stands out immediately. Open any workflow's "Summary" tab and look for the "QA Metrics" section for the latest numbers.
Current Test Metrics (full Linux matrix):
- 930 test executions (parameterized) with +44 TaskService tests covering query methods, user isolation, and defensive copies
- +71 mutation-focused tests targeting boundary conditions and comparison operators
- ~84% mutation kill rate (PIT) and ~90% line coverage overall (higher on stores/mappers)
- All domain entities have comprehensive boundary testing
- Test fixtures use centralized
TestCleanupUtilityto reset the SecurityContext, reseed test users, and reset singleton instances via reflection, ensuring complete test isolation and eliminating DuplicateResource exceptions - Windows matrix executes 700 tests under
-DskipTestcontainersTests=true(H2), with a reduced JaCoCo gate that excludes container-only code paths; full coverage/mutation gates are enforced on the Linux Testcontainers lanes. - Note: Full test suite requires Docker for Testcontainers-based integration tests; the H2 lane is for portability and sanity checks.
Recent PIT survivors in the rate-limiting/logging filters and the TaskService legacy fallback are now covered with dedicated unit tests (log capturing + legacy-store spies), so sanitization helpers and legacy data migration can't be removed without failing tests.
Need richer visuals?
Download the quality-reports-<os>-jdk<ver> artifact from the workflow run, unzip it, and from the artifact root run:
cd ~/Downloads/quality-reports-<os>-jdk<ver> python serve_quality_dashboard.py --path site(If the artifact retains the target/site structure, change --path site to --path target/site.)
Modern browsers block ES modules when loaded directly from file:// URLs
So the helper launches a tiny HTTP server, opens http://localhost:<port>/qa-dashboard/index.html, and serves the React dashboard with the correct metrics.json.
You’ll see the same KPIs, inline progress bars, and quick links over to the JaCoCo, SpotBugs, Dependency-Check, and PITest HTML reports, all sourced from the exact results of that build.
| | The sunburst shows which packages and classes are covered by tests. Open the full-screen interactive sunburst on Codecov » |
-
Register a runner per GitHub's instructions (Settings -> Actions -> Runners -> New self-hosted runner). Choose macOS/Linux + architecture.
-
Install + configure:
- Go to your repository on GitHub
- Navigate to Settings -> Actions -> Runners -> New self-hosted runner
- Select your OS (macOS for Mac, Linux for Linux) and architecture (x64 for Intel, arm64 for Apple Silicon)
- Follow GitHub's provided commands to download and configure the runner.
For macOS:
# Create runner directory mkdir actions-runner && cd actions-runner # Download the latest runner package (check GitHub for exact URL as it includes version) # For Intel Mac: curl -o actions-runner-osx-x64-2.321.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-x64-2.321.0.tar.gz # For Apple Silicon Mac: # curl -o actions-runner-osx-arm64-2.321.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gz # Extract the installer tar xzf ./actions-runner-osx-*.tar.gz # Configure the runner (get token from GitHub UI) ./config.sh --url https://github.com/jguida941/contact-suite-spring-react --token YOUR_TOKEN_FROM_GITHUB # Set MAVEN_OPTS permanently (choose based on your shell) # For zsh (default on modern macOS): echo 'export MAVEN_OPTS="--enable-native-access=ALL-UNNAMED -Djdk.attach.allowAttachSelf=true"' >> ~/.zshrc source ~/.zshrc # For bash: # echo 'export MAVEN_OPTS="--enable-native-access=ALL-UNNAMED -Djdk.attach.allowAttachSelf=true"' >> ~/.bash_profile # source ~/.bash_profile # Start the runner ./run.sh
Leave ./run.sh running so the mutation-test job can execute on your machine.
When you're done, press Ctrl+C to stop the runner.
Workflow toggle - the mutation-test job only runs when the repository variable RUN_SELF_HOSTED is set to true.
- Default (variable unset/false): the job is skipped so GitHub-hosted runners finish cleanly even if your machine is offline.
- When you want to run mutation tests: start the runner and set
Settings → Secrets and variables → Actions → Variables → RUN_SELF_HOSTED = true, then re-run the workflow. - Turn the variable back to
false(or delete it) when you shut down the runner, so future workflows don’t wait for a machine that isn’t listening.
If you're working through CS320 (or just exploring the project), the recommended flow is:
- Read the requirements in
docs/cs320-requirements/contact-requirements/so you understand the contact rules and service behavior. - Study
Contact.java,ContactService.java, andValidation.java, then jump into the paired tests (ContactTest.javaandContactServiceTest.java) to see every rule exercised. - Run
mvn verifyand inspect the JUnit (Contact + service suites), JaCoCo, PITest, and dependency reports intarget/to understand how the quality gates evaluate the project. - Experiment by breaking a rule on purpose, rerunning the build, and seeing which tests/gates fail, then fix the tests or code (and add/update assertions in both test classes) as needed.
| Item | Purpose |
|---|---|
| docs/cs320-requirements/ | Original CS320 assignment requirements (contact, task, appointment). |
| docs/REQUIREMENTS.md | Master document with scope, phases, checklist, and code examples. |
| docs/INDEX.md | Full file/folder navigation for the repository. |
| GitHub Actions workflows | CI/CD definitions described above. |
| config/checkstyle | Checkstyle rules enforced in CI. |
| Java 17 (Temurin) | JDK used locally and in CI. |
| Apache Maven | Build tool powering the project. |
| JUnit 5 | Test framework leveraged in ContactTest and ContactServiceTest. |
| AssertJ | Fluent assertion library used across the test suites. |
| PITest | Mutation testing engine enforced in CI. |
| OWASP Dependency-Check | CVE scanning tool wired into Maven/CI. |
| Checkstyle | Style/complexity checks. |
| SpotBugs | Bug pattern detector. |
| CodeQL | Semantic security analysis. |
Distributed under the MIT License. See LICENSE for details.