From ca0be9e35a8ee4f175146467b955fe8c70f8c574 Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 13 Mar 2024 16:48:44 +0100 Subject: [PATCH] feat: implement move resource operation (#273)(#227) --- .../java/com/epam/aidial/core/AiDial.java | 4 +- src/main/java/com/epam/aidial/core/Proxy.java | 2 + .../core/controller/ControllerSelector.java | 8 + .../ResourceOperationController.java | 97 +++++++ .../core/data/MoveResourcesRequest.java | 14 + .../epam/aidial/core/data/SharedByMeDto.java | 5 + .../core/service/InvitationService.java | 21 ++ .../service/ResourceOperationService.java | 64 +++++ .../aidial/core/service/ResourceService.java | 8 +- .../aidial/core/service/ShareService.java | 54 ++++ .../aidial/core/ResourceOperationApiTest.java | 264 ++++++++++++++++++ 11 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/epam/aidial/core/controller/ResourceOperationController.java create mode 100644 src/main/java/com/epam/aidial/core/data/MoveResourcesRequest.java create mode 100644 src/main/java/com/epam/aidial/core/service/ResourceOperationService.java create mode 100644 src/test/java/com/epam/aidial/core/ResourceOperationApiTest.java diff --git a/src/main/java/com/epam/aidial/core/AiDial.java b/src/main/java/com/epam/aidial/core/AiDial.java index 1aca0aaa9..f9bb71be6 100644 --- a/src/main/java/com/epam/aidial/core/AiDial.java +++ b/src/main/java/com/epam/aidial/core/AiDial.java @@ -14,6 +14,7 @@ import com.epam.aidial.core.service.InvitationService; import com.epam.aidial.core.service.LockService; import com.epam.aidial.core.service.PublicationService; +import com.epam.aidial.core.service.ResourceOperationService; import com.epam.aidial.core.service.ResourceService; import com.epam.aidial.core.service.ShareService; import com.epam.aidial.core.storage.BlobStorage; @@ -108,6 +109,7 @@ void start() throws Exception { InvitationService invitationService = new InvitationService(resourceService, encryptionService, settings("invitations")); ShareService shareService = new ShareService(resourceService, invitationService, encryptionService); PublicationService publicationService = new PublicationService(encryptionService, resourceService, storage, generator, clock); + ResourceOperationService resourceOperationService = new ResourceOperationService(resourceService, storage, invitationService, shareService); AccessService accessService = new AccessService(encryptionService, shareService, publicationService); RateLimiter rateLimiter = new RateLimiter(vertx, resourceService); @@ -115,7 +117,7 @@ void start() throws Exception { proxy = new Proxy(vertx, client, configStore, logStore, rateLimiter, upstreamBalancer, accessTokenValidator, storage, encryptionService, apiKeyStore, tokenStatsTracker, resourceService, invitationService, - shareService, publicationService, accessService, lockService); + shareService, publicationService, accessService, lockService, resourceOperationService); server = vertx.createHttpServer(new HttpServerOptions(settings("server"))).requestHandler(proxy); open(server, HttpServer::listen); diff --git a/src/main/java/com/epam/aidial/core/Proxy.java b/src/main/java/com/epam/aidial/core/Proxy.java index cf3e646a5..1719cb364 100644 --- a/src/main/java/com/epam/aidial/core/Proxy.java +++ b/src/main/java/com/epam/aidial/core/Proxy.java @@ -15,6 +15,7 @@ import com.epam.aidial.core.service.InvitationService; import com.epam.aidial.core.service.LockService; import com.epam.aidial.core.service.PublicationService; +import com.epam.aidial.core.service.ResourceOperationService; import com.epam.aidial.core.service.ResourceService; import com.epam.aidial.core.service.ShareService; import com.epam.aidial.core.storage.BlobStorage; @@ -80,6 +81,7 @@ public class Proxy implements Handler { private final PublicationService publicationService; private final AccessService accessService; private final LockService lockService; + private final ResourceOperationService resourceOperationService; @Override public void handle(HttpServerRequest request) { diff --git a/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java b/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java index 926852e9d..ae6bf3b72 100644 --- a/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java +++ b/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java @@ -50,6 +50,8 @@ public class ControllerSelector { private static final Pattern INVITATION = Pattern.compile("^/v1/invitations/([a-zA-Z0-9]+)$"); private static final Pattern PUBLICATIONS = Pattern.compile("^/v1/ops/publications/(list|get|create|delete)$"); + private static final Pattern RESOURCE_OPERATIONS = Pattern.compile("^/v1/ops/resources/(move)$"); + private static final Pattern DEPLOYMENT_LIMITS = Pattern.compile("^/v1/deployments/([^/]+)/limits$"); public Controller select(Proxy proxy, ProxyContext context) { @@ -272,6 +274,12 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p }; } + match = match(RESOURCE_OPERATIONS, path); + if (match != null) { + ResourceOperationController controller = new ResourceOperationController(proxy, context); + return controller::move; + } + return null; } diff --git a/src/main/java/com/epam/aidial/core/controller/ResourceOperationController.java b/src/main/java/com/epam/aidial/core/controller/ResourceOperationController.java new file mode 100644 index 000000000..985fa2a77 --- /dev/null +++ b/src/main/java/com/epam/aidial/core/controller/ResourceOperationController.java @@ -0,0 +1,97 @@ +package com.epam.aidial.core.controller; + +import com.epam.aidial.core.Proxy; +import com.epam.aidial.core.ProxyContext; +import com.epam.aidial.core.data.MoveResourcesRequest; +import com.epam.aidial.core.security.EncryptionService; +import com.epam.aidial.core.service.LockService; +import com.epam.aidial.core.service.ResourceOperationService; +import com.epam.aidial.core.storage.BlobStorageUtil; +import com.epam.aidial.core.storage.ResourceDescription; +import com.epam.aidial.core.util.HttpException; +import com.epam.aidial.core.util.HttpStatus; +import com.epam.aidial.core.util.ProxyUtil; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ResourceOperationController { + + private final ProxyContext context; + private final Proxy proxy; + private final Vertx vertx; + private final EncryptionService encryptionService; + private final ResourceOperationService resourceOperationService; + private final LockService lockService; + + public ResourceOperationController(Proxy proxy, ProxyContext context) { + this.context = context; + this.proxy = proxy; + this.vertx = proxy.getVertx(); + this.encryptionService = proxy.getEncryptionService(); + this.resourceOperationService = proxy.getResourceOperationService(); + this.lockService = proxy.getLockService(); + } + + public Future move() { + context.getRequest() + .body() + .compose(buffer -> { + MoveResourcesRequest request; + try { + request = ProxyUtil.convertToObject(buffer, MoveResourcesRequest.class); + } catch (Exception e) { + log.error("Invalid request body provided", e); + throw new IllegalArgumentException("Can't initiate move resource request. Incorrect body provided"); + } + + String sourceUrl = request.getSourceUrl(); + if (sourceUrl == null) { + throw new IllegalArgumentException("sourceUrl must be provided"); + } + + String destinationUrl = request.getDestinationUrl(); + if (destinationUrl == null) { + throw new IllegalArgumentException("destinationUrl must be provided"); + } + + String bucketLocation = BlobStorageUtil.buildInitiatorBucket(context); + String bucket = encryptionService.encrypt(bucketLocation); + + ResourceDescription sourceResource = ResourceDescription.fromLink(sourceUrl, encryptionService); + if (!sourceResource.getBucketName().equals(bucket)) { + throw new IllegalArgumentException("sourceUrl do not belong to the user"); + } + + ResourceDescription destinationResource = ResourceDescription.fromLink(destinationUrl, encryptionService); + if (!destinationResource.getBucketName().equals(bucket)) { + throw new IllegalArgumentException("destinationUrl do not belong to the user"); + } + + if (!sourceResource.getType().equals(destinationResource.getType())) { + throw new IllegalArgumentException("source and destination resources must be the same type"); + } + + + return vertx.executeBlocking(() -> lockService.underBucketLock(proxy, bucketLocation, () -> { + resourceOperationService.moveResource(bucket, bucketLocation, sourceResource, destinationResource, request.isOverwrite()); + return null; + })); + }) + .onSuccess(ignore -> context.respond(HttpStatus.OK)) + .onFailure(this::handleServiceError); + + return Future.succeededFuture(); + } + + private void handleServiceError(Throwable error) { + if (error instanceof IllegalArgumentException) { + context.respond(HttpStatus.BAD_REQUEST, error.getMessage()); + } else if (error instanceof HttpException httpException) { + context.respond(httpException.getStatus(), httpException.getMessage()); + } else { + context.respond(HttpStatus.INTERNAL_SERVER_ERROR, error.getMessage()); + } + } +} diff --git a/src/main/java/com/epam/aidial/core/data/MoveResourcesRequest.java b/src/main/java/com/epam/aidial/core/data/MoveResourcesRequest.java new file mode 100644 index 000000000..6e6710658 --- /dev/null +++ b/src/main/java/com/epam/aidial/core/data/MoveResourcesRequest.java @@ -0,0 +1,14 @@ +package com.epam.aidial.core.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class MoveResourcesRequest { + String sourceUrl; + String destinationUrl; + boolean overwrite; +} diff --git a/src/main/java/com/epam/aidial/core/data/SharedByMeDto.java b/src/main/java/com/epam/aidial/core/data/SharedByMeDto.java index cb4758a57..09fba5532 100644 --- a/src/main/java/com/epam/aidial/core/data/SharedByMeDto.java +++ b/src/main/java/com/epam/aidial/core/data/SharedByMeDto.java @@ -18,4 +18,9 @@ public void addUserToResource(String url, String userLocation) { Set users = resourceToUsers.computeIfAbsent(url, k -> new HashSet<>()); users.add(userLocation); } + + public void addUsersToResource(String url, Set userLocations) { + Set users = resourceToUsers.computeIfAbsent(url, k -> new HashSet<>()); + users.addAll(userLocations); + } } diff --git a/src/main/java/com/epam/aidial/core/service/InvitationService.java b/src/main/java/com/epam/aidial/core/service/InvitationService.java index a114cf628..a4da22f64 100644 --- a/src/main/java/com/epam/aidial/core/service/InvitationService.java +++ b/src/main/java/com/epam/aidial/core/service/InvitationService.java @@ -150,6 +150,27 @@ public void cleanUpResourceLinks(String bucket, String location, Set { + InvitationsMap invitations = ProxyUtil.convertToObject(state, InvitationsMap.class); + if (invitations == null) { + return null; + } + Map invitationMap = invitations.getInvitations(); + for (Invitation invitation : invitationMap.values()) { + Set invitationResourceLinks = invitation.getResources(); + if (invitationResourceLinks.remove(sourceLink)) { + invitationResourceLinks.add(destinationLink); + } + } + + return ProxyUtil.convertToString(invitations); + }); + } + private void cleanUpExpiredInvitations(ResourceDescription resource, Collection idsToEvict) { resourceService.computeResource(resource, state -> { InvitationsMap invitations = ProxyUtil.convertToObject(state, InvitationsMap.class); diff --git a/src/main/java/com/epam/aidial/core/service/ResourceOperationService.java b/src/main/java/com/epam/aidial/core/service/ResourceOperationService.java new file mode 100644 index 000000000..4f0c16b7a --- /dev/null +++ b/src/main/java/com/epam/aidial/core/service/ResourceOperationService.java @@ -0,0 +1,64 @@ +package com.epam.aidial.core.service; + +import com.epam.aidial.core.data.ResourceType; +import com.epam.aidial.core.storage.BlobStorage; +import com.epam.aidial.core.storage.ResourceDescription; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class ResourceOperationService { + + private final ResourceService resourceService; + private final BlobStorage storage; + private final InvitationService invitationService; + private final ShareService shareService; + + public void moveResource(String bucket, String location, ResourceDescription source, ResourceDescription destination, boolean overwriteIfExists) { + if (source.isFolder() || destination.isFolder()) { + throw new IllegalArgumentException("Moving folders is not supported"); + } + + String sourceResourcePath = source.getAbsoluteFilePath(); + String sourceResourceUrl = source.getUrl(); + String destinationResourcePath = destination.getAbsoluteFilePath(); + String destinationResourceUrl = destination.getUrl(); + + if (!hasResource(source)) { + throw new IllegalArgumentException("Source resource %s do not exists".formatted(sourceResourceUrl)); + } + + ResourceType resourceType = source.getType(); + switch (resourceType) { + case FILE -> { + if (!overwriteIfExists && storage.exists(destinationResourcePath)) { + throw new IllegalArgumentException("Can't move resource %s to %s, because destination resource already exists" + .formatted(sourceResourceUrl, destinationResourceUrl)); + } + storage.copy(sourceResourcePath, destinationResourcePath); + storage.delete(sourceResourcePath); + } + case CONVERSATION, PROMPT -> { + boolean copied = resourceService.copyResource(source, destination, overwriteIfExists); + if (!copied) { + throw new IllegalArgumentException("Can't move resource %s to %s, because destination resource already exists" + .formatted(sourceResourceUrl, destinationResourceUrl)); + } + resourceService.deleteResource(source); + } + default -> throw new IllegalArgumentException("Unsupported resource type " + resourceType); + } + // move source links to destination if any + invitationService.moveResource(bucket, location, source, destination); + // move shared access if any + shareService.moveSharedAccess(bucket, location, source, destination); + } + + private boolean hasResource(ResourceDescription resource) { + return switch (resource.getType()) { + case FILE -> storage.exists(resource.getAbsoluteFilePath()); + case CONVERSATION, PROMPT -> resourceService.hasResource(resource); + default -> throw new IllegalArgumentException("Unsupported resource type " + resource.getType()); + }; + } + +} diff --git a/src/main/java/com/epam/aidial/core/service/ResourceService.java b/src/main/java/com/epam/aidial/core/service/ResourceService.java index 7d3cfe617..c548e361c 100644 --- a/src/main/java/com/epam/aidial/core/service/ResourceService.java +++ b/src/main/java/com/epam/aidial/core/service/ResourceService.java @@ -278,14 +278,18 @@ public boolean deleteResource(ResourceDescription descriptor) { } public boolean copyResource(ResourceDescription from, ResourceDescription to) { + return copyResource(from, to, true); + } + + public boolean copyResource(ResourceDescription from, ResourceDescription to, boolean overwrite) { String body = getResource(from); if (body == null) { return false; } - putResource(to, body, true); - return true; + ResourceItemMetadata metadata = putResource(to, body, overwrite); + return metadata != null; } private Void sync() { diff --git a/src/main/java/com/epam/aidial/core/service/ShareService.java b/src/main/java/com/epam/aidial/core/service/ShareService.java index 33621ad6d..ae0bd4749 100644 --- a/src/main/java/com/epam/aidial/core/service/ShareService.java +++ b/src/main/java/com/epam/aidial/core/service/ShareService.java @@ -303,6 +303,48 @@ public void discardSharedAccess(String bucket, String location, ResourceLinkColl } } + public void copySharedAccess(String bucket, String location, ResourceDescription source, ResourceDescription destination) { + ResourceType sourceResourceType = source.getType(); + ResourceDescription sharedByMeResource = getShareResource(ResourceType.SHARED_BY_ME, sourceResourceType, bucket, location); + SharedByMeDto sharedByMeDto = ProxyUtil.convertToObject(resourceService.getResource(sharedByMeResource), SharedByMeDto.class); + if (sharedByMeDto == null) { + return; + } + + Set userLocations = sharedByMeDto.getResourceToUsers().get(source.getUrl()); + + ResourceType destinationResourceType = destination.getType(); + String destinationResourceLink = destination.getUrl(); + // source and destination resource type might be different + sharedByMeResource = getShareResource(ResourceType.SHARED_BY_ME, destinationResourceType, bucket, location); + + // copy user locations from source to destination + resourceService.computeResource(sharedByMeResource, state -> { + SharedByMeDto dto = ProxyUtil.convertToObject(state, SharedByMeDto.class); + if (dto == null) { + dto = new SharedByMeDto(new HashMap<>()); + } + + // add shared access to the destination resource + dto.addUsersToResource(destinationResourceLink, userLocations); + + return ProxyUtil.convertToString(dto); + }); + + // add each user shared access to the destination resource + for (String userLocation : userLocations) { + String userBucket = encryptionService.encrypt(userLocation); + addSharedResource(userBucket, userLocation, destinationResourceLink, destinationResourceType); + } + } + + public void moveSharedAccess(String bucket, String location, ResourceDescription source, ResourceDescription destination) { + // copy shared access from source to destination + copySharedAccess(bucket, location, source, destination); + // revoke shared access from source + revokeSharedAccess(bucket, location, new ResourceLinkCollection(Set.of(new ResourceLink(source.getUrl())))); + } + private void removeSharedResource(String bucket, String location, String link, ResourceType resourceType) { ResourceDescription sharedByMeResource = getShareResource(ResourceType.SHARED_WITH_ME, resourceType, bucket, location); resourceService.computeResource(sharedByMeResource, state -> { @@ -315,6 +357,18 @@ private void removeSharedResource(String bucket, String location, String link, R }); } + private void addSharedResource(String bucket, String location, String link, ResourceType resourceType) { + ResourceDescription sharedByMeResource = getShareResource(ResourceType.SHARED_WITH_ME, resourceType, bucket, location); + resourceService.computeResource(sharedByMeResource, state -> { + ResourceLinkCollection sharedWithMe = ProxyUtil.convertToObject(state, ResourceLinkCollection.class); + if (sharedWithMe != null) { + sharedWithMe.getResources().add(new ResourceLink(link)); + } + + return ProxyUtil.convertToString(sharedWithMe); + }); + } + private List linksToMetadata(Stream links) { return links .map(link -> ResourceDescription.fromLink(link, encryptionService)) diff --git a/src/test/java/com/epam/aidial/core/ResourceOperationApiTest.java b/src/test/java/com/epam/aidial/core/ResourceOperationApiTest.java new file mode 100644 index 000000000..80b5de229 --- /dev/null +++ b/src/test/java/com/epam/aidial/core/ResourceOperationApiTest.java @@ -0,0 +1,264 @@ +package com.epam.aidial.core; + +import com.epam.aidial.core.data.InvitationLink; +import com.epam.aidial.core.util.ProxyUtil; +import io.vertx.core.http.HttpMethod; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ResourceOperationApiTest extends ResourceBaseTest { + + @Test + void testMoveResourceWorkflow() { + // upload resource + Response response = resourceRequest(HttpMethod.PUT, "/folder/conversation", CONVERSATION_BODY_1); + verifyNotExact(response, 200, "\"url\":\"conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation\""); + + // verify resource can be downloaded + response = resourceRequest(HttpMethod.GET, "/folder/conversation"); + verifyJson(response, 200, CONVERSATION_BODY_1); + + // verify move operation + response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "sourceUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation", + "destinationUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2" + } + """); + verify(response, 200); + + // verify old resource deleted + response = resourceRequest(HttpMethod.GET, "/folder/conversation"); + verify(response, 404); + + // verify new resource can be downloaded + response = resourceRequest(HttpMethod.GET, "/folder2/conversation2"); + verifyJson(response, 200, CONVERSATION_BODY_1); + } + + @Test + void testMoveResourceWorkflowWhenDestinationResourceExists() { + // upload resource + Response response = resourceRequest(HttpMethod.PUT, "/folder/conversation", CONVERSATION_BODY_1); + verifyNotExact(response, 200, "\"url\":\"conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation\""); + + response = resourceRequest(HttpMethod.PUT, "/folder2/conversation2", CONVERSATION_BODY_2); + verifyNotExact(response, 200, "\"url\":\"conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2\""); + + // verify resource can be downloaded + response = resourceRequest(HttpMethod.GET, "/folder/conversation"); + verifyJson(response, 200, CONVERSATION_BODY_1); + + response = resourceRequest(HttpMethod.GET, "/folder2/conversation2"); + verifyJson(response, 200, CONVERSATION_BODY_2); + + // verify move operation + response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "sourceUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation", + "destinationUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2" + } + """); + verify(response, 400); + + // verify resource can be downloaded + response = resourceRequest(HttpMethod.GET, "/folder/conversation"); + verifyJson(response, 200, CONVERSATION_BODY_1); + + response = resourceRequest(HttpMethod.GET, "/folder2/conversation2"); + verifyJson(response, 200, CONVERSATION_BODY_2); + + response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "sourceUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation", + "destinationUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2", + "overwrite": true + } + """); + verify(response, 200); + + // verify old resource deleted + response = resourceRequest(HttpMethod.GET, "/folder/conversation"); + verify(response, 404); + + // verify new resource can be downloaded + response = resourceRequest(HttpMethod.GET, "/folder2/conversation2"); + verifyJson(response, 200, CONVERSATION_BODY_1); + } + + @Test + void testMoveOperationCopySharedAccess() { + Response response = resourceRequest(HttpMethod.PUT, "/folder/conversation", CONVERSATION_BODY_1); + verifyNotExact(response, 200, "\"url\":\"conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation\""); + + // initialize share request + response = operationRequest("/v1/ops/resource/share/create", """ + { + "invitationType": "link", + "resources": [ + { + "url": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation" + } + ] + } + """); + verify(response, 200); + InvitationLink invitationLink = ProxyUtil.convertToObject(response.body(), InvitationLink.class); + assertNotNull(invitationLink); + + // verify invitation details + response = send(HttpMethod.GET, invitationLink.invitationLink(), null, null); + verifyNotExact(response, 200, "\"url\":\"conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation\""); + + // verify user2 do not have access to the conversation + response = resourceRequest(HttpMethod.GET, "/folder/conversation", null, "Api-key", "proxyKey2"); + verify(response, 403); + + // accept invitation + response = send(HttpMethod.GET, invitationLink.invitationLink(), "accept=true", null, "Api-key", "proxyKey2"); + verify(response, 200); + + // verify user2 has access to the conversation + response = resourceRequest(HttpMethod.GET, "/folder/conversation", null, "Api-key", "proxyKey2"); + verify(response, 200, CONVERSATION_BODY_1); + + // verify move operation + response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "sourceUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation", + "destinationUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2" + } + """); + verify(response, 200); + + // verify user2 has access to the moved conversation + response = resourceRequest(HttpMethod.GET, "/folder2/conversation2", null, "Api-key", "proxyKey2"); + verify(response, 200, CONVERSATION_BODY_1); + + // verify invitation contains moved conversation + response = send(HttpMethod.GET, invitationLink.invitationLink(), null, null); + verifyNotExact(response, 200, "\"url\":\"conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2\""); + + // verify user1 has no shared_with_me resources + response = operationRequest("/v1/ops/resource/share/list", """ + { + "resourceTypes": ["CONVERSATION"], + "with": "me" + } + """); + verifyJson(response, 200, """ + { + "resources": [] + } + """); + + // verify user2 has shared_with_me resource + response = operationRequest("/v1/ops/resource/share/list", """ + { + "resourceTypes": ["CONVERSATION"], + "with": "me" + } + """, "Api-key", "proxyKey2"); + verifyJson(response, 200, """ + { + "resources" : [ { + "name" : "conversation2", + "parentPath" : "folder2", + "bucket" : "3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST", + "url" : "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2", + "nodeType" : "ITEM", + "resourceType" : "CONVERSATION" + } ] + } + """); + + // verify user1 has shared_by_me resource + response = operationRequest("/v1/ops/resource/share/list", """ + { + "resourceTypes": ["CONVERSATION"], + "with": "others" + } + """); + verifyJson(response, 200, """ + { + "resources" : [ { + "name" : "conversation2", + "parentPath" : "folder2", + "bucket" : "3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST", + "url" : "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2", + "nodeType" : "ITEM", + "resourceType" : "CONVERSATION" + } ] + } + """); + + // verify user2 has no shared_by_me resources + response = operationRequest("/v1/ops/resource/share/list", """ + { + "resourceTypes": ["CONVERSATION"], + "with": "others" + } + """, "Api-key", "proxyKey2"); + verifyJson(response, 200, """ + { + "resources": [] + } + """); + } + + @Test + void testMoveOperationErrors() { + // verify sourceUrl must be present + Response response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "destinationUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2" + } + """); + verify(response, 400); + + // verify destinationUrl must be present + response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "sourceUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation" + } + """); + verify(response, 400); + + // verify source and dest must be the same type + response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "sourceUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation", + "destinationUrl": "files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2" + } + """); + verify(response, 400); + + // verify source must belong to the user + response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "sourceUrl": "conversations/7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/folder/conversation", + "destinationUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2" + } + """); + verify(response, 400); + + // verify move do not support folders + response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "sourceUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/", + "destinationUrl": "files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2" + } + """); + verify(response, 400); + + // verify sourceUrl do not exists + response = send(HttpMethod.POST, "/v1/ops/resources/move", null, """ + { + "sourceUrl": "conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation", + "destinationUrl": "files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder2/conversation2" + } + """); + verify(response, 400); + } +}