From faae645e8869d190d62fb80c8f6ad2e5945815f1 Mon Sep 17 00:00:00 2001 From: John Niang Date: Wed, 13 Jul 2022 15:17:08 +0800 Subject: [PATCH] Add an endpoint to grant permissions to user (#2239) #### What type of PR is this? /kind feature /kind api-change /area core /milestone 2.0 #### What this PR does / why we need it: Add an endpoint to grant permissions to user. #### Which issue(s) this PR fixes: Fixes # #### Special notes for your reviewer: Test steps: 1. Start Halo 2. Check the initial password in the console log 3. Request from browser and you will be redirected to login page 4. Input the username(admin) and the password you got just now 5. Grant permission as you wish #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../run/halo/app/core/extension/Role.java | 13 ++- .../halo/app/core/extension/RoleBinding.java | 71 +++++++++++- .../run/halo/app/core/extension/User.java | 14 ++- .../core/extension/endpoint/UserEndpoint.java | 76 +++++++++++- .../reconciler/RoleBindingReconciler.java | 22 +--- .../halo/app/extension/ExtensionOperator.java | 4 + .../app/core/extension/RoleBindingTest.java | 43 +++++++ .../extension/endpoint/UserEndpointTest.java | 109 ++++++++++++++++++ .../reconciler/RoleBindingReconcilerTest.java | 7 +- .../app/extension/ExtensionOperatorTest.java | 25 ++++ 10 files changed, 355 insertions(+), 29 deletions(-) create mode 100644 src/test/java/run/halo/app/core/extension/RoleBindingTest.java create mode 100644 src/test/java/run/halo/app/extension/ExtensionOperatorTest.java diff --git a/src/main/java/run/halo/app/core/extension/Role.java b/src/main/java/run/halo/app/core/extension/Role.java index e64905b01..be53befd8 100644 --- a/src/main/java/run/halo/app/core/extension/Role.java +++ b/src/main/java/run/halo/app/core/extension/Role.java @@ -1,6 +1,9 @@ package run.halo.app.core.extension; import static java.util.Arrays.compare; +import static run.halo.app.core.extension.Role.GROUP; +import static run.halo.app.core.extension.Role.KIND; +import static run.halo.app.core.extension.Role.VERSION; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -18,13 +21,17 @@ import run.halo.app.extension.GVK; @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) -@GVK(group = "", - version = "v1alpha1", - kind = "Role", +@GVK(group = GROUP, + version = VERSION, + kind = KIND, plural = "roles", singular = "role") public class Role extends AbstractExtension { + public static final String GROUP = ""; + public static final String VERSION = "v1alpha1"; + public static final String KIND = "Role"; + @Schema(required = true) List rules; diff --git a/src/main/java/run/halo/app/core/extension/RoleBinding.java b/src/main/java/run/halo/app/core/extension/RoleBinding.java index 7f99904cf..1d559522e 100644 --- a/src/main/java/run/halo/app/core/extension/RoleBinding.java +++ b/src/main/java/run/halo/app/core/extension/RoleBinding.java @@ -1,13 +1,22 @@ package run.halo.app.core.extension; +import static run.halo.app.core.extension.RoleBinding.GROUP; +import static run.halo.app.core.extension.RoleBinding.KIND; +import static run.halo.app.core.extension.RoleBinding.VERSION; + +import java.util.LinkedList; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import run.halo.app.extension.AbstractExtension; +import run.halo.app.extension.ExtensionOperator; import run.halo.app.extension.GVK; +import run.halo.app.extension.Metadata; /** * RoleBinding references a role, but does not contain it. @@ -20,13 +29,19 @@ import run.halo.app.extension.GVK; @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) -@GVK(group = "", - version = "v1alpha1", - kind = "RoleBinding", +@GVK(group = GROUP, + version = VERSION, + kind = KIND, plural = "rolebindings", singular = "rolebinding") public class RoleBinding extends AbstractExtension { + public static final String GROUP = ""; + + public static final String VERSION = "v1alpha1"; + + public static final String KIND = "RoleBinding"; + /** * Subjects holds references to the objects the role applies to. */ @@ -91,5 +106,55 @@ public class RoleBinding extends AbstractExtension { * Defaults to "rbac.authorization.halo.run" for User and Group subjects. */ String apiGroup; + + public static Predicate isUser(String username) { + return subject -> User.KIND.equals(subject.getKind()) + && User.GROUP.equals(subject.getApiGroup()) + && username.equals(subject.getName()); + } + + public static Predicate containsUser(Set usernames) { + return subject -> User.KIND.equals(subject.getKind()) + && User.GROUP.equals(subject.apiGroup) + && usernames.contains(subject.getName()); + } + } + + public static RoleBinding create(String username, String roleName) { + var metadata = new Metadata(); + metadata.setName(String.join("-", username, roleName, "binding")); + + var roleRef = new RoleRef(); + roleRef.setKind(Role.KIND); + roleRef.setName(roleName); + roleRef.setApiGroup(Role.GROUP); + + var subject = new Subject(); + subject.setKind(User.KIND); + subject.setName(username); + subject.setApiGroup(User.GROUP); + + var binding = new RoleBinding(); + binding.setMetadata(metadata); + binding.setRoleRef(roleRef); + + // keep the subjects mutable + var subjects = new LinkedList(); + subjects.add(subject); + + binding.setSubjects(subjects); + return binding; + } + + public static Predicate containsUser(String username) { + return ExtensionOperator.isNotDeleted().and( + binding -> binding.getSubjects().stream() + .anyMatch(Subject.isUser(username))); + } + + public static Predicate containsUser(Set usernames) { + return ExtensionOperator.isNotDeleted() + .and(binding -> binding.getSubjects().stream() + .anyMatch(Subject.containsUser(usernames))); } } diff --git a/src/main/java/run/halo/app/core/extension/User.java b/src/main/java/run/halo/app/core/extension/User.java index 55da16b58..d7d78012c 100644 --- a/src/main/java/run/halo/app/core/extension/User.java +++ b/src/main/java/run/halo/app/core/extension/User.java @@ -1,5 +1,9 @@ package run.halo.app.core.extension; +import static run.halo.app.core.extension.User.GROUP; +import static run.halo.app.core.extension.User.KIND; +import static run.halo.app.core.extension.User.VERSION; + import io.swagger.v3.oas.annotations.media.Schema; import java.time.Instant; import java.util.List; @@ -17,13 +21,17 @@ import run.halo.app.extension.GVK; @Data @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) -@GVK(group = "", - version = "v1alpha1", - kind = "User", +@GVK(group = GROUP, + version = VERSION, + kind = KIND, singular = "user", plural = "users") public class User extends AbstractExtension { + public static final String GROUP = ""; + public static final String VERSION = "v1alpha1"; + public static final String KIND = "User"; + @Schema(required = true) private UserSpec spec; diff --git a/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java index ea5b30c73..ae91cca94 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java @@ -1,15 +1,26 @@ package run.halo.app.core.extension.endpoint; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; +import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import java.util.HashSet; +import java.util.Set; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Mono; +import run.halo.app.core.extension.Role; +import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.User; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.exception.ExtensionNotFoundException; @@ -35,13 +46,76 @@ public class UserEndpoint implements CustomEndpoint { .bodyValue(user)); } + Mono grantPermission(ServerRequest request) { + var username = request.pathVariable("name"); + return request.bodyToMono(GrantRequest.class) + .switchIfEmpty(Mono.error(() -> new ServerWebInputException("Request body is empty"))) + .flatMap(grant -> { + // preflight check + client.fetch(User.class, username) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + "User " + username + " was not found")); + + grant.roles.forEach(roleName -> client.fetch(Role.class, roleName) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + "Role " + roleName + " was not found"))); + + var bindings = + client.list(RoleBinding.class, RoleBinding.containsUser(username), null); + var bindingToUpdate = new HashSet(); + var bindingToDelete = new HashSet(); + var existingRoles = new HashSet(); + bindings.forEach(binding -> { + var roleName = binding.getRoleRef().getName(); + if (grant.roles.contains(roleName)) { + existingRoles.add(roleName); + return; + } + binding.getSubjects().removeIf(RoleBinding.Subject.isUser(username)); + if (CollectionUtils.isEmpty(binding.getSubjects())) { + // remove it if subjects is empty + bindingToDelete.add(binding); + } else { + bindingToUpdate.add(binding); + } + }); + + bindingToUpdate.forEach(client::update); + bindingToDelete.forEach(client::delete); + + // remove existing roles + var roles = new HashSet<>(grant.roles); + roles.removeAll(existingRoles); + roles.stream() + .map(roleName -> RoleBinding.create(username, roleName)) + .forEach(client::create); + + return ServerResponse.ok().build(); + }); + } + + record GrantRequest(Set roles) { + } + @Override public RouterFunction endpoint() { + var tag = "api.halo.run/v1alpha1/User"; return SpringdocRouteBuilder.route() .GET("/users/-", this::me, builder -> builder.operationId("GetCurrentUserDetail") .description("Get current user detail") - .tag("api.halo.run/v1alpha1/User") + .tag(tag) .response(responseBuilder().implementation(User.class))) + .POST("/users/{name}/permissions", this::grantPermission, + builder -> builder.operationId("GrantPermission") + .description("Grant permissions to user") + .tag(tag) + .parameter(parameterBuilder().in(ParameterIn.PATH).name("name") + .description("User name") + .required(true)) + .requestBody( + requestBodyBuilder().required(true).implementation(GrantRequest.class)) + .response(responseBuilder().implementation(User.class))) .build(); } + } diff --git a/src/main/java/run/halo/app/core/extension/reconciler/RoleBindingReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/RoleBindingReconciler.java index 67fefeb89..1b3b9e3e0 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/RoleBindingReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/RoleBindingReconciler.java @@ -1,14 +1,15 @@ package run.halo.app.core.extension.reconciler; +import static run.halo.app.core.extension.RoleBinding.containsUser; + import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; -import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.data.util.Lazy; +import run.halo.app.core.extension.Role; import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.RoleBinding.Subject; import run.halo.app.core.extension.User; @@ -32,7 +33,7 @@ public class RoleBindingReconciler implements Reconciler { client.fetch(RoleBinding.class, request.name()).ifPresent(roleBinding -> { // get all usernames; var usernames = roleBinding.getSubjects().stream() - .filter(subject -> "User".equals(subject.getKind())) + .filter(subject -> User.KIND.equals(subject.getKind())) .map(Subject::getName) .collect(Collectors.toSet()); @@ -44,7 +45,7 @@ public class RoleBindingReconciler implements Reconciler { var roleNames = bindings.get().stream() .filter(containsUser(username)) .map(RoleBinding::getRoleRef) - .filter(roleRef -> Objects.equals(roleRef.getKind(), "Role")) + .filter(roleRef -> Objects.equals(roleRef.getKind(), Role.KIND)) .map(RoleBinding.RoleRef::getName) .sorted() // we have to use LinkedHashSet below to make sure the sorted above functional @@ -68,17 +69,4 @@ public class RoleBindingReconciler implements Reconciler { return new Result(false, null); } - Predicate containsUser(String username) { - return roleBinding -> roleBinding.getMetadata().getDeletionTimestamp() == null - && roleBinding.getSubjects().stream() - .anyMatch(subject -> "User".equals(subject.getKind()) - && username.equals(subject.getName())); - } - - Predicate containsUser(Set usernames) { - return roleBinding -> roleBinding.getMetadata().getDeletionTimestamp() == null - && roleBinding.getSubjects().stream() - .anyMatch(subject -> "User".equals(subject.getKind()) - && usernames.contains(subject.getName())); - } } diff --git a/src/main/java/run/halo/app/extension/ExtensionOperator.java b/src/main/java/run/halo/app/extension/ExtensionOperator.java index 343f9d3ab..49005dcc1 100644 --- a/src/main/java/run/halo/app/extension/ExtensionOperator.java +++ b/src/main/java/run/halo/app/extension/ExtensionOperator.java @@ -3,6 +3,7 @@ package run.halo.app.extension; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.function.Predicate; import org.springframework.util.StringUtils; /** @@ -91,4 +92,7 @@ public interface ExtensionOperator { return GroupVersionKind.fromAPIVersionAndKind(getApiVersion(), getKind()); } + static Predicate isNotDeleted() { + return ext -> ext.getMetadata().getDeletionTimestamp() == null; + } } diff --git a/src/test/java/run/halo/app/core/extension/RoleBindingTest.java b/src/test/java/run/halo/app/core/extension/RoleBindingTest.java new file mode 100644 index 000000000..1eca22462 --- /dev/null +++ b/src/test/java/run/halo/app/core/extension/RoleBindingTest.java @@ -0,0 +1,43 @@ +package run.halo.app.core.extension; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.Test; +import run.halo.app.extension.Metadata; + +class RoleBindingTest { + + @Test + void shouldContainUser() { + var subject = new RoleBinding.Subject(); + subject.setName("fake-name"); + subject.setApiGroup(""); + subject.setKind("User"); + + var binding = new RoleBinding(); + binding.setMetadata(new Metadata()); + binding.setSubjects(List.of(subject)); + assertTrue(RoleBinding.containsUser("fake-name").test(binding)); + assertFalse(RoleBinding.containsUser("non-exist-fake-name").test(binding)); + } + + @Test + void shouldNotContainUserWhenBindingIsDeleted() { + var subject = new RoleBinding.Subject(); + subject.setName("fake-name"); + subject.setApiGroup(""); + subject.setKind("User"); + + var binding = new RoleBinding(); + var metadata = new Metadata(); + metadata.setDeletionTimestamp(Instant.now()); + binding.setMetadata(metadata); + binding.setSubjects(List.of(subject)); + assertFalse(RoleBinding.containsUser("fake-name").test(binding)); + assertFalse(RoleBinding.containsUser("non-exist-fake-name").test(binding)); + } + +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java b/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java index 85672f00c..7999c6b58 100644 --- a/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java +++ b/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java @@ -1,11 +1,22 @@ package run.halo.app.core.extension.endpoint; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.List; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; @@ -15,6 +26,7 @@ import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.reactive.server.WebTestClient; import run.halo.app.core.extension.Role; +import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.User; import run.halo.app.core.extension.service.RoleService; import run.halo.app.extension.ExtensionClient; @@ -44,6 +56,8 @@ class UserEndpointTest { var role = new Role(); role.setRules(List.of(rule)); when(roleService.getRole(anyString())).thenReturn(role); + // prevent from initializing the super admin. + when(client.fetch(User.class, "admin")).thenReturn(Optional.of(mock(User.class))); } @Test @@ -70,4 +84,99 @@ class UserEndpointTest { .expectBody(User.class) .isEqualTo(user); } + + @Nested + class GrantPermissionEndpointTest { + + @Test + @WithMockUser("fake-user") + void shouldGetBadRequestIfRequestBodyIsEmpty() { + webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions") + .contentType(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isBadRequest(); + + // Why one more time to verify? Because the SuperAdminInitializer will fetch admin user. + verify(client, never()).fetch(same(User.class), eq("fake-user")); + verify(client, never()).fetch(same(Role.class), eq("fake-role")); + } + + @Test + @WithMockUser("fake-user") + void shouldGetNotFoundIfUserNotFound() { + when(client.fetch(User.class, "fake-user")).thenReturn(Optional.empty()); + when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.of(mock(Role.class))); + + webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role"))) + .exchange() + .expectStatus().isNotFound(); + + verify(client, times(1)).fetch(same(User.class), eq("fake-user")); + verify(client, never()).fetch(same(Role.class), eq("fake-role")); + } + + @Test + @WithMockUser("fake-user") + void shouldGetNotFoundIfRoleNotFound() { + when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(mock(User.class))); + when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.empty()); + + webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role"))) + .exchange() + .expectStatus().isNotFound(); + + verify(client, times(1)).fetch(same(User.class), eq("fake-user")); + verify(client, times(1)).fetch(same(Role.class), eq("fake-role")); + } + + @Test + @WithMockUser("fake-user") + void shouldCreateRoleBindingIfNotExist() { + when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(mock(User.class))); + var role = mock(Role.class); + when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.of(role)); + + webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role"))) + .exchange() + .expectStatus().isOk(); + + verify(client, times(1)).fetch(same(User.class), eq("fake-user")); + verify(client, times(1)).fetch(same(Role.class), eq("fake-role")); + verify(client, times(1)).create(RoleBinding.create("fake-user", "fake-role")); + verify(client, never()).update(isA(RoleBinding.class)); + verify(client, never()).delete(isA(RoleBinding.class)); + } + + @Test + @WithMockUser("fake-user") + void shouldDeleteRoleBindingIfNotProvided() { + when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(mock(User.class))); + var role = mock(Role.class); + when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.of(role)); + var roleBinding = RoleBinding.create("fake-user", "non-provided-fake-role"); + when(client.list(same(RoleBinding.class), any(), any())).thenReturn( + List.of(roleBinding)); + + webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role"))) + .exchange() + .expectStatus().isOk(); + + verify(client, times(1)).fetch(same(User.class), eq("fake-user")); + verify(client, times(1)).fetch(same(Role.class), eq("fake-role")); + verify(client, times(1)).create(RoleBinding.create("fake-user", "fake-role")); + verify(client, times(1)).delete(argThat(binding -> + binding.getMetadata().getName().equals(roleBinding.getMetadata().getName()))); + verify(client, never()).update(isA(RoleBinding.class)); + } + + } + } \ No newline at end of file diff --git a/src/test/java/run/halo/app/core/extension/reconciler/RoleBindingReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/RoleBindingReconcilerTest.java index b82c08b22..0ccf1ea4c 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/RoleBindingReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/RoleBindingReconcilerTest.java @@ -137,6 +137,10 @@ class RoleBindingReconcilerTest { var bindingName = "fake-binding-name"; var userName = "fake-user-name"; var subject = mock(Subject.class); + when(subject.getKind()).thenReturn("User"); + when(subject.getName()).thenReturn(userName); + when(subject.getApiGroup()).thenReturn(""); + var user = mock(User.class); var userMetadata = mock(Metadata.class); var binding = createRoleBinding("Role", "fake-role", false, subject); @@ -145,8 +149,6 @@ class RoleBindingReconcilerTest { .thenReturn(Optional.of(binding)); when(binding.getSubjects()).thenReturn(List.of(subject)); - when(subject.getKind()).thenReturn("User"); - when(subject.getName()).thenReturn(userName); when(client.fetch(User.class, userName)).thenReturn(Optional.of(user)); when(user.getMetadata()).thenReturn(userMetadata); var bindings = List.of( @@ -182,6 +184,7 @@ class RoleBindingReconcilerTest { var metadata = mock(Metadata.class); lenient().when(roleRef.getKind()).thenReturn(roleRefKind); lenient().when(roleRef.getName()).thenReturn(roleRefName); + lenient().when(roleRef.getApiGroup()).thenReturn(""); lenient().when(metadata.getDeletionTimestamp()).thenReturn(deleting ? Instant.now() : null); lenient().when(binding.getRoleRef()).thenReturn(roleRef); lenient().when(binding.getMetadata()).thenReturn(metadata); diff --git a/src/test/java/run/halo/app/extension/ExtensionOperatorTest.java b/src/test/java/run/halo/app/extension/ExtensionOperatorTest.java new file mode 100644 index 000000000..08534e89e --- /dev/null +++ b/src/test/java/run/halo/app/extension/ExtensionOperatorTest.java @@ -0,0 +1,25 @@ +package run.halo.app.extension; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import org.junit.jupiter.api.Test; + +class ExtensionOperatorTest { + + @Test + void testIsNotDeleted() { + var ext = mock(ExtensionOperator.class); + var metadata = mock(Metadata.class); + when(metadata.getDeletionTimestamp()).thenReturn(null); + when(ext.getMetadata()).thenReturn(metadata); + + assertTrue(ExtensionOperator.isNotDeleted().test(ext)); + + when(metadata.getDeletionTimestamp()).thenReturn(Instant.now()); + assertFalse(ExtensionOperator.isNotDeleted().test(ext)); + } +} \ No newline at end of file