From 64bc2c14c8f5fc9bffec09c207b0d85d2240c659 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Wed, 8 Jul 2026 14:48:53 -0400 Subject: [PATCH 01/17] fix(security): bump jackson-databind to 2.18.8 to remediate CVE-2026-54512 and CVE-2026-54513 Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 6 ++++++ pom.xml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c6bf7da..6be08078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.4 + +### Chore & Maintenance + +- Update `jackson-databind` dependency due to vulnerability + ## 1.0.3 ### Chore & Maintenance diff --git a/pom.xml b/pom.xml index 87300363..0fda0899 100644 --- a/pom.xml +++ b/pom.xml @@ -309,7 +309,7 @@ ${java.version} 1.8.0 2.0.0 - 2.18.2 + 2.18.8 2.9.0 1.3.5 1.0.2 From f9cab6f213328a8b0f0692edaada0998bb969710 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Wed, 8 Jul 2026 17:28:33 -0400 Subject: [PATCH 02/17] Bump project version to 1.0.4 to match CHANGELOG Co-Authored-By: Claude Sonnet 5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0fda0899..9553d53f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ lf-repository-api-client-v2 jar Laserfiche Repository API Client V2 - 1.0.0 + 1.0.4 https://github.com/Laserfiche/lf-repository-api-client-java The Java Laserfiche Repository API Client library for accessing the v2 Laserfiche Repository APIs. From fa6adccb1727faacad178a8e02252f1840332069 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Wed, 8 Jul 2026 19:23:09 -0400 Subject: [PATCH 03/17] ci: grant checks:write to build-n-test so publish-unit-test-result-action can post check runs Default GITHUB_TOKEN permissions are read-only, so EnricoMi/publish-unit-test-result-action was failing with 403 Resource not accessible by integration on every PR. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 32a6c6d3..c98a0fd1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,10 @@ jobs: build-n-test: runs-on: ubuntu-latest + permissions: + contents: read + checks: write + steps: - uses: actions/checkout@v4 From 2f1aad75bd9201293d0df4308197ede6ff6279ac Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Wed, 8 Jul 2026 19:45:59 -0400 Subject: [PATCH 04/17] ci: grant pull-requests:write for publish-unit-test-result-action's PR comment checks:write alone got the check runs created, but the action's default comment_mode also posts/updates a PR comment, which needs pull-requests:write. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c98a0fd1..2eb53312 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,7 @@ jobs: permissions: contents: read checks: write + pull-requests: write steps: - uses: actions/checkout@v4 From 397492549ad682ce5b8b0557c452a11b62f49607 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Thu, 9 Jul 2026 18:03:21 -0400 Subject: [PATCH 05/17] ci: diagnostic run against REPOSITORY_ID_1 with hardcoded readonly folder Access denied errors trace back to the service principal not having access to whatever repo DEV_CA_PUBLIC_USE_REPOSITORY_ID_2 points to (confirmed via RepositoriesClientTest.listRepositoriesWorks: listRepositories() succeeds but that repo ID isn't in the returned list). Trying REPOSITORY_ID_1 (used by the dotnet/JS clients) with a hardcoded folder ID to see what actually fails now that auth itself isn't the blocker. Not a real fix - just a diagnostic. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2eb53312..081bd60e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,10 +54,10 @@ jobs: env: ACCESS_KEY: ${{ secrets.DEV_CA_PUBLIC_USE_INTEGRATION_TEST_ACCESS_KEY }} SERVICE_PRINCIPAL_KEY: ${{ secrets.DEV_CA_PUBLIC_USE_TESTOAUTHSERVICEPRINCIPAL_SERVICE_PRINCIPAL_KEY }} - REPOSITORY_ID: ${{ secrets.DEV_CA_PUBLIC_USE_REPOSITORY_ID_2 }} + REPOSITORY_ID: ${{ secrets.DEV_CA_PUBLIC_USE_REPOSITORY_ID_1 }} AUTHORIZATION_TYPE: ${{ secrets.AUTHORIZATION_TYPE }} TEST_HEADER: ${{ secrets.TEST_HEADER }} - READONLY_TEST_FOLDER_ID: ${{ secrets.DEV_CA_READONLY_TEST_FOLDER_ID }} + READONLY_TEST_FOLDER_ID: "2729986" # TODO: hardcoded for diagnostic run, revert to secrets.DEV_CA_READONLY_TEST_FOLDER_ID run: mvn -Dtest=com.laserfiche.repository.api.integration.** test - name: Publish cloud test results From 1da0e7c33852fc57e6dd96ae8ccbc0255cfe22cf Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Fri, 10 Jul 2026 16:31:31 -0400 Subject: [PATCH 06/17] dummy commit to trigger build From 030ca55e454822139599bc12f4d1186845b591d8 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Fri, 10 Jul 2026 16:37:35 -0400 Subject: [PATCH 07/17] hardcode different folder --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 081bd60e..7c3fde07 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,7 +57,7 @@ jobs: REPOSITORY_ID: ${{ secrets.DEV_CA_PUBLIC_USE_REPOSITORY_ID_1 }} AUTHORIZATION_TYPE: ${{ secrets.AUTHORIZATION_TYPE }} TEST_HEADER: ${{ secrets.TEST_HEADER }} - READONLY_TEST_FOLDER_ID: "2729986" # TODO: hardcoded for diagnostic run, revert to secrets.DEV_CA_READONLY_TEST_FOLDER_ID + READONLY_TEST_FOLDER_ID: "2732097" # TODO: hardcoded for diagnostic run, revert to secrets.DEV_CA_READONLY_TEST_FOLDER_ID run: mvn -Dtest=com.laserfiche.repository.api.integration.** test - name: Publish cloud test results From 27a11d5ec2408dbf6b0dee08de4287022fb3be6d Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Mon, 13 Jul 2026 08:30:53 -0400 Subject: [PATCH 08/17] add debuggin --- .../repository/api/integration/BaseTest.java | 5 ++++ .../integration/ExportDocumentApiTest.java | 25 ++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java b/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java index 280a568a..05a967c1 100644 --- a/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java +++ b/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java @@ -156,6 +156,11 @@ private static void waitUntilTaskEnds(String taskId, Duration interval) { .setTaskIds(taskId)); TaskProgress progress = collectionResponse.getValue().get(0); if (progress.getStatus() != TaskStatus.IN_PROGRESS) { + if (progress.getStatus() != TaskStatus.COMPLETED) { + throw new RuntimeException(String.format( + "Task %s ended with status %s instead of COMPLETED. Errors: %s", + taskId, progress.getStatus(), progress.getErrors())); + } return; } try { diff --git a/src/test/java/com/laserfiche/repository/api/integration/ExportDocumentApiTest.java b/src/test/java/com/laserfiche/repository/api/integration/ExportDocumentApiTest.java index b24e6674..35f0b669 100644 --- a/src/test/java/com/laserfiche/repository/api/integration/ExportDocumentApiTest.java +++ b/src/test/java/com/laserfiche/repository/api/integration/ExportDocumentApiTest.java @@ -107,6 +107,19 @@ static void classCleanUp() { deleteEntry(testEntryId); } + // Wraps exportEntry so a failure's ApiException status code/headers/problem details + // (not just its message) show up in the surefire report, since we can't get the raw + // job log from this environment to see the actual HTTP response otherwise. + private ExportEntryResponse exportEntry(ParametersForExportEntry parameters) { + try { + return client.exportEntry(parameters); + } catch (ApiException e) { + throw new AssertionError(String.format( + "exportEntry failed: statusCode=%d, headers=%s, problemDetails=%s", + e.getStatusCode(), e.getHeaders(), e.getProblemDetails()), e); + } + } + @Test void exportDocumentCanExportEDocPart() { final String FILE_NAME = "exportDocument_temp_file.pdf"; @@ -116,7 +129,7 @@ void exportDocumentCanExportEDocPart() { request.setAuditReasonId(auditReasonId); request.setAuditReasonComment(auditReasonComment); } - ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry() + ExportEntryResponse response = exportEntry(new ParametersForExportEntry() .setRepositoryId(repositoryId) .setEntryId(testEntryId) .setRequestBody(request)); @@ -137,7 +150,7 @@ void exportDocumentCanExportTextPart() { request.setAuditReasonId(auditReasonId); request.setAuditReasonComment(auditReasonComment); } - ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry() + ExportEntryResponse response = exportEntry(new ParametersForExportEntry() .setRepositoryId(repositoryId) .setEntryId(testEntryId) .setRequestBody(request)); @@ -188,7 +201,7 @@ void exportDocumentCanExportPagesAsSinglePageTIFF() { request.setAuditReasonId(auditReasonId); request.setAuditReasonComment(auditReasonComment); } - ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry() + ExportEntryResponse response = exportEntry(new ParametersForExportEntry() .setRepositoryId(repositoryId) .setEntryId(testEntryId) .setRequestBody(request)); @@ -217,7 +230,7 @@ void exportDocumentCanExportPagesAsMultiPageTIFF() { request.setAuditReasonId(auditReasonId); request.setAuditReasonComment(auditReasonComment); } - ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry() + ExportEntryResponse response = exportEntry(new ParametersForExportEntry() .setRepositoryId(repositoryId) .setEntryId(testEntryId) .setRequestBody(request)); @@ -244,7 +257,7 @@ void exportDocumentCanExportPagesAsJPEG() { request.setAuditReasonId(auditReasonId); request.setAuditReasonComment(auditReasonComment); } - ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry() + ExportEntryResponse response = exportEntry(new ParametersForExportEntry() .setRepositoryId(repositoryId) .setEntryId(testEntryId) .setRequestBody(request)); @@ -271,7 +284,7 @@ void exportDocumentCanExportPagesAsJPEGWithPageRange() { request.setAuditReasonId(auditReasonId); request.setAuditReasonComment(auditReasonComment); } - ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry() + ExportEntryResponse response = exportEntry(new ParametersForExportEntry() .setRepositoryId(repositoryId) .setEntryId(testEntryId) .setPageRange("1") From e481d15ceb5f5d36923a388e10f2520d0294e1b8 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Mon, 13 Jul 2026 08:53:43 -0400 Subject: [PATCH 09/17] update test folder id --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7c3fde07..f9efdbfb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,7 +57,7 @@ jobs: REPOSITORY_ID: ${{ secrets.DEV_CA_PUBLIC_USE_REPOSITORY_ID_1 }} AUTHORIZATION_TYPE: ${{ secrets.AUTHORIZATION_TYPE }} TEST_HEADER: ${{ secrets.TEST_HEADER }} - READONLY_TEST_FOLDER_ID: "2732097" # TODO: hardcoded for diagnostic run, revert to secrets.DEV_CA_READONLY_TEST_FOLDER_ID + READONLY_TEST_FOLDER_ID: "2737397" # TODO: hardcoded for diagnostic run, revert to secrets.DEV_CA_READONLY_TEST_FOLDER_ID run: mvn -Dtest=com.laserfiche.repository.api.integration.** test - name: Publish cloud test results From 17849921ffe03b9f2b455eea95584a9e3fa03195 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Mon, 13 Jul 2026 09:15:23 -0400 Subject: [PATCH 10/17] debug tests --- .../repository/api/integration/BaseTest.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java b/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java index 05a967c1..469add8a 100644 --- a/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java +++ b/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; enum AuthorizationType { CLOUD_ACCESS_KEY, @@ -159,7 +160,7 @@ private static void waitUntilTaskEnds(String taskId, Duration interval) { if (progress.getStatus() != TaskStatus.COMPLETED) { throw new RuntimeException(String.format( "Task %s ended with status %s instead of COMPLETED. Errors: %s", - taskId, progress.getStatus(), progress.getErrors())); + taskId, progress.getStatus(), formatErrors(progress.getErrors()))); } return; } @@ -173,6 +174,19 @@ private static void waitUntilTaskEnds(String taskId, Duration interval) { throw new RuntimeException("WaitUntilTaskEnds timeout"); } + // ProblemDetails doesn't override toString(), so printing the list directly just + // shows "ProblemDetails@" with none of the actual server-provided error info. + private static String formatErrors(List errors) { + if (errors == null || errors.isEmpty()) { + return "none"; + } + return errors.stream() + .map(e -> String.format( + "[title=%s, detail=%s, status=%s, errorCode=%s, errorSource=%s]", + e.getTitle(), e.getDetail(), e.getStatus(), e.getErrorCode(), e.getErrorSource())) + .collect(Collectors.joining(", ")); + } + public static void deleteEntry(int entryId) { if (entryId != 0) { StartTaskResponse startTaskResponse = repositoryApiClient From 31254ecfc1509d856e1ecc349f400562124ea0cf Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Mon, 13 Jul 2026 10:04:30 -0400 Subject: [PATCH 11/17] add audit reason --- .../repository/api/integration/BaseTest.java | 33 ++++++++++++++++++- .../api/integration/EntriesClientTest.java | 2 +- .../api/integration/TasksClientTest.java | 4 +-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java b/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java index 469add8a..45301eaf 100644 --- a/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java +++ b/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java @@ -8,6 +8,7 @@ import com.laserfiche.repository.api.RepositoryApiClientImpl; import com.laserfiche.repository.api.clients.impl.model.*; import com.laserfiche.repository.api.clients.params.ParametersForCreateEntry; +import com.laserfiche.repository.api.clients.params.ParametersForListAuditReasons; import com.laserfiche.repository.api.clients.params.ParametersForListTasks; import com.laserfiche.repository.api.clients.params.ParametersForStartDeleteEntry; import io.github.cdimascio.dotenv.Dotenv; @@ -187,6 +188,36 @@ private static String formatErrors(List errors) { .collect(Collectors.joining(", ")); } + private static Integer deleteAuditReasonId; + private static boolean deleteAuditReasonResolved = false; + + // This repository requires an audit reason for DeleteEntry (errorCode 216: "Need to + // provide correct audit reason for DeleteEntry"), unlike the old dedicated test repo. + // Resolved once and reused, mirroring ExportDocumentApiTest.findAuditReasonForExport. + protected static Integer getDeleteAuditReasonId() { + if (!deleteAuditReasonResolved) { + deleteAuditReasonResolved = true; + AuditReasonCollectionResponse auditReasons = repositoryApiClient + .getAuditReasonsClient() + .listAuditReasons(new ParametersForListAuditReasons().setRepositoryId(repositoryId)); + deleteAuditReasonId = auditReasons.getValue().stream() + .filter(reason -> reason.getAuditEventType() == AuditEventType.DELETE_ENTRY) + .map(AuditReason::getId) + .findFirst() + .orElse(null); + } + return deleteAuditReasonId; + } + + protected static StartDeleteEntryRequest newDeleteEntryRequest() { + StartDeleteEntryRequest request = new StartDeleteEntryRequest(); + Integer auditReasonId = getDeleteAuditReasonId(); + if (auditReasonId != null) { + request.setAuditReasonId(auditReasonId); + } + return request; + } + public static void deleteEntry(int entryId) { if (entryId != 0) { StartTaskResponse startTaskResponse = repositoryApiClient @@ -194,7 +225,7 @@ public static void deleteEntry(int entryId) { .startDeleteEntry(new ParametersForStartDeleteEntry() .setRepositoryId(repositoryId) .setEntryId(entryId) - .setRequestBody(new StartDeleteEntryRequest())); + .setRequestBody(newDeleteEntryRequest())); waitUntilTaskEnds(startTaskResponse.getTaskId()); } } diff --git a/src/test/java/com/laserfiche/repository/api/integration/EntriesClientTest.java b/src/test/java/com/laserfiche/repository/api/integration/EntriesClientTest.java index fbcefa9d..6c6c80f7 100644 --- a/src/test/java/com/laserfiche/repository/api/integration/EntriesClientTest.java +++ b/src/test/java/com/laserfiche/repository/api/integration/EntriesClientTest.java @@ -420,7 +420,7 @@ void startDeleteEntryCanDeleteFolder() { StartTaskResponse deleteEntryResponse = client.startDeleteEntry(new ParametersForStartDeleteEntry() .setRepositoryId(repositoryId) .setEntryId(entryToDelete.getId()) - .setRequestBody(new StartDeleteEntryRequest())); + .setRequestBody(newDeleteEntryRequest())); String taskId = deleteEntryResponse.getTaskId(); assertNotNull(taskId); diff --git a/src/test/java/com/laserfiche/repository/api/integration/TasksClientTest.java b/src/test/java/com/laserfiche/repository/api/integration/TasksClientTest.java index 597bfad0..24f047e7 100644 --- a/src/test/java/com/laserfiche/repository/api/integration/TasksClientTest.java +++ b/src/test/java/com/laserfiche/repository/api/integration/TasksClientTest.java @@ -30,7 +30,7 @@ void cancelTasksDoesNotReturnErrorWhenCancellingACompletedTask() { .startDeleteEntry(new ParametersForStartDeleteEntry() .setRepositoryId(repositoryId) .setEntryId(deleteEntry.getId()) - .setRequestBody(new StartDeleteEntryRequest())); + .setRequestBody(newDeleteEntryRequest())); String taskId = result.getTaskId(); assertNotNull(taskId); @@ -58,7 +58,7 @@ void listTasksWorksAndAcceptsMultipleTaskIdsAndCanBeCalledWithNoTaskIdsAndIgnore .startDeleteEntry(new ParametersForStartDeleteEntry() .setRepositoryId(repositoryId) .setEntryId(entry.getId()) - .setRequestBody(new StartDeleteEntryRequest())); + .setRequestBody(newDeleteEntryRequest())); assertNotNull(startTaskResponse); taskIds[i] = startTaskResponse.getTaskId(); } From c088a67cbcb4053870552b5fa9fc5c7e60dce210 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Mon, 13 Jul 2026 11:54:11 -0400 Subject: [PATCH 12/17] try fix --- .../ImportUploadedPartsApiTest.java | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/laserfiche/repository/api/integration/ImportUploadedPartsApiTest.java b/src/test/java/com/laserfiche/repository/api/integration/ImportUploadedPartsApiTest.java index c5156600..46d86c2f 100644 --- a/src/test/java/com/laserfiche/repository/api/integration/ImportUploadedPartsApiTest.java +++ b/src/test/java/com/laserfiche/repository/api/integration/ImportUploadedPartsApiTest.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. package com.laserfiche.repository.api.integration; +import com.laserfiche.api.client.model.ApiException; import com.laserfiche.repository.api.clients.EntriesClient; import com.laserfiche.repository.api.clients.TasksClient; import com.laserfiche.repository.api.clients.impl.model.*; @@ -59,6 +60,19 @@ static void classCleanUp() { deleteEntry(testClassParentFolder.getId()); } + // Surfaces the real HTTP status code/headers on failure instead of just the generic + // message, so an "Invalid or expired access token" error tells us whether it was + // actually a 401 (and thus in principle retryable) or something else entirely. + private static T call(String callName, java.util.function.Supplier apiCall) { + try { + return apiCall.get(); + } catch (ApiException e) { + throw new AssertionError(String.format( + "%s failed: statusCode=%d, headers=%s, problemDetails=%s", + callName, e.getStatusCode(), e.getHeaders(), e.getProblemDetails()), e); + } + } + @Test void createMultipartUploadUrlsCanBeCalledForSecondBatchOfURLs() { String fileName = "Sample.pdf"; @@ -108,8 +122,8 @@ void startImportUploadedPartsCanImportLargeFileAndGeneratePages() { requestBody.setMimeType(mimeType); requestBody.setNumberOfParts(parts); - CreateMultipartUploadUrlsResponse response = client.createMultipartUploadUrls(new ParametersForCreateMultipartUploadUrls() - .setRepositoryId(repositoryId).setRequestBody(requestBody)); + CreateMultipartUploadUrlsResponse response = call("createMultipartUploadUrls", () -> client.createMultipartUploadUrls(new ParametersForCreateMultipartUploadUrls() + .setRepositoryId(repositoryId).setRequestBody(requestBody))); assertNotNull(response); String uploadId = response.getUploadId(); @@ -130,17 +144,17 @@ void startImportUploadedPartsCanImportLargeFileAndGeneratePages() { pdfOptions.setGeneratePages(true); pdfOptions.setKeepPdfAfterImport(true); requestBody2.setPdfOptions(pdfOptions); - StartTaskResponse response2 = client.startImportUploadedParts(new ParametersForStartImportUploadedParts() + StartTaskResponse response2 = call("startImportUploadedParts", () -> client.startImportUploadedParts(new ParametersForStartImportUploadedParts() .setRepositoryId(repositoryId) .setEntryId(testClassParentFolder.getId()) - .setRequestBody(requestBody2)); + .setRequestBody(requestBody2))); assertNotNull(response2); String taskId = response2.getTaskId(); assertNotNull(taskId); waitUntilTaskEnds(taskId); - TaskCollectionResponse tasks = tasksClient.listTasks(new ParametersForListTasks().setRepositoryId(repositoryId).setTaskIds(taskId)); + TaskCollectionResponse tasks = call("listTasks(import)", () -> tasksClient.listTasks(new ParametersForListTasks().setRepositoryId(repositoryId).setTaskIds(taskId))); assertNotNull(tasks); assertEquals(1, tasks.getValue().size()); TaskProgress taskProgress = tasks.getValue().get(0); @@ -161,16 +175,16 @@ void startImportUploadedPartsCanImportLargeFileAndGeneratePages() { exportRequestBody.setAuditReasonId(auditReasonId); exportRequestBody.setAuditReasonComment(auditReasonComment); } - StartTaskResponse exportResponse = client.startExportEntry(new ParametersForStartExportEntry() + StartTaskResponse exportResponse = call("startExportEntry", () -> client.startExportEntry(new ParametersForStartExportEntry() .setRepositoryId(repositoryId) .setEntryId(createdEntryId) - .setRequestBody(exportRequestBody)); + .setRequestBody(exportRequestBody))); assertNotNull(exportResponse); - taskId = exportResponse.getTaskId(); - assertNotNull(taskId); + String exportTaskId = exportResponse.getTaskId(); + assertNotNull(exportTaskId); - tasks = tasksClient.listTasks(new ParametersForListTasks().setRepositoryId(repositoryId).setTaskIds(taskId)); + tasks = call("listTasks(export)", () -> tasksClient.listTasks(new ParametersForListTasks().setRepositoryId(repositoryId).setTaskIds(exportTaskId))); assertNotNull(tasks); assertEquals(1, tasks.getValue().size()); taskProgress = tasks.getValue().get(0); From a4e6e4346d174041692d20abf3414cd014c5aea8 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Mon, 13 Jul 2026 12:48:40 -0400 Subject: [PATCH 13/17] trigger a new build From 5fa00573c58c68677a9cf1d1724023811687f7e6 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Mon, 13 Jul 2026 12:58:32 -0400 Subject: [PATCH 14/17] add debug logging --- .../repository/api/integration/BaseTest.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java b/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java index 45301eaf..092625be 100644 --- a/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java +++ b/src/test/java/com/laserfiche/repository/api/integration/BaseTest.java @@ -3,6 +3,7 @@ package com.laserfiche.repository.api.integration; import com.laserfiche.api.client.model.AccessKey; +import com.laserfiche.api.client.model.ApiException; import com.laserfiche.api.client.model.ProblemDetails; import com.laserfiche.repository.api.RepositoryApiClient; import com.laserfiche.repository.api.RepositoryApiClientImpl; @@ -196,15 +197,26 @@ private static String formatErrors(List errors) { // Resolved once and reused, mirroring ExportDocumentApiTest.findAuditReasonForExport. protected static Integer getDeleteAuditReasonId() { if (!deleteAuditReasonResolved) { - deleteAuditReasonResolved = true; - AuditReasonCollectionResponse auditReasons = repositoryApiClient - .getAuditReasonsClient() - .listAuditReasons(new ParametersForListAuditReasons().setRepositoryId(repositoryId)); + AuditReasonCollectionResponse auditReasons; + try { + auditReasons = repositoryApiClient + .getAuditReasonsClient() + .listAuditReasons(new ParametersForListAuditReasons().setRepositoryId(repositoryId)); + } catch (ApiException e) { + // Don't cache "resolved" on failure — a transient hiccup here would otherwise + // permanently strand every later delete in the run with no audit reason, silently + // re-triggering "Need to provide correct audit reason for DeleteEntry" for the rest + // of the suite instead of just this one call. + throw new RuntimeException(String.format( + "listAuditReasons failed while resolving delete audit reason: statusCode=%d, headers=%s, problemDetails=%s", + e.getStatusCode(), e.getHeaders(), e.getProblemDetails()), e); + } deleteAuditReasonId = auditReasons.getValue().stream() .filter(reason -> reason.getAuditEventType() == AuditEventType.DELETE_ENTRY) .map(AuditReason::getId) .findFirst() .orElse(null); + deleteAuditReasonResolved = true; } return deleteAuditReasonId; } From efb1ac0cc83dfc6c8cc8294cc0599b7bad41fb66 Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Mon, 13 Jul 2026 16:02:08 -0400 Subject: [PATCH 15/17] add logging --- .../api/clients/impl/ApiClientUtils.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/main/java/com/laserfiche/repository/api/clients/impl/ApiClientUtils.java b/src/main/java/com/laserfiche/repository/api/clients/impl/ApiClientUtils.java index f47547eb..cdc65b90 100644 --- a/src/main/java/com/laserfiche/repository/api/clients/impl/ApiClientUtils.java +++ b/src/main/java/com/laserfiche/repository/api/clients/impl/ApiClientUtils.java @@ -174,8 +174,12 @@ public static TResponse sendRequestWithRetry( HttpResponse httpResponse = null; while (retryCount <= maxRetries && shouldRetry) { try { + if (retryCount > 0) { + System.out.println("[RETRY-DEBUG] attempt " + (retryCount + 1) + " starting for " + requestMethod + " " + url); + } String requestUrl = ApiClientUtils.beforeSend(url, headerParametersWithStringTypeValue, httpRequestHandler); + String authFingerprint = tokenFingerprint(headerParametersWithStringTypeValue); final HttpRequestWithBody httpRequestWithBody = httpClient.request(requestMethod, requestUrl); HttpRequest httpRequest = httpRequestWithBody; if (queryParameters != null) { @@ -212,10 +216,17 @@ public static TResponse sendRequestWithRetry( int statusCode = httpResponse.getStatus(); shouldRetry = httpRequestHandler.afterSend(new ResponseImpl((short) statusCode)) || ApiClientUtils.isRetryableStatusCode(statusCode, httpMethod); + if (statusCode == 401 || retryCount > 0 || shouldRetry) { + System.out.println("[RETRY-DEBUG] attempt " + (retryCount + 1) + " " + requestMethod + " " + url + + " -> status=" + statusCode + " shouldRetry=" + shouldRetry + " authToken=" + authFingerprint); + } if (!shouldRetry) { return parseResponse.apply(httpResponse); } } catch (Exception err) { + if (retryCount > 0 || err instanceof ApiException) { + System.out.println("[RETRY-DEBUG] attempt " + (retryCount + 1) + " threw " + err.getClass().getName() + ": " + err.getMessage()); + } if (err instanceof ApiException || retryCount >= maxRetries) { throw err; } @@ -229,4 +240,19 @@ public static TResponse sendRequestWithRetry( } return parseResponse.apply(httpResponse); } + + // TEMPORARY debug aid — prints the last 8 characters of whatever bearer token was actually + // attached for this attempt, so repeated log lines across retries can be compared to prove + // whether a retry after a 401 is really presenting a freshly-fetched token. Never logs the + // full token. Remove once the retry behavior is confirmed. + private static String tokenFingerprint(Map headerParametersWithStringTypeValue) { + if (headerParametersWithStringTypeValue == null) { + return "n/a"; + } + String auth = headerParametersWithStringTypeValue.get("Authorization"); + if (auth == null || auth.length() < 8) { + return "n/a"; + } + return "..." + auth.substring(auth.length() - 8); + } } From 66b4dcc49fd2eb48d4d2c1334e5a43b5bb5d452c Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Mon, 13 Jul 2026 16:52:00 -0400 Subject: [PATCH 16/17] test stability --- .github/workflows/main.yml | 2 +- .../api/clients/impl/ApiClientUtils.java | 18 +----------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f9efdbfb..8c5ef638 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,7 +57,7 @@ jobs: REPOSITORY_ID: ${{ secrets.DEV_CA_PUBLIC_USE_REPOSITORY_ID_1 }} AUTHORIZATION_TYPE: ${{ secrets.AUTHORIZATION_TYPE }} TEST_HEADER: ${{ secrets.TEST_HEADER }} - READONLY_TEST_FOLDER_ID: "2737397" # TODO: hardcoded for diagnostic run, revert to secrets.DEV_CA_READONLY_TEST_FOLDER_ID + READONLY_TEST_FOLDER_ID: ${{ secrets.DEV_CA_READONLY_TEST_FOLDER_ID }} run: mvn -Dtest=com.laserfiche.repository.api.integration.** test - name: Publish cloud test results diff --git a/src/main/java/com/laserfiche/repository/api/clients/impl/ApiClientUtils.java b/src/main/java/com/laserfiche/repository/api/clients/impl/ApiClientUtils.java index cdc65b90..11744a5d 100644 --- a/src/main/java/com/laserfiche/repository/api/clients/impl/ApiClientUtils.java +++ b/src/main/java/com/laserfiche/repository/api/clients/impl/ApiClientUtils.java @@ -179,7 +179,6 @@ public static TResponse sendRequestWithRetry( } String requestUrl = ApiClientUtils.beforeSend(url, headerParametersWithStringTypeValue, httpRequestHandler); - String authFingerprint = tokenFingerprint(headerParametersWithStringTypeValue); final HttpRequestWithBody httpRequestWithBody = httpClient.request(requestMethod, requestUrl); HttpRequest httpRequest = httpRequestWithBody; if (queryParameters != null) { @@ -218,7 +217,7 @@ public static TResponse sendRequestWithRetry( || ApiClientUtils.isRetryableStatusCode(statusCode, httpMethod); if (statusCode == 401 || retryCount > 0 || shouldRetry) { System.out.println("[RETRY-DEBUG] attempt " + (retryCount + 1) + " " + requestMethod + " " + url - + " -> status=" + statusCode + " shouldRetry=" + shouldRetry + " authToken=" + authFingerprint); + + " -> status=" + statusCode + " shouldRetry=" + shouldRetry); } if (!shouldRetry) { return parseResponse.apply(httpResponse); @@ -240,19 +239,4 @@ public static TResponse sendRequestWithRetry( } return parseResponse.apply(httpResponse); } - - // TEMPORARY debug aid — prints the last 8 characters of whatever bearer token was actually - // attached for this attempt, so repeated log lines across retries can be compared to prove - // whether a retry after a 401 is really presenting a freshly-fetched token. Never logs the - // full token. Remove once the retry behavior is confirmed. - private static String tokenFingerprint(Map headerParametersWithStringTypeValue) { - if (headerParametersWithStringTypeValue == null) { - return "n/a"; - } - String auth = headerParametersWithStringTypeValue.get("Authorization"); - if (auth == null || auth.length() < 8) { - return "n/a"; - } - return "..." + auth.substring(auth.length() - 8); - } } From 3df406a9e652a7351f618c3cace74da9e6b468bd Mon Sep 17 00:00:00 2001 From: "alexandria.gomez" Date: Tue, 14 Jul 2026 09:46:53 -0400 Subject: [PATCH 17/17] dummy commit to trigger build