diff --git a/.github/workflows/validate-branches.yml b/.github/workflows/validate-branches.yml index 57dd5e6..1b30b99 100644 --- a/.github/workflows/validate-branches.yml +++ b/.github/workflows/validate-branches.yml @@ -103,7 +103,7 @@ jobs: node-version: '22' - uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0' + dotnet-version: '10.0' - uses: actions/setup-python@v5 with: python-version: '3.12' @@ -111,11 +111,6 @@ jobs: with: distribution: 'temurin' java-version: '21' - - name: Provision Java 11 (audit-svc / auth-svc) - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '11' - name: Ensure base commit present run: git fetch --no-tags origin "$(node -e "console.log(require('./course-build/manifest.json').base.sha)")" || true - name: Configure git identity (for git am) diff --git a/course-build/REFS.md b/course-build/REFS.md index 2703fc4..cae19d5 100644 --- a/course-build/REFS.md +++ b/course-build/REFS.md @@ -30,7 +30,7 @@ The state-changing chain is `base -> M01 -> M02 -> ... -> M07`. `M00` is the pri Three layers, from most to least durable: 1. **Base (immutable tag) — `acc-base`** - Pinned at the pristine `legacy-app` app tree (origin/main HEAD before course tooling landed: `3689288d1f5a9ed6871db94a09c2e651bc20af3e`). Every module delta applies on top of this. Learner branches contain the app + module content only — never `course-build/` or the course automation workflows. + Pinned at the pristine, tooling-free `legacy-app` app tree updated to the current main app state (.NET 10 assets-svc + Spring Boot 3.5.16 / Java 17 Java services): `510b4ffa1b1f95234ec6c7bd1d0983060e5081c3`. Every module delta applies on top of this. Learner branches contain the app + module content only — never `course-build/` or the course automation workflows. The tag **moves** only via an approved promotion: when a regen PR that advances `manifest.base.sha` is merged, the `acc-base` tag is re-pointed to the new base commit as part of the gated promotion. 2. **Mutable convenience branches — `start-of-module-N`** The learner-facing `git checkout` targets. They **move** (only via an approved, all-or-nothing promotion). `N` runs `02..07`. diff --git a/course-build/deltas/module-01/0001-Update-postCreateCommand-to-install-GitHub-Copilot-u.patch b/course-build/deltas/module-01/0001-Update-postCreateCommand-to-install-GitHub-Copilot-u.patch index 306a62d..c72d32d 100644 --- a/course-build/deltas/module-01/0001-Update-postCreateCommand-to-install-GitHub-Copilot-u.patch +++ b/course-build/deltas/module-01/0001-Update-postCreateCommand-to-install-GitHub-Copilot-u.patch @@ -9,10 +9,10 @@ Subject: [PATCH 1/2] Update postCreateCommand to install GitHub Copilot using 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json -index ef54ecc..c5b19a8 100644 +index 21a78c5..ad838ab 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json -@@ -41,6 +41,6 @@ +@@ -40,6 +40,6 @@ ] } }, diff --git a/course-build/deltas/module-01/0002-01-solution-Add-ARCHITECTURE.md-to-document-service-.patch b/course-build/deltas/module-01/0002-01-solution-Add-ARCHITECTURE.md-to-document-service-.patch index 60820c7..3ebfab6 100644 --- a/course-build/deltas/module-01/0002-01-solution-Add-ARCHITECTURE.md-to-document-service-.patch +++ b/course-build/deltas/module-01/0002-01-solution-Add-ARCHITECTURE.md-to-document-service-.patch @@ -6,8 +6,8 @@ Subject: [PATCH 2/2] 01-solution-Add ARCHITECTURE.md to document service --- ARCHITECTURE.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++++ - README.md | 12 ++++--- - 2 files changed, 102 insertions(+), 5 deletions(-) + README.md | 8 +++-- + 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md @@ -112,19 +112,10 @@ index 0000000..eec8bab +- **Tech debt:** SQL injection in username lookup; plain-text seeded passwords; no tests; legacy Spring Boot 2.7 / Java 11 +- **Unenforced rules:** authentication data is intentionally insecure for course material diff --git a/README.md b/README.md -index 22351d8..886803d 100644 +index f5aa1d2..8d32df3 100644 --- a/README.md +++ b/README.md -@@ -9,7 +9,7 @@ flowchart LR - browser([Browser]) --> web - - subgraph frontend -- web[web
Astro SSR + React islands] -+ web[web
Astro SSR + Bootstrap 5] - end - - subgraph modern[Modern services] -@@ -34,11 +34,11 @@ flowchart LR +@@ -34,7 +34,7 @@ flowchart LR workforce -.JWKs.-> auth ``` @@ -133,11 +124,6 @@ index 22351d8..886803d 100644 | Service | Stack | Port | Owns | |----------------------|----------------------------------------|-------|-------------------------------------| --| `web` | Astro (SSR) + React islands + Bootstrap 5 | 4321 | UI, BFF composition | -+| `web` | Astro (SSR) + Bootstrap 5 | 4321 | UI, BFF composition | - | `assets-svc` | .NET 8 (ASP.NET Core minimal APIs) | 5001 | Asset CRUD + search | - | `workforce-svc` | Java 21 / Spring Boot 3 | 5002 | Employees + Assignments | - | `reporting-svc` | Python 3.12 / FastAPI | 5003 | Reports, CSV bulk import | @@ -82,9 +82,11 @@ docker compose up --build Open http://localhost:4321. diff --git a/course-build/deltas/module-03/0001-test-add-Playwright-accessibility-foundation.patch b/course-build/deltas/module-03/0001-test-add-Playwright-accessibility-foundation.patch index 2dc3ae2..55af866 100644 --- a/course-build/deltas/module-03/0001-test-add-Playwright-accessibility-foundation.patch +++ b/course-build/deltas/module-03/0001-test-add-Playwright-accessibility-foundation.patch @@ -37,7 +37,7 @@ index 3797129..b13f322 100644 +playwright-report/ +playwright/.cache/ diff --git a/package-lock.json b/package-lock.json -index 3001b53..6a0f4fd 100644 +index e86c5d4..1e1ce81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,9 +6,26 @@ @@ -45,29 +45,29 @@ index 3001b53..6a0f4fd 100644 "name": "assettrack-dev", "devDependencies": { + "@playwright/test": "^1.49.1", - "concurrently": "^9.1.0" + "concurrently": "^10.0.5" } }, + "node_modules/@playwright/test": { -+ "version": "1.61.1", -+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", -+ "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", ++ "version": "1.62.1", ++ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", ++ "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { -+ "playwright": "1.61.1" ++ "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { -+ "node": ">=18" ++ "node": ">=20" + } + }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", -@@ -142,6 +159,21 @@ + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", +@@ -105,6 +122,21 @@ "node": ">=6" } }, @@ -89,51 +89,51 @@ index 3001b53..6a0f4fd 100644 "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", -@@ -172,6 +204,38 @@ - "node": ">=8" +@@ -128,6 +160,38 @@ + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { -+ "version": "1.61.1", -+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", -+ "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", ++ "version": "1.62.1", ++ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", ++ "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { -+ "playwright-core": "1.61.1" ++ "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { -+ "node": ">=18" ++ "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { -+ "version": "1.61.1", -+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", -+ "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", ++ "version": "1.62.1", ++ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", ++ "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { -+ "node": ">=18" ++ "node": ">=20" + } + }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", diff --git a/package.json b/package.json -index 0baf6c1..abf18f1 100644 +index f677727..e86fcc5 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,12 @@ - "dev:auth": "cd services/auth-svc && mkdir -p data && SERVER_PORT=5006 AUTH_DB_PATH=$PWD/data/auth.db LOGGING_LEVEL_ROOT=${LOG_LEVEL_JAVA:-WARN} ../../scripts/with-java11 mvn -q spring-boot:run", + "dev:auth": "cd services/auth-svc && mkdir -p data && SERVER_PORT=5006 AUTH_DB_PATH=$PWD/data/auth.db LOGGING_LEVEL_ROOT=${LOG_LEVEL_JAVA:-WARN} mvn -q spring-boot:run", "dev": "node scripts/dev-banner.mjs && concurrently --names web,assets,workforce,reporting,notif,audit,auth --prefix-colors auto --kill-others-on-fail npm:dev:web npm:dev:assets npm:dev:workforce npm:dev:reporting npm:dev:notifications npm:dev:audit npm:dev:auth", "dev:verbose": "LOG_LEVEL_JAVA=INFO LOG_LEVEL_DOTNET=Information LOG_LEVEL_PY=info npm run dev", - "install:all": "npm --prefix services/web install && pip install -e services/reporting-svc && pip install -e services/notifications-svc" @@ -143,7 +143,7 @@ index 0baf6c1..abf18f1 100644 }, "devDependencies": { + "@playwright/test": "^1.49.1", - "concurrently": "^9.1.0" + "concurrently": "^10.0.5" } } diff --git a/playwright.config.ts b/playwright.config.ts diff --git a/course-build/deltas/module-03/0004-test-add-test-suite-backfill.patch b/course-build/deltas/module-03/0004-test-add-test-suite-backfill.patch index e0ffc64..b8607d1 100644 --- a/course-build/deltas/module-03/0004-test-add-test-suite-backfill.patch +++ b/course-build/deltas/module-03/0004-test-add-test-suite-backfill.patch @@ -556,16 +556,16 @@ index 0000000..3befbbe + #endregion +} diff --git a/services/reporting-svc/pyproject.toml b/services/reporting-svc/pyproject.toml -index d93c870..29ffedf 100644 +index cabeaaa..da0b06c 100644 --- a/services/reporting-svc/pyproject.toml +++ b/services/reporting-svc/pyproject.toml @@ -18,5 +18,7 @@ include = ["app*"] dev = [ - "pytest==9.0.3", - "pytest-asyncio==1.3.0", + "pytest==9.1.1", + "pytest-asyncio==1.4.0", + "pytest-mock==3.14.0", + "respx==0.22.0", - "ruff==0.15.13", + "ruff==0.16.3", ] diff --git a/services/reporting-svc/tests/test_csv_import.py b/services/reporting-svc/tests/test_csv_import.py new file mode 100644 diff --git a/course-build/deltas/module-04/0001-feat-hooks-add-lifecycle-test-router-hook-hooks.json.patch b/course-build/deltas/module-04/0001-feat-hooks-add-lifecycle-test-router-hook-hooks.json.patch index 4504b5c..90d24d4 100644 --- a/course-build/deltas/module-04/0001-feat-hooks-add-lifecycle-test-router-hook-hooks.json.patch +++ b/course-build/deltas/module-04/0001-feat-hooks-add-lifecycle-test-router-hook-hooks.json.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: acc-course-bot Date: Mon, 17 Aug 2026 09:14:38 -0700 -Subject: [PATCH 1/1] feat(hooks): add lifecycle test-router hook + hooks.json +Subject: [PATCH] feat(hooks): add lifecycle test-router hook + hooks.json (module 04) --- diff --git a/course-build/deltas/module-05/0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch b/course-build/deltas/module-05/0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch index 32df5b3..b262338 100644 --- a/course-build/deltas/module-05/0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch +++ b/course-build/deltas/module-05/0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch @@ -1,8 +1,8 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: acc-course-bot Date: Wed, 1 Jan 2020 00:00:00 +0000 -Subject: [PATCH 1/1] feat(module-05): add QR/barcode support + QA agent, - research, plan +Subject: [PATCH] feat(module-05): add QR/barcode support + QA agent, research, + plan Module 5 seed: Playwright MCP-backed Quality assurance agent, /research report, /plan, and the barcode/QR feature across assets-svc (.NET) and web (Astro) with tests. --- @@ -157,15 +157,15 @@ index 0000000..64d3870 +> [!NOTE] +> This report is evidence for the decision, not the decision itself. If a reviewer prefers a different option that still satisfies the pure-managed + SVG + permissive-license constraints, the integration sketch is the only part that changes. diff --git a/services/assets-svc/AssetsService.csproj b/services/assets-svc/AssetsService.csproj -index adeaf54..db736a3 100644 +index 8a74b57..40c8bcd 100644 --- a/services/assets-svc/AssetsService.csproj +++ b/services/assets-svc/AssetsService.csproj @@ -9,6 +9,7 @@ - - + + + - + diff --git a/services/assets-svc/Data/AssetsDb.cs b/services/assets-svc/Data/AssetsDb.cs diff --git a/course-build/deltas/module-06/0001-feat-module-06-modernize-audit-svc-auth-svc-to-Sprin.patch b/course-build/deltas/module-06/0001-feat-module-06-modernize-audit-svc-auth-svc-to-Sprin.patch index 299efe4..9db6342 100644 --- a/course-build/deltas/module-06/0001-feat-module-06-modernize-audit-svc-auth-svc-to-Sprin.patch +++ b/course-build/deltas/module-06/0001-feat-module-06-modernize-audit-svc-auth-svc-to-Sprin.patch @@ -1,57 +1,70 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: acc-course-bot -Date: Wed, 1 Jan 2020 00:00:00 +0000 -Subject: [PATCH 1/1] feat(module-06): modernize audit-svc + auth-svc to Spring - Boot 3.5 / Java 21 +Date: Tue, 18 Aug 2026 13:58:56 -0700 +Subject: [PATCH] feat(module-06): modernize audit-svc + auth-svc to Spring + Boot 4.1 / Java 21 -Module 6 seed: Java LSP config, saved migration plan, Java migrator agent, playbook, extended test router, and the modernization of audit-svc and auth-svc (Boot 2.7->3.5.3, Java 11->21, javax->jakarta) with baseline test suites. +Currency upgrade of the two lagging Java services from the CVE-clean but +generation-behind Boot 3.5.16 / Java 17 baseline to the current Boot 4.1.0 / +Java 21 stack, with baseline test suites. + +- audit-svc, auth-svc: parent 3.5.16 -> 4.1.0; java.version 17 -> 21; Dockerfiles + temurin 17 -> 21; pin jackson-bom (Jackson 3) 3.1.6 and log4j2 2.25.5 (Boot 4.1 + natively resolves the vulnerable 3.1.4 / 2.25.4). +- Jackson 3 (tools.jackson) is the Boot 4 default; audit-svc's Map/List responses + serialize with no source change. +- auth-svc: jjwt 0.11.5 -> 0.12.7 with the fluent builder API in JwtIssuer, and + jjwt-jackson -> jjwt-gson so JWT (de)serialization stays off Jackson 2 (which + Boot 4 no longer manages). +- Baseline tests (RestClient + @LocalServerPort, isolated temp SQLite) incl. an + auth token round-trip proving the gson serializer under Jackson 3. +- Java LSP config, Java migrator agent, migration plan + playbook, extended test router. + +Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- - .github/agents/java-migrator.agent.md | 27 +++ - .github/hooks/scripts/test-router.sh | 4 + - .github/lsp.json | 11 + - docs/modernization/audit-svc-plan.md | 226 ++++++++++++++++++ - docs/modernization/migration-playbook.md | 30 +++ - package.json | 4 +- - services/audit-svc/pom.xml | 11 +- - .../contoso/audit/AuditApplicationTests.java | 14 ++ - .../contoso/audit/AuditControllerTest.java | 51 ++++ - .../contoso/audit/AuditRepositoryTest.java | 40 ++++ - .../com/contoso/audit/AuditTestSupport.java | 24 ++ - .../java/com/contoso/audit/DataInitTest.java | 31 +++ - services/auth-svc/pom.xml | 11 +- - .../main/java/com/contoso/auth/JwtIssuer.java | 2 +- - .../contoso/auth/AuthApplicationTests.java | 13 + - .../com/contoso/auth/AuthTestSupport.java | 20 ++ - .../com/contoso/auth/TokenControllerTest.java | 58 +++++ - 17 files changed, 568 insertions(+), 9 deletions(-) + .github/agents/java-migrator.agent.md | 26 +++++++++ + .github/hooks/scripts/test-router.sh | 4 ++ + .github/lsp.json | 11 ++++ + docs/modernization/audit-svc-plan.md | 40 +++++++++++++ + docs/modernization/migration-playbook.md | 30 ++++++++++ + services/audit-svc/Dockerfile | 4 +- + services/audit-svc/pom.xml | 15 +++-- + .../contoso/audit/AuditApplicationTests.java | 12 ++++ + .../contoso/audit/AuditControllerTest.java | 45 +++++++++++++++ + .../com/contoso/audit/AuditTestSupport.java | 25 ++++++++ + services/auth-svc/Dockerfile | 4 +- + services/auth-svc/pom.xml | 25 +++++--- + .../main/java/com/contoso/auth/JwtIssuer.java | 12 ++-- + .../contoso/auth/AuthApplicationTests.java | 12 ++++ + .../com/contoso/auth/AuthTestSupport.java | 25 ++++++++ + .../com/contoso/auth/TokenControllerTest.java | 57 +++++++++++++++++++ + 16 files changed, 322 insertions(+), 25 deletions(-) create mode 100644 .github/agents/java-migrator.agent.md create mode 100644 .github/lsp.json create mode 100644 docs/modernization/audit-svc-plan.md create mode 100644 docs/modernization/migration-playbook.md create mode 100644 services/audit-svc/src/test/java/com/contoso/audit/AuditApplicationTests.java create mode 100644 services/audit-svc/src/test/java/com/contoso/audit/AuditControllerTest.java - create mode 100644 services/audit-svc/src/test/java/com/contoso/audit/AuditRepositoryTest.java create mode 100644 services/audit-svc/src/test/java/com/contoso/audit/AuditTestSupport.java - create mode 100644 services/audit-svc/src/test/java/com/contoso/audit/DataInitTest.java create mode 100644 services/auth-svc/src/test/java/com/contoso/auth/AuthApplicationTests.java create mode 100644 services/auth-svc/src/test/java/com/contoso/auth/AuthTestSupport.java create mode 100644 services/auth-svc/src/test/java/com/contoso/auth/TokenControllerTest.java diff --git a/.github/agents/java-migrator.agent.md b/.github/agents/java-migrator.agent.md new file mode 100644 -index 0000000..f0eb961 +index 0000000..614990a --- /dev/null +++ b/.github/agents/java-migrator.agent.md -@@ -0,0 +1,27 @@ +@@ -0,0 +1,26 @@ +--- -+description: 'Modernizes one legacy Java service (and only its wiring) through a saved migration plan, one reviewable phase at a time.' ++description: 'Modernizes one lagging Java service (and only its wiring) through a saved migration plan, one reviewable phase at a time.' +name: 'Java migrator' +tools: [read, edit, execute, search] +--- + +# Java migrator + -+You modernize **one** legacy Java service at a time by following a saved migration plan. You are scoped to the target service and the repository wiring that service depends on — **never** another service's source or the frontend. Your scope is an instruction you must hold to, not a sandbox: `edit` and `execute` can reach the whole repo, so stay inside the service you were asked to migrate. ++You bring **one** currency-lagging Java service current at a time by following a saved migration plan. You are scoped to the target service and the repository wiring that service depends on — **never** another service's source or the frontend. Your scope is an instruction you must hold to, not a sandbox: `edit` and `execute` can reach the whole repo, so stay inside the service you were asked to migrate. + +## Process (one phase at a time) + @@ -61,14 +74,13 @@ index 0000000..f0eb961 +4. **Finish the job.** Once every phase is approved and green: + - Run the Playwright end-to-end suite (`npm run test:e2e`) as a final cross-system gate. + - Wire the modernized service into the repo's test router (`.github/hooks/scripts/test-router.sh`) so its tests run on edits. -+ - Clean up the dev script in `package.json` (drop the `scripts/with-java11` wrapper once the service targets Java 21). -+5. **Report.** End with a testing status report that names the target stack (e.g. Java 21 / Spring Boot 3.5.3) and confirms **both** layers passed on it: the service's unit/integration tests and the end-to-end suite. State clearly anything you could not verify. ++5. **Report.** End with a testing status report that names the target stack (Java 21 / Spring Boot 4.1.0) and confirms **both** layers passed on it: the service's unit/integration tests and the end-to-end suite. State clearly anything you could not verify. + +## Guardrails + +- Keep `JdbcTemplate` data access as-is during a framework bump. A move to Spring Data JPA is a separate project, not part of the upgrade. +- Do not copy another service's dependencies (e.g. `workforce-svc`'s JPA/Hibernate) into the service you're migrating. -+- Only `javax.*` packages that Jakarta EE renamed move to `jakarta.*` (e.g. `javax.annotation.PostConstruct` → `jakarta.annotation.PostConstruct`). `javax.sql.*` is Java SE and does **not** change. ++- Boot 4 defaults to **Jackson 3** (`tools.jackson`) and no longer manages Jackson 2. Watch libraries that carry their own Jackson 2 (e.g. `jjwt-jackson`): prefer swapping to a Jackson-free serializer (`jjwt-gson`) over reintroducing an unmanaged Jackson 2. Pin BOM-managed libs (Jackson 3, Log4j2) to CVE-clean patches. +- Read the diff at every gate. Validation between phases is where a fast upgrade catches its own mistakes. diff --git a/.github/hooks/scripts/test-router.sh b/.github/hooks/scripts/test-router.sh index fc8d073..c5ea1c0 100755 @@ -111,245 +123,59 @@ index 0000000..a064c1c +} diff --git a/docs/modernization/audit-svc-plan.md b/docs/modernization/audit-svc-plan.md new file mode 100644 -index 0000000..30bdb47 +index 0000000..c094c32 --- /dev/null +++ b/docs/modernization/audit-svc-plan.md -@@ -0,0 +1,226 @@ -+# Migration plan: `audit-svc` from Spring Boot 2.7.18 / Java 11 to Spring Boot 3.5.3 / Java 21 +@@ -0,0 +1,40 @@ ++# Migration plan: `audit-svc` from Spring Boot 3.5.16 / Java 17 to Spring Boot 4.1.0 / Java 21 + +## 1. Executive summary + -+`audit-svc` is a small, append-only audit-log microservice [pinned to Spring Boot 2.7.18 / Java 11][pom-audit-L8], while its sibling [`workforce-svc` already runs on Spring Boot 3.5.3 / Java 21][pom-wf]. It uses only `spring-boot-starter-web`, `spring-boot-starter-jdbc`, raw `JdbcTemplate`, and `sqlite-jdbc` — [no JPA, no Hibernate, and no `javax.*` application imports][pom-audit-L22] — so the dependency surface of this migration is unusually narrow. The mandatory changes are small: two edits in `pom.xml` and one in `package.json`, on top of a baseline test suite added first. The plan isolates the work into sequential phases — baseline safety net, toolchain alignment, framework bump, and stabilization — so each phase can be verified and rolled back independently. -+ -+## 2. Current state vs. target state -+ -+| Dimension | Current (`audit-svc`) | Target | Source | -+|---|---|---|---| -+| Spring Boot parent | `2.7.18` | `3.5.3` | [pom.xml L8][pom-audit-L8] → [workforce pom][pom-wf] | -+| Java source/target | `11` | `21` | [pom.xml L19][pom-audit-L19] | -+| Local launcher | `scripts/with-java11 mvn …` | plain `mvn …` | [package.json L11][pkg-L11] | -+| Persistence | Raw `JdbcTemplate` + SQLite | unchanged | [AuditRepository.java][repo-audit] | -+| `javax.*` imports | None | N/A — no changes needed | all four `.java` files | -+| Tests | None | Baseline suite before migration | — | -+ -+## 3. Scope and key decisions -+ -+### In scope -+ -+- Bump the Spring Boot parent from `2.7.18` to `3.5.3` and Java from `11` to `21`. -+- Align the toolchain references (`package.json` launcher, IDE metadata) with the new runtime. -+- Add a baseline test suite *before* any framework change, so regressions are detectable. -+ -+### Explicitly out of scope -+ -+| Out-of-scope item | Rationale | -+|---|---| -+| Adding Spring Data JPA / Hibernate | `audit-svc` uses raw `JdbcTemplate`, which is fully supported under [Spring Framework 6.2 / Boot 3.5][jdbctemplate]. JPA would add an ORM layer, schema-management risk, and dialect pinning to a service that runs three simple SQL statements. See the optional follow-on. | -+| Copying `workforce-svc` JPA dependencies | `workforce-svc` carries `spring-boot-starter-data-jpa`, `hibernate-community-dialects`, and `spring.jpa.*` properties. Those are workforce-specific and must not be introduced into `audit-svc`. | -+| Fixing the intentional SQL injection | [`AuditRepository.search()`][repo-audit] carries a documented course-exercise comment. That is a separate educational concern, unrelated to the platform migration, and should be tracked as its own issue. | -+| Mandating a Hikari `maximum-pool-size=1` | SQLite's single-writer model and connection-scoped `last_insert_rowid()` are pre-existing realities. Pool hardening reduces write throughput, so it belongs in Phase 4 as a separately validated option. | -+ -+### Why no `javax` → `jakarta` source changes are needed -+ -+[Jakarta EE 9 renamed the `javax.*` packages][jakarta-ee-9] (for example `javax.persistence` → `jakarta.persistence`, `javax.servlet` → `jakarta.servlet`), and [Spring Boot 3.0 pulls in those Jakarta EE 9+ artifacts][boot3-migration]. However, `javax.sql` is part of the **Java SE** standard library, not Jakarta EE, and has never been renamed — it remains `javax.sql` in Java 21. A scan of all four `audit-svc` source files confirms zero `javax.*` imports: they use only `org.springframework.*` and `java.util.*`. No application source file requires modification for the namespace change. -+ -+### Direct version jump is appropriate -+ -+The [Spring Boot migration guidance][boot-upgrading] is to start from the latest 2.7.x before upgrading to 3.x. `audit-svc` is already on `2.7.18`, the final 2.7.x maintenance release, so a direct jump to `3.5.3` — the version `workforce-svc` already runs — keeps both services on a single, supported baseline. The phases below still isolate the Java/toolchain change from the framework bump so each step is independently verifiable. -+ -+## 4. Phased migration plan -+ -+### Phase 0 — Baseline safety net (no functional changes) -+ -+**Goal:** establish tests and confirm the service compiles and runs on Java 11 / Boot 2.7.18 before anything touches the runtime. Without tests, any regression during the migration is invisible, and the service currently has none. -+ -+1. Add `spring-boot-starter-test` to `services/audit-svc/pom.xml` inside ``. It comes from the Boot parent BOM, needs no explicit version, and brings JUnit 5, AssertJ, Mockito, MockMvc, and `@SpringBootTest`: -+ -+ ```xml -+ -+ org.springframework.boot -+ spring-boot-starter-test -+ test -+ -+ ``` -+ -+2. Create `src/test/java/com/contoso/audit/` with tests covering these contracts (exact content is your choice): -+ -+ | Test class | What it verifies | -+ |---|---| -+ | `AuditApplicationTests` | `@SpringBootTest` context loads without errors | -+ | `AuditControllerTest` | `GET /health` → 200, body `{"status":"ok","service":"audit-svc"}`; `POST /events` with a valid body → 200 with a numeric `id`; `GET /events` → 200 returning a JSON array | -+ | `AuditRepositoryTest` | `insert(...)` persists a row and returns a positive `id`; `recent(n)` returns ≤ n rows; `search("assign")` returns matching rows | -+ | `DataInitTest` | running schema init twice does not throw and does not duplicate seed rows (idempotency) | -+ -+ Point the tests at an isolated SQLite database (`jdbc:sqlite::memory:` or a temp-file URL) so they never touch production data. -+ -+3. Validate on the current stack, still Java 11 / Boot 2.7.18, and record the pass counts: -+ -+ ```bash -+ cd services/audit-svc && mvn verify -+ ``` -+ -+**Exit criteria:** proceed only when `mvn verify` passes cleanly. Do not start Phase 1 with failing tests — pre-existing failures would mask migration regressions. -+ -+### Phase 1 — Java 21 and toolchain alignment (Boot stays 2.7.18) -+ -+**Goal:** switch the local JDK and every toolchain reference to Java 21 while keeping Spring Boot at 2.7.18, so the Java change is independently verifiable. Running Boot 2.7.18 on Java 21 is an intentionally *brief*, transitional step — validate quickly and move straight to Phase 2, where the framework bump puts the service on a combination Spring fully supports. -+ -+1. In [`services/audit-svc/pom.xml` L19][pom-audit-L19], change `11` to `21`. The `` block stays `2.7.18` for now. -+ -+2. In [`package.json` L11][pkg-L11], remove the `../../scripts/with-java11` wrapper from the `dev:audit` script so it runs `mvn` directly, matching `dev:workforce`. Leave `dev:auth` (L12) untouched — `auth-svc` still needs the Java 11 shim, and the devcontainer still installs both JDKs. -+ -+3. Optionally, update the IDE metadata the language server generates so it stops flagging false Java 21 errors. These files aren't committed — the language server creates them locally when it imports the project — so edit them only if you see stale errors: in `services/audit-svc/.settings/org.eclipse.jdt.core.prefs`, set the three compiler values to `21`, and in `services/audit-svc/.classpath`, change the JRE container from `JavaSE-11` to `JavaSE-21`: -+ -+ ```properties -+ org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 -+ org.eclipse.jdt.core.compiler.compliance=21 -+ org.eclipse.jdt.core.compiler.source=21 -+ ``` -+ -+4. Confirm `java -version` reports a 21.x JDK, then validate: -+ -+ ```bash -+ cd services/audit-svc && mvn verify -+ ``` ++`audit-svc` is a small, append-only audit-log microservice currently on Spring Boot `3.5.16` / Java `17` — CVE-clean, but a Spring Boot generation and an LTS behind the team's current target. Its sibling `workforce-svc` runs the modern Boot 3.5 / Java 21 baseline, and the goal here is to bring `audit-svc` (and then `auth-svc`) fully current on **Spring Boot 4.1.0 / Java 21**. `audit-svc` uses only `spring-boot-starter-web`, `spring-boot-starter-jdbc`, raw `JdbcTemplate`, and `sqlite-jdbc` — no JPA, no Hibernate, and no direct Jackson use — so its migration surface is unusually narrow. The plan isolates the work into sequential phases — baseline safety net, toolchain alignment, framework bump, and dependency reconciliation — so each phase can be verified and rolled back independently. + -+**Exit criteria:** proceed when the Phase 0 tests still pass under Java 21. If Boot 2.7.18 on Java 21 throws fatal startup errors, document them and move to Phase 2 immediately — the Boot 3 bump resolves them. The `package.json` change is safe regardless and need not be reverted. ++## 2. Target stack (pinned, CVE-clean today) + -+### Phase 2 — Spring Boot parent bump to 3.5.3 (core phase) ++| Coordinate | Before | After | ++| --- | --- | --- | ++| `spring-boot-starter-parent` | `3.5.16` | `4.1.0` | ++| `java.version` | `17` | `21` | ++| Jackson | 2.x (managed) | **Jackson 3** (`tools.jackson`), pinned `jackson-bom.version` = `3.1.6` | ++| Log4j2 API | `2.25.5` | `2.25.5` (pin retained: Boot 4.1.0 manages `2.25.4`, which is vulnerable) | ++| Dockerfile base | `eclipse-temurin-17` | `eclipse-temurin-21` | + -+**Goal:** bump the Spring Boot parent to `3.5.3`, confirm dependencies resolve, verify no `javax`→`jakarta` renames are needed, and run the full suite. ++Boot 4.1.0 natively resolves Jackson 3 `3.1.4` and Log4j2 `2.25.4`, both of which carry a known CVE today; the two pins hold them at CVE-clean `3.1.6` / `2.25.5`. Spring Framework `7.0.8`, Tomcat `11.0.22`, snakeyaml, and HikariCP resolve clean at Boot 4.1.0 with no pin. + -+1. In [`services/audit-svc/pom.xml`][pom-audit-L8], change the parent `` from `2.7.18` to `3.5.3`: ++## 3. Jackson 3 reconciliation + -+ ```xml -+ -+ org.springframework.boot -+ spring-boot-starter-parent -+ 3.5.3 -+ -+ -+ ``` ++Spring Boot 4 makes **Jackson 3** (package `tools.jackson`, immutable `JsonMapper`) the default and no longer manages Jackson 2. `audit-svc` returns `Map`/`List` from its `@RestController` methods and imports no Jackson types, so Spring MVC serializes them under Jackson 3 with **zero source changes**. (The coupling that does matter — a library carrying its own Jackson 2 — shows up in `auth-svc`, whose `jjwt-jackson` is swapped for `jjwt-gson`; see the playbook.) + -+ That is the only required `pom.xml` change beyond the `java.version` already set in Phase 1. The three existing dependencies (`spring-boot-starter-web`, `spring-boot-starter-jdbc`, `sqlite-jdbc 3.45.3.0`) all work under Boot 3.5 — `workforce-svc` uses the same `sqlite-jdbc` version. ++## 4. Phases + -+2. Optionally add `spring-boot-properties-migrator` (runtime scope) for this phase only, to catch any [renamed configuration keys][boot-upgrading], then remove it before committing. `audit-svc`'s `application.properties` has only three stable keys (`server.port`, `spring.datasource.url`, `spring.datasource.driver-class-name`), none renamed between Boot 2.7 and 3.5, so the migrator is a safety net rather than a necessity. ++- **Phase 0 — safety net.** Add `spring-boot-starter-test` (`test` scope) and characterization tests on the current 3.5.16 / 17 stack: a `@SpringBootTest` context-load test and `@LocalServerPort` + `RestClient` endpoint tests, pointed at an isolated temp SQLite DB. Confirm green before touching the runtime. ++- **Phase 1 — Java 17 → 21.** Bump `` to `21` and the Dockerfile bases `temurin-17` → `temurin-21`. Re-run the Phase 0 tests. ++- **Phase 2 — Boot 3.5.16 → 4.1.0.** Bump the parent, add `3.1.6` and keep `2.25.5`. Note the Boot 4 test-API relocation (`TestRestTemplate`/`LocalServerPort` moved) — the Phase 0 tests already use `RestClient` to avoid it. ++- **Phase 3 — validate.** `mvn verify` green; confirm the resolved tree is OSV-clean; run the Playwright e2e suite as the cross-system gate. + -+3. Confirm the Jakarta review needs no code changes: all four source files import only `org.springframework.*` and `java.util.*`, and the one JDBC-adjacent type (`javax.sql.DataSource`, injected by auto-configuration) is Java SE and unchanged in Java 21. ++## 5. Risks + -+4. Do **not** copy `workforce-svc`'s JPA dependencies (`spring-boot-starter-data-jpa`, `hibernate-community-dialects`, `spring-boot-starter-validation`) or its `spring.jpa.*` properties — they are irrelevant to `audit-svc` and must stay absent. ++| Risk | Mitigation | ++| --- | --- | ++| Boot 4.1.0 ships a vulnerable Jackson 3 (`3.1.4`) / Log4j2 (`2.25.4`) | Explicit `jackson-bom.version` / `log4j2.version` pins; re-scan the resolved tree with OSV. | ++| Boot 4 test-API relocation breaks characterization tests | Use `@LocalServerPort` + `RestClient` instead of the moved `TestRestTemplate`. | ++| Behavior drift across the major bump | Characterization tests written and green on 3.5.16 / 17 first, held constant across the bump. | + -+5. Validate: ++## 6. Out of scope + -+ ```bash -+ cd services/audit-svc -+ mvn dependency:tree # confirm no resolution failures -+ mvn verify # compile + all baseline tests pass -+ ``` -+ -+**Exit criteria:** proceed only when `mvn verify` is fully green with the same test count as Phase 0/1. Any failure here is directly attributable to the Boot 2.7 → 3.5 delta. Rollback is a single revert of the parent version. -+ -+### Phase 3 — Stabilization -+ -+**Goal:** post-migration cleanup and optional hardening. -+ -+1. Remove `spring-boot-properties-migrator` if it was added in Phase 2 — it is not safe to ship. -+2. Evaluate SQLite concurrency only if load testing shows it: `last_insert_rowid()` is connection-scoped, so under a multi-connection pool concurrent `POST /events` requests can read the wrong id. If confirmed, either set `spring.datasource.hikari.maximum-pool-size=1` (serializes writes, lowers throughput) or wrap the insert and id read in a single-connection block. This is not a required consequence of the Boot 3 upgrade. -+3. Add a minimal CI workflow running `mvn verify` on Java 21 so the modern stack stays green: -+ -+ ```yaml -+ name: audit-svc CI -+ on: [push, pull_request] -+ jobs: -+ build: -+ runs-on: ubuntu-latest -+ steps: -+ - uses: actions/checkout@v4 -+ - uses: actions/setup-java@v4 -+ with: { java-version: '21', distribution: 'temurin' } -+ - run: mvn -B verify -+ working-directory: services/audit-svc -+ ``` -+ -+### Optional follow-on: Spring Data JPA (not required) -+ -+If a future decision moves `audit-svc` from `JdbcTemplate` to Spring Data JPA for consistency with `workforce-svc`, treat it as a **separate project on its own branch**: add the JPA and dialect dependencies, model an `AuditEvent` `@Entity` and a `JpaRepository`, rewrite `AuditRepository`/`DataInit` and all tests, and add `spring.jpa.*` properties. It changes schema management and connection handling and risks data-type mapping issues, and the [Boot 3.0 guide][boot3-migration] specifically warns about Hibernate 6 behavioral changes. It does not belong in this framework bump. -+ -+## 5. Exact file change matrix -+ -+| File | Phase | Change | Before | After | -+|---|---|---|---|---| -+| [`audit-svc/pom.xml` L19][pom-audit-L19] | 1 | Edit | `11` | `21` | -+| [`audit-svc/pom.xml` L8][pom-audit-L8] | 2 | Edit | `2.7.18` | `3.5.3` | -+| [`audit-svc/pom.xml` L22–36][pom-audit-L22] | 0 | Add | *(no test dep)* | `spring-boot-starter-test` (`test` scope) | -+| [`package.json` L11][pkg-L11] | 1 | Edit | `../../scripts/with-java11 mvn …` | `mvn …` (drop wrapper) | -+| `services/auth-svc/*` | — | No change | `with-java11` still needed | left as-is | -+| `audit-svc/src/**/*.java` | — | No change | zero `javax` imports | no changes needed | -+| `audit-svc/.../application.properties` | — | No change | 3 stable keys | no changes needed | -+ -+## 6. Risk register -+ -+| ID | Risk | Likelihood | Impact | Phase | Mitigation | -+|---|---|---|---|---|---| -+| R1 | No existing tests — regressions invisible | High (confirmed) | High | Pre-0 | Add the baseline suite in Phase 0 before any change | -+| R2 | Boot 2.7.18 on Java 21 is transitional | Medium | Medium | 1 | Keep Phase 1 brief; proceed to Phase 2 | -+| R3 | IDE shows false Java 11 errors after the pom change | Medium | Low | 1 | Update the locally generated `.classpath`/`.settings` | -+| R4 | `with-java11` still needed by `auth-svc` | Medium | High if broken | 1 | Do not touch `dev:auth` in `package.json` | -+| R5 | Boot 3 / Jakarta dependency incompatibility | Low (small tree) | High | 2 | `mvn dependency:tree`; properties migrator; [migration guide][boot3-migration] | -+| R6 | Transitive dependency pulls a `javax.*` artifact | Low | Medium | 2 | `mvn dependency:tree` and inspect | -+| R7 | `last_insert_rowid()` wrong under concurrency | Medium (pre-existing) | Medium | 3 | Load test; pool-size=1 or single-connection wrapper | -+| R8 | SQL injection in `search()` | High (intentional) | High | Out of scope | Track as a separate issue; keep it out of the migration | -+| R9 | Scope creep into `workforce-svc` JPA | Medium | High | All | Scope boundary above explicitly excludes JPA | -+ -+## 7. Validation checklist -+ -+**Phase 0 — baseline** -+ -+- [ ] `spring-boot-starter-test` added to `pom.xml` -+- [ ] Context-load, controller, repository, and idempotency tests written -+- [ ] `mvn verify` passes on Java 11 / Boot 2.7.18 -+ -+**Phase 1 — toolchain** -+ -+- [ ] `pom.xml` `java.version` set to `21` -+- [ ] `package.json` `dev:audit` no longer invokes `with-java11` -+- [ ] *(optional)* locally generated `.settings`/`.classpath` set to Java 21 -+- [ ] `mvn verify` passes on Java 21 / Boot 2.7.18 -+ -+**Phase 2 — framework** -+ -+- [ ] `pom.xml` parent set to `3.5.3` -+- [ ] `mvn dependency:tree` shows no resolution failures -+- [ ] `mvn verify` — all baseline tests pass on Java 21 / Boot 3.5.3 -+- [ ] Endpoints respond: `GET /health`, `POST /events`, `GET /events` -+ -+**Phase 3 — stabilization** -+ -+- [ ] `spring-boot-properties-migrator` removed if it was added -+- [ ] SQLite pool behavior evaluated and the decision documented -+- [ ] CI workflow added and passing on Java 21 -+ -+## Sources -+ -+[pom-audit-L8]: https://github.com/GeekTrainer/legacy-app/blob/3689288d1f5a9ed6871db94a09c2e651bc20af3e/services/audit-svc/pom.xml#L8 -+[pom-audit-L19]: https://github.com/GeekTrainer/legacy-app/blob/3689288d1f5a9ed6871db94a09c2e651bc20af3e/services/audit-svc/pom.xml#L19 -+[pom-audit-L22]: https://github.com/GeekTrainer/legacy-app/blob/3689288d1f5a9ed6871db94a09c2e651bc20af3e/services/audit-svc/pom.xml#L22-L36 -+[pom-wf]: https://github.com/GeekTrainer/legacy-app/blob/3689288d1f5a9ed6871db94a09c2e651bc20af3e/services/workforce-svc/pom.xml#L7-L21 -+[pkg-L11]: https://github.com/GeekTrainer/legacy-app/blob/3689288d1f5a9ed6871db94a09c2e651bc20af3e/package.json#L11 -+[repo-audit]: https://github.com/GeekTrainer/legacy-app/blob/3689288d1f5a9ed6871db94a09c2e651bc20af3e/services/audit-svc/src/main/java/com/contoso/audit/AuditRepository.java -+[boot3-migration]: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide -+[boot-upgrading]: https://docs.spring.io/spring-boot/upgrading.html -+[jakarta-ee-9]: https://jakartaee.github.io/platform/jakartaee9/JakartaEE9ReleasePlan -+[jdbctemplate]: https://docs.spring.io/spring-framework/reference/data-access/jdbc/core.html ++Introducing Spring Data JPA / Hibernate; fixing the intentional SQL-injection course-exercise code; SQLite pool hardening beyond test isolation. Each is tracked separately. diff --git a/docs/modernization/migration-playbook.md b/docs/modernization/migration-playbook.md new file mode 100644 -index 0000000..0d6bd53 +index 0000000..a2396ef --- /dev/null +++ b/docs/modernization/migration-playbook.md @@ -0,0 +1,30 @@ +# Java modernization playbook + -+> A reusable recipe for moving an AssetTrack Java service from Spring Boot 2.7 / Java 11 to Spring Boot 3.5 / Java 21. Distilled from modernizing `audit-svc` and `auth-svc`. Use it with the `Java migrator` agent. ++> A reusable recipe for bringing an AssetTrack Java service current — from Spring Boot 3.5 / Java 17 to Spring Boot 4.1 / Java 21. Distilled from modernizing `audit-svc` and `auth-svc`. Use it with the `Java migrator` agent. + +## The loop + @@ -357,68 +183,75 @@ index 0000000..0d6bd53 + +## Recipe + -+1. **Assess.** Confirm the current stack (`pom.xml` parent + `java.version`), the dependency surface, and whether the service imports any renamed `javax.*` packages. Use the LSP (`.github/lsp.json` → `jdtls`) for precise caller/symbol lookups rather than text search. -+2. **Plan.** Target the exact versions `workforce-svc` already runs (`spring-boot-starter-parent` `3.5.3`, `java.version` `21`) rather than a generic "3.x". Keep the plan phased so each step is independently verifiable and revertible. -+3. **Protect — build the safety net first.** Add `spring-boot-starter-test` (`test` scope) and characterization tests *before* touching the framework: a `@SpringBootTest` context-load test, endpoint contract tests, and repository behavior tests. Point them at an **isolated temporary SQLite database** (never `/data/*.db`) and set `spring.datasource.hikari.maximum-pool-size=1` so connection-scoped `last_insert_rowid()` is deterministic. Confirm green on the *old* stack first. ++1. **Assess.** Confirm the current stack (`pom.xml` parent + `java.version`), the dependency surface, and how the service touches JSON. Boot 4 makes **Jackson 3** (`tools.jackson`) the default and no longer manages Jackson 2, so any library that carries its own Jackson 2 (e.g. `jjwt-jackson`) needs attention. Use the LSP (`.github/lsp.json` → `jdtls`) for precise caller/symbol lookups rather than text search. ++2. **Plan.** Target the current supported baseline (`spring-boot-starter-parent` `4.1.0`, `java.version` `21`) rather than a generic "4.x". Pin the BOM-managed libraries that ship a still-vulnerable patch to a CVE-clean one (`jackson-bom.version` → Jackson 3 `3.1.6`; `log4j2.version` → `2.25.5`). Keep the plan phased so each step is independently verifiable and revertible. ++3. **Protect — build the safety net first.** Add `spring-boot-starter-test` (`test` scope) and characterization tests *before* touching the framework: a `@SpringBootTest` context-load test, endpoint contract tests, and (for `auth-svc`) a token round-trip. Point them at an **isolated temporary SQLite database** (never `/data/*.db`). Note the Boot 4 test-API relocation: `TestRestTemplate` / `LocalServerPort` moved, so prefer `@LocalServerPort` + `RestClient`. Confirm green on the *old* (3.5 / 17) stack first. +4. **Migrate in order.** -+ - Phase 1 — toolchain: `java.version` `11` → `21`; drop the `../../scripts/with-java11` wrapper from the service's `dev:*` script in `package.json`. -+ - Phase 2 — framework: `spring-boot-starter-parent` `2.7.18` → `3.5.3`. -+ - Rename only Jakarta-affected imports (`javax.annotation.*`, `javax.servlet.*`, `javax.persistence.*` → `jakarta.*`). Leave `javax.sql.*` (Java SE) alone. ++ - Phase 1 — toolchain: `java.version` `17` → `21`; bump the Dockerfile base images `temurin-17` → `temurin-21`. ++ - Phase 2 — framework: `spring-boot-starter-parent` `3.5.16` → `4.1.0`; add the Jackson 3 / Log4j2 currency pins. ++ - Phase 3 — reconcile dependencies. Jackson 3 serializes the controllers' `Map`/`List` responses with **no** source change. For `jjwt`, bump `0.11.5` → `0.12.7` (the fluent builder API: `.issuer()/.subject()/.signWith(key, Jwts.SIG.RS256)`) and swap the serializer `jjwt-jackson` → `jjwt-gson` so JWT (de)serialization stays off Jackson 2, which Boot 4 no longer manages. +5. **Validate after every phase.** `mvn verify` from the service directory; the same test count must stay green. Run the Playwright e2e suite as the final cross-system gate. Wire the service into `.github/hooks/scripts/test-router.sh` so edits trigger its tests. +6. **Document.** Update this playbook and the `Java migrator` agent with anything the upgrade taught you, so the next service is a repeat rather than a fresh start. + +## Lessons from the two services + -+- **`audit-svc`** was the clean case: raw `JdbcTemplate`, **no** application `javax` imports, three dependencies. Only two `pom.xml` edits (parent + `java.version`) plus the test starter, and one `package.json` edit. 8 tests green on 3.5.3 / 21. -+- **`auth-svc`** is where the second-service value showed up: it carries the `jjwt` library for JWT issuance and is the one place that imports `javax.annotation.PostConstruct` → `jakarta.annotation.PostConstruct`. A red test after the framework bump is the safety net doing its job — hand the precise signal ("this name moved") back and adapt. 6 tests green on 3.5.3 / 21. ++- **`audit-svc`** was the clean case: raw `JdbcTemplate`, no direct Jackson use, three dependencies. Two `pom.xml` edits (parent + `java.version`) plus the currency pins and the test starter, and a Dockerfile bump. Jackson 3 handled its `Map`/`List` JSON with zero source changes. Tests green on 4.1.0 / 21. ++- **`auth-svc`** is where the second-service value showed up: it issues JWTs with `jjwt`. Boot 4 stops managing Jackson 2, so `jjwt-jackson` would drag in an old, vulnerable `jackson-databind`. The fix is two concrete moves — migrate `JwtIssuer` to the jjwt `0.12.x` fluent API, and swap `jjwt-jackson` → `jjwt-gson` to drop Jackson 2 entirely. A red test after the bump is the safety net doing its job. Tests green on 4.1.0 / 21. + +## Explicitly out of scope for a framework bump + +- Introducing Spring Data JPA / Hibernate (separate project, own branch). +- Fixing the intentional SQL-injection course-exercise code in the repositories (track separately). +- Hardening SQLite pool behavior beyond test isolation (evaluate under load, separately). -diff --git a/package.json b/package.json -index abf18f1..040e873 100644 ---- a/package.json -+++ b/package.json -@@ -8,8 +8,8 @@ - "dev:workforce": "cd services/workforce-svc && mkdir -p data && SERVER_PORT=5002 WORKFORCE_DB_PATH=$PWD/data/workforce.db AUDIT_SVC_URL=http://localhost:5005 NOTIFICATIONS_SVC_URL=http://localhost:5004 AUTH_JWKS_URL=http://localhost:5006/.well-known/jwks LOGGING_LEVEL_ROOT=${LOG_LEVEL_JAVA:-WARN} mvn -q spring-boot:run", - "dev:reporting": "cd services/reporting-svc && mkdir -p data && ASSETS_SVC_URL=http://localhost:5001 WORKFORCE_SVC_URL=http://localhost:5002 uvicorn app.main:app --reload --host 0.0.0.0 --port 5003 --log-level ${LOG_LEVEL_PY:-warning}", - "dev:notifications": "cd services/notifications-svc && mkdir -p data && NOTIFICATIONS_DB_PATH=$PWD/data/notifications.db uvicorn app.main:app --reload --host 0.0.0.0 --port 5004 --log-level ${LOG_LEVEL_PY:-warning}", -- "dev:audit": "cd services/audit-svc && mkdir -p data && SERVER_PORT=5005 AUDIT_DB_PATH=$PWD/data/audit.db LOGGING_LEVEL_ROOT=${LOG_LEVEL_JAVA:-WARN} ../../scripts/with-java11 mvn -q spring-boot:run", -- "dev:auth": "cd services/auth-svc && mkdir -p data && SERVER_PORT=5006 AUTH_DB_PATH=$PWD/data/auth.db LOGGING_LEVEL_ROOT=${LOG_LEVEL_JAVA:-WARN} ../../scripts/with-java11 mvn -q spring-boot:run", -+ "dev:audit": "cd services/audit-svc && mkdir -p data && SERVER_PORT=5005 AUDIT_DB_PATH=$PWD/data/audit.db LOGGING_LEVEL_ROOT=${LOG_LEVEL_JAVA:-WARN} mvn -q spring-boot:run", -+ "dev:auth": "cd services/auth-svc && mkdir -p data && SERVER_PORT=5006 AUTH_DB_PATH=$PWD/data/auth.db LOGGING_LEVEL_ROOT=${LOG_LEVEL_JAVA:-WARN} mvn -q spring-boot:run", - "dev": "node scripts/dev-banner.mjs && concurrently --names web,assets,workforce,reporting,notif,audit,auth --prefix-colors auto --kill-others-on-fail npm:dev:web npm:dev:assets npm:dev:workforce npm:dev:reporting npm:dev:notifications npm:dev:audit npm:dev:auth", - "dev:verbose": "LOG_LEVEL_JAVA=INFO LOG_LEVEL_DOTNET=Information LOG_LEVEL_PY=info npm run dev", - "install:all": "npm --prefix services/web install && pip install -e services/reporting-svc && pip install -e services/notifications-svc", +diff --git a/services/audit-svc/Dockerfile b/services/audit-svc/Dockerfile +index 04c9777..bd61f19 100644 +--- a/services/audit-svc/Dockerfile ++++ b/services/audit-svc/Dockerfile +@@ -1,11 +1,11 @@ +-FROM maven:3.9-eclipse-temurin-17 AS build ++FROM maven:3.9-eclipse-temurin-21 AS build + WORKDIR /src + COPY pom.xml ./ + RUN mvn -B -q dependency:go-offline + COPY src ./src + RUN mvn -B -q package -DskipTests + +-FROM eclipse-temurin:17-jre ++FROM eclipse-temurin:21-jre + WORKDIR /app + COPY --from=build /src/target/*.jar app.jar + EXPOSE 8080 diff --git a/services/audit-svc/pom.xml b/services/audit-svc/pom.xml -index 41f9bb2..b0ac182 100644 +index d42ae42..3db1acb 100644 --- a/services/audit-svc/pom.xml +++ b/services/audit-svc/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent -- 2.7.18 -+ 3.5.3 +- 3.5.16 ++ 4.1.0 -@@ -13,10 +13,10 @@ +@@ -13,12 +13,12 @@ audit-svc 0.1.0 audit-svc -- Append-only audit log (intentionally legacy: Spring Boot 2.7 / Java 11) -+ Append-only audit log (modernized: Spring Boot 3.5 / Java 21) +- Append-only audit log (a generation behind: Spring Boot 3.5 / Java 17, due for a currency upgrade) ++ Append-only audit log (modernized: Spring Boot 4.1 / Java 21) -- 11 +- 17 +- +- 2.22.2 + 21 ++ ++ 3.1.6 + 2.25.5 - -@@ -33,6 +33,11 @@ +@@ -36,6 +36,11 @@ sqlite-jdbc 3.45.3.0 @@ -432,10 +265,10 @@ index 41f9bb2..b0ac182 100644 diff --git a/services/audit-svc/src/test/java/com/contoso/audit/AuditApplicationTests.java b/services/audit-svc/src/test/java/com/contoso/audit/AuditApplicationTests.java new file mode 100644 -index 0000000..0a7c791 +index 0000000..71f3029 --- /dev/null +++ b/services/audit-svc/src/test/java/com/contoso/audit/AuditApplicationTests.java -@@ -0,0 +1,14 @@ +@@ -0,0 +1,12 @@ +package com.contoso.audit; + +import org.junit.jupiter.api.Test; @@ -444,210 +277,163 @@ index 0000000..0a7c791 +/** Verifies the Spring application context loads without errors. */ +@SpringBootTest +class AuditApplicationTests extends AuditTestSupport { -+ + @Test + void contextLoads() { -+ // The context starting (including the DataInit CommandLineRunner) is the assertion. + } +} diff --git a/services/audit-svc/src/test/java/com/contoso/audit/AuditControllerTest.java b/services/audit-svc/src/test/java/com/contoso/audit/AuditControllerTest.java new file mode 100644 -index 0000000..633946b +index 0000000..4e46c51 --- /dev/null +++ b/services/audit-svc/src/test/java/com/contoso/audit/AuditControllerTest.java -@@ -0,0 +1,51 @@ +@@ -0,0 +1,45 @@ +package com.contoso.audit; + +import org.junit.jupiter.api.Test; -+import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; -+import org.springframework.boot.test.web.client.TestRestTemplate; ++import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; ++import org.springframework.web.client.RestClient; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + -+/** Contract tests for the audit HTTP endpoints. */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class AuditControllerTest extends AuditTestSupport { + -+ @Autowired -+ private TestRestTemplate rest; ++ @LocalServerPort ++ int port; ++ ++ private RestClient rest() { ++ return RestClient.create("http://localhost:" + port); ++ } + + @Test + void health_returnsOk() { -+ ResponseEntity res = rest.getForEntity("/health", Map.class); ++ ResponseEntity res = rest().get().uri("/health").retrieve().toEntity(Map.class); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(res.getBody()).containsEntry("status", "ok").containsEntry("service", "audit-svc"); + } + + @Test -+ void postEvents_withValidBody_returnsNumericId() { ++ void postThenGetEvents_roundTrips() { + Map body = Map.of( + "actor", "tester", "action", "create", "entityType", "asset", + "entityId", "CON-TST-001", "details", "created in test"); ++ ResponseEntity post = rest().post().uri("/events").body(body).retrieve().toEntity(Map.class); ++ assertThat(post.getStatusCode()).isEqualTo(HttpStatus.OK); ++ assertThat(((Number) post.getBody().get("id")).longValue()).isPositive(); + -+ ResponseEntity res = rest.postForEntity("/events", body, Map.class); -+ -+ assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); -+ assertThat(res.getBody()).containsKey("id"); -+ Number id = (Number) res.getBody().get("id"); -+ assertThat(id.longValue()).isPositive(); -+ } -+ -+ @Test -+ void getEvents_returnsJsonArray() { -+ ResponseEntity res = rest.getForEntity("/events", List.class); -+ assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); -+ assertThat(res.getBody()).isNotNull(); -+ // Seed data is inserted on startup. -+ assertThat(res.getBody()).isNotEmpty(); -+ } -+} -diff --git a/services/audit-svc/src/test/java/com/contoso/audit/AuditRepositoryTest.java b/services/audit-svc/src/test/java/com/contoso/audit/AuditRepositoryTest.java -new file mode 100644 -index 0000000..c0cef06 ---- /dev/null -+++ b/services/audit-svc/src/test/java/com/contoso/audit/AuditRepositoryTest.java -@@ -0,0 +1,40 @@ -+package com.contoso.audit; -+ -+import org.junit.jupiter.api.Test; -+import org.springframework.beans.factory.annotation.Autowired; -+import org.springframework.boot.test.context.SpringBootTest; -+ -+import java.util.List; -+import java.util.Map; -+ -+import static org.assertj.core.api.Assertions.assertThat; -+ -+/** Behavioral tests for the JdbcTemplate-backed repository. */ -+@SpringBootTest -+class AuditRepositoryTest extends AuditTestSupport { -+ -+ @Autowired -+ private AuditRepository repo; -+ -+ @Test -+ void insert_persistsRow_andReturnsPositiveId() { -+ long id = repo.insert("tester", "assign", "asset", "CON-RPT-001", "repo test"); -+ assertThat(id).isPositive(); -+ } -+ -+ @Test -+ void recent_returnsAtMostLimitRows() { -+ for (int i = 0; i < 5; i++) { -+ repo.insert("tester", "create", "asset", "CON-RPT-" + i, "row " + i); -+ } -+ List> rows = repo.recent(3); -+ assertThat(rows).hasSizeLessThanOrEqualTo(3); -+ } -+ -+ @Test -+ void search_returnsMatchingRows() { -+ repo.insert("tester", "assign", "asset", "CON-RPT-777", "assign match"); -+ List> rows = repo.search("assign"); -+ assertThat(rows).isNotEmpty(); ++ ResponseEntity list = rest().get().uri("/events").retrieve().toEntity(List.class); ++ assertThat(list.getStatusCode()).isEqualTo(HttpStatus.OK); ++ assertThat(list.getBody()).isNotEmpty(); + } +} diff --git a/services/audit-svc/src/test/java/com/contoso/audit/AuditTestSupport.java b/services/audit-svc/src/test/java/com/contoso/audit/AuditTestSupport.java new file mode 100644 -index 0000000..5928e82 +index 0000000..c82d730 --- /dev/null +++ b/services/audit-svc/src/test/java/com/contoso/audit/AuditTestSupport.java -@@ -0,0 +1,24 @@ +@@ -0,0 +1,25 @@ +package com.contoso.audit; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import java.nio.file.Path; -+import java.util.UUID; + -+/** -+ * Shared test configuration: point every test context at an isolated, temporary -+ * SQLite database (never production data) and force a single connection so -+ * connection-scoped {@code last_insert_rowid()} behaves deterministically. -+ */ ++/** Points the service at an isolated temp SQLite DB so tests never touch /data/audit.db. */ +abstract class AuditTestSupport { -+ -+ @DynamicPropertySource -+ static void datasource(DynamicPropertyRegistry registry) { -+ Path db = Path.of(System.getProperty("java.io.tmpdir"), -+ "audit-test-" + UUID.randomUUID().toString().replace("-", "") + ".db"); -+ registry.add("spring.datasource.url", () -> "jdbc:sqlite:" + db); -+ registry.add("spring.datasource.driver-class-name", () -> "org.sqlite.JDBC"); -+ registry.add("spring.datasource.hikari.maximum-pool-size", () -> "1"); ++ static final Path DB = tempDb(); ++ ++ private static Path tempDb() { ++ try { ++ Path dir = java.nio.file.Files.createTempDirectory("audit-test"); ++ return dir.resolve("audit.db"); ++ } catch (Exception e) { ++ throw new RuntimeException(e); ++ } + } -+} -diff --git a/services/audit-svc/src/test/java/com/contoso/audit/DataInitTest.java b/services/audit-svc/src/test/java/com/contoso/audit/DataInitTest.java -new file mode 100644 -index 0000000..62288df ---- /dev/null -+++ b/services/audit-svc/src/test/java/com/contoso/audit/DataInitTest.java -@@ -0,0 +1,31 @@ -+package com.contoso.audit; + -+import org.junit.jupiter.api.Test; -+import org.springframework.beans.factory.annotation.Autowired; -+import org.springframework.boot.test.context.SpringBootTest; -+import org.springframework.jdbc.core.JdbcTemplate; -+ -+import static org.assertj.core.api.Assertions.assertThat; -+ -+/** Schema init is idempotent: re-running it neither throws nor duplicates seed rows. */ -+@SpringBootTest -+class DataInitTest extends AuditTestSupport { -+ -+ @Autowired -+ private JdbcTemplate jdbc; -+ -+ @Autowired -+ private DataInit dataInit; -+ -+ @Test -+ void reinitialize_isIdempotent() throws Exception { -+ Integer before = jdbc.queryForObject("SELECT COUNT(*) FROM audit_events", Integer.class); -+ assertThat(before).isNotNull(); -+ -+ // Run the initializer a second time; it must not throw or re-seed. -+ dataInit.initSchema(jdbc).run(); -+ -+ Integer after = jdbc.queryForObject("SELECT COUNT(*) FROM audit_events", Integer.class); -+ assertThat(after).isEqualTo(before); ++ @DynamicPropertySource ++ static void props(DynamicPropertyRegistry registry) { ++ registry.add("AUDIT_DB_PATH", DB::toString); + } +} +diff --git a/services/auth-svc/Dockerfile b/services/auth-svc/Dockerfile +index 156b758..de5c9ec 100644 +--- a/services/auth-svc/Dockerfile ++++ b/services/auth-svc/Dockerfile +@@ -1,11 +1,11 @@ +-FROM maven:3.9-eclipse-temurin-17 AS build ++FROM maven:3.9-eclipse-temurin-21 AS build + WORKDIR /src + COPY pom.xml ./ + RUN mvn -B -q dependency:go-offline + COPY src ./src + RUN mvn -B -q package -DskipTests + +-FROM eclipse-temurin:17-jre ++FROM eclipse-temurin:21-jre + WORKDIR /app + COPY --from=build /src/target/*.jar app.jar + EXPOSE 8080 diff --git a/services/auth-svc/pom.xml b/services/auth-svc/pom.xml -index 775b857..d140cbb 100644 +index ded8899..578ff83 100644 --- a/services/auth-svc/pom.xml +++ b/services/auth-svc/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent -- 2.7.18 -+ 3.5.3 +- 3.5.16 ++ 4.1.0 -@@ -13,10 +13,10 @@ +@@ -13,13 +13,14 @@ auth-svc 0.1.0 auth-svc -- JWT issuer + user lookup (intentionally legacy: Spring Boot 2.7 / Java 11) -+ JWT issuer + user lookup (modernized: Spring Boot 3.5 / Java 21) +- JWT issuer + user lookup (a generation behind: Spring Boot 3.5 / Java 17, due for a currency upgrade) ++ JWT issuer + user lookup (modernized: Spring Boot 4.1 / Java 21) -- 11 +- 17 +- +- 2.22.2 + 21 ++ ++ 3.1.6 + 2.25.5 ++ 0.12.7 -@@ -50,6 +50,11 @@ - 0.11.5 +@@ -39,20 +40,26 @@ + + io.jsonwebtoken + jjwt-api +- 0.11.5 ++ ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl +- 0.11.5 ++ ${jjwt.version} + runtime + ++ + + io.jsonwebtoken +- jjwt-jackson +- 0.11.5 ++ jjwt-gson ++ ${jjwt.version} runtime + @@ -659,124 +445,151 @@ index 775b857..d140cbb 100644 diff --git a/services/auth-svc/src/main/java/com/contoso/auth/JwtIssuer.java b/services/auth-svc/src/main/java/com/contoso/auth/JwtIssuer.java -index 1236f5f..66b1296 100644 +index 66b1296..096fce7 100644 --- a/services/auth-svc/src/main/java/com/contoso/auth/JwtIssuer.java +++ b/services/auth-svc/src/main/java/com/contoso/auth/JwtIssuer.java -@@ -5,7 +5,7 @@ import io.jsonwebtoken.SignatureAlgorithm; +@@ -1,14 +1,12 @@ + package com.contoso.auth; + + import io.jsonwebtoken.Jwts; +-import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; --import javax.annotation.PostConstruct; -+import jakarta.annotation.PostConstruct; + import jakarta.annotation.PostConstruct; import java.security.KeyPair; import java.security.KeyPairGenerator; - import java.security.interfaces.RSAPrivateKey; +-import java.security.interfaces.RSAPrivateKey; + import java.security.interfaces.RSAPublicKey; + import java.util.Base64; + import java.util.Date; +@@ -38,12 +36,12 @@ public class JwtIssuer { + public String issue(String username, String role) { + Date now = new Date(); + return Jwts.builder() +- .setIssuer(issuer) +- .setSubject(username) ++ .issuer(issuer) ++ .subject(username) + .claim("role", role) +- .setIssuedAt(now) +- .setExpiration(new Date(now.getTime() + ttlSeconds * 1000L)) +- .signWith((RSAPrivateKey) keyPair.getPrivate(), SignatureAlgorithm.RS256) ++ .issuedAt(now) ++ .expiration(new Date(now.getTime() + ttlSeconds * 1000L)) ++ .signWith(keyPair.getPrivate(), Jwts.SIG.RS256) + .compact(); + } + diff --git a/services/auth-svc/src/test/java/com/contoso/auth/AuthApplicationTests.java b/services/auth-svc/src/test/java/com/contoso/auth/AuthApplicationTests.java new file mode 100644 -index 0000000..30631b5 +index 0000000..eccbc0a --- /dev/null +++ b/services/auth-svc/src/test/java/com/contoso/auth/AuthApplicationTests.java -@@ -0,0 +1,13 @@ +@@ -0,0 +1,12 @@ +package com.contoso.auth; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + -+/** Verifies the Spring application context loads (including @PostConstruct key init). */ ++/** Verifies the context loads (including @PostConstruct RSA key init). */ +@SpringBootTest +class AuthApplicationTests extends AuthTestSupport { -+ + @Test + void contextLoads() { + } +} diff --git a/services/auth-svc/src/test/java/com/contoso/auth/AuthTestSupport.java b/services/auth-svc/src/test/java/com/contoso/auth/AuthTestSupport.java new file mode 100644 -index 0000000..dd2909d +index 0000000..de4ec90 --- /dev/null +++ b/services/auth-svc/src/test/java/com/contoso/auth/AuthTestSupport.java -@@ -0,0 +1,20 @@ +@@ -0,0 +1,25 @@ +package com.contoso.auth; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import java.nio.file.Path; -+import java.util.UUID; + -+/** Point each test context at an isolated temporary SQLite database. */ ++/** Points the service at an isolated temp SQLite DB so tests never touch /data/auth.db. */ +abstract class AuthTestSupport { ++ static final Path DB = tempDb(); ++ ++ private static Path tempDb() { ++ try { ++ Path dir = java.nio.file.Files.createTempDirectory("auth-test"); ++ return dir.resolve("auth.db"); ++ } catch (Exception e) { ++ throw new RuntimeException(e); ++ } ++ } + + @DynamicPropertySource -+ static void datasource(DynamicPropertyRegistry registry) { -+ Path db = Path.of(System.getProperty("java.io.tmpdir"), -+ "auth-test-" + UUID.randomUUID().toString().replace("-", "") + ".db"); -+ registry.add("spring.datasource.url", () -> "jdbc:sqlite:" + db); -+ registry.add("spring.datasource.driver-class-name", () -> "org.sqlite.JDBC"); -+ registry.add("spring.datasource.hikari.maximum-pool-size", () -> "1"); ++ static void props(DynamicPropertyRegistry registry) { ++ registry.add("AUTH_DB_PATH", DB::toString); + } +} diff --git a/services/auth-svc/src/test/java/com/contoso/auth/TokenControllerTest.java b/services/auth-svc/src/test/java/com/contoso/auth/TokenControllerTest.java new file mode 100644 -index 0000000..8912fae +index 0000000..a2933c1 --- /dev/null +++ b/services/auth-svc/src/test/java/com/contoso/auth/TokenControllerTest.java -@@ -0,0 +1,58 @@ +@@ -0,0 +1,57 @@ +package com.contoso.auth; + +import org.junit.jupiter.api.Test; -+import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; -+import org.springframework.boot.test.web.client.TestRestTemplate; ++import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; ++import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; ++import org.springframework.web.client.RestClient; + -+import java.util.List; ++import java.util.Base64; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + -+/** Contract tests for the auth endpoints (health, token issuance, JWKS). */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class TokenControllerTest extends AuthTestSupport { + -+ @Autowired -+ private TestRestTemplate rest; -+ -+ @Test -+ void health_returnsOk() { -+ ResponseEntity res = rest.getForEntity("/health", Map.class); -+ assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); -+ assertThat(res.getBody()).containsEntry("status", "ok").containsEntry("service", "auth-svc"); -+ } ++ @LocalServerPort ++ int port; + -+ @Test -+ void jwks_returnsKeySet() { -+ ResponseEntity res = rest.getForEntity("/.well-known/jwks", Map.class); -+ assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); -+ assertThat(res.getBody()).containsKey("keys"); -+ assertThat((List) res.getBody().get("keys")).isNotEmpty(); ++ private RestClient rest() { ++ return RestClient.create("http://localhost:" + port); + } + + @Test -+ void token_withValidCredentials_returnsAccessToken() { -+ Map body = Map.of("username", "admin", "password", "password"); -+ ResponseEntity res = rest.postForEntity("/token", body, Map.class); ++ void token_issuesJwt_viaGsonSerializer() { ++ ResponseEntity res = rest().post().uri("/token") ++ .body(Map.of("username", "admin", "password", "password")) ++ .retrieve().toEntity(Map.class); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); -+ assertThat(res.getBody()).containsKey("access_token"); -+ assertThat((String) res.getBody().get("access_token")).isNotBlank(); ++ String jwt = (String) res.getBody().get("access_token"); ++ assertThat(jwt).isNotBlank(); ++ ++ // A signed JWT is header.payload.signature; decode the payload to prove the ++ // jjwt-gson serializer emitted the claims (the Jackson-2-free path under Boot 4). ++ String[] parts = jwt.split("\\."); ++ assertThat(parts).hasSize(3); ++ String payload = new String(Base64.getUrlDecoder().decode(parts[1])); ++ assertThat(payload).contains("\"sub\":\"admin\"").contains("\"role\":\"admin\""); + } + + @Test -+ void token_withBadCredentials_returns401() { -+ Map body = Map.of("username", "admin", "password", "wrong"); -+ ResponseEntity res = rest.postForEntity("/token", body, Map.class); -+ assertThat(res.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); ++ void badCredentials_returns401() { ++ HttpStatusCode status = rest().post().uri("/token") ++ .body(Map.of("username", "admin", "password", "wrong")) ++ .exchange((req, res) -> res.getStatusCode()); ++ assertThat(status).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test -+ void token_withMissingFields_returns400() { -+ ResponseEntity res = rest.postForEntity("/token", Map.of("username", "admin"), Map.class); -+ assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); ++ void jwks_exposesKey() { ++ ResponseEntity res = rest().get().uri("/.well-known/jwks").retrieve().toEntity(Map.class); ++ assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); ++ assertThat(res.getBody()).containsKey("keys"); + } +} diff --git a/course-build/deltas/module-06/README.md b/course-build/deltas/module-06/README.md index 7566e05..31d6ddb 100644 --- a/course-build/deltas/module-06/README.md +++ b/course-build/deltas/module-06/README.md @@ -2,8 +2,11 @@ **Produces:** `start-of-module-07` (= cumulative end state of Module 06) -**Adds** (seed — Copilot-authored, performing ACC module 06; incl. deterministic pom/package edits): -- Modernization of `services/audit-svc` and `services/auth-svc`: Spring Boot `2.7.18` → `3.5.3`, Java `11` → `21`, `javax.annotation.PostConstruct` → `jakarta.annotation.PostConstruct` (auth-svc), baseline test suites, `package.json` drops the `with-java11` shim. +**Adds** (seed — Copilot-authored, performing ACC module 06; secure-to-secure currency upgrade): +- Modernization of `services/audit-svc` and `services/auth-svc`: Spring Boot `3.5.16` → `4.1.0`, Java `17` → `21`, Dockerfiles `temurin-17` → `temurin-21`, and `jackson-bom`/`log4j2` currency pins (Jackson 3 `3.1.6`, Log4j2 `2.25.5`) since Boot 4.1.0 natively resolves the vulnerable `3.1.4` / `2.25.4`. Baseline test suites added. +- `auth-svc` only: `jjwt` `0.11.5` → `0.12.7` with the fluent builder API in `JwtIssuer`, and `jjwt-jackson` → `jjwt-gson` so JWT (de)serialization stays off Jackson 2 (which Boot 4 no longer manages). - `.github/lsp.json` (Java `jdtls`), `.github/agents/java-migrator.agent.md`, `docs/modernization/audit-svc-plan.md`, `docs/modernization/migration-playbook.md`, extended `.github/hooks/scripts/test-router.sh`. -**Verified:** `build-branches.mjs --check` reproduces `expectedTreeSha` `f4bf2d4326384507e78ac8bf20e959de9922d650`. audit-svc: 8/8, auth-svc: 6/6 `mvn verify` on Boot 3.5.3/Java 21. See `manifest.json` (module 6). +**Note:** the previous `javax.annotation.PostConstruct` → `jakarta` rename and the `package.json` `with-java11` shim removal are no longer part of this delta — both are pre-done on the re-baselined base (the secure Boot 3.5.16 / Java 17 "before" already runs on Jakarta and a single JDK 21). + +**Verified:** `build-branches.mjs --check` reproduces `expectedTreeSha` `6de6b9b871d27b02d051f31ea1fb9ecc1c895bb2`. audit-svc: 3/3, auth-svc: 4/4 `mvn verify` on Boot 4.1.0 / Java 21; resolved trees OSV-clean. See `manifest.json` (module 6). diff --git a/course-build/manifest.json b/course-build/manifest.json index dde6bf1..96932d2 100644 --- a/course-build/manifest.json +++ b/course-build/manifest.json @@ -4,8 +4,8 @@ "description": "Canonical source of truth for building ACC learner start-of-module-N branches from deterministic per-module deltas. See course-build/REFS.md for the full ref/naming model and contract.", "base": { "ref": "acc-base", - "sha": "3689288d1f5a9ed6871db94a09c2e651bc20af3e", - "note": "Immutable tag pinned at pristine legacy-app app tree (origin/main HEAD, pre-course-tooling). All module deltas apply on top of this. Learner branches never contain course-build/ tooling or .github/workflows course automation." + "sha": "510b4ffa1b1f95234ec6c7bd1d0983060e5081c3", + "note": "Immutable tag pinned at the pristine, tooling-free legacy-app app tree updated to the current main app state (.NET 10 assets-svc + Spring Boot 3.5.16 / Java 17 Java services). All module deltas apply on top of this. Learner branches never contain course-build/ tooling or .github/workflows course automation. On promotion the acc-base tag is moved to this commit (see course-build/REFS.md)." }, "accVersionDefault": "auto-YYYY-MM", "modules": [ @@ -27,7 +27,7 @@ "0001-Update-postCreateCommand-to-install-GitHub-Copilot-u.patch", "0002-01-solution-Add-ARCHITECTURE.md-to-document-service-.patch" ], - "expectedTreeSha": "6e37bcab9ddbd78bf4fcb2819298555765fb5498", + "expectedTreeSha": "e23092983499b518165efc85fe51d829ea256ea8", "expectedAssets": ["ARCHITECTURE.md"], "touchesAppCode": false }, @@ -51,7 +51,7 @@ "0003-add-make-contribution-skill-for-contribution-guideli.patch", "0004-implemented-minor-accessibility-improvements-with-th.patch" ], - "expectedTreeSha": "17feab3963ea97b423fb3add8c0d377382fe646b", + "expectedTreeSha": "6058c846473986f2fc534b304c7cc7bbd4c98c08", "expectedAssets": [ ".github/copilot-instructions.md", ".github/agents/accessibility-expert.agent.md", @@ -87,7 +87,7 @@ "0003-docs-add-test-backfill-delegation-brief.patch", "0004-test-add-test-suite-backfill.patch" ], - "expectedTreeSha": "0690ccec6ddac59289f1b1afa0ee7cbcda7d5f9e", + "expectedTreeSha": "76b69b5ea5724d35909acd25b331e893be8443cb", "expectedAssets": [ "playwright.config.ts", "tests/playwright/smoke.spec.ts", @@ -117,7 +117,7 @@ "patches": [ "0001-feat-hooks-add-lifecycle-test-router-hook-hooks.json.patch" ], - "expectedTreeSha": "9bb1cb347bb20bce703a9bd9fc1aefba7113f936", + "expectedTreeSha": "6f33a42da97e6221ab787caf7a7b6160cec4677e", "expectedAssets": [ ".github/hooks/hooks.json", ".github/hooks/scripts/test-router.sh" @@ -135,7 +135,7 @@ "patches": [ "0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch" ], - "expectedTreeSha": "83337f347a5ee8b7d86374abbc738e89d9534e94", + "expectedTreeSha": "a18e9dca075fa69040c9a15195dbcd0e6cf98cb0", "expectedAssets": [ ".github/agents/qa.agent.md", "reports/qr-code-research.md", @@ -149,15 +149,15 @@ { "module": 6, "startBranch": "start-of-module-07", - "title": "LSP+MCP config + research report + REAL modernization app code", + "title": "LSP+MCP config + research report + REAL secure modernization app code (Boot 4.1 / Java 21)", "status": "backfilled", "source": "seed", - "provenance": { "kind": "module-runner", "note": "Copilot-generated modernization (audit-svc + auth-svc: Spring Boot 2.7->3.5.3, Java 11->21, javax->jakarta) authored via the module-runner seed path — performing ACC module 06 as a developer, incl. the deterministic pom.xml/package.json edits. Delta is now canonical." }, + "provenance": { "kind": "module-runner", "note": "Copilot-generated secure modernization (audit-svc + auth-svc: Spring Boot 3.5.16->4.1.0, Java 17->21, Jackson 3 default + jackson-bom/log4j2 currency pins, auth-svc jjwt 0.11.5->0.12.7 with jjwt-jackson->jjwt-gson) authored via the module-runner seed path — performing ACC module 06 as a developer. Re-authored on the re-baselined tooling-free base C. Delta is now canonical." }, "deltaDir": "deltas/module-06", "patches": [ "0001-feat-module-06-modernize-audit-svc-auth-svc-to-Sprin.patch" ], - "expectedTreeSha": "f4bf2d4326384507e78ac8bf20e959de9922d650", + "expectedTreeSha": "6de6b9b871d27b02d051f31ea1fb9ecc1c895bb2", "expectedAssets": [ ".github/lsp.json", ".github/agents/java-migrator.agent.md", diff --git a/course-build/scripts/validate-branch.sh b/course-build/scripts/validate-branch.sh index 3296443..5bbd2c7 100755 --- a/course-build/scripts/validate-branch.sh +++ b/course-build/scripts/validate-branch.sh @@ -69,10 +69,10 @@ if [ -d services/assets-svc ]; then fi fi -echo "==> Java services build (workforce = Java 21; audit/auth = Java 11)" +echo "==> Java services build (all on Java 21; audit/auth target Java 17 bytecode before module 06, Java 21 after)" [ -f services/workforce-svc/pom.xml ] && ( cd services/workforce-svc && mvn -q -B -DskipTests=false test ) -[ -f services/audit-svc/pom.xml ] && ( cd services/audit-svc && ../../scripts/with-java11 mvn -q -B -DskipTests=false test ) -[ -f services/auth-svc/pom.xml ] && ( cd services/auth-svc && ../../scripts/with-java11 mvn -q -B -DskipTests=false test ) +[ -f services/audit-svc/pom.xml ] && ( cd services/audit-svc && mvn -q -B -DskipTests=false test ) +[ -f services/auth-svc/pom.xml ] && ( cd services/auth-svc && mvn -q -B -DskipTests=false test ) echo "==> Python services install + pytest" for svc in reporting-svc notifications-svc; do