diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/PublicIds.java b/server/src/main/java/org/eclipse/openvsx/adapter/PublicIds.java new file mode 100644 index 000000000..cb25d857d --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/adapter/PublicIds.java @@ -0,0 +1,13 @@ +/** ****************************************************************************** + * Copyright (c) 2024 Precies. Software Ltd and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * ****************************************************************************** */ +package org.eclipse.openvsx.adapter; + +public record PublicIds(String namespace, String extension) { +} diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java index 1805cbd56..72a8362d8 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdService.java @@ -31,6 +31,7 @@ import org.springframework.web.client.RestTemplate; import java.time.ZoneId; +import java.util.UUID; @Component public class VSCodeIdService { @@ -66,18 +67,24 @@ public void applicationStarted(ApplicationStartedEvent event) { scheduler.scheduleRecurrently("VSCodeIdDailyUpdate", Cron.daily(3), ZoneId.of("UTC"), new HandlerJobRequest<>(VSCodeIdDailyUpdateJobRequestHandler.class)); } - public void getUpstreamPublicIds(Extension extension) { - extension.setPublicId(null); - extension.getNamespace().setPublicId(null); + public String getRandomPublicId() { + return UUID.randomUUID().toString(); + } + + public PublicIds getUpstreamPublicIds(Extension extension) { + String extensionPublicId = null; + String namespacePublicId = null; var upstream = getUpstreamExtension(extension); if (upstream != null) { if (upstream.extensionId != null) { - extension.setPublicId(upstream.extensionId); + extensionPublicId = upstream.extensionId; } if (upstream.publisher != null && upstream.publisher.publisherId != null) { - extension.getNamespace().setPublicId(upstream.publisher.publisherId); + namespacePublicId = upstream.publisher.publisherId; } } + + return new PublicIds(namespacePublicId, extensionPublicId); } private ExtensionQueryResult.Extension getUpstreamExtension(Extension extension) { diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateService.java b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateService.java index 04433d16f..b4237a0c4 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateService.java @@ -20,14 +20,11 @@ import org.springframework.stereotype.Component; import java.util.*; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Component public class VSCodeIdUpdateService { private static final Logger LOGGER = LoggerFactory.getLogger(VSCodeIdUpdateService.class); - private static final Semaphore LOCK = new Semaphore(1); @Autowired RepositoryService repositories; @@ -36,10 +33,6 @@ public class VSCodeIdUpdateService { VSCodeIdService service; public void update(String namespaceName, String extensionName) throws InterruptedException { - var acquired = LOCK.tryAcquire(15, TimeUnit.SECONDS); - if(!acquired) { - throw new RuntimeException("Failed to update public id for " + NamingUtil.toExtensionId(namespaceName, extensionName)); - } if(BuiltInExtensionUtil.isBuiltIn(namespaceName)) { LOGGER.debug("SKIP BUILT-IN EXTENSION {}", NamingUtil.toExtensionId(namespaceName, extensionName)); return; @@ -47,64 +40,62 @@ public void update(String namespaceName, String extensionName) throws Interrupte var extension = repositories.findPublicId(namespaceName, extensionName); var extensionUpdates = new HashMap(); - updateExtensionPublicId(extension, extensionUpdates); + updateExtensionPublicId(extension, extensionUpdates, false); if(!extensionUpdates.isEmpty()) { repositories.updateExtensionPublicIds(extensionUpdates); } var namespaceUpdates = new HashMap(); - updateNamespacePublicId(extension, namespaceUpdates); + updateNamespacePublicId(extension, namespaceUpdates, false); if(!namespaceUpdates.isEmpty()) { repositories.updateNamespacePublicIds(namespaceUpdates); } - LOCK.release(); } - private void updateExtensionPublicId(Extension extension, Map updates) { + private void updateExtensionPublicId(Extension extension, Map updates, boolean mustUpdate) { LOGGER.debug("updateExtensionPublicId: {}", NamingUtil.toExtensionId(extension)); - service.getUpstreamPublicIds(extension); - if(extension.getPublicId() == null) { - var publicId = ""; + var oldPublicId = extension.getPublicId(); + var newPublicId = service.getUpstreamPublicIds(extension).extension(); + if(newPublicId == null || (mustUpdate && newPublicId.equals(oldPublicId))) { do { - publicId = UUID.randomUUID().toString(); - LOGGER.debug("RANDOM EXTENSION PUBLIC ID: {}", publicId); - } while(updates.containsValue(publicId) || repositories.extensionPublicIdExists(publicId)); - LOGGER.debug("RANDOM PUT UPDATE: {} - {}", extension.getId(), publicId); - updates.put(extension.getId(), publicId); - } else { - LOGGER.debug("UPSTREAM PUT UPDATE: {} - {}", extension.getId(), extension.getPublicId()); - updates.put(extension.getId(), extension.getPublicId()); - var duplicatePublicId = repositories.findPublicId(extension.getPublicId()); + newPublicId = service.getRandomPublicId(); + LOGGER.debug("RANDOM EXTENSION PUBLIC ID: {}", newPublicId); + } while(updates.containsValue(newPublicId) || repositories.extensionPublicIdExists(newPublicId)); + LOGGER.debug("RANDOM PUT UPDATE: {} - {}", extension.getId(), newPublicId); + updates.put(extension.getId(), newPublicId); + } else if (!newPublicId.equals(oldPublicId)) { + LOGGER.debug("UPSTREAM PUT UPDATE: {} - {}", extension.getId(), newPublicId); + updates.put(extension.getId(), newPublicId); + var duplicatePublicId = repositories.findPublicId(newPublicId); if(duplicatePublicId != null) { - updateExtensionPublicId(duplicatePublicId, updates); + updateExtensionPublicId(duplicatePublicId, updates, true); } } } - private void updateNamespacePublicId(Extension extension, Map updates) { + private void updateNamespacePublicId(Extension extension, Map updates, boolean mustUpdate) { LOGGER.debug("updateNamespacePublicId: {}", extension.getNamespace().getName()); - service.getUpstreamPublicIds(extension); - var namespace = extension.getNamespace(); - if(namespace.getPublicId() == null) { - var publicId = ""; + var oldPublicId = extension.getNamespace().getPublicId(); + var newPublicId = service.getUpstreamPublicIds(extension).namespace(); + var id = extension.getNamespace().getId(); + if(newPublicId == null || (mustUpdate && newPublicId.equals(oldPublicId))) { do { - publicId = UUID.randomUUID().toString(); - LOGGER.debug("RANDOM NAMESPACE PUBLIC ID: {}", publicId); - } while(updates.containsValue(publicId) || repositories.namespacePublicIdExists(publicId)); - LOGGER.debug("RANDOM PUT UPDATE: {} - {}", namespace.getId(), publicId); - updates.put(namespace.getId(), publicId); - } else { - LOGGER.debug("UPSTREAM PUT UPDATE: {} - {}", namespace.getId(), namespace.getPublicId()); - updates.put(namespace.getId(), namespace.getPublicId()); - var duplicatePublicId = repositories.findNamespacePublicId(namespace.getPublicId()); + newPublicId = service.getRandomPublicId(); + LOGGER.debug("RANDOM NAMESPACE PUBLIC ID: {}", newPublicId); + } while(updates.containsValue(newPublicId) || repositories.namespacePublicIdExists(newPublicId)); + LOGGER.debug("RANDOM PUT UPDATE: {} - {}", id, newPublicId); + updates.put(id, newPublicId); + } else if(!newPublicId.equals(oldPublicId)) { + LOGGER.debug("UPSTREAM PUT UPDATE: {} - {}", id, newPublicId); + updates.put(id, newPublicId); + var duplicatePublicId = repositories.findNamespacePublicId(newPublicId); if(duplicatePublicId != null) { - updateNamespacePublicId(duplicatePublicId, updates); + updateNamespacePublicId(duplicatePublicId, updates, true); } } } public void updateAll() throws InterruptedException { - LOCK.acquire(); LOGGER.debug("DAILY UPDATE ALL"); var extensions = repositories.findAllPublicIds(); var extensionPublicIdsMap = extensions.stream() @@ -124,16 +115,16 @@ public void updateAll() throws InterruptedException { } LOGGER.trace("GET UPSTREAM PUBLIC ID: {} | {}", extension.getId(), NamingUtil.toExtensionId(extension)); - service.getUpstreamPublicIds(extension); + var publicIds = service.getUpstreamPublicIds(extension); if(upstreamExtensionPublicIds.get(extension.getId()) == null) { - LOGGER.trace("ADD EXTENSION PUBLIC ID: {} - {}", extension.getId(), extension.getPublicId()); - upstreamExtensionPublicIds.put(extension.getId(), extension.getPublicId()); + LOGGER.trace("ADD EXTENSION PUBLIC ID: {} - {}", extension.getId(), publicIds.extension()); + upstreamExtensionPublicIds.put(extension.getId(), publicIds.extension()); } var namespace = extension.getNamespace(); if(upstreamNamespacePublicIds.get(namespace.getId()) == null) { - LOGGER.trace("ADD NAMESPACE PUBLIC ID: {} - {}", namespace.getId(), namespace.getPublicId()); - upstreamNamespacePublicIds.put(namespace.getId(), namespace.getPublicId()); + LOGGER.trace("ADD NAMESPACE PUBLIC ID: {} - {}", namespace.getId(), publicIds.namespace()); + upstreamNamespacePublicIds.put(namespace.getId(), publicIds.namespace()); } } @@ -160,8 +151,6 @@ public void updateAll() throws InterruptedException { repositories.updateNamespacePublicIds(changedNamespacePublicIds); } - - LOCK.release(); } private Map getChangedPublicIds(Map upstreamPublicIds, Map currentPublicIds) { @@ -190,7 +179,7 @@ private void updatePublicIdNulls(Map changedPublicIds, Set return remove; }); - // put random UUIDs where upstream public id is missing + // put random public ids where upstream public id is missing for(var key : changedPublicIds.keySet()) { if(changedPublicIds.get(key) != null) { continue; @@ -198,7 +187,7 @@ private void updatePublicIdNulls(Map changedPublicIds, Set String publicId = null; while(newPublicIds.contains(publicId)) { - publicId = UUID.randomUUID().toString(); + publicId = service.getRandomPublicId(); LOGGER.debug("NEW PUBLIC ID - {}: '{}'", key, publicId); } diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java index 9b42b9293..cca986ec0 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeIdUpdateServiceTest.java @@ -21,14 +21,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.test.context.junit.jupiter.SpringExtension; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; - -import static org.junit.jupiter.api.Assertions.assertThrows; @ExtendWith(SpringExtension.class) public class VSCodeIdUpdateServiceTest { @@ -93,6 +88,11 @@ public void testUpdateAllNoChanges() throws InterruptedException { extension3.setNamespace(namespace3); Mockito.when(repositories.findAllPublicIds()).thenReturn(List.of(extension1, extension2, extension3)); + Mockito.doAnswer(invocation -> { + var extension = invocation.getArgument(0, Extension.class); + return new PublicIds(extension.getNamespace().getPublicId(), extension.getPublicId()); + }).when(idService).getUpstreamPublicIds(Mockito.any(Extension.class)); + updateService.updateAll(); Mockito.verify(repositories, Mockito.never()).updateExtensionPublicIds(Mockito.anyMap()); Mockito.verify(repositories, Mockito.never()).updateNamespacePublicIds(Mockito.anyMap()); @@ -148,24 +148,10 @@ public void testUpdateAllRandomNoChanges() throws InterruptedException { extension3.setPublicId(extensionPublicId3); extension3.setNamespace(namespace3); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(null); - ext.getNamespace().setPublicId(null); - return null; - }).when(idService).getUpstreamPublicIds(extension1); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(null); - ext.getNamespace().setPublicId(null); - return null; - }).when(idService).getUpstreamPublicIds(extension2); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(null); - ext.getNamespace().setPublicId(null); - return null; - }).when(idService).getUpstreamPublicIds(extension3); + var upstreamPublicIds = new PublicIds(null, null); + Mockito.when(idService.getUpstreamPublicIds(extension1)).thenReturn(upstreamPublicIds); + Mockito.when(idService.getUpstreamPublicIds(extension2)).thenReturn(upstreamPublicIds); + Mockito.when(idService.getUpstreamPublicIds(extension3)).thenReturn(upstreamPublicIds); Mockito.when(repositories.findAllPublicIds()).thenReturn(List.of(extension1, extension2, extension3)); updateService.updateAll(); @@ -223,49 +209,32 @@ public void testUpdateAllChange() throws InterruptedException { extension3.setPublicId(extensionPublicId3); extension3.setNamespace(namespace3); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(null); - ext.getNamespace().setPublicId(null); - return null; - }).when(idService).getUpstreamPublicIds(extension1); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(extensionPublicId3); - ext.getNamespace().setPublicId(namespacePublicId3); - return null; - }).when(idService).getUpstreamPublicIds(extension2); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(null); - ext.getNamespace().setPublicId(null); - return null; - }).when(idService).getUpstreamPublicIds(extension3); + Mockito.when(idService.getUpstreamPublicIds(extension1)).thenReturn(new PublicIds(null, null)); + Mockito.when(idService.getUpstreamPublicIds(extension2)).thenReturn(new PublicIds(namespacePublicId3, extensionPublicId3)); + Mockito.when(idService.getUpstreamPublicIds(extension3)).thenReturn(new PublicIds(null, null)); Mockito.when(repositories.findAllPublicIds()).thenReturn(List.of(extension1, extension2, extension3)); - var extensionUuid = UUID.randomUUID(); - var namespaceUuid = UUID.randomUUID(); - var uuidMock = Mockito.mockStatic(UUID.class); - uuidMock.when(UUID::randomUUID).thenReturn(extensionUuid, namespaceUuid); + var extensionPublicId = UUID.randomUUID().toString(); + var namespacePublicId = UUID.randomUUID().toString(); + Mockito.when(idService.getRandomPublicId()).thenReturn(extensionPublicId, namespacePublicId); updateService.updateAll(); Mockito.verify(repositories, Mockito.times(1)).updateExtensionPublicIds(Map.of( extension2.getId(), extensionPublicId3, - extension3.getId(), extensionUuid.toString() + extension3.getId(), extensionPublicId )); Mockito.verify(repositories, Mockito.times(1)).updateNamespacePublicIds(Map.of( namespace2.getId(), namespacePublicId3, - namespace3.getId(), namespaceUuid.toString() + namespace3.getId(), namespacePublicId )); - uuidMock.close(); } @Test - public void testUpdateUpstream() throws InterruptedException { + public void testUpdateRandom() throws InterruptedException { var namespaceName = "foo"; - var namespacePublicId = "123-456-789"; + var namespacePublicId = UUID.randomUUID().toString(); var extensionName = "bar"; - var extensionPublicId = "abc-def-ghi"; + var extensionPublicId = UUID.randomUUID().toString(); var namespace = new Namespace(); namespace.setId(1L); @@ -277,12 +246,8 @@ public void testUpdateUpstream() throws InterruptedException { extension.setNamespace(namespace); Mockito.when(repositories.findPublicId(namespaceName, extensionName)).thenReturn(extension); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(extensionPublicId); - ext.getNamespace().setPublicId(namespacePublicId); - return null; - }).when(idService).getUpstreamPublicIds(extension); + Mockito.when(idService.getUpstreamPublicIds(extension)).thenReturn(new PublicIds(null, null)); + Mockito.when(idService.getRandomPublicId()).thenReturn(extensionPublicId, namespacePublicId); updateService.update(namespaceName, extensionName); Mockito.verify(repositories).updateExtensionPublicIds(Mockito.argThat((Map map) -> { @@ -294,11 +259,13 @@ public void testUpdateUpstream() throws InterruptedException { } @Test - public void testUpdateRandom() throws InterruptedException { + public void testUpdateRandomExistsDb() throws InterruptedException { var namespaceName = "foo"; - var namespaceUuid = UUID.randomUUID(); + var namespacePublicId1 = UUID.randomUUID().toString(); + var namespacePublicId2 = UUID.randomUUID().toString(); var extensionName = "bar"; - var extensionUuid = UUID.randomUUID(); + var extensionPublicId1 = UUID.randomUUID().toString(); + var extensionPublicId2 = UUID.randomUUID().toString(); var namespace = new Namespace(); namespace.setId(1L); @@ -310,56 +277,75 @@ public void testUpdateRandom() throws InterruptedException { extension.setNamespace(namespace); Mockito.when(repositories.findPublicId(namespaceName, extensionName)).thenReturn(extension); - var uuidMock = Mockito.mockStatic(UUID.class); - uuidMock.when(UUID::randomUUID).thenReturn(extensionUuid, namespaceUuid); + Mockito.when(idService.getUpstreamPublicIds(extension)).thenReturn(new PublicIds(null, null)); + Mockito.when(repositories.extensionPublicIdExists(extensionPublicId1)).thenReturn(true); + Mockito.when(repositories.namespacePublicIdExists(namespacePublicId1)).thenReturn(true); + Mockito.when(idService.getRandomPublicId()).thenReturn(extensionPublicId1, extensionPublicId2, namespacePublicId1, namespacePublicId2); updateService.update(namespaceName, extensionName); Mockito.verify(repositories).updateExtensionPublicIds(Mockito.argThat((Map map) -> { - return map.size() == 1 && map.get(extension.getId()).equals(extensionUuid.toString()); + return map.size() == 1 && map.get(extension.getId()).equals(extensionPublicId2); })); Mockito.verify(repositories).updateNamespacePublicIds(Mockito.argThat((Map map) -> { - return map.size() == 1 && map.get(namespace.getId()).equals(namespaceUuid.toString()); + return map.size() == 1 && map.get(namespace.getId()).equals(namespacePublicId2); })); - uuidMock.close(); } @Test - public void testUpdateRandomExists() throws InterruptedException { - var namespaceName = "foo"; - var namespaceUuid1 = UUID.randomUUID(); - var namespaceUuid2 = UUID.randomUUID(); - var extensionName = "bar"; - var extensionUuid1 = UUID.randomUUID(); - var extensionUuid2 = UUID.randomUUID(); + public void testMustUpdateRandom() throws InterruptedException { + var namespaceName1 = "foo"; + var namespacePublicId1 = UUID.randomUUID().toString(); + var extensionName1 = "bar"; + var extensionPublicId1 = UUID.randomUUID().toString(); - var namespace = new Namespace(); - namespace.setId(1L); - namespace.setName(namespaceName); + var namespace1 = new Namespace(); + namespace1.setId(1L); + namespace1.setName(namespaceName1); - var extension = new Extension(); - extension.setId(2L); - extension.setName(extensionName); - extension.setNamespace(namespace); + var extension1 = new Extension(); + extension1.setId(2L); + extension1.setName(extensionName1); + extension1.setNamespace(namespace1); - Mockito.when(repositories.findPublicId(namespaceName, extensionName)).thenReturn(extension); - Mockito.when(repositories.extensionPublicIdExists(extensionUuid1.toString())).thenReturn(true); - Mockito.when(repositories.namespacePublicIdExists(namespaceUuid1.toString())).thenReturn(true); - var uuidMock = Mockito.mockStatic(UUID.class); - uuidMock.when(UUID::randomUUID) - .thenReturn(extensionUuid1, extensionUuid2, namespaceUuid1, namespaceUuid2); + var namespaceName2 = "baz"; + var namespacePublicId2 = UUID.randomUUID().toString(); + var extensionName2 = "foobar"; + var extensionPublicId2 = UUID.randomUUID().toString(); - updateService.update(namespaceName, extensionName); + var namespace2 = new Namespace(); + namespace2.setId(3L); + namespace2.setName(namespaceName2); + namespace2.setPublicId(namespacePublicId1); + + var extension2 = new Extension(); + extension2.setId(4L); + extension2.setName(extensionName2); + extension2.setPublicId(extensionPublicId1); + extension2.setNamespace(namespace2); + + Mockito.when(repositories.findPublicId(namespaceName1, extensionName1)).thenReturn(extension1); + Mockito.when(repositories.findPublicId(extensionPublicId1)).thenReturn(extension2); + Mockito.when(repositories.findNamespacePublicId(namespacePublicId1)).thenReturn(extension2); + var upstreamPublicIds = new PublicIds(namespacePublicId1, extensionPublicId1); + Mockito.when(idService.getUpstreamPublicIds(extension1)).thenReturn(upstreamPublicIds); + Mockito.when(idService.getUpstreamPublicIds(extension2)).thenReturn(upstreamPublicIds); + Mockito.when(idService.getRandomPublicId()).thenReturn(extensionPublicId2, namespacePublicId2); + + updateService.update(namespaceName1, extensionName1); Mockito.verify(repositories).updateExtensionPublicIds(Mockito.argThat((Map map) -> { - return map.size() == 1 && map.get(extension.getId()).equals(extensionUuid2.toString()); + return map.size() == 2 + && map.get(extension1.getId()).equals(extensionPublicId1) + && map.get(extension2.getId()).equals(extensionPublicId2); })); Mockito.verify(repositories).updateNamespacePublicIds(Mockito.argThat((Map map) -> { - return map.size() == 1 && map.get(namespace.getId()).equals(namespaceUuid2.toString()); + return map.size() == 2 + && map.get(namespace1.getId()).equals(namespacePublicId1) + && map.get(namespace2.getId()).equals(namespacePublicId2); })); - uuidMock.close(); } @Test - public void testUpdateDuplicateUpstreamChanges() throws InterruptedException { + public void testMustUpdateRandomExists() throws InterruptedException { var namespaceName1 = "foo"; var namespacePublicId1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; @@ -375,9 +361,9 @@ public void testUpdateDuplicateUpstreamChanges() throws InterruptedException { extension1.setNamespace(namespace1); var namespaceName2 = "baz"; - var namespaceUuid2 = UUID.randomUUID(); + var namespacePublicId2 = UUID.randomUUID().toString(); var extensionName2 = "foobar"; - var extensionUuid2 = UUID.randomUUID(); + var extensionPublicId2 = UUID.randomUUID().toString(); var namespace2 = new Namespace(); namespace2.setId(3L); @@ -390,94 +376,33 @@ public void testUpdateDuplicateUpstreamChanges() throws InterruptedException { extension2.setPublicId(extensionPublicId1); extension2.setNamespace(namespace2); - var namespaceName3 = "baz2"; - var namespaceUuid3 = UUID.randomUUID(); - var extensionName3 = "foobar2"; - var extensionUuid3 = UUID.randomUUID(); - - var namespace3 = new Namespace(); - namespace3.setId(5L); - namespace3.setName(namespaceName3); - namespace3.setPublicId(namespaceUuid2.toString()); - - var extension3 = new Extension(); - extension3.setId(6L); - extension3.setName(extensionName3); - extension3.setPublicId(extensionUuid2.toString()); - extension3.setNamespace(namespace3); - - var namespaceName4 = "baz3"; - var namespaceUuid4 = UUID.randomUUID(); - var extensionName4 = "foobar3"; - var extensionUuid4 = UUID.randomUUID(); - - var namespace4 = new Namespace(); - namespace4.setId(7L); - namespace4.setName(namespaceName4); - namespace4.setPublicId(namespaceUuid3.toString()); - - var extension4 = new Extension(); - extension4.setId(8L); - extension4.setName(extensionName4); - extension4.setPublicId(extensionUuid3.toString()); - extension4.setNamespace(namespace4); - Mockito.when(repositories.findPublicId(namespaceName1, extensionName1)).thenReturn(extension1); Mockito.when(repositories.findPublicId(extensionPublicId1)).thenReturn(extension2); Mockito.when(repositories.findNamespacePublicId(namespacePublicId1)).thenReturn(extension2); - Mockito.when(repositories.findPublicId(extensionUuid2.toString())).thenReturn(extension3); - Mockito.when(repositories.findNamespacePublicId(namespaceUuid2.toString())).thenReturn(extension3); - Mockito.when(repositories.findPublicId(extensionUuid3.toString())).thenReturn(extension4); - Mockito.when(repositories.findNamespacePublicId(namespaceUuid3.toString())).thenReturn(extension4); - - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(extensionPublicId1); - ext.getNamespace().setPublicId(namespacePublicId1); - return null; - }).when(idService).getUpstreamPublicIds(extension1); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(extensionUuid2.toString()); - ext.getNamespace().setPublicId(namespaceUuid2.toString()); - return null; - }).when(idService).getUpstreamPublicIds(extension2); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(extensionUuid3.toString()); - ext.getNamespace().setPublicId(namespaceUuid3.toString()); - return null; - }).when(idService).getUpstreamPublicIds(extension3); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(extensionUuid4.toString()); - ext.getNamespace().setPublicId(namespaceUuid4.toString()); - return null; - }).when(idService).getUpstreamPublicIds(extension4); + var upstreamPublicIds = new PublicIds(namespacePublicId1, extensionPublicId1); + Mockito.when(idService.getUpstreamPublicIds(extension1)).thenReturn(upstreamPublicIds); + Mockito.when(idService.getUpstreamPublicIds(extension2)).thenReturn(upstreamPublicIds); + Mockito.when(idService.getRandomPublicId()).thenReturn(extensionPublicId1, extensionPublicId2, namespacePublicId1, namespacePublicId2); updateService.update(namespaceName1, extensionName1); Mockito.verify(repositories).updateExtensionPublicIds(Mockito.argThat((Map map) -> { - return map.size() == 4 + return map.size() == 2 && map.get(extension1.getId()).equals(extensionPublicId1) - && map.get(extension2.getId()).equals(extensionUuid2.toString()) - && map.get(extension3.getId()).equals(extensionUuid3.toString()) - && map.get(extension4.getId()).equals(extensionUuid4.toString()); + && map.get(extension2.getId()).equals(extensionPublicId2); })); Mockito.verify(repositories).updateNamespacePublicIds(Mockito.argThat((Map map) -> { - return map.size() == 4 + return map.size() == 2 && map.get(namespace1.getId()).equals(namespacePublicId1) - && map.get(namespace2.getId()).equals(namespaceUuid2.toString()) - && map.get(namespace3.getId()).equals(namespaceUuid3.toString()) - && map.get(namespace4.getId()).equals(namespaceUuid4.toString()); + && map.get(namespace2.getId()).equals(namespacePublicId2); })); } @Test - public void testUpdateDuplicateRandom() throws InterruptedException { + public void testMustUpdateRandomExistsDb() throws InterruptedException { var namespaceName1 = "foo"; - var namespacePublicId1 = UUID.randomUUID().toString(); + var namespaceUuid1 = UUID.randomUUID().toString(); var extensionName1 = "bar"; - var extensionPublicId1 = UUID.randomUUID().toString(); + var extensionUuid1 = UUID.randomUUID().toString(); var namespace1 = new Namespace(); namespace1.setId(1L); @@ -489,56 +414,49 @@ public void testUpdateDuplicateRandom() throws InterruptedException { extension1.setNamespace(namespace1); var namespaceName2 = "baz"; - var namespaceUuid2 = UUID.randomUUID(); + var namespacePublicId2 = UUID.randomUUID().toString(); var extensionName2 = "foobar"; - var extensionUuid2 = UUID.randomUUID(); + var extensionPublicId2 = UUID.randomUUID().toString(); var namespace2 = new Namespace(); namespace2.setId(3L); namespace2.setName(namespaceName2); - namespace2.setPublicId(namespacePublicId1); + namespace2.setPublicId(namespaceUuid1); var extension2 = new Extension(); extension2.setId(4L); extension2.setName(extensionName2); - extension2.setPublicId(extensionPublicId1); + extension2.setPublicId(extensionUuid1); extension2.setNamespace(namespace2); + var dbExtensionPublicId = UUID.randomUUID().toString(); + var dbNamespacePublicId = UUID.randomUUID().toString(); + Mockito.when(repositories.extensionPublicIdExists(dbExtensionPublicId)).thenReturn(true); + Mockito.when(repositories.namespacePublicIdExists(dbNamespacePublicId)).thenReturn(true); Mockito.when(repositories.findPublicId(namespaceName1, extensionName1)).thenReturn(extension1); - Mockito.when(repositories.findPublicId(extensionPublicId1)).thenReturn(extension2); - Mockito.when(repositories.findNamespacePublicId(namespacePublicId1)).thenReturn(extension2); - - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(extensionPublicId1); - ext.getNamespace().setPublicId(namespacePublicId1); - return null; - }).when(idService).getUpstreamPublicIds(extension1); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(null); - ext.getNamespace().setPublicId(null); - return null; - }).when(idService).getUpstreamPublicIds(extension2); - var uuidMock = Mockito.mockStatic(UUID.class); - uuidMock.when(UUID::randomUUID).thenReturn(extensionUuid2, namespaceUuid2); + Mockito.when(repositories.findPublicId(extensionUuid1)).thenReturn(extension2); + Mockito.when(repositories.findNamespacePublicId(namespaceUuid1)).thenReturn(extension2); + var upstreamPublicIds = new PublicIds(namespaceUuid1, extensionUuid1); + Mockito.when(idService.getUpstreamPublicIds(extension1)).thenReturn(upstreamPublicIds); + Mockito.when(idService.getUpstreamPublicIds(extension2)).thenReturn(upstreamPublicIds); + Mockito.when(idService.getRandomPublicId()).thenReturn(dbExtensionPublicId, extensionPublicId2, dbNamespacePublicId, namespacePublicId2); updateService.update(namespaceName1, extensionName1); Mockito.verify(repositories).updateExtensionPublicIds(Mockito.argThat((Map map) -> { return map.size() == 2 - && map.get(extension1.getId()).equals(extensionPublicId1) - && map.get(extension2.getId()).equals(extensionUuid2.toString()); + && map.get(extension1.getId()).equals(extensionUuid1) + && map.get(extension2.getId()).equals(extensionPublicId2); })); Mockito.verify(repositories).updateNamespacePublicIds(Mockito.argThat((Map map) -> { return map.size() == 2 - && map.get(namespace1.getId()).equals(namespacePublicId1) - && map.get(namespace2.getId()).equals(namespaceUuid2.toString()); + && map.get(namespace1.getId()).equals(namespaceUuid1) + && map.get(namespace2.getId()).equals(namespacePublicId2); })); - uuidMock.close(); } + @Test - public void testUpdateWaitsUntilUpdateAllIsFinished() throws InterruptedException { + public void testUpdateNoChange() throws InterruptedException { var namespaceName = "foo"; var namespacePublicId = UUID.randomUUID().toString(); var extensionName = "bar"; @@ -547,109 +465,145 @@ public void testUpdateWaitsUntilUpdateAllIsFinished() throws InterruptedExceptio var namespace = new Namespace(); namespace.setId(1L); namespace.setName(namespaceName); + namespace.setPublicId(namespacePublicId); var extension = new Extension(); extension.setId(2L); extension.setName(extensionName); extension.setNamespace(namespace); + extension.setPublicId(extensionPublicId); - Mockito.doAnswer(invocation -> { - var ext = invocation.getArgument(0, Extension.class); - ext.setPublicId(extensionPublicId); - ext.getNamespace().setPublicId(namespacePublicId); - return null; - }).when(idService).getUpstreamPublicIds(extension); Mockito.when(repositories.findPublicId(namespaceName, extensionName)).thenReturn(extension); - Mockito.when(repositories.findAllPublicIds()).thenAnswer(invocation -> { - Thread.sleep(1000); - return List.of(extension); - }); - - var executor = Executors.newFixedThreadPool(2); - var future1 = CompletableFuture.runAsync(() -> { - try { - updateService.updateAll(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }, executor); - var future2 = CompletableFuture.runAsync(() -> { - try { - updateService.update("foo", "bar"); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }, executor); - - CompletableFuture.allOf(future1, future2).join(); - var order = Mockito.inOrder(repositories); - order.verify(repositories).findAllPublicIds(); - order.verify(repositories).updateExtensionPublicIds(Mockito.anyMap()); - order.verify(repositories).updateNamespacePublicIds(Mockito.anyMap()); - order.verify(repositories).findPublicId(namespaceName, extensionName); - order.verify(repositories).updateExtensionPublicIds(Mockito.anyMap()); - order.verify(repositories).updateNamespacePublicIds(Mockito.anyMap()); - } + Mockito.when(repositories.findPublicId(extensionPublicId)).thenReturn(extension); + Mockito.when(repositories.findNamespacePublicId(namespacePublicId)).thenReturn(extension); + Mockito.when(idService.getUpstreamPublicIds(extension)).thenReturn(new PublicIds(namespacePublicId, extensionPublicId)); - @Test - public void testUpdateTimeout() throws InterruptedException { - Mockito.when(repositories.findAllPublicIds()).thenAnswer(invocation -> { - Thread.sleep(20000); - return Collections.emptyList(); - }); - - var executor = Executors.newFixedThreadPool(2); - var future1 = CompletableFuture.runAsync(() -> { - try { - updateService.updateAll(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }, executor); - var future2 = CompletableFuture.runAsync(() -> { - assertThrows(RuntimeException.class, () -> updateService.update("foo", "bar")); - }, executor); - CompletableFuture.allOf(future1, future2).join(); + updateService.update(namespaceName, extensionName); + Mockito.verify(repositories, Mockito.never()).updateExtensionPublicIds(Mockito.anyMap()); + Mockito.verify(repositories, Mockito.never()).updateNamespacePublicIds(Mockito.anyMap()); } @Test - public void testUpdateAllWait() throws InterruptedException { + public void testUpdateUpstream() throws InterruptedException { var namespaceName = "foo"; + var namespacePublicId = "123-456-789"; var extensionName = "bar"; - Mockito.when(repositories.findPublicId(namespaceName, extensionName)).thenAnswer(invocation -> { - Thread.sleep(20000); - - var namespace = new Namespace(); - namespace.setId(1L); - namespace.setName(namespaceName); - - var extension = new Extension(); - extension.setId(2L); - extension.setName(extensionName); - extension.setNamespace(namespace); - return extension; - }); - - var executor = Executors.newFixedThreadPool(2); - var future1 = CompletableFuture.runAsync(() -> { - try { - updateService.update(namespaceName, extensionName); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }, executor); - var future2 = CompletableFuture.runAsync(() -> { - try { - updateService.updateAll(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }, executor); - CompletableFuture.allOf(future1, future2).join(); - - var order = Mockito.inOrder(repositories); - order.verify(repositories).findPublicId(namespaceName, extensionName); - order.verify(repositories).findAllPublicIds(); + var extensionPublicId = "abc-def-ghi"; + + var namespace = new Namespace(); + namespace.setId(1L); + namespace.setName(namespaceName); + namespace.setPublicId("zzz-zzz-zzz"); + + var extension = new Extension(); + extension.setId(2L); + extension.setName(extensionName); + extension.setPublicId("000-000-000"); + extension.setNamespace(namespace); + + Mockito.when(repositories.findPublicId(namespaceName, extensionName)).thenReturn(extension); + Mockito.when(idService.getUpstreamPublicIds(extension)).thenReturn(new PublicIds(namespacePublicId, extensionPublicId)); + + updateService.update(namespaceName, extensionName); + Mockito.verify(repositories).updateExtensionPublicIds(Mockito.argThat((Map map) -> { + return map.size() == 1 && map.get(extension.getId()).equals(extensionPublicId); + })); + Mockito.verify(repositories).updateNamespacePublicIds(Mockito.argThat((Map map) -> { + return map.size() == 1 && map.get(namespace.getId()).equals(namespacePublicId); + })); + } + + @Test + public void testUpdateDuplicateRecursive() throws InterruptedException { + var namespaceName1 = "foo"; + var namespacePublicId1 = UUID.randomUUID().toString(); + var extensionName1 = "bar"; + var extensionPublicId1 = UUID.randomUUID().toString(); + + var namespace1 = new Namespace(); + namespace1.setId(1L); + namespace1.setName(namespaceName1); + + var extension1 = new Extension(); + extension1.setId(2L); + extension1.setName(extensionName1); + extension1.setNamespace(namespace1); + + var namespaceName2 = "baz"; + var namespacePublicId2 = UUID.randomUUID().toString(); + var extensionName2 = "foobar"; + var extensionPublicId2 = UUID.randomUUID().toString(); + + var namespace2 = new Namespace(); + namespace2.setId(3L); + namespace2.setName(namespaceName2); + namespace2.setPublicId(namespacePublicId1); + + var extension2 = new Extension(); + extension2.setId(4L); + extension2.setName(extensionName2); + extension2.setPublicId(extensionPublicId1); + extension2.setNamespace(namespace2); + + var namespaceName3 = "baz2"; + var namespacePublicId3 = UUID.randomUUID().toString(); + var extensionName3 = "foobar2"; + var extensionPublicId3 = UUID.randomUUID().toString(); + + var namespace3 = new Namespace(); + namespace3.setId(5L); + namespace3.setName(namespaceName3); + namespace3.setPublicId(namespacePublicId2); + + var extension3 = new Extension(); + extension3.setId(6L); + extension3.setName(extensionName3); + extension3.setPublicId(extensionPublicId2); + extension3.setNamespace(namespace3); + + var namespaceName4 = "baz3"; + var namespacePublicId4 = UUID.randomUUID().toString(); + var extensionName4 = "foobar3"; + var extensionPublicId4 = UUID.randomUUID().toString(); + + var namespace4 = new Namespace(); + namespace4.setId(7L); + namespace4.setName(namespaceName4); + namespace4.setPublicId(namespacePublicId3); + + var extension4 = new Extension(); + extension4.setId(8L); + extension4.setName(extensionName4); + extension4.setPublicId(extensionPublicId3); + extension4.setNamespace(namespace4); + + Mockito.when(repositories.findPublicId(namespaceName1, extensionName1)).thenReturn(extension1); + Mockito.when(repositories.findPublicId(extensionPublicId1)).thenReturn(extension2); + Mockito.when(repositories.findNamespacePublicId(namespacePublicId1)).thenReturn(extension2); + Mockito.when(repositories.findPublicId(extensionPublicId2)).thenReturn(extension3); + Mockito.when(repositories.findNamespacePublicId(namespacePublicId2)).thenReturn(extension3); + Mockito.when(repositories.findPublicId(extensionPublicId3)).thenReturn(extension4); + Mockito.when(repositories.findNamespacePublicId(namespacePublicId3)).thenReturn(extension4); + Mockito.when(idService.getUpstreamPublicIds(extension1)).thenReturn(new PublicIds(namespacePublicId1, extensionPublicId1)); + Mockito.when(idService.getUpstreamPublicIds(extension2)).thenReturn(new PublicIds(namespacePublicId2, extensionPublicId2)); + Mockito.when(idService.getUpstreamPublicIds(extension3)).thenReturn(new PublicIds(namespacePublicId3, extensionPublicId3)); + Mockito.when(idService.getUpstreamPublicIds(extension4)).thenReturn(new PublicIds(namespacePublicId4, extensionPublicId4)); + + updateService.update(namespaceName1, extensionName1); + Mockito.verify(repositories).updateExtensionPublicIds(Mockito.argThat((Map map) -> { + return map.size() == 4 + && map.get(extension1.getId()).equals(extensionPublicId1) + && map.get(extension2.getId()).equals(extensionPublicId2) + && map.get(extension3.getId()).equals(extensionPublicId3) + && map.get(extension4.getId()).equals(extensionPublicId4); + })); + Mockito.verify(repositories).updateNamespacePublicIds(Mockito.argThat((Map map) -> { + return map.size() == 4 + && map.get(namespace1.getId()).equals(namespacePublicId1) + && map.get(namespace2.getId()).equals(namespacePublicId2) + && map.get(namespace3.getId()).equals(namespacePublicId3) + && map.get(namespace4.getId()).equals(namespacePublicId4); + })); } @TestConfiguration