From a961a94d6e7559370511263beeeae52aa3648711 Mon Sep 17 00:00:00 2001 From: Rudra Chopra Date: Mon, 18 Mar 2024 15:25:51 +0530 Subject: [PATCH] feat(rest): saveUsages in project page Signed-off-by: Rudra Chopra --- .../src/docs/asciidoc/projects.adoc | 24 +++ .../project/ProjectController.java | 82 ++++++++ .../project/Sw360ProjectService.java | 178 +++++++++++++++++- .../restdocs/ProjectSpecTest.java | 59 +++++- 4 files changed, 331 insertions(+), 12 deletions(-) diff --git a/rest/resource-server/src/docs/asciidoc/projects.adoc b/rest/resource-server/src/docs/asciidoc/projects.adoc index ea2a84fea3..dd8936b5d2 100644 --- a/rest/resource-server/src/docs/asciidoc/projects.adoc +++ b/rest/resource-server/src/docs/asciidoc/projects.adoc @@ -729,6 +729,30 @@ include::{snippets}/should_document_get_attachment_usage_for_project/curl-reques ===== Example response include::{snippets}/should_document_get_attachment_usage_for_project/http-response.adoc[] +[[resources-project-save-attachmentUsages]] +==== Save attachmentUsages of the project + +A `POST` request is used to save attachmentUsages of the project. Please pass a Map> having string as key and list as value in request body. + +===== Request structure 1 +Pass a Map> in request body. + +Keys can be selected, deselected, selectedConcludedUsages or deselectedConcludedUsages. + +Format of String inside list varies according to the usageFields (sourcePackage, licenseInfo, manuallySet) : + +for `sourcePackage`, releaseId_sourcePakage_attachmentContentId + +for `licenseInfo`, projectId-releaseId_licenseInfo_attachmentContentId (for directly linked project) OR projectPath-releaseId_licenseInfo_attachmentContentId (for nested projects) + +for `manuallySet`, releaseId_manuallySet_attachmentContentId + +===== Example request 1 +include::{snippets}/should_document_save_usages/curl-request.adoc[] + +===== Example response 1 +include::{snippets}/should_document_save_usages/http-response.adoc[] + [[resources-project-get-project-vulnerabilities]] ==== Listing project vulnerabilities diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java index 16d31fc9ad..337bbd53ca 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java @@ -63,6 +63,7 @@ import org.eclipse.sw360.datahandler.thrift.Source; import org.eclipse.sw360.datahandler.thrift.attachments.Attachment; import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentContent; +import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentService; import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentType; import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentUsage; import org.eclipse.sw360.datahandler.thrift.attachments.UsageData; @@ -1555,6 +1556,87 @@ public ResponseEntity>> getUsedByProjectDet return new ResponseEntity<>(resources, status); } + @PreAuthorize("hasAuthority('WRITE')") + @Operation( + summary = "save attachment usages", + description = "Pass an array of string in request body.", + tags = {"Projects"} + ) + @RequestMapping(value = PROJECTS_URL + "/{id}/saveAttachmentUsages", method = RequestMethod.POST) + public ResponseEntity saveAttachmentUsages( + @Parameter(description = "Project ID.") + @PathVariable("id") String id, + @Parameter(description = "Map of key-value pairs where each key is associated with a list of strings.", + example = "{\"selected\": [\"4427a8e723ad405db63f75170ef240a2_sourcePackage_5c5d6f54ac6a4b33bcd3c5d3a8fefc43\", \"value2\"]," + + " \"deselected\": [\"de213309ba0842ac8a7251bf27ea8f36_manuallySet_eec66c3465f64f0292dfc2564215c681\", \"value2\"]," + + " \"selectedConcludedUsages\": [\"de213309ba0842ac8a7251bf27ea8f36_licenseInfo_eec66c3465f64f0292dfc2564215c681\", \"value2\"]," + + " \"deselectedConcludedUsages\": [\"ade213309ba0842ac8a7251bf27ea8f36_licenseInfo_aeec66c3465f64f0292dfc2564215c681\", \"value2\"]}" + ) + @RequestBody Map> allUsages + ) throws TException { + final User user = restControllerHelper.getSw360UserFromAuthentication(); + final Project project = projectService.getProjectForUserById(id, user); + try { + if (PermissionUtils.makePermission(project, user).isActionAllowed(RequestedAction.WRITE)) { + Source usedBy = Source.projectId(id); + List selectedUsages = new ArrayList<>(); + List deselectedUsages = new ArrayList<>(); + List selectedConcludedUsages = new ArrayList<>(); + List deselectedConcludedUsages = new ArrayList<>(); + List changedUsages = new ArrayList<>(); + for (Map.Entry> entry : allUsages.entrySet()) { + String key = entry.getKey(); + List list = entry.getValue(); + if (key.equals("selected")) { + selectedUsages.addAll(list); + } else if (key.equals("deselected")) { + deselectedUsages.addAll(list); + } else if (key.equals("selectedConcludedUsages")) { + selectedConcludedUsages.addAll(list); + } else if (key.equals("deselectedConcludedUsages")) { + deselectedConcludedUsages.addAll(list); + } + } + Set totalReleaseIds = projectService.getReleaseIds(id, user, true); + changedUsages.addAll(selectedUsages); + changedUsages.addAll(deselectedUsages); + boolean valid = projectService.validate(changedUsages, user, releaseService, totalReleaseIds); + if (!valid) { + return new ResponseEntity<>("Not a valid attachment type OR release does not belong to project", HttpStatus.CONFLICT); + } + List allUsagesByProject = projectService.getUsedAttachments(usedBy, null); + List savedUsages = projectService.savedUsages(allUsagesByProject); + savedUsages.removeAll(deselectedUsages); + deselectedUsages.addAll(selectedUsages); + selectedUsages.addAll(savedUsages); + deselectedConcludedUsages.addAll(selectedConcludedUsages); + List deselectedUsagesFromRequest = projectService.deselectedAttachmentUsagesFromRequest(deselectedUsages, selectedUsages, deselectedConcludedUsages, selectedConcludedUsages, id); + List selectedUsagesFromRequest = projectService.selectedAttachmentUsagesFromRequest(deselectedUsages, selectedUsages, deselectedConcludedUsages, selectedConcludedUsages, id); + List usagesToDelete = allUsagesByProject.stream() + .filter(usage -> deselectedUsagesFromRequest.stream() + .anyMatch(projectService.isUsageEquivalent(usage))) + .collect(Collectors.toList()); + if (!usagesToDelete.isEmpty()) { + projectService.deleteAttachmentUsages(usagesToDelete); + } + List allUsagesByProjectAfterCleanUp = projectService.getUsedAttachments(usedBy, null); + List usagesToCreate = selectedUsagesFromRequest.stream() + .filter(usage -> allUsagesByProjectAfterCleanUp.stream() + .noneMatch(projectService.isUsageEquivalent(usage))) + .collect(Collectors.toList()); + + if (!usagesToCreate.isEmpty()) { + projectService.makeAttachmentUsages(usagesToCreate); + } + return new ResponseEntity<>("AttachmentUsages Saved Successfully", HttpStatus.CREATED); + } else { + return new ResponseEntity<>("No write permission for project", HttpStatus.FORBIDDEN); + } + } catch (TException e) { + return new ResponseEntity<>("Saving attachment usages for project " + id + " failed", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + @Operation( description = "Get all attachmentUsages of the projects.", tags = {"Projects"} diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java index 4dd89a41d4..9eeb0db1d7 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java @@ -34,11 +34,8 @@ import org.eclipse.sw360.datahandler.thrift.SW360Exception; import org.eclipse.sw360.datahandler.thrift.Source; import org.eclipse.sw360.datahandler.thrift.ThriftClients; -import org.eclipse.sw360.datahandler.thrift.attachments.*; import org.eclipse.sw360.datahandler.thrift.components.ComponentService; -import org.eclipse.sw360.datahandler.thrift.attachments.Attachment; -import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentType; -import org.eclipse.sw360.datahandler.thrift.attachments.CheckStatus; +import org.eclipse.sw360.datahandler.thrift.attachments.*; import org.eclipse.sw360.datahandler.thrift.components.Release; import org.eclipse.sw360.datahandler.thrift.components.ReleaseClearingStatusData; import org.eclipse.sw360.datahandler.thrift.components.ReleaseLink; @@ -63,6 +60,7 @@ import org.eclipse.sw360.rest.resourceserver.core.RestControllerHelper; import org.eclipse.sw360.rest.resourceserver.release.ReleaseController; import org.eclipse.sw360.rest.resourceserver.release.Sw360ReleaseService; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataIntegrityViolationException; @@ -104,6 +102,7 @@ import java.util.stream.Collectors; import static com.google.common.base.Strings.nullToEmpty; +import static org.eclipse.sw360.datahandler.common.CommonUtils.arrayToList; import static org.eclipse.sw360.datahandler.common.CommonUtils.getSortedMap; import static org.eclipse.sw360.datahandler.common.CommonUtils.isNullEmptyOrWhitespace; import static org.eclipse.sw360.datahandler.common.CommonUtils.nullToEmptyList; @@ -156,6 +155,177 @@ public Project getProjectForUserById(String projectId, User sw360User) throws TE } } + public boolean validate(List changedUsages, User sw360User, Sw360ReleaseService releaseService, Set totalReleaseIds) throws TException { + for (String data : changedUsages) { + String releaseId; + String usageData; + String attachmentContentId; + String[] parts = data.split("-"); + if (parts.length > 1) { + String[] components = parts[1].split("_"); + releaseId = components[0]; + usageData = components[1]; + attachmentContentId = components[2]; + } + else { + String[] components = data.split("_"); + releaseId = components[0]; + usageData = components[1]; + attachmentContentId = components[2]; + } + boolean relPresent = totalReleaseIds.contains(releaseId); + if (!relPresent) { + return false; + } + Release release = releaseService.getReleaseForUserById(releaseId, sw360User); + Set attachments = release.getAttachments(); + if (usageData.equals("sourcePackage")) { + for (Attachment attach : attachments) { + if (attach.getAttachmentContentId().equals(attachmentContentId) && (attach.getAttachmentType() != AttachmentType.SOURCE && attach.getAttachmentType() != AttachmentType.SOURCE_SELF)) { + return false; + } + } + } + if (usageData.equals("licenseInfo")) { + for (Attachment attach : attachments) { + if (attach.getAttachmentContentId().equals(attachmentContentId) && (attach.getAttachmentType() != AttachmentType.COMPONENT_LICENSE_INFO_COMBINED && attach.getAttachmentType() != AttachmentType.COMPONENT_LICENSE_INFO_XML)) { + return false; + } + } + } + } return true; + } + + public List savedUsages(List allUsagesByProject) { + List selectedData = new ArrayList<>(); + for (AttachmentUsage usage : allUsagesByProject) { + if (usage.getUsageData().getSetField().equals(UsageData._Fields.LICENSE_INFO)) { + StringBuilder result = new StringBuilder(); + result.append(usage.getUsageData().getLicenseInfo().getProjectPath()) + .append("-") + .append(usage.getOwner().getReleaseId()) + .append("_") + .append(usage.getUsageData().getSetField().getFieldName()) + .append("_") + .append(usage.getAttachmentContentId()); + String stringResult = result.toString(); + selectedData.add(stringResult); + } + else { + StringBuilder result = new StringBuilder(); + result.append(usage.getOwner().getReleaseId()) + .append("_") + .append(usage.getUsageData().getSetField().getFieldName()) + .append("_") + .append(usage.getAttachmentContentId()); + String stringResult = result.toString(); + selectedData.add(stringResult); + } + } + System.out.println("Resulting string: " + selectedData); + return selectedData; + } + + public List deselectedAttachmentUsagesFromRequest(List deselectedUsages, List selectedUsages, List deselectedConcludedUsages, List selectedConcludedUsages, String id) { + return makeAttachmentUsagesFromParameters(deselectedUsages, selectedUsages, deselectedConcludedUsages, selectedConcludedUsages, Sets::difference, true, id); + } + + public List selectedAttachmentUsagesFromRequest(List deselectedUsages, List selectedUsages, List deselectedConcludedUsages, List selectedConcludedUsages, String id) { + return makeAttachmentUsagesFromParameters(deselectedUsages, selectedUsages, deselectedConcludedUsages, selectedConcludedUsages, Sets::intersection, false, id); + } + + private static List makeAttachmentUsagesFromParameters(List deselectedUsages, List selectedUsage, + List deselectedConcludedUsages, List selectedConcludedUsages, + BiFunction, Set, Set> computeUsagesFromCheckboxes, boolean deselectUsage, String projectId) { + Set selectedUsages = new HashSet<>(selectedUsage); + Set changedUsages = new HashSet<>(deselectedUsages); + Set changedIncludeConludedLicenses = new HashSet<>(deselectedConcludedUsages); + changedUsages = Sets.union(changedUsages, new HashSet(changedIncludeConludedLicenses)); + List includeConludedLicenses = new ArrayList<>(selectedConcludedUsages); + Set usagesSubset = computeUsagesFromCheckboxes.apply(changedUsages, selectedUsages); + if (deselectUsage) { + usagesSubset = Sets.union(usagesSubset, new HashSet(changedIncludeConludedLicenses)); + } + return usagesSubset.stream() + .map(s -> parseAttachmentUsageFromString(projectId, s, includeConludedLicenses)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static AttachmentUsage parseAttachmentUsageFromString(String projectId, String s, List includeConludedLicense) { + String[] split = s.split("_"); + if (split.length != 3) { + log.warn(String.format("cannot parse attachment usage from %s for project id %s", s, projectId)); + return null; + } + + String releaseId = split[0]; + String type = split[1]; + String attachmentContentId = split[2]; + String projectPath = null; + if (UsageData._Fields.findByName(type).equals(UsageData._Fields.LICENSE_INFO)) { + String[] projectPath_releaseId = split[0].split("-"); + if (projectPath_releaseId.length == 2) { + releaseId = projectPath_releaseId[1]; + projectPath = projectPath_releaseId[0]; + } + } + + AttachmentUsage usage = new AttachmentUsage(Source.releaseId(releaseId), attachmentContentId, Source.projectId(projectId)); + final UsageData usageData; + switch (UsageData._Fields.findByName(type)) { + case LICENSE_INFO: + LicenseInfoUsage licenseInfoUsage = new LicenseInfoUsage(Collections.emptySet()); + licenseInfoUsage.setIncludeConcludedLicense(includeConludedLicense.contains(s)); + if (projectPath != null) { + licenseInfoUsage.setProjectPath(projectPath); + } + usageData = UsageData.licenseInfo(licenseInfoUsage); + break; + case SOURCE_PACKAGE: + usageData = UsageData.sourcePackage(new SourcePackageUsage()); + break; + case MANUALLY_SET: + usageData = UsageData.manuallySet(new ManuallySetUsage()); + break; + default: + throw new IllegalArgumentException("Unexpected UsageData type: " + type); + } + usage.setUsageData(usageData); + return usage; + } + + /** + * Here, "equivalent" means an AttachmentUsage should replace another one in the DB, not that they are equal. + * I.e, they have the same attachmentContentId, owner, usedBy, and same UsageData type. + */ + @NotNull + public Predicate isUsageEquivalent(AttachmentUsage usage) { + return equivalentUsage -> usage.getAttachmentContentId().equals(equivalentUsage.getAttachmentContentId()) && + usage.getOwner().equals(equivalentUsage.getOwner()) && + usage.getUsedBy().equals(equivalentUsage.getUsedBy()) && + usage.getUsageData().getSetField().equals(equivalentUsage.getUsageData().getSetField()); + } + + public void deleteAttachmentUsages(List usagesToDelete) throws TException { + ThriftClients thriftClients = new ThriftClients(); + AttachmentService.Iface attachmentClient = thriftClients.makeAttachmentClient(); + attachmentClient.deleteAttachmentUsages(usagesToDelete); + } + + public void makeAttachmentUsages(List usagesToCreate) throws TException { + ThriftClients thriftClients = new ThriftClients(); + AttachmentService.Iface attachmentClient = thriftClients.makeAttachmentClient(); + attachmentClient.makeAttachmentUsages(usagesToCreate); + } + + public List getUsedAttachments(Source usedBy, Object object) throws TException { + ThriftClients thriftClients = new ThriftClients(); + AttachmentService.Iface attachmentClient = thriftClients.makeAttachmentClient(); + List allUsagesByProjectAfterCleanUp = attachmentClient.getUsedAttachments(usedBy, null); + return allUsagesByProjectAfterCleanUp; + } + public String getCyclicLinkedProjectPath(Project project, User user) throws TException { ProjectService.Iface sw360ProjectClient = getThriftProjectClient(); String cyclicLinkedProjectPath = sw360ProjectClient.getCyclicLinkedProjectPath(project, user); diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java index 0e78996269..4b40686d78 100644 --- a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java @@ -28,13 +28,7 @@ import org.eclipse.sw360.datahandler.thrift.ClearingRequestPriority; import org.eclipse.sw360.datahandler.thrift.Source; import org.eclipse.sw360.datahandler.thrift.Visibility; -import org.eclipse.sw360.datahandler.thrift.attachments.Attachment; -import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentContent; -import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentType; -import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentUsage; -import org.eclipse.sw360.datahandler.thrift.attachments.CheckStatus; -import org.eclipse.sw360.datahandler.thrift.attachments.LicenseInfoUsage; -import org.eclipse.sw360.datahandler.thrift.attachments.UsageData; +import org.eclipse.sw360.datahandler.thrift.attachments.*; import org.eclipse.sw360.datahandler.thrift.components.ClearingState; import org.eclipse.sw360.datahandler.thrift.components.Component; import org.eclipse.sw360.datahandler.thrift.components.ComponentType; @@ -366,6 +360,8 @@ public void before() throws TException, IOException { projectList.add(project2); Map linkedReleases2 = new HashMap<>(); + Map linkedReleases3 = new HashMap<>(); + Map linkedReleases4 = new HashMap<>(); Map linkedProjects2 = new HashMap<>(); Map linkedProjects3 = new HashMap<>(); Project project4 = new Project(); @@ -412,7 +408,6 @@ public void before() throws TException, IOException { project7.setClearingState(ProjectClearingState.OPEN); project7.setSecurityResponsibles(new HashSet<>(Arrays.asList("securityresponsible1@sw360.org", "securityresponsible2@sw360.org"))); - Map linkedReleases3 = new HashMap<>(); Set attachmentSet = new HashSet(); List obligationList = new ArrayList<>(); Set licenseIds2 = new HashSet<>(); @@ -492,6 +487,32 @@ public void before() throws TException, IOException { release5.setVersion("2.3.1"); release5.setCreatedOn("2016-12-28"); + Project project9 = new Project(); + project9.setId("0000007"); + project9.setName("attachUsages"); + project9.setVersion("2"); + project9.setCreatedBy(testUserId); + project9.setProjectType(ProjectType.PRODUCT); + project9.setState(ProjectState.ACTIVE); + project9.setClearingState(ProjectClearingState.OPEN); + linkedReleases4.put("00000071", projectReleaseRelationship); + project9.setReleaseIdToUsage(linkedReleases4); + + Release release9 = new Release(); + release9.setId("00000071"); + release9.setName("docker"); + release9.setCpeid("cpe:/a:Google:Angular:2.3.1:"); + release9.setReleaseDate("2024-03-17"); + release9.setVersion("2"); + release9.setCreatedOn("2024-03-28"); + release9.setAttachments(setOfAttachment); + + List attachmentUsageNewList = new ArrayList<>(); + List deselectedUsagesFromRequest = new ArrayList<>(); + List selectedUsagesFromRequest = new ArrayList<>(); + List selectedUsages = Arrays.asList("00000071_sourcePackage_1234"); + + Set releaseIds2 = new HashSet<>(Collections.singletonList("00000071")); Set releaseIds = new HashSet<>(Collections.singletonList("3765276512")); Set releaseIdsTransitive = new HashSet<>(Arrays.asList("3765276512", "5578999")); @@ -569,6 +590,12 @@ public void before() throws TException, IOException { given(this.projectServiceMock.addLinkedObligations(any(), any(), eq(obligationStatusMap))).willReturn(RequestStatus.SUCCESS); given(this.projectServiceMock.compareObligationStatusMap(any(), any(), any())).willReturn(obligationStatusMap); given(this.projectServiceMock.patchLinkedObligations(any(), any(), any())).willReturn(RequestStatus.SUCCESS); + given(this.projectServiceMock.updateLinkedObligations(any(), any(), eq(obligationStatusMap))).willReturn(RequestStatus.SUCCESS); + given(this.projectServiceMock.getProjectForUserById(eq(project9.getId()), any())).willReturn(project9); + given(this.projectServiceMock.getUsedAttachments(any(), any())).willReturn(attachmentUsageNewList); + given(this.projectServiceMock.validate(any(), any(), any(), any())).willReturn(true); + given(this.projectServiceMock.deselectedAttachmentUsagesFromRequest(any(), eq(selectedUsages), any(), any(), any())).willReturn(deselectedUsagesFromRequest); + given(this.projectServiceMock.selectedAttachmentUsagesFromRequest(any(), eq(selectedUsages), any(), any(), any())).willReturn(selectedUsagesFromRequest); given(this.projectServiceMock.getProjectForUserById(eq(projectForAtt.getId()), any())).willReturn(projectForAtt); given(this.projectServiceMock.getProjectForUserById(eq(SPDXProject.getId()), any())).willReturn(SPDXProject); given(this.projectServiceMock.getProjectForUserById(eq(cycloneDXProject.getId()), any())).willReturn(cycloneDXProject); @@ -579,6 +606,7 @@ public void before() throws TException, IOException { given(this.projectServiceMock.searchProjectByGroup(any(), any())).willReturn(new ArrayList(projectList)); given(this.projectServiceMock.refineSearch(any(), any())).willReturn(projectListByName); given(this.projectServiceMock.getReleaseIds(eq(project.getId()), any(), eq(false))).willReturn(releaseIds); + given(this.projectServiceMock.getReleaseIds(eq(project9.getId()), any(), eq(true))).willReturn(releaseIds2); given(this.projectServiceMock.getReleaseIds(eq(project.getId()), any(), eq(true))).willReturn(releaseIdsTransitive); given(this.projectServiceMock.deleteProject(eq(project.getId()), any())).willReturn(RequestStatus.SUCCESS); given(this.projectServiceMock.updateProjectReleaseRelationship(any(), any(), any())).willReturn(projectReleaseRelationshipResponseBody); @@ -1954,6 +1982,21 @@ public void should_document_link_projects() throws Exception { ))); } + @Test + public void should_document_save_usages() throws Exception { + MockHttpServletRequestBuilder requestBuilder = post("/api/projects/" + "0000007" + "/saveAttachmentUsages"); + Map> usages = Map.of( + "selected", new ArrayList<>(List.of("00000071_sourcePackage_1234")), + "deselected", new ArrayList<>(List.of()), + "selectedConcludedUsages", new ArrayList<>(List.of()), + "deselectedConcludedUsages", new ArrayList<>(List.of())); + + this.mockMvc.perform(requestBuilder.contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(usages)) + .header("Authorization", TestHelper.generateAuthHeader(testUserId, testUserPassword))) + .andExpect(status().isCreated()); + } + @Test public void should_document_upload_attachment_to_project() throws Exception { testAttachmentUpload("/api/projects/", project.getId());