diff --git a/src/main/java/com/michelin/ns4kafka/controller/topic/TopicController.java b/src/main/java/com/michelin/ns4kafka/controller/topic/TopicController.java index 2a1ed02c..9c644e6a 100644 --- a/src/main/java/com/michelin/ns4kafka/controller/topic/TopicController.java +++ b/src/main/java/com/michelin/ns4kafka/controller/topic/TopicController.java @@ -49,14 +49,15 @@ public class TopicController extends NamespacedResourceController { ResourceQuotaService resourceQuotaService; /** - * List topics by namespace. + * List topics by namespace, filtered by name parameter. * * @param namespace The namespace + * @param name The name parameter * @return A list of topics */ @Get - public List list(String namespace) { - return topicService.findAllForNamespace(getNamespace(namespace)); + public List list(String namespace, @QueryValue(defaultValue = "*") String name) { + return topicService.findByWildcardName(getNamespace(namespace), name); } /** @@ -65,9 +66,11 @@ public List list(String namespace) { * @param namespace The name * @param topic The topic name * @return The topic + * @deprecated use list(String, String name) instead. */ @Get("/{topic}") - public Optional getTopic(String namespace, String topic) { + @Deprecated(since = "1.12.0") + public Optional get(String namespace, String topic) { return topicService.findByName(getNamespace(namespace), topic); } @@ -156,8 +159,8 @@ public HttpResponse apply(String namespace, @Valid @Body Topic topic, */ @Status(HttpStatus.NO_CONTENT) @Delete("/{topic}{?dryrun}") - public HttpResponse deleteTopic(String namespace, String topic, - @QueryValue(defaultValue = "false") boolean dryrun) + public HttpResponse delete(String namespace, String topic, + @QueryValue(defaultValue = "false") boolean dryrun) throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = getNamespace(namespace); if (!topicService.isNamespaceOwnerOfTopic(namespace, topic)) { diff --git a/src/main/java/com/michelin/ns4kafka/service/AclService.java b/src/main/java/com/michelin/ns4kafka/service/AclService.java index 37abd2e4..23de59b4 100644 --- a/src/main/java/com/michelin/ns4kafka/service/AclService.java +++ b/src/main/java/com/michelin/ns4kafka/service/AclService.java @@ -262,6 +262,24 @@ public List findAllGrantedToNamespace(Namespace namespace) { .toList(); } + /** + * Find all owner-ACLs on a resource for a given namespace. + * + * @param namespace The namespace + * @param resourceType The resource + * @return A list of ACLs + */ + public List findResourceOwnerGrantedToNamespace(Namespace namespace, + AccessControlEntry.ResourceType resourceType) { + return accessControlEntryRepository.findAll() + .stream() + .filter(accessControlEntry -> + accessControlEntry.getSpec().getGrantedTo().equals(namespace.getMetadata().getName()) + && accessControlEntry.getSpec().getPermission() == AccessControlEntry.Permission.OWNER + && accessControlEntry.getSpec().getResourceType() == resourceType) + .toList(); + } + /** * Find all public granted ACLs. * @@ -340,4 +358,20 @@ public boolean isNamespaceOwnerOfResource(String namespace, AccessControlEntry.R public Optional findByName(String namespace, String name) { return accessControlEntryRepository.findByName(namespace, name); } -} + + /** + * Check if there is any ACL concerning the given resource. + * + * @param acls The OWNER ACL list on resource + * @param resourceName The resource name to check ACL against + * @return true if there is any OWNER ACL concerning the given resource, false otherwise + */ + public boolean isAnyAclOfResource(List acls, String resourceName) { + return acls + .stream() + .anyMatch(acl -> switch (acl.getSpec().getResourcePatternType()) { + case PREFIXED -> resourceName.startsWith(acl.getSpec().getResource()); + case LITERAL -> resourceName.equals(acl.getSpec().getResource()); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/michelin/ns4kafka/service/TopicService.java b/src/main/java/com/michelin/ns4kafka/service/TopicService.java index 90a3bee9..751a9e42 100644 --- a/src/main/java/com/michelin/ns4kafka/service/TopicService.java +++ b/src/main/java/com/michelin/ns4kafka/service/TopicService.java @@ -15,6 +15,7 @@ import com.michelin.ns4kafka.property.ManagedClusterProperties; import com.michelin.ns4kafka.repository.TopicRepository; import com.michelin.ns4kafka.service.executor.TopicAsyncExecutor; +import com.michelin.ns4kafka.util.RegexUtils; import io.micronaut.context.ApplicationContext; import io.micronaut.inject.qualifiers.Qualifiers; import jakarta.inject.Inject; @@ -63,24 +64,26 @@ public List findAll() { * @return A list of topics */ public List findAllForNamespace(Namespace namespace) { - List acls = aclService.findAllGrantedToNamespace(namespace); + List acls = aclService + .findResourceOwnerGrantedToNamespace(namespace, AccessControlEntry.ResourceType.TOPIC); return topicRepository.findAllForCluster(namespace.getMetadata().getCluster()) .stream() - .filter(topic -> acls.stream().anyMatch(accessControlEntry -> { - //need to check accessControlEntry.Permission, we want OWNER - if (accessControlEntry.getSpec().getPermission() != AccessControlEntry.Permission.OWNER) { - return false; - } - if (accessControlEntry.getSpec().getResourceType() == AccessControlEntry.ResourceType.TOPIC) { - return switch (accessControlEntry.getSpec().getResourcePatternType()) { - case PREFIXED -> - topic.getMetadata().getName().startsWith(accessControlEntry.getSpec().getResource()); - case LITERAL -> - topic.getMetadata().getName().equals(accessControlEntry.getSpec().getResource()); - }; - } - return false; - })) + .filter(topic -> aclService.isAnyAclOfResource(acls, topic.getMetadata().getName())) + .toList(); + } + + /** + * Find all topics of a given namespace, filtered by name parameter. + * + * @param namespace The namespace + * @param name The name filter + * @return A list of topics + */ + public List findByWildcardName(Namespace namespace, String name) { + List nameFilterPatterns = RegexUtils.wildcardStringsToRegexPatterns(List.of(name)); + return findAllForNamespace(namespace) + .stream() + .filter(topic -> RegexUtils.filterByPattern(topic.getMetadata().getName(), nameFilterPatterns)) .toList(); } diff --git a/src/test/java/com/michelin/ns4kafka/controller/TopicControllerTest.java b/src/test/java/com/michelin/ns4kafka/controller/TopicControllerTest.java index 0025515a..6e54d480 100644 --- a/src/test/java/com/michelin/ns4kafka/controller/TopicControllerTest.java +++ b/src/test/java/com/michelin/ns4kafka/controller/TopicControllerTest.java @@ -29,6 +29,7 @@ import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.security.utils.SecurityService; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -65,7 +66,7 @@ class TopicControllerTest { TopicController topicController; @Test - void listEmptyTopics() { + void shouldListTopicsWhenEmpty() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -75,15 +76,14 @@ void listEmptyTopics() { when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.findAllForNamespace(ns)) + when(topicService.findByWildcardName(ns, "*")) .thenReturn(List.of()); - List actual = topicController.list("test"); - assertEquals(0, actual.size()); + assertEquals(List.of(), topicController.list("test", "*")); } @Test - void listMultipleTopics() { + void shouldListTopicsWithWildcardParameter() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -91,23 +91,52 @@ void listMultipleTopics() { .build()) .build(); + Topic topic1 = Topic.builder() + .metadata(Metadata.builder() + .name("topic1") + .build()) + .build(); + + Topic topic2 = Topic.builder() + .metadata(Metadata.builder() + .name("topic2") + .build()) + .build(); + when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.findAllForNamespace(ns)) - .thenReturn(List.of( - Topic.builder().metadata(Metadata.builder().name("topic1").build()).build(), - Topic.builder().metadata(Metadata.builder().name("topic2").build()).build() - )); + when(topicService.findByWildcardName(ns, "*")) + .thenReturn(List.of(topic1, topic2)); + + assertEquals(List.of(topic1, topic2), topicController.list("test", "*")); + } + + @Test + void shouldListTopicWithNoWildcardParameter() { + Namespace ns = Namespace.builder() + .metadata(Metadata.builder() + .name("test") + .cluster("local") + .build()) + .build(); - List actual = topicController.list("test"); + Topic topic1 = Topic.builder() + .metadata(Metadata.builder() + .name("topic1") + .build()) + .build(); - assertEquals(2, actual.size()); - assertEquals("topic1", actual.get(0).getMetadata().getName()); - assertEquals("topic2", actual.get(1).getMetadata().getName()); + when(namespaceService.findByName("test")) + .thenReturn(Optional.of(ns)); + when(topicService.findByWildcardName(ns, "topic1")) + .thenReturn(List.of(topic1)); + + assertEquals(List.of(topic1), topicController.list("test", "topic1")); } @Test - void getEmptyTopic() { + @SuppressWarnings("deprecation") + void shouldGetTopicWhenEmpty() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -120,13 +149,14 @@ void getEmptyTopic() { when(topicService.findByName(ns, "topic.notfound")) .thenReturn(Optional.empty()); - Optional actual = topicController.getTopic("test", "topic.notfound"); + Optional actual = topicController.get("test", "topic.notfound"); assertTrue(actual.isEmpty()); } @Test - void getTopic() { + @SuppressWarnings("deprecation") + void shouldGetTopic() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -141,14 +171,14 @@ void getTopic() { Topic.builder().metadata(Metadata.builder().name("topic.found").build()).build() )); - Optional actual = topicController.getTopic("test", "topic.found"); + Optional actual = topicController.get("test", "topic.found"); assertTrue(actual.isPresent()); assertEquals("topic.found", actual.get().getMetadata().getName()); } @Test - void deleteTopic() throws InterruptedException, ExecutionException, TimeoutException { + void shouldDeleteTopic() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -169,13 +199,13 @@ void deleteTopic() throws InterruptedException, ExecutionException, TimeoutExcep doNothing().when(topicService).delete(toDelete.get()); doNothing().when(applicationEventPublisher).publishEvent(any()); - HttpResponse actual = topicController.deleteTopic("test", "topic.delete", false); + HttpResponse actual = topicController.delete("test", "topic.delete", false); assertEquals(HttpStatus.NO_CONTENT, actual.getStatus()); } @Test - void deleteTopicDryRun() throws InterruptedException, ExecutionException, TimeoutException { + void shouldDeleteTopicInDryRunMode() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -185,20 +215,26 @@ void deleteTopicDryRun() throws InterruptedException, ExecutionException, Timeou when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); + Optional toDelete = Optional.of( - Topic.builder().metadata(Metadata.builder().name("topic.delete").build()).build()); + Topic.builder() + .metadata(Metadata.builder() + .name("topic.delete") + .build()) + .build()); + when(topicService.findByName(ns, "topic.delete")) .thenReturn(toDelete); when(topicService.isNamespaceOwnerOfTopic("test", "topic.delete")) .thenReturn(true); - topicController.deleteTopic("test", "topic.delete", true); + topicController.delete("test", "topic.delete", true); verify(topicService, never()).delete(any()); } @Test - void deleteTopicUnauthorized() { + void shouldNotDeleteTopicWhenUnauthorized() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -212,11 +248,11 @@ void deleteTopicUnauthorized() { .thenReturn(false); assertThrows(ResourceValidationException.class, - () -> topicController.deleteTopic("test", "topic.delete", false)); + () -> topicController.delete("test", "topic.delete", false)); } @Test - void createNewTopic() throws InterruptedException, ExecutionException, TimeoutException { + void shouldCreateTopic() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -242,11 +278,16 @@ void createNewTopic() throws InterruptedException, ExecutionException, TimeoutEx when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.isNamespaceOwnerOfTopic(any(), any())).thenReturn(true); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.empty()); - when(resourceQuotaService.validateTopicQuota(ns, Optional.empty(), topic)).thenReturn(List.of()); - when(securityService.username()).thenReturn(Optional.of("test-user")); - when(securityService.hasRole(ResourceBasedSecurityRule.IS_ADMIN)).thenReturn(false); + when(topicService.isNamespaceOwnerOfTopic(any(), any())) + .thenReturn(true); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.empty()); + when(resourceQuotaService.validateTopicQuota(ns, Optional.empty(), topic)) + .thenReturn(List.of()); + when(securityService.username()) + .thenReturn(Optional.of("test-user")); + when(securityService.hasRole(ResourceBasedSecurityRule.IS_ADMIN)) + .thenReturn(false); doNothing().when(applicationEventPublisher).publishEvent(any()); when(topicService.create(topic)).thenReturn(topic); @@ -258,7 +299,7 @@ void createNewTopic() throws InterruptedException, ExecutionException, TimeoutEx } @Test - void shouldCreateNewTopicWithNoConstraint() throws InterruptedException, ExecutionException, TimeoutException { + void shouldCreateTopicWithNoConstraint() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -285,11 +326,16 @@ void shouldCreateNewTopicWithNoConstraint() throws InterruptedException, Executi when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.isNamespaceOwnerOfTopic(any(), any())).thenReturn(true); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.empty()); - when(resourceQuotaService.validateTopicQuota(ns, Optional.empty(), topic)).thenReturn(List.of()); - when(securityService.username()).thenReturn(Optional.of("test-user")); - when(securityService.hasRole(ResourceBasedSecurityRule.IS_ADMIN)).thenReturn(false); + when(topicService.isNamespaceOwnerOfTopic(any(), any())) + .thenReturn(true); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.empty()); + when(resourceQuotaService.validateTopicQuota(ns, Optional.empty(), topic)) + .thenReturn(List.of()); + when(securityService.username()) + .thenReturn(Optional.of("test-user")); + when(securityService.hasRole(ResourceBasedSecurityRule.IS_ADMIN)) + .thenReturn(false); doNothing().when(applicationEventPublisher).publishEvent(any()); when(topicService.create(topic)).thenReturn(topic); @@ -301,7 +347,7 @@ void shouldCreateNewTopicWithNoConstraint() throws InterruptedException, Executi } @Test - void updateTopic() throws InterruptedException, ExecutionException, TimeoutException { + void shouldUpdateTopic() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -340,10 +386,14 @@ void updateTopic() throws InterruptedException, ExecutionException, TimeoutExcep when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.of(existing)); - when(topicService.create(topic)).thenReturn(topic); - when(securityService.username()).thenReturn(Optional.of("test-user")); - when(securityService.hasRole(ResourceBasedSecurityRule.IS_ADMIN)).thenReturn(false); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.of(existing)); + when(topicService.create(topic)) + .thenReturn(topic); + when(securityService.username()) + .thenReturn(Optional.of("test-user")); + when(securityService.hasRole(ResourceBasedSecurityRule.IS_ADMIN)) + .thenReturn(false); doNothing().when(applicationEventPublisher).publishEvent(any()); var response = topicController.apply("test", topic, false); @@ -394,8 +444,10 @@ void shouldValidateNewTags() { when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.of(existing)); - when(topicService.validateTags(ns, topic)).thenReturn(List.of("Error on tags")); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.of(existing)); + when(topicService.validateTags(ns, topic)) + .thenReturn(List.of("Error on tags")); ResourceValidationException actual = assertThrows(ResourceValidationException.class, () -> topicController.apply("test", topic, false)); @@ -434,7 +486,7 @@ void shouldNotValidateTagsWhenNoNewTag() throws InterruptedException, ExecutionE .name("test.topic") .build()) .spec(Topic.TopicSpec.builder() - .tags(Arrays.asList("TAG1")) + .tags(new ArrayList<>(List.of("TAG1"))) .replicationFactor(3) .partitions(3) .configs(Map.of("cleanup.policy", "delete", @@ -445,10 +497,14 @@ void shouldNotValidateTagsWhenNoNewTag() throws InterruptedException, ExecutionE when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.of(existing)); - when(topicService.create(topic)).thenReturn(topic); - when(securityService.username()).thenReturn(Optional.of("test-user")); - when(securityService.hasRole(ResourceBasedSecurityRule.IS_ADMIN)).thenReturn(false); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.of(existing)); + when(topicService.create(topic)) + .thenReturn(topic); + when(securityService.username()) + .thenReturn(Optional.of("test-user")); + when(securityService.hasRole(ResourceBasedSecurityRule.IS_ADMIN)) + .thenReturn(false); doNothing().when(applicationEventPublisher).publishEvent(any()); var response = topicController.apply("test", topic, false); @@ -456,14 +512,11 @@ void shouldNotValidateTagsWhenNoNewTag() throws InterruptedException, ExecutionE assertEquals("changed", response.header("X-Ns4kafka-Result")); assertEquals("test.topic", actual.getMetadata().getName()); assertEquals(1, actual.getSpec().getTags().size()); - assertEquals("TAG1", actual.getSpec().getTags().get(0)); + assertEquals("TAG1", actual.getSpec().getTags().getFirst()); } - /** - * Validate topic update when there are validations errors. - */ @Test - void updateTopicValidationErrors() { + void shouldNotUpdateTopicWhenValidationErrors() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -514,7 +567,7 @@ void updateTopicValidationErrors() { } @Test - void updateTopicAlreadyExistsUnchanged() throws InterruptedException, ExecutionException, TimeoutException { + void shouldNotUpdateTopicWhenUnchanged() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -555,7 +608,8 @@ void updateTopicAlreadyExistsUnchanged() throws InterruptedException, ExecutionE when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.of(existing)); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.of(existing)); var response = topicController.apply("test", topic, false); Topic actual = response.body(); @@ -565,7 +619,7 @@ void updateTopicAlreadyExistsUnchanged() throws InterruptedException, ExecutionE } @Test - void createNewTopicDryRun() throws InterruptedException, ExecutionException, TimeoutException { + void shouldCreateTopicInDryRunMode() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -591,9 +645,12 @@ void createNewTopicDryRun() throws InterruptedException, ExecutionException, Tim when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.isNamespaceOwnerOfTopic(any(), any())).thenReturn(true); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.empty()); - when(resourceQuotaService.validateTopicQuota(ns, Optional.empty(), topic)).thenReturn(List.of()); + when(topicService.isNamespaceOwnerOfTopic(any(), any())) + .thenReturn(true); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.empty()); + when(resourceQuotaService.validateTopicQuota(ns, Optional.empty(), topic)) + .thenReturn(List.of()); var response = topicController.apply("test", topic, true); assertEquals("created", response.header("X-Ns4kafka-Result")); @@ -601,7 +658,7 @@ void createNewTopicDryRun() throws InterruptedException, ExecutionException, Tim } @Test - void createNewTopicFailValidation() { + void shouldNotCreateTopicWhenValidationErrors() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -625,8 +682,10 @@ void createNewTopicFailValidation() { when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.isNamespaceOwnerOfTopic(any(), any())).thenReturn(true); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.empty()); + when(topicService.isNamespaceOwnerOfTopic(any(), any())) + .thenReturn(true); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.empty()); ResourceValidationException actual = assertThrows(ResourceValidationException.class, () -> topicController.apply("test", topic, false)); @@ -635,7 +694,7 @@ void createNewTopicFailValidation() { } @Test - void shouldNotFailWhenCreatingNewTopicWithNoValidator() + void shouldNotFailWhenCreatingTopicWithNoValidator() throws ExecutionException, InterruptedException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() @@ -648,7 +707,9 @@ void shouldNotFailWhenCreatingNewTopicWithNoValidator() .build(); Topic topic = Topic.builder() - .metadata(Metadata.builder().name("test.topic").build()) + .metadata(Metadata.builder() + .name("test.topic") + .build()) .spec(Topic.TopicSpec.builder() .replicationFactor(1) .partitions(3) @@ -660,8 +721,10 @@ void shouldNotFailWhenCreatingNewTopicWithNoValidator() when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.isNamespaceOwnerOfTopic(any(), any())).thenReturn(true); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.empty()); + when(topicService.isNamespaceOwnerOfTopic(any(), any())) + .thenReturn(true); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.empty()); var response = topicController.apply("test", topic, true); assertEquals("created", response.header("X-Ns4kafka-Result")); @@ -669,7 +732,7 @@ void shouldNotFailWhenCreatingNewTopicWithNoValidator() } @Test - void shouldNotFailWhenCreatingNewTopicWithNoValidationConstraint() + void shouldNotFailWhenCreatingTopicWithNoValidationConstraint() throws ExecutionException, InterruptedException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() @@ -683,7 +746,9 @@ void shouldNotFailWhenCreatingNewTopicWithNoValidationConstraint() .build(); Topic topic = Topic.builder() - .metadata(Metadata.builder().name("test.topic").build()) + .metadata(Metadata.builder() + .name("test.topic") + .build()) .spec(Topic.TopicSpec.builder() .replicationFactor(1) .partitions(3) @@ -695,8 +760,10 @@ void shouldNotFailWhenCreatingNewTopicWithNoValidationConstraint() when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.isNamespaceOwnerOfTopic(any(), any())).thenReturn(true); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.empty()); + when(topicService.isNamespaceOwnerOfTopic(any(), any())) + .thenReturn(true); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.empty()); var response = topicController.apply("test", topic, true); assertEquals("created", response.header("X-Ns4kafka-Result")); @@ -704,7 +771,7 @@ void shouldNotFailWhenCreatingNewTopicWithNoValidationConstraint() } @Test - void createNewTopicFailQuotaValidation() { + void shouldNotCreateTopicWhenQuotaValidationErrors() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -714,6 +781,7 @@ void createNewTopicFailQuotaValidation() { .topicValidator(TopicValidator.makeDefault()) .build()) .build(); + Topic topic = Topic.builder() .metadata(Metadata.builder() .name("test.topic") @@ -729,9 +797,12 @@ void createNewTopicFailQuotaValidation() { when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.isNamespaceOwnerOfTopic(any(), any())).thenReturn(true); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.empty()); - when(resourceQuotaService.validateTopicQuota(ns, Optional.empty(), topic)).thenReturn(List.of("Quota error")); + when(topicService.isNamespaceOwnerOfTopic(any(), any())) + .thenReturn(true); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.empty()); + when(resourceQuotaService.validateTopicQuota(ns, Optional.empty(), topic)) + .thenReturn(List.of("Quota error")); ResourceValidationException actual = assertThrows(ResourceValidationException.class, () -> topicController.apply("test", topic, false)); @@ -740,7 +811,7 @@ void createNewTopicFailQuotaValidation() { } @Test - void importTopic() throws InterruptedException, ExecutionException, TimeoutException { + void shouldImportTopic() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -808,7 +879,7 @@ void importTopic() throws InterruptedException, ExecutionException, TimeoutExcep } @Test - void importTopicDryRun() throws InterruptedException, ExecutionException, TimeoutException { + void shouldImportTopicInDryRunMode() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -873,7 +944,7 @@ void importTopicDryRun() throws InterruptedException, ExecutionException, Timeou } @Test - void deleteRecordsSuccess() throws ExecutionException, InterruptedException { + void shouldDeleteRecords() throws ExecutionException, InterruptedException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -881,7 +952,11 @@ void deleteRecordsSuccess() throws ExecutionException, InterruptedException { .build()) .build(); - Topic toEmpty = Topic.builder().metadata(Metadata.builder().name("topic.empty").build()).build(); + Topic toEmpty = Topic.builder() + .metadata(Metadata.builder() + .name("topic.empty") + .build()) + .build(); Map partitionsToDelete = Map.of( new TopicPartition("topic.empty", 0), 100L, @@ -928,7 +1003,7 @@ void deleteRecordsSuccess() throws ExecutionException, InterruptedException { } @Test - void deleteRecordsCompactedTopic() { + void shouldDeleteRecordsInCompactedTopic() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -956,7 +1031,7 @@ void deleteRecordsCompactedTopic() { } @Test - void deleteRecordsDryRun() throws InterruptedException, ExecutionException { + void shouldDeleteRecordsInDryRunMode() throws InterruptedException, ExecutionException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -1011,7 +1086,7 @@ void deleteRecordsDryRun() throws InterruptedException, ExecutionException { } @Test - void deleteRecordsNotOwner() { + void shouldNotDeleteRecordsWhenNotOwner() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -1034,7 +1109,7 @@ void deleteRecordsNotOwner() { } @Test - void deleteRecordsNotExistingTopic() { + void shouldNotDeleteRecordsWhenTopicNotExist() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -1058,7 +1133,9 @@ void deleteRecordsNotExistingTopic() { } @Test - void createCollidingTopic() throws InterruptedException, ExecutionException, TimeoutException { + void shouldNotCreateTopicWhenNameCollidesOnSpecialChar() throws InterruptedException, + ExecutionException, + TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("test") @@ -1068,6 +1145,7 @@ void createCollidingTopic() throws InterruptedException, ExecutionException, Tim .topicValidator(TopicValidator.makeDefault()) .build()) .build(); + Topic topic = Topic.builder() .metadata(Metadata.builder() .name("test.topic") @@ -1083,9 +1161,12 @@ void createCollidingTopic() throws InterruptedException, ExecutionException, Tim when(namespaceService.findByName("test")) .thenReturn(Optional.of(ns)); - when(topicService.isNamespaceOwnerOfTopic(any(), any())).thenReturn(true); - when(topicService.findByName(ns, "test.topic")).thenReturn(Optional.empty()); - when(topicService.findCollidingTopics(ns, topic)).thenReturn(List.of("test_topic")); + when(topicService.isNamespaceOwnerOfTopic(any(), any())) + .thenReturn(true); + when(topicService.findByName(ns, "test.topic")) + .thenReturn(Optional.empty()); + when(topicService.findCollidingTopics(ns, topic)) + .thenReturn(List.of("test_topic")); ResourceValidationException actual = assertThrows(ResourceValidationException.class, () -> topicController.apply("test", topic, false)); diff --git a/src/test/java/com/michelin/ns4kafka/service/AclServiceTest.java b/src/test/java/com/michelin/ns4kafka/service/AclServiceTest.java index 0bc76966..64e52630 100644 --- a/src/test/java/com/michelin/ns4kafka/service/AclServiceTest.java +++ b/src/test/java/com/michelin/ns4kafka/service/AclServiceTest.java @@ -932,4 +932,115 @@ void shouldNotCollideIfDifferentResource() { assertFalse(aclService.topicAclsCollide(aceTopicPrefixedOwner, aceConnectLiteralOwner)); assertFalse(aclService.topicAclsCollide(aceConnectLiteralOwner, aceTopicPrefixedOwner)); } + + @Test + void findResourceOwnerAclGrantedToNamespace() { + Namespace ns = Namespace.builder() + .metadata(Metadata.builder().name("namespace1").build()).build(); + AccessControlEntry acl1 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace1").build()) + .build(); + AccessControlEntry acl2 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .permission(AccessControlEntry.Permission.READ) + .grantedTo("namespace1").build()) + .build(); + AccessControlEntry acl3 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.CONNECT) + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace1").build()) + .build(); + AccessControlEntry acl4 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace2").build()) + .build(); + AccessControlEntry acl5 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .permission(AccessControlEntry.Permission.READ) + .grantedTo("*").build()) + .build(); + AccessControlEntry acl6 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.GROUP) + .permission(AccessControlEntry.Permission.WRITE) + .grantedTo("namespace1").build()) + .build(); + + when(accessControlEntryRepository.findAll()).thenReturn(List.of(acl1, acl2, acl3, acl4, acl5, acl6)); + + assertEquals(List.of(acl1), + aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)); + assertEquals(List.of(acl3), + aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.CONNECT)); + assertEquals(List.of(), + aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.GROUP)); + } + + @Test + void isPrefixedAclOfResource() { + AccessControlEntry acl1 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) + .permission(AccessControlEntry.Permission.OWNER) + .resource("abc.") + .grantedTo("namespace") + .build()) + .build(); + + AccessControlEntry acl2 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.CONNECT) + .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) + .permission(AccessControlEntry.Permission.OWNER) + .resource("abc_") + .grantedTo("namespace") + .build()) + .build(); + List acls = List.of(acl1, acl2); + + assertFalse(aclService.isAnyAclOfResource(acls, "xyz.topic1")); + assertFalse(aclService.isAnyAclOfResource(acls, "topic1-abc")); + assertFalse(aclService.isAnyAclOfResource(acls, "abc-topic1")); + assertTrue(aclService.isAnyAclOfResource(acls, "abc.topic1")); + assertTrue(aclService.isAnyAclOfResource(acls, "abc_topic1")); + } + + @Test + void isLiteralAclOfResource() { + AccessControlEntry acl1 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) + .permission(AccessControlEntry.Permission.OWNER) + .resource("abc.topic1") + .grantedTo("namespace") + .build()) + .build(); + + AccessControlEntry acl2 = AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .resourceType(AccessControlEntry.ResourceType.CONNECT) + .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) + .permission(AccessControlEntry.Permission.OWNER) + .resource("abc-topic1") + .grantedTo("namespace") + .build()) + .build(); + List acls = List.of(acl1, acl2); + + assertFalse(aclService.isAnyAclOfResource(acls, "xyz.topic1")); + assertFalse(aclService.isAnyAclOfResource(acls, "abc.topic12")); + assertFalse(aclService.isAnyAclOfResource(acls, "abc_topic1")); + assertTrue(aclService.isAnyAclOfResource(acls, "abc.topic1")); + assertTrue(aclService.isAnyAclOfResource(acls, "abc-topic1")); + } } diff --git a/src/test/java/com/michelin/ns4kafka/service/TopicServiceTest.java b/src/test/java/com/michelin/ns4kafka/service/TopicServiceTest.java index 260c26aa..5ad1ec6d 100644 --- a/src/test/java/com/michelin/ns4kafka/service/TopicServiceTest.java +++ b/src/test/java/com/michelin/ns4kafka/service/TopicServiceTest.java @@ -1,10 +1,12 @@ package com.michelin.ns4kafka.service; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertLinesMatch; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -28,7 +30,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -56,11 +57,8 @@ class TopicServiceTest { @Mock List managedClusterProperties; - /** - * Validate find topic by name. - */ @Test - void findByName() { + void shouldFindByName() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -72,25 +70,25 @@ void findByName() { .build(); Topic t1 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .build(); Topic t2 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic2").build()) + .metadata(Metadata.builder() + .name("ns-topic2") + .build()) .build(); Topic t3 = Topic.builder() - .metadata(Metadata.builder().name("ns1-topic1").build()) - .build(); - - Topic t4 = Topic.builder() - .metadata(Metadata.builder().name("ns2-topic1").build()) + .metadata(Metadata.builder() + .name("ns1-topic1") + .build()) .build(); - when(topicRepository.findAllForCluster("local")) - .thenReturn(List.of(t1, t2, t3, t4)); - - when(aclService.findAllGrantedToNamespace(ns)) + when(topicRepository.findAllForCluster("local")).thenReturn(List.of(t1, t2, t3)); + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) .thenReturn(List.of( AccessControlEntry.builder() .spec(AccessControlEntry.AccessControlEntrySpec.builder() @@ -109,22 +107,22 @@ void findByName() { .resourceType(AccessControlEntry.ResourceType.TOPIC) .resource("ns1-topic1") .build()) - .build() - )); + .build())); + when(aclService.isAnyAclOfResource(any(), anyString())).thenReturn(true); // search topic by name Optional actualTopicPrefixed = topicService.findByName(ns, "ns-topic1"); - assertEquals(actualTopicPrefixed.get(), t1); + assertEquals(actualTopicPrefixed.orElse(Topic.builder().build()), t1); Optional actualTopicLiteral = topicService.findByName(ns, "ns1-topic1"); - assertEquals(actualTopicLiteral.get(), t3); + assertEquals(actualTopicLiteral.orElse(Topic.builder().build()), t3); Optional actualTopicNotFound = topicService.findByName(ns, "ns2-topic1"); assertThrows(NoSuchElementException.class, actualTopicNotFound::get, "No value present"); } @Test - void findAllForNamespaceNoTopics() { + void shouldFindAllForNamespaceWhenEmpty() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -136,22 +134,17 @@ void findAllForNamespaceNoTopics() { .build(); // no ns4kfk access control entries - when(aclService.findAllGrantedToNamespace(ns)) + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) .thenReturn(List.of()); - // no ns4kfk topics - when(topicRepository.findAllForCluster("local")) - .thenReturn(List.of()); - - // get list of topics - List list = topicService.findAllForNamespace(ns); + // no ns4kfk topics + when(topicRepository.findAllForCluster("local")).thenReturn(List.of()); - // list of topics is empty - assertTrue(list.isEmpty()); + assertTrue(topicService.findAllForNamespace(ns).isEmpty()); } @Test - void findAllForNamespaceNoAcls() { + void shouldFindAllForNamespaceWhenNoAcl() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -162,33 +155,44 @@ void findAllForNamespaceNoAcls() { .build()) .build(); - // init ns4kfk topics Topic t1 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .build(); + Topic t2 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic2").build()) + .metadata(Metadata.builder() + .name("ns-topic2") + .build()) .build(); + Topic t3 = Topic.builder() - .metadata(Metadata.builder().name("ns1-topic1").build()) + .metadata(Metadata.builder() + .name("ns1-topic1") + .build()) .build(); + Topic t4 = Topic.builder() - .metadata(Metadata.builder().name("ns2-topic1").build()) + .metadata(Metadata.builder() + .name("ns2-topic1") + .build()) .build(); + when(topicRepository.findAllForCluster("local")) .thenReturn(List.of(t1, t2, t3, t4)); // no ns4kfk access control entries - when(aclService.findAllGrantedToNamespace(ns)) + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) .thenReturn(List.of()); - // list of topics is empty - List actual = topicService.findAllForNamespace(ns); - assertTrue(actual.isEmpty()); + when(aclService.isAnyAclOfResource(any(), anyString())).thenReturn(false); + + assertTrue(topicService.findAllForNamespace(ns).isEmpty()); } @Test - void findAllForNamespace() { + void shouldFindAllForNamespaceWhenNoAclOnTopic() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -199,25 +203,35 @@ void findAllForNamespace() { .build()) .build(); - Topic t0 = Topic.builder() - .metadata(Metadata.builder().name("ns0-topic1").build()) - .build(); Topic t1 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .build(); + Topic t2 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic2").build()) + .metadata(Metadata.builder() + .name("ns-topic2") + .build()) .build(); + Topic t3 = Topic.builder() - .metadata(Metadata.builder().name("ns1-topic1").build()) + .metadata(Metadata.builder() + .name("ns1-topic1") + .build()) .build(); + Topic t4 = Topic.builder() - .metadata(Metadata.builder().name("ns2-topic1").build()) + .metadata(Metadata.builder() + .name("ns2-topic1") + .build()) .build(); + when(topicRepository.findAllForCluster("local")) - .thenReturn(List.of(t0, t1, t2, t3, t4)); + .thenReturn(List.of(t1, t2, t3, t4)); - when(aclService.findAllGrantedToNamespace(ns)) + // all ns4kafka access control entries are not for the topics + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) .thenReturn(List.of( AccessControlEntry.builder() .spec(AccessControlEntry.AccessControlEntrySpec.builder() @@ -225,7 +239,7 @@ void findAllForNamespace() { .grantedTo("namespace") .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) .resourceType(AccessControlEntry.ResourceType.TOPIC) - .resource("ns0-topic1") + .resource("ns-topic5") .build()) .build(), AccessControlEntry.builder() @@ -234,45 +248,91 @@ void findAllForNamespace() { .grantedTo("namespace") .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) .resourceType(AccessControlEntry.ResourceType.TOPIC) - .resource("ns-") - .build()) - .build(), - AccessControlEntry.builder() - .spec(AccessControlEntry.AccessControlEntrySpec.builder() - .permission(AccessControlEntry.Permission.READ) - .grantedTo("namespace") - .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) - .resourceType(AccessControlEntry.ResourceType.TOPIC) - .resource("ns1-topic1") + .resource("ns0-") .build()) - .build(), - AccessControlEntry.builder() - .spec(AccessControlEntry.AccessControlEntrySpec.builder() - .permission(AccessControlEntry.Permission.WRITE) - .grantedTo("namespace") - .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) - .resourceType(AccessControlEntry.ResourceType.TOPIC) - .resource("ns2-topic1") - .build()) - .build() - )); + .build())); + when(aclService.isAnyAclOfResource(any(), anyString())).thenReturn(false); - // search for topics into namespace - List actual = topicService.findAllForNamespace(ns); + assertTrue(topicService.findAllForNamespace(ns).isEmpty()); + } - assertEquals(3, actual.size()); - // contains - assertTrue(actual.stream().anyMatch(topic -> topic.getMetadata().getName().equals("ns0-topic1"))); - assertTrue(actual.stream().anyMatch(topic -> topic.getMetadata().getName().equals("ns-topic1"))); - assertTrue(actual.stream().anyMatch(topic -> topic.getMetadata().getName().equals("ns-topic2"))); - // doesn't contain - Assertions.assertFalse(actual.stream().anyMatch(topic -> topic.getMetadata().getName().equals("ns1-topic1"))); - Assertions.assertFalse(actual.stream().anyMatch(topic -> topic.getMetadata().getName().equals("ns2-topic1"))); + @Test + void shouldFindAllForNamespace() { + Namespace ns = Namespace.builder() + .metadata(Metadata.builder() + .name("namespace") + .cluster("local") + .build()) + .spec(NamespaceSpec.builder() + .connectClusters(List.of("local-name")) + .build()) + .build(); + + Topic t0 = Topic.builder() + .metadata(Metadata.builder() + .name("ns0-topic1") + .build()) + .build(); + + Topic t1 = Topic.builder() + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) + .build(); + + Topic t2 = Topic.builder() + .metadata(Metadata.builder() + .name("ns-topic2") + .build()) + .build(); + + Topic t3 = Topic.builder() + .metadata(Metadata.builder() + .name("ns1-topic1") + .build()) + .build(); + + Topic t4 = Topic.builder() + .metadata(Metadata.builder() + .name("ns2-topic1") + .build()) + .build(); + + List acls = List.of( + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("ns0-topic1") + .build()) + .build(), + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("ns-") + .build()) + .build()); + + when(topicRepository.findAllForCluster("local")).thenReturn(List.of(t0, t1, t2, t3, t4)); + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) + .thenReturn(acls); + when(aclService.isAnyAclOfResource(acls, "ns1-topic1")).thenReturn(false); + when(aclService.isAnyAclOfResource(acls, "ns2-topic1")).thenReturn(false); + when(aclService.isAnyAclOfResource(acls, "ns0-topic1")).thenReturn(true); + when(aclService.isAnyAclOfResource(acls, "ns-topic1")).thenReturn(true); + when(aclService.isAnyAclOfResource(acls, "ns-topic2")).thenReturn(true); + + assertEquals(List.of(t0, t1, t2), topicService.findAllForNamespace(ns)); } @Test - void listUnsynchronizedNoExistingTopics() throws InterruptedException, ExecutionException, TimeoutException { + void shouldListUnsynchronizedTopics() throws InterruptedException, ExecutionException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -289,8 +349,8 @@ void listUnsynchronizedNoExistingTopics() throws InterruptedException, Execution Qualifiers.byName(ns.getMetadata().getCluster()))).thenReturn(topicAsyncExecutor); // list of existing broker topics - when(topicAsyncExecutor.listBrokerTopicNames()).thenReturn(List.of("ns-topic1", "ns-topic2", - "ns1-topic1", "ns2-topic1")); + when(topicAsyncExecutor.listBrokerTopicNames()) + .thenReturn(List.of("ns-topic1", "ns-topic2", "ns1-topic1", "ns2-topic1")); // list of existing ns4kfk access control entries when(aclService.isNamespaceOwnerOfResource("namespace", AccessControlEntry.ResourceType.TOPIC, @@ -306,7 +366,7 @@ void listUnsynchronizedNoExistingTopics() throws InterruptedException, Execution "ns2-topic1")) .thenReturn(false); - when(aclService.findAllGrantedToNamespace(ns)) + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) .thenReturn(List.of( AccessControlEntry.builder() .spec(AccessControlEntry.AccessControlEntrySpec.builder() @@ -325,26 +385,27 @@ void listUnsynchronizedNoExistingTopics() throws InterruptedException, Execution .resourceType(AccessControlEntry.ResourceType.TOPIC) .resource("ns1-topic1") .build()) - .build() - )); + .build())); // no topic exists into ns4kfk when(topicRepository.findAllForCluster("local")) .thenReturn(List.of()); + List actual = topicService.listUnsynchronizedTopicNames(ns); assertEquals(3, actual.size()); - // contains + assertTrue(actual.stream().anyMatch(topic -> topic.equals("ns-topic1"))); assertTrue(actual.stream().anyMatch(topic -> topic.equals("ns-topic2"))); assertTrue(actual.stream().anyMatch(topic -> topic.equals("ns1-topic1"))); - // doesn't contain - Assertions.assertFalse(actual.stream().anyMatch(topic -> topic.equals("ns2-topic1"))); + assertFalse(actual.stream().anyMatch(topic -> topic.equals("ns2-topic1"))); } @Test - void listUnsynchronizedAllExistingTopics() throws InterruptedException, ExecutionException, TimeoutException { + void shouldListUnsynchronizedWhenAllExistingTopics() throws InterruptedException, + ExecutionException, + TimeoutException { // init ns4kfk namespace Namespace ns = Namespace.builder() .metadata(Metadata.builder() @@ -357,27 +418,63 @@ void listUnsynchronizedAllExistingTopics() throws InterruptedException, Executio .build(); Topic t1 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .build(); + Topic t2 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic2").build()) + .metadata(Metadata.builder() + .name("ns-topic2") + .build()) .build(); + Topic t3 = Topic.builder() - .metadata(Metadata.builder().name("ns1-topic1").build()) + .metadata(Metadata.builder() + .name("ns1-topic1") + .build()) .build(); + Topic t4 = Topic.builder() - .metadata(Metadata.builder().name("ns2-topic1").build()) + .metadata(Metadata.builder() + .name("ns2-topic1") + .build()) .build(); + List acls = List.of( + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("ns-") + .build()) + .build(), + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("ns1-topic1") + .build()) + .build() + ); + // init topicAsyncExecutor TopicAsyncExecutor topicAsyncExecutor = Mockito.mock(TopicAsyncExecutor.class); when(applicationContext.getBean(TopicAsyncExecutor.class, Qualifiers.byName(ns.getMetadata().getCluster()))).thenReturn(topicAsyncExecutor); // list of existing broker topics - when(topicAsyncExecutor.listBrokerTopicNames()).thenReturn( - List.of(t1.getMetadata().getName(), t2.getMetadata().getName(), - t3.getMetadata().getName(), t4.getMetadata().getName())); + when(topicAsyncExecutor.listBrokerTopicNames()) + .thenReturn(List.of( + t1.getMetadata().getName(), + t2.getMetadata().getName(), + t3.getMetadata().getName(), + t4.getMetadata().getName()) + ); // list of existing ns4kfk access control entries when(aclService.isNamespaceOwnerOfResource("namespace", AccessControlEntry.ResourceType.TOPIC, @@ -393,40 +490,27 @@ void listUnsynchronizedAllExistingTopics() throws InterruptedException, Executio t4.getMetadata().getName())) .thenReturn(false); - when(aclService.findAllGrantedToNamespace(ns)) - .thenReturn(List.of( - AccessControlEntry.builder() - .spec(AccessControlEntry.AccessControlEntrySpec.builder() - .permission(AccessControlEntry.Permission.OWNER) - .grantedTo("namespace") - .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) - .resourceType(AccessControlEntry.ResourceType.TOPIC) - .resource("ns-") - .build()) - .build(), - AccessControlEntry.builder() - .spec(AccessControlEntry.AccessControlEntrySpec.builder() - .permission(AccessControlEntry.Permission.OWNER) - .grantedTo("namespace") - .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) - .resourceType(AccessControlEntry.ResourceType.TOPIC) - .resource("ns1-topic1") - .build()) - .build() - )); + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) + .thenReturn(acls); // all topic exists into ns4kfk when(topicRepository.findAllForCluster("local")) .thenReturn(List.of(t1, t2, t3, t4)); + when(aclService.isAnyAclOfResource(acls, "ns-topic1")).thenReturn(true); + when(aclService.isAnyAclOfResource(acls, "ns-topic2")).thenReturn(true); + when(aclService.isAnyAclOfResource(acls, "ns1-topic1")).thenReturn(true); + when(aclService.isAnyAclOfResource(acls, "ns2-topic1")).thenReturn(false); + List actual = topicService.listUnsynchronizedTopicNames(ns); assertEquals(0, actual.size()); - } @Test - void listUnsynchronizedPartialExistingTopics() throws InterruptedException, ExecutionException, TimeoutException { + void shouldListUnsynchronizedWhenNotAllTopicsAlreadyExist() throws InterruptedException, + ExecutionException, + TimeoutException { // init ns4kfk namespace Namespace ns = Namespace.builder() .metadata(Metadata.builder() @@ -439,9 +523,32 @@ void listUnsynchronizedPartialExistingTopics() throws InterruptedException, Exec .build(); Topic t1 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .build(); + List acls = List.of( + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("ns-") + .build()) + .build(), + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("ns1-topic1") + .build()) + .build() + ); + // init topicAsyncExecutor TopicAsyncExecutor topicAsyncExecutor = Mockito.mock(TopicAsyncExecutor.class); when(applicationContext.getBean(TopicAsyncExecutor.class, @@ -465,54 +572,37 @@ void listUnsynchronizedPartialExistingTopics() throws InterruptedException, Exec "ns2-topic1")) .thenReturn(false); - when(aclService.findAllGrantedToNamespace(ns)) - .thenReturn(List.of( - AccessControlEntry.builder() - .spec(AccessControlEntry.AccessControlEntrySpec.builder() - .permission(AccessControlEntry.Permission.OWNER) - .grantedTo("namespace") - .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) - .resourceType(AccessControlEntry.ResourceType.TOPIC) - .resource("ns-") - .build()) - .build(), - AccessControlEntry.builder() - .spec(AccessControlEntry.AccessControlEntrySpec.builder() - .permission(AccessControlEntry.Permission.OWNER) - .grantedTo("namespace") - .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) - .resourceType(AccessControlEntry.ResourceType.TOPIC) - .resource("ns1-topic1") - .build()) - .build() - )); + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) + .thenReturn(acls); // partial number of topics exists into ns4kfk - when(topicRepository.findAllForCluster("local")) - .thenReturn(List.of(t1)); + when(topicRepository.findAllForCluster("local")).thenReturn(List.of(t1)); + when(aclService.isAnyAclOfResource(acls, "ns-topic1")).thenReturn(true); List actual = topicService.listUnsynchronizedTopicNames(ns); assertEquals(2, actual.size()); - // contains + assertTrue(actual.stream().anyMatch(topic -> topic.equals("ns-topic2"))); assertTrue(actual.stream().anyMatch(topic -> topic.equals("ns1-topic1"))); - // doesn't contain - Assertions.assertFalse(actual.stream().anyMatch(topic -> topic.equals("ns-topic1"))); - Assertions.assertFalse(actual.stream().anyMatch(topic -> topic.equals("ns2-topic1"))); + assertFalse(actual.stream().anyMatch(topic -> topic.equals("ns-topic1"))); + assertFalse(actual.stream().anyMatch(topic -> topic.equals("ns2-topic1"))); } @Test - void findCollidingTopicsNoCollision() throws ExecutionException, InterruptedException, TimeoutException { + void shouldNotFindAnyCollidingTopic() throws ExecutionException, InterruptedException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") .cluster("local") .build()) .build(); + Topic topic = Topic.builder() - .metadata(Metadata.builder().name("project1.topic").build()) + .metadata(Metadata.builder() + .name("project1.topic") + .build()) .build(); TopicAsyncExecutor topicAsyncExecutor = Mockito.mock(TopicAsyncExecutor.class); @@ -521,21 +611,22 @@ void findCollidingTopicsNoCollision() throws ExecutionException, InterruptedExce when(topicAsyncExecutor.listBrokerTopicNames()) .thenReturn(List.of("project2.topic", "project1.other")); - List actual = topicService.findCollidingTopics(ns, topic); - - assertTrue(actual.isEmpty()); + assertTrue(topicService.findCollidingTopics(ns, topic).isEmpty()); } @Test - void findCollidingTopicsIdenticalName() throws ExecutionException, InterruptedException, TimeoutException { + void shouldFindCollidingTopicWhenIdenticalName() throws ExecutionException, InterruptedException, TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") .cluster("local") .build()) .build(); + Topic topic = Topic.builder() - .metadata(Metadata.builder().name("project1.topic").build()) + .metadata(Metadata.builder() + .name("project1.topic") + .build()) .build(); TopicAsyncExecutor topicAsyncExecutor = Mockito.mock(TopicAsyncExecutor.class); @@ -550,15 +641,20 @@ void findCollidingTopicsIdenticalName() throws ExecutionException, InterruptedEx } @Test - void findCollidingTopicsCollidingName() throws ExecutionException, InterruptedException, TimeoutException { + void shouldFindCollidingTopicsWhenCollidingName() throws ExecutionException, + InterruptedException, + TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") .cluster("local") .build()) .build(); + Topic topic = Topic.builder() - .metadata(Metadata.builder().name("project1.topic").build()) + .metadata(Metadata.builder() + .name("project1.topic") + .build()) .build(); TopicAsyncExecutor topicAsyncExecutor = Mockito.mock(TopicAsyncExecutor.class); @@ -574,15 +670,20 @@ void findCollidingTopicsCollidingName() throws ExecutionException, InterruptedEx } @Test - void findCollidingTopicsInterruptedException() throws ExecutionException, InterruptedException, TimeoutException { + void shouldHandleInterruptedExceptionWhenFindingCollidingTopics() throws ExecutionException, + InterruptedException, + TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") .cluster("local") .build()) .build(); + Topic topic = Topic.builder() - .metadata(Metadata.builder().name("project1.topic").build()) + .metadata(Metadata.builder() + .name("project1.topic") + .build()) .build(); TopicAsyncExecutor topicAsyncExecutor = Mockito.mock(TopicAsyncExecutor.class); @@ -598,7 +699,9 @@ void findCollidingTopicsInterruptedException() throws ExecutionException, Interr } @Test - void findCollidingTopicsOtherException() throws ExecutionException, InterruptedException, TimeoutException { + void shouldHandleOtherExceptionWhenFindingCollidingTopics() throws ExecutionException, + InterruptedException, + TimeoutException { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -607,7 +710,9 @@ void findCollidingTopicsOtherException() throws ExecutionException, InterruptedE .build(); Topic topic = Topic.builder() - .metadata(Metadata.builder().name("project1.topic").build()) + .metadata(Metadata.builder() + .name("project1.topic") + .build()) .build(); TopicAsyncExecutor topicAsyncExecutor = Mockito.mock(TopicAsyncExecutor.class); @@ -621,7 +726,167 @@ void findCollidingTopicsOtherException() throws ExecutionException, InterruptedE } @Test - void validateDeleteRecordsTopic() { + void shouldFindTopicsWithNameParameter() { + Namespace ns = Namespace.builder() + .metadata(Metadata.builder() + .name("namespace") + .cluster("local") + .build()) + .build(); + + Topic topic1 = Topic.builder() + .metadata(Metadata.builder() + .name("prefix.topic1") + .build()) + .build(); + + Topic topic2 = Topic.builder() + .metadata(Metadata.builder() + .name("prefix.topic2") + .build()) + .build(); + + Topic topic3 = Topic.builder() + .metadata(Metadata.builder() + .name("prefix.topic3") + .build()) + .build(); + + Topic topic4 = Topic.builder() + .metadata(Metadata.builder() + .name("prefix2.topic") + .build()) + .build(); + + List acls = List.of( + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("prefix.") + .build()) + .build() + ); + + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) + .thenReturn(acls); + when(topicRepository.findAllForCluster("local")).thenReturn(List.of(topic1, topic2, topic3, topic4)); + when(aclService.isAnyAclOfResource(acls, "prefix.topic1")).thenReturn(true); + when(aclService.isAnyAclOfResource(acls, "prefix.topic2")).thenReturn(true); + when(aclService.isAnyAclOfResource(acls, "prefix.topic3")).thenReturn(true); + when(aclService.isAnyAclOfResource(acls, "prefix2.topic")).thenReturn(false); + + assertEquals(List.of(topic1, topic2, topic3), topicService.findByWildcardName(ns, "")); + assertEquals(List.of(topic2), topicService.findByWildcardName(ns, "prefix.topic2")); + assertTrue(topicService.findByWildcardName(ns, "topic2.suffix").isEmpty()); // doesn't exist + assertTrue(topicService.findByWildcardName(ns, "prefix2.topic").isEmpty()); // no acl + } + + @Test + void shouldFindTopicsWithWildcardNameParameter() { + Namespace ns = Namespace.builder() + .metadata(Metadata.builder() + .name("namespace") + .cluster("local") + .build()) + .build(); + + Topic topic1 = Topic.builder() + .metadata(Metadata.builder() + .name("prefix1.topic1") + .build()) + .build(); + + Topic topic2 = Topic.builder() + .metadata(Metadata.builder() + .name("prefix1.topic2") + .build()) + .build(); + + Topic topic3 = Topic.builder() + .metadata(Metadata.builder() + .name("prefix1.topic3") + .build()) + .build(); + + Topic topic4 = Topic.builder() + .metadata(Metadata.builder() + .name("prefix2.topic1") + .build()) + .build(); + + Topic topic5 = Topic.builder() + .metadata(Metadata.builder() + .name("prefix2.topic2") + .build()) + .build(); + + Topic topic6 = Topic.builder() + .metadata(Metadata.builder() + .name("topic1") + .build()) + .build(); + + List allTopics = List.of(topic1, topic2, topic3, topic4, topic5, topic6); + + when(aclService.findResourceOwnerGrantedToNamespace(ns, AccessControlEntry.ResourceType.TOPIC)) + .thenReturn(List.of( + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("prefix1.") + .build()) + .build(), + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.PREFIXED) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("prefix2.") + .build()) + .build(), + AccessControlEntry.builder() + .spec(AccessControlEntry.AccessControlEntrySpec.builder() + .permission(AccessControlEntry.Permission.OWNER) + .grantedTo("namespace") + .resourcePatternType(AccessControlEntry.ResourcePatternType.LITERAL) + .resourceType(AccessControlEntry.ResourceType.TOPIC) + .resource("topic1") + .build()) + .build() + )); + when(topicRepository.findAllForCluster("local")).thenReturn(allTopics); + when(aclService.isAnyAclOfResource(any(), any())).thenReturn(true); + + // find one or multiple topics with wildcard + assertEquals(List.of(topic1, topic2, topic3), topicService.findByWildcardName(ns, "prefix1.*")); + assertEquals(List.of(topic1, topic2, topic3), topicService.findByWildcardName(ns, "prefix1.topic?")); + assertEquals(List.of(topic1, topic4, topic6), topicService.findByWildcardName(ns, "*topic1")); + assertEquals(List.of(topic1, topic4), topicService.findByWildcardName(ns, "*.topic1")); + assertEquals(List.of(topic2, topic5), topicService.findByWildcardName(ns, "prefix?.topic2")); + assertEquals(List.of(topic1, topic2, topic3, topic4, topic5), topicService.findByWildcardName(ns, "*.topic?")); + assertEquals(List.of(topic1, topic4, topic6), topicService.findByWildcardName(ns, "*topic1*")); + assertEquals(List.of(topic1, topic2, topic3, topic4, topic5), topicService.findByWildcardName(ns, "*.*")); + assertEquals(allTopics, topicService.findByWildcardName(ns, "*")); + assertEquals(allTopics, topicService.findByWildcardName(ns, "********")); + assertEquals(List.of(topic6), topicService.findByWildcardName(ns, "??????")); // 6-characters topic + + // find no topics with wildcard + assertTrue(topicService.findByWildcardName(ns, "prefix3.*").isEmpty()); // no ACL + assertTrue(topicService.findByWildcardName(ns, "prefix4.*").isEmpty()); // doesn't exist + assertTrue(topicService.findByWildcardName(ns, "*.???").isEmpty()); + assertTrue(topicService.findByWildcardName(ns, ".*").isEmpty()); // .* is regex + assertTrue(topicService.findByWildcardName(ns, "......").isEmpty()); // . is regex + } + + @Test + void shouldValidateDeleteRecords() { Topic topic = Topic.builder() .metadata(Metadata.builder() .name("project1.topic") @@ -640,7 +905,7 @@ void validateDeleteRecordsTopic() { } @Test - void validateTopicUpdatePartitions() { + void shouldNotValidateUpdateTopicPartitions() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -685,7 +950,7 @@ void validateTopicUpdatePartitions() { } @Test - void validateTopicUpdateReplicationFactor() { + void shouldNotValidateUpdateTopicReplicationFactor() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -729,7 +994,7 @@ void validateTopicUpdateReplicationFactor() { } @Test - void validateTopicUpdateCleanupPolicyDeleteToCompactOnCloud() { + void shouldNotValidateUpdateTopicCleanupPolicyDeleteToCompactOnCloud() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -777,7 +1042,7 @@ void validateTopicUpdateCleanupPolicyDeleteToCompactOnCloud() { } @Test - void validateTopicUpdateCleanupPolicyCompactToDeleteOnCloud() { + void shouldValidateUpdateTopicCleanupPolicyCompactToDeleteOnCloud() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -820,7 +1085,7 @@ void validateTopicUpdateCleanupPolicyCompactToDeleteOnCloud() { } @Test - void validateTopicUpdateCleanupPolicyDeleteToCompactOnSelfManaged() { + void shouldValidateUpdateTopicCleanupPolicyDeleteToCompactOnSelfManaged() { Namespace ns = Namespace.builder() .metadata(Metadata.builder() .name("namespace") @@ -863,21 +1128,29 @@ void validateTopicUpdateCleanupPolicyDeleteToCompactOnSelfManaged() { } @Test - void findAll() { + void shouldFindAllTopics() { Topic t1 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .build(); Topic t2 = Topic.builder() - .metadata(Metadata.builder().name("ns-topic2").build()) + .metadata(Metadata.builder() + .name("ns-topic2") + .build()) .build(); Topic t3 = Topic.builder() - .metadata(Metadata.builder().name("ns1-topic1").build()) + .metadata(Metadata.builder() + .name("ns1-topic1") + .build()) .build(); Topic t4 = Topic.builder() - .metadata(Metadata.builder().name("ns2-topic1").build()) + .metadata(Metadata.builder() + .name("ns2-topic1") + .build()) .build(); when(topicRepository.findAll()).thenReturn(List.of(t1, t2, t3, t4)); @@ -951,11 +1224,21 @@ void shouldTagsBeValid() { when(managedClusterProperties.stream()).thenReturn(Stream.of(managedClusterProps)); List validationErrors = topicService.validateTags( - Namespace.builder().metadata( - Metadata.builder().name("namespace").cluster("local").build()).build(), - Topic.builder().metadata( - Metadata.builder().name("ns-topic1").build()).spec(Topic.TopicSpec.builder() - .tags(List.of("TAG_TEST")).build()).build()); + Namespace.builder() + .metadata(Metadata.builder() + .name("namespace") + .cluster("local") + .build()) + .build(), + Topic.builder() + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) + .spec(Topic.TopicSpec.builder() + .tags(List.of("TAG_TEST")) + .build()) + .build()); + assertEquals(0, validationErrors.size()); } @@ -969,9 +1252,12 @@ void shouldTagsBeInvalidWhenNotConfluentCloud() { .build(); Topic topic = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("TAG_TEST")).build()) + .tags(List.of("TAG_TEST")) + .build()) .build(); when(managedClusterProperties.stream()).thenReturn(Stream.of( @@ -986,95 +1272,127 @@ void shouldTagsBeInvalidWhenNotConfluentCloud() { @Test void shouldReturnTrueWhenTagFormatIsValid() { Topic topicWithTag = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("test")).build()) + .tags(List.of("test")) + .build()) .build(); - assertEquals(true, topicService.isTagsFormatValid(topicWithTag)); + assertTrue(topicService.isTagsFormatValid(topicWithTag)); - Topic topicWithSimpliestTag = Topic.builder() - .metadata(Metadata.builder().name("ns-topic2").build()) + Topic topicWithEasiestTag = Topic.builder() + .metadata(Metadata.builder() + .name("ns-topic2") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("A")).build()) + .tags(List.of("A")) + .build()) .build(); - assertEquals(true, topicService.isTagsFormatValid(topicWithSimpliestTag)); + assertTrue(topicService.isTagsFormatValid(topicWithEasiestTag)); Topic topicWithUnderscoreAndNumberTag = Topic.builder() - .metadata(Metadata.builder().name("ns-topic3").build()) + .metadata(Metadata.builder() + .name("ns-topic3") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("TEST1_TAG")).build()) + .tags(List.of("TEST1_TAG")) + .build()) .build(); - assertEquals(true, topicService.isTagsFormatValid(topicWithUnderscoreAndNumberTag)); + assertTrue(topicService.isTagsFormatValid(topicWithUnderscoreAndNumberTag)); Topic topicWithUnderscoreAndUpperLowerCase = Topic.builder() - .metadata(Metadata.builder().name("ns-topic4").build()) + .metadata(Metadata.builder() + .name("ns-topic4") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("t1_T_a_g2")).build()) + .tags(List.of("t1_T_a_g2")) + .build()) .build(); - assertEquals(true, topicService.isTagsFormatValid(topicWithUnderscoreAndUpperLowerCase)); + assertTrue(topicService.isTagsFormatValid(topicWithUnderscoreAndUpperLowerCase)); Topic topicWithMultipleCorrectTags = Topic.builder() - .metadata(Metadata.builder().name("ns-topic5").build()) + .metadata(Metadata.builder() + .name("ns-topic5") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("TEST1", "test2", "tEST_3", "T_a_g")).build()) + .tags(List.of("TEST1", "test2", "tEST_3", "T_a_g")) + .build()) .build(); - assertEquals(true, topicService.isTagsFormatValid(topicWithMultipleCorrectTags)); + assertTrue(topicService.isTagsFormatValid(topicWithMultipleCorrectTags)); } @Test void shouldReturnFalseWhenTagFormatIsInvalid() { Topic topicWithBeginningDigitTag = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("0test")).build()) + .tags(List.of("0test")) + .build()) .build(); - assertEquals(false, topicService.isTagsFormatValid(topicWithBeginningDigitTag)); + assertFalse(topicService.isTagsFormatValid(topicWithBeginningDigitTag)); Topic topicWithBeginningUnderscoreTag = Topic.builder() - .metadata(Metadata.builder().name("ns-topic2").build()) + .metadata(Metadata.builder() + .name("ns-topic2") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("_TEST")).build()) + .tags(List.of("_TEST")) + .build()) .build(); - assertEquals(false, topicService.isTagsFormatValid(topicWithBeginningUnderscoreTag)); + assertFalse(topicService.isTagsFormatValid(topicWithBeginningUnderscoreTag)); Topic topicWithForbiddenCharacterTag = Topic.builder() - .metadata(Metadata.builder().name("ns-topic3").build()) + .metadata(Metadata.builder() + .name("ns-topic3") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("test-tag")).build()) + .tags(List.of("test-tag")) + .build()) .build(); - assertEquals(false, topicService.isTagsFormatValid(topicWithForbiddenCharacterTag)); + assertFalse(topicService.isTagsFormatValid(topicWithForbiddenCharacterTag)); Topic topicWithManyForbiddenCharactersTag = Topic.builder() - .metadata(Metadata.builder().name("ns-topic4").build()) + .metadata(Metadata.builder() + .name("ns-topic4") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("&~#()[]{}-+=*%:.,;!?^°çé")).build()) + .tags(List.of("&~#()[]{}-+=*%:.,;!?^°çé")) + .build()) .build(); - assertEquals(false, topicService.isTagsFormatValid(topicWithManyForbiddenCharactersTag)); + assertFalse(topicService.isTagsFormatValid(topicWithManyForbiddenCharactersTag)); Topic topicWithMultipleIncorrectTags = Topic.builder() - .metadata(Metadata.builder().name("ns-topic5").build()) + .metadata(Metadata.builder().name("ns-topic5") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("test-tag", "TEST.tag", "0TEST")).build()) + .tags(List.of("test-tag", "TEST.tag", "0TEST")) + .build()) .build(); - assertEquals(false, topicService.isTagsFormatValid(topicWithMultipleIncorrectTags)); + assertFalse(topicService.isTagsFormatValid(topicWithMultipleIncorrectTags)); Topic topicWithOneIncorrectAndMultipleCorrectTags = Topic.builder() - .metadata(Metadata.builder().name("ns-topic5").build()) + .metadata(Metadata.builder() + .name("ns-topic5") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("testTag", "0TEST-tag", "TEST")).build()) + .tags(List.of("testTag", "0TEST-tag", "TEST")) + .build()) .build(); - assertEquals(false, topicService.isTagsFormatValid(topicWithOneIncorrectAndMultipleCorrectTags)); + assertFalse(topicService.isTagsFormatValid(topicWithOneIncorrectAndMultipleCorrectTags)); } @Test @@ -1087,9 +1405,12 @@ void shouldTagsBeInvalidWhenFormatIsWrong() { .build(); Topic topic = Topic.builder() - .metadata(Metadata.builder().name("ns-topic1").build()) + .metadata(Metadata.builder() + .name("ns-topic1") + .build()) .spec(Topic.TopicSpec.builder() - .tags(List.of("0TAG-TEST")).build()) + .tags(List.of("0TAG-TEST")) + .build()) .build(); when(managedClusterProperties.stream()).thenReturn(Stream.of( @@ -1104,4 +1425,3 @@ void shouldTagsBeInvalidWhenFormatIsWrong() { } -