From fb68b1fecfd72ac20eddd13a515a535c44262e41 Mon Sep 17 00:00:00 2001 From: gerlach Date: Thu, 11 Jun 2026 14:41:37 +0200 Subject: [PATCH] feat: prevent draft creation for unchanged dataset metadata --- .../harvard/iq/dataverse/api/Datasets.java | 13 +++- .../harvard/iq/dataverse/api/DatasetsIT.java | 60 +++++++++++++++++++ .../iq/dataverse/api/DatasetsTest.java | 27 +++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 7660d80aead..f5a9ff4f933 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -849,7 +849,12 @@ public Response updateDraftVersion(@Context ContainerRequestContext crc, String return error( Response.Status.BAD_REQUEST, "You may not add files via this api."); } - boolean updateDraft = ds.getLatestVersion().isDraft(); + DatasetVersion latestVersion = ds.getLatestVersion(); + if (isDatasetVersionNoOp(incomingVersion, latestVersion)) { + return ok(json(latestVersion, true)); + } + + boolean updateDraft = latestVersion.isDraft(); DatasetVersion managedVersion; if (updateDraft) { @@ -882,6 +887,12 @@ public Response updateDraftVersion(@Context ContainerRequestContext crc, String } } + // Helper extracted to make no-op detection testable. + static boolean isDatasetVersionNoOp(DatasetVersion incomingVersion, DatasetVersion latestVersion) { + DatasetVersionDifference diff = new DatasetVersionDifference(incomingVersion, latestVersion); + return diff.getDetailDataByBlock().isEmpty() && diff.getChangedTermsAccess().isEmpty(); + } + @GET @AuthRequired @Path("{id}/versions/{versionId}/metadata") diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 583a76f07a7..7abf385f0c0 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -7540,6 +7540,66 @@ public void testGetDatasetWithTermsOfUseAndGuestbook() throws IOException, JsonP .body("data.guestbookId", equalTo(guestbook.getId().intValue())); } + @Test + public void testUpdateDatasetMetadataNoOp() { + String apiToken = getSuperuserToken(); + String collectionAlias = UtilIT.createRandomCollectionGetAlias(apiToken); + UtilIT.publishDataverseViaNativeApi(collectionAlias, apiToken) + .then().assertThat().statusCode(OK.getStatusCode()); + + String updateJsonFile = "doc/sphinx-guides/source/_static/api/dataset-update-metadata.json"; + + // 1. Create a dataset and update it with known metadata from a file + Response createDataset = UtilIT.createRandomDatasetViaNativeApi(collectionAlias, apiToken); + createDataset.then().assertThat().statusCode(CREATED.getStatusCode()); + Integer datasetId = UtilIT.getDatasetIdFromResponse(createDataset); + String datasetPid = UtilIT.getDatasetPersistentIdFromResponse(createDataset); + + UtilIT.updateDatasetMetadataViaNative(datasetPid, updateJsonFile, apiToken) + .then().assertThat().statusCode(OK.getStatusCode()); + UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken) + .then().assertThat().statusCode(OK.getStatusCode()); + UtilIT.getDatasetVersion(datasetPid, DS_VERSION_DRAFT, apiToken) + .then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + // 2. Call API with identical metadata (Published case) + UtilIT.updateDatasetMetadataViaNative(datasetPid, updateJsonFile, apiToken) + .then().assertThat().statusCode(OK.getStatusCode()); + + // 3. Verify no draft was created + UtilIT.getDatasetVersion(datasetPid, DS_VERSION_DRAFT, apiToken) + .then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + // 4. Test the scenario where a draft ALREADY exists + String currentMetadata = UtilIT.getDatasetJson(updateJsonFile); + String draftJson = currentMetadata.replace("\"newTitle\"", "\"Updated Title\""); + + given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .body(draftJson) + .contentType("application/json") + .put("/api/datasets/:persistentId/versions/" + DS_VERSION_DRAFT + "?persistentId=" + datasetPid) + .then().assertThat().statusCode(OK.getStatusCode()); + + Response draftResponse = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_DRAFT, apiToken); + draftResponse.then().assertThat().statusCode(OK.getStatusCode()); + String lastUpdateTimeBefore = draftResponse.jsonPath().getString("data.lastUpdateTime"); + + // 5. Call API with identical metadata to the current draft + given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .body(draftJson) + .contentType("application/json") + .put("/api/datasets/:persistentId/versions/" + DS_VERSION_DRAFT + "?persistentId=" + datasetPid) + .then().assertThat().statusCode(OK.getStatusCode()); + + // 6. Verify it was a no-op (lastUpdateTime should NOT have changed) + Response draftResponseAfter = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_DRAFT, apiToken); + String lastUpdateTimeAfter = draftResponseAfter.jsonPath().getString("data.lastUpdateTime"); + + assertEquals(lastUpdateTimeBefore, lastUpdateTimeAfter, "Last update time should not change for no-op metadata update on draft"); + } + private String getSuperuserToken() { Response createResponse = UtilIT.createRandomUser(); String adminApiToken = UtilIT.getApiTokenFromResponse(createResponse); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsTest.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsTest.java index 39c5168fc0b..40635769c57 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsTest.java @@ -3,17 +3,44 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.*; import org.junit.jupiter.api.Test; + import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; public class DatasetsTest { + @Test + public void testIsDatasetVersionNoOp() { + DatasetVersion incoming = emptyVersion(); + DatasetVersion latest = emptyVersion(); + assertTrue(Datasets.isDatasetVersionNoOp(incoming, latest)); + + DatasetField field = new DatasetField(); + DatasetFieldType type = new DatasetFieldType(); + type.setName("title"); + type.setChildDatasetFieldTypes(List.of()); + field.setDatasetFieldType(type); + field.setDatasetFieldValues(List.of(new DatasetFieldValue(field, "Changed title"))); + incoming.setDatasetFields(List.of(field)); + + assertFalse(Datasets.isDatasetVersionNoOp(incoming, latest)); + } + + private static DatasetVersion emptyVersion() { + DatasetVersion v = new DatasetVersion(); + v.setTermsOfUseAndAccess(new TermsOfUseAndAccess()); + return v; + } + /** * Test cleanup filter */