diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 32a6c6d3..8c5ef638 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -19,6 +19,11 @@ jobs:
build-n-test:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ checks: write
+ pull-requests: write
+
steps:
- uses: actions/checkout@v4
@@ -49,7 +54,7 @@ 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 }}
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..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.
@@ -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
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..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,11 +3,13 @@
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;
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;
@@ -24,6 +26,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
enum AuthorizationType {
CLOUD_ACCESS_KEY,
@@ -156,6 +159,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(), formatErrors(progress.getErrors())));
+ }
return;
}
try {
@@ -168,6 +176,60 @@ 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(", "));
+ }
+
+ 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) {
+ 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;
+ }
+
+ 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
@@ -175,7 +237,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/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")
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);
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();
}