diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index e74a65f..1fe871e 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -2,6 +2,10 @@ ## Audit candidate 0.1.4-rc.1 +HTTP follow-up: the runtime-inventory commit `60ce6e4` passed hosted security, CodeQL and PR gates, but its UI-profile build exposed a connection failure on the first status event after a claim. A deterministic split-body regression proved claim returned before consuming its request body. Claim and cancel now consume the existing bounded JSON body after device authentication and before mutation. The focused regression fails before the change and passes afterward; full isolated UI-profile verification passes **66 tests and zero Checkstyle violations**. The exact Ubuntu timing failure was not reproduced locally; fresh hosted verification is required. No retries or assertions were weakened. + +Native deployment qualification is still in progress. Exact-artifact startup and native authentication/query/retention checks pass against fresh closed-backup copies, with the default 365-day policy. A prepared launch against a particular earlier copy returned a missing-settings-table error, while changing only the copy's filename passed; the cause remains under investigation. This is not claimed to be a fixed application defect. The real paired host remains unchanged, and no cleanup or physical printing was submitted by these checks. + Runtime inventory follow-up: the authenticated host commit `78e0911` passed hosted checks, but a local scanner comparison found that the original POM scan omitted the frontend profile and selected a different transitive version from Maven. The pending workflow fix enables `ui-demo` for verification and generates a pinned CycloneDX Maven runtime SBOM for both Trivy and OSV. Exact coordinate/version guards reject incomplete or divergent inventories. Locally, all **204** selected runtime dependencies match the SBOM and Trivy inventory; ten new guard tests, six existing SARIF tests, actionlint and offline zizmor medium checks pass. Local vulnerability checks used the existing cached database; fresh hosted gates are still required for these workflow changes. The corrected deployment dependency payload excludes test libraries; no host was deployed by this follow-up. Final isolated verification passed **65 tests, zero failures/errors/skips and zero Checkstyle violations**, including the frontend runtime profile. The independent baseline scan's two server findings were remediated: configurable atomic tenant/device admission covers outstanding work, retained bytes and hourly allocations; cumulative printer limits and cursor pagination bound inventory. A fresh reviewer found a PostgreSQL wide-index regression, corrected before the final gate. Durable upload accounting, legacy unknown reservations, stable replay and single-connection cleanup have regression coverage. diff --git a/docs/TODO.md b/docs/TODO.md index 03ca852..ceab602 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,5 +1,7 @@ # Follow-up +- [x] Consume bounded claim/cancel bodies before side effects; split-body regression fails before fix, full isolated UI-profile gate passes 66 tests. +- [ ] Verify the HTTP follow-up on fresh hosted checks and resolve the isolated prepared-copy retention discrepancy before live cutover. - [x] Reconcile UI-profile Maven runtime dependencies with scanner inventories; 204 exact coordinates, ten guard regressions and workflow lint pass locally. - [ ] Run fresh hosted security/build gates for the runtime-SBOM workflow correction before authenticated-host deployment. diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingRoutes.java b/src/main/java/com/kingsrook/qbits/printing/PrintingRoutes.java index 3318b02..fb3dcd6 100644 --- a/src/main/java/com/kingsrook/qbits/printing/PrintingRoutes.java +++ b/src/main/java/com/kingsrook/qbits/printing/PrintingRoutes.java @@ -76,11 +76,21 @@ public class PrintingRoutes implements QJavalinRouteProviderInterface return accepted(); })); config.routes.post(BASE + "/device/heartbeat", context -> handle(context, () -> service.heartbeat(device(context), body(context)))); - config.routes.post(BASE + "/device/jobs/claim", context -> handle(context, () -> service.claim(device(context)))); + config.routes.post(BASE + "/device/jobs/claim", context -> handle(context, () -> + { + String device = device(context); + body(context); + return service.claim(device); + })); config.routes.get(BASE + "/device/jobs/queued", context -> handle(context, () -> new JSONObject().put("jobs", service.queuedJobs(device(context))))); config.routes.post(BASE + "/device/test-print", context -> handle(context, () -> service.testPrint(device(context), body(context)))); config.routes.post(BASE + "/device/jobs/{jobId}/retry", context -> handle(context, () -> service.retry(device(context), context.pathParam("jobId"), body(context)))); - config.routes.post(BASE + "/device/jobs/{jobId}/cancel", context -> handle(context, () -> service.cancel(device(context), context.pathParam("jobId")))); + config.routes.post(BASE + "/device/jobs/{jobId}/cancel", context -> handle(context, () -> + { + String device = device(context); + body(context); + return service.cancel(device, context.pathParam("jobId")); + })); config.routes.get(BASE + "/device/jobs/{jobId}/content", context -> handle(context, () -> { byte[] bytes = service.content(device(context), context.pathParam("jobId")); diff --git a/src/test/java/com/kingsrook/qbits/printing/PrintingContractTest.java b/src/test/java/com/kingsrook/qbits/printing/PrintingContractTest.java index cd60556..65fb5b3 100644 --- a/src/test/java/com/kingsrook/qbits/printing/PrintingContractTest.java +++ b/src/test/java/com/kingsrook/qbits/printing/PrintingContractTest.java @@ -21,10 +21,13 @@ package com.kingsrook.qbits.printing; +import java.net.Socket; +import java.net.SocketTimeoutException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; @@ -36,6 +39,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -122,6 +126,41 @@ class PrintingContractTest + /*************************************************************************** + ** Claim waits for its bounded JSON body before committing the response. + ***************************************************************************/ + @Test + void claimConsumesSplitRequestBodyBeforeResponding() throws Exception + { + PrintingService service = new PrintingService(new DriverDataSource("jdbc:h2:mem:split-body-" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1", "sa", ""), "contract-tenant"); + service.initialize(); + String code = service.createEnrollment("default", "Synthetic device").getString("enrollmentCode"); + String token = service.enroll(fixture("enroll.request.json").put("enrollmentCode", code)).getString("deviceToken"); + PrintingRoutes routes = new PrintingRoutes(service, context -> false); + Javalin app = Javalin.create(routes::acceptJavalinConfig).start("127.0.0.1", 0); + try(Socket socket = new Socket("127.0.0.1", app.port())) + { + socket.setSoTimeout(250); + String headers = "POST " + PrintingRoutes.BASE + "/device/jobs/claim HTTP/1.1\r\nHost: 127.0.0.1:" + app.port() + + "\r\nAuthorization: Bearer " + token + "\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{"; + socket.getOutputStream().write(headers.getBytes(StandardCharsets.US_ASCII)); + socket.getOutputStream().flush(); + assertThrows(SocketTimeoutException.class, () -> socket.getInputStream().read()); + socket.setSoTimeout(5000); + socket.getOutputStream().write('}'); + socket.getOutputStream().flush(); + String response = new String(socket.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + assertTrue(response.startsWith("HTTP/1.1 200")); + assertTrue(response.contains("\"job\":null")); + } + finally + { + app.stop(); + } + } + + + /*************************************************************************** ** Load the same fixture consumed by the Windows protocol tests. ***************************************************************************/