diff --git a/service/src/main/java/org/kiwiproject/champagne/App.java b/service/src/main/java/org/kiwiproject/champagne/App.java index cc5e18ca..6584792a 100644 --- a/service/src/main/java/org/kiwiproject/champagne/App.java +++ b/service/src/main/java/org/kiwiproject/champagne/App.java @@ -119,7 +119,7 @@ public void run(AppConfig configuration, Environment environment) { environment.jersey().register(new DeploymentEnvironmentResource(deploymentEnvironmentDao, auditRecordDao, errorDao, manualTaskService)); environment.jersey().register(new HostConfigurationResource(hostDao, componentDao, tagDao, auditRecordDao, errorDao)); environment.jersey().register(new TaskResource(releaseDao, releaseStatusDao, taskDao, taskStatusDao, deploymentEnvironmentDao, auditRecordDao, errorDao)); - environment.jersey().register(new UserResource(userDao, auditRecordDao, errorDao)); + environment.jersey().register(new UserResource(userDao, deployableSystemDao, auditRecordDao, errorDao)); environment.jersey().register(new ApplicationErrorResource(errorDao)); environment.jersey().register(new DeployableSystemResource(deployableSystemDao, userDao, auditRecordDao, errorDao)); environment.jersey().register(new TagResource(tagDao, auditRecordDao, errorDao)); diff --git a/service/src/main/java/org/kiwiproject/champagne/dao/UserDao.java b/service/src/main/java/org/kiwiproject/champagne/dao/UserDao.java index 5e70da43..028780a4 100644 --- a/service/src/main/java/org/kiwiproject/champagne/dao/UserDao.java +++ b/service/src/main/java/org/kiwiproject/champagne/dao/UserDao.java @@ -9,8 +9,10 @@ import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; -import org.kiwiproject.champagne.model.User; +import org.kiwiproject.champagne.dao.mappers.UserInSystemMapper; import org.kiwiproject.champagne.dao.mappers.UserMapper; +import org.kiwiproject.champagne.model.User; +import org.kiwiproject.champagne.model.UserInSystem; @RegisterRowMapper(UserMapper.class) public interface UserDao { @@ -33,4 +35,23 @@ public interface UserDao { @SqlUpdate("delete from users where id = :id") int deleteUser(@Bind("id") long id); + + @SqlQuery("select u.*, usd.system_admin as system_admin from users u left join users_deployable_systems usd on u.id = usd.user_id where usd.deployable_system_id = :systemId offset :offset limit :limit") + @RegisterRowMapper(UserInSystemMapper.class) + List findPagedUsersInSystem(@Bind("systemId") long systemId, @Bind("offset") int offset, @Bind("limit") int limit); + + @SqlQuery("select count(*) from users u left join users_deployable_systems usd on u.id = usd.user_id where usd.deployable_system_id = :systemId") + long countUsersInSystem(@Bind("systemId") long systemId); + + @SqlUpdate("delete from users_deployable_systems where user_id = :userId and deployable_system_id = :systemId") + int removeUserFromSystem(@Bind("userId") long userId, @Bind("systemId") long systemId); + + @SqlUpdate("update users_deployable_systems set system_admin = true where user_id = :userId and deployable_system_id = :systemId") + int makeUserAdminInSystem(@Bind("userId") long userId, @Bind("systemId") long systemId); + + @SqlUpdate("update users_deployable_systems set system_admin = false where user_id = :userId and deployable_system_id = :systemId") + int makeUserNonAdminInSystem(@Bind("userId") long userId, @Bind("systemId") long systemId); + + @SqlUpdate("insert into users_deployable_systems (user_id, deployable_system_id) values (:userId, :systemId)") + int addUserToSystem(@Bind("userId") long userId, @Bind("systemId") long systemId); } diff --git a/service/src/main/java/org/kiwiproject/champagne/dao/mappers/UserInSystemMapper.java b/service/src/main/java/org/kiwiproject/champagne/dao/mappers/UserInSystemMapper.java new file mode 100644 index 00000000..351e3a84 --- /dev/null +++ b/service/src/main/java/org/kiwiproject/champagne/dao/mappers/UserInSystemMapper.java @@ -0,0 +1,34 @@ +package org.kiwiproject.champagne.dao.mappers; + +import static org.kiwiproject.jdbc.KiwiJdbc.instantFromTimestamp; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; +import org.kiwiproject.champagne.model.User; +import org.kiwiproject.champagne.model.UserInSystem; + +public class UserInSystemMapper implements RowMapper { + + @Override + public UserInSystem map(ResultSet r, StatementContext ctx) throws SQLException { + + var user = User.builder() + .id(r.getLong("id")) + .createdAt(instantFromTimestamp(r, "created_at")) + .updatedAt(instantFromTimestamp(r, "updated_at")) + .firstName(r.getString("first_name")) + .lastName(r.getString("last_name")) + .displayName(r.getString("display_name")) + .systemIdentifier(r.getString("system_identifier")) + .admin(r.getBoolean("admin")) + .build(); + + return UserInSystem.builder() + .user(user) + .systemAdmin(r.getBoolean("system_admin")) + .build(); + } +} diff --git a/service/src/main/java/org/kiwiproject/champagne/model/User.java b/service/src/main/java/org/kiwiproject/champagne/model/User.java index e39b2ecb..6fb759b1 100644 --- a/service/src/main/java/org/kiwiproject/champagne/model/User.java +++ b/service/src/main/java/org/kiwiproject/champagne/model/User.java @@ -1,16 +1,15 @@ package org.kiwiproject.champagne.model; import java.time.Instant; +import java.util.List; import jakarta.validation.constraints.NotBlank; import lombok.Builder; import lombok.Value; +import lombok.With; /** * Core model for user information. - * - * @implNote This model is soft-deletable because we will be linking to users to track auditable events (created/updated) and if the user is hard deleted - * then we will lose the pedigree of the changes. */ @Value @Builder @@ -33,4 +32,7 @@ public class User { String systemIdentifier; boolean admin; + + @With + List systems; } diff --git a/service/src/main/java/org/kiwiproject/champagne/model/UserInSystem.java b/service/src/main/java/org/kiwiproject/champagne/model/UserInSystem.java new file mode 100644 index 00000000..5c514ebd --- /dev/null +++ b/service/src/main/java/org/kiwiproject/champagne/model/UserInSystem.java @@ -0,0 +1,18 @@ +package org.kiwiproject.champagne.model; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Delegate; + +/** + * Core model for user information in relation to a specific deployable system. This model delegates a true User object. + */ +@Value +@Builder +public class UserInSystem { + + @Delegate + User user; + + boolean systemAdmin; +} diff --git a/service/src/main/java/org/kiwiproject/champagne/resource/UserResource.java b/service/src/main/java/org/kiwiproject/champagne/resource/UserResource.java index 29017d2a..fb6471be 100644 --- a/service/src/main/java/org/kiwiproject/champagne/resource/UserResource.java +++ b/service/src/main/java/org/kiwiproject/champagne/resource/UserResource.java @@ -2,6 +2,8 @@ import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static org.kiwiproject.base.KiwiPreconditions.requireNotNull; +import static org.kiwiproject.champagne.util.DeployableSystems.checkUserAdminOfSystem; +import static org.kiwiproject.champagne.util.DeployableSystems.getSystemIdOrThrowBadRequest; import static org.kiwiproject.jaxrs.KiwiStandardResponses.standardGetResponse; import static org.kiwiproject.search.KiwiSearching.zeroBasedOffset; @@ -25,9 +27,11 @@ import jakarta.ws.rs.core.Response; import org.dhatim.dropwizard.jwt.cookie.authentication.DefaultJwtCookiePrincipal; import org.kiwiproject.champagne.dao.AuditRecordDao; +import org.kiwiproject.champagne.dao.DeployableSystemDao; import org.kiwiproject.champagne.dao.UserDao; import org.kiwiproject.champagne.model.AuditRecord.Action; import org.kiwiproject.champagne.model.User; +import org.kiwiproject.champagne.model.UserInSystem; import org.kiwiproject.dropwizard.error.dao.ApplicationErrorDao; import org.kiwiproject.spring.data.KiwiPage; @@ -38,14 +42,18 @@ public class UserResource extends AuditableResource { private final UserDao userDao; + private final DeployableSystemDao deployableSystemDao; - public UserResource(UserDao userDao, AuditRecordDao auditRecordDao, ApplicationErrorDao errorDao) { + public UserResource(UserDao userDao, DeployableSystemDao deployableSystemDao, AuditRecordDao auditRecordDao, ApplicationErrorDao errorDao) { super(auditRecordDao, errorDao); this.userDao = userDao; + this.deployableSystemDao = deployableSystemDao; } @GET + @Path("/all") + @RolesAllowed("admin") @Timed @ExceptionMetered public Response listUsers(@QueryParam("pageNumber") @DefaultValue("1") int pageNumber, @@ -54,12 +62,29 @@ public Response listUsers(@QueryParam("pageNumber") @DefaultValue("1") int pageN var offset = zeroBasedOffset(pageNumber, pageSize); var users = userDao.findPagedUsers(offset, pageSize); + users = users.stream().map(user -> user.withSystems(deployableSystemDao.findDeployableSystemsForUser(user.getId()))).toList(); var total = userDao.countUsers(); var page = KiwiPage.of(pageNumber, pageSize, total, users); return Response.ok(page).build(); } + @GET + @Timed + @ExceptionMetered + public Response listUsersInSystem(@QueryParam("pageNumber") @DefaultValue("1") int pageNumber, + @QueryParam("pageSize") @DefaultValue("25") int pageSize) { + + var systemId = getSystemIdOrThrowBadRequest(); + var offset = zeroBasedOffset(pageNumber, pageSize); + + var users = userDao.findPagedUsersInSystem(systemId, offset, pageSize); + var total = userDao.countUsersInSystem(systemId); + + var page = KiwiPage.of(pageNumber, pageSize, total, users); + return Response.ok(page).build(); + } + @POST @RolesAllowed("admin") @Timed @@ -112,4 +137,60 @@ public Response updateUser(@NotNull @Valid User userToUpdate) { return Response.accepted().build(); } + + @DELETE + @Path("/system/{id}") + @Timed + @ExceptionMetered + public Response removeUserFromSystem(@PathParam("id") long id) { + checkUserAdminOfSystem(); + + var systemId = getSystemIdOrThrowBadRequest(); + var removeCount = userDao.removeUserFromSystem(id, systemId); + if (removeCount > 0) { + auditAction(id, UserInSystem.class, Action.DELETED); + } + + return Response.noContent().build(); + } + + @PUT + @Path("/system/{id}") + @Timed + @ExceptionMetered + public Response updateSystemAdminStatus(@PathParam("id") long id, SystemAdminRequest request) { + checkUserAdminOfSystem(); + + var systemId = getSystemIdOrThrowBadRequest(); + var updateCount = 0; + + if (request.admin) { + updateCount = userDao.makeUserAdminInSystem(id, systemId); + } else { + updateCount = userDao.makeUserNonAdminInSystem(id, systemId); + } + + if (updateCount > 0) { + auditAction(id, UserInSystem.class, Action.UPDATED); + } + + return Response.accepted().build(); + } + + public record SystemAdminRequest(boolean admin) {} + + @POST + @Path("/system/{systemId}/{id}") + @RolesAllowed("admin") + @Timed + @ExceptionMetered + public Response addUserToSystem(@PathParam("systemId") long systemId, @PathParam("id") long id) { + var insertCount = userDao.addUserToSystem(id, systemId); + + if (insertCount > 0) { + auditAction(id, User.class, Action.UPDATED); + } + + return Response.accepted().build(); + } } diff --git a/service/src/test/java/org/kiwiproject/champagne/dao/UserDaoTest.java b/service/src/test/java/org/kiwiproject/champagne/dao/UserDaoTest.java index 308247fc..3f488af1 100644 --- a/service/src/test/java/org/kiwiproject/champagne/dao/UserDaoTest.java +++ b/service/src/test/java/org/kiwiproject/champagne/dao/UserDaoTest.java @@ -2,7 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; +import static org.kiwiproject.champagne.util.TestObjects.insertDeployableSystem; import static org.kiwiproject.champagne.util.TestObjects.insertUserRecord; +import static org.kiwiproject.champagne.util.TestObjects.insertUserToDeployableSystemLink; import static org.kiwiproject.collect.KiwiLists.first; import static org.kiwiproject.test.util.DateTimeTestHelper.assertTimeDifferenceWithinTolerance; @@ -12,14 +14,14 @@ import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; import org.jdbi.v3.core.Handle; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; -import org.kiwiproject.champagne.model.User; import org.kiwiproject.champagne.dao.mappers.UserMapper; +import org.kiwiproject.champagne.model.User; import org.kiwiproject.test.junit.jupiter.Jdbi3DaoExtension; import org.kiwiproject.test.junit.jupiter.PostgresLiquibaseTestExtension; @@ -181,7 +183,7 @@ void shouldReturnZeroWhenNoUsersFound() { class DeleteUser { @Test - void shouldDeleteUserSuccessfully(SoftAssertions softly) { + void shouldDeleteUserSuccessfully() { var userId = insertUserRecord(handle, "jdoe"); dao.deleteUser(userId); @@ -192,4 +194,103 @@ void shouldDeleteUserSuccessfully(SoftAssertions softly) { } + @Nested + class FindPagedUsersInSystem { + + @Test + void shouldReturnListOfUsers() { + var systemId = insertDeployableSystem(handle, "kiwi"); + var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar"); + insertUserToDeployableSystemLink(handle, userId, systemId, false); + + var users = dao.findPagedUsersInSystem(systemId, 0, 10); + assertThat(users) + .extracting("systemIdentifier", "firstName", "lastName") + .contains(tuple("fooBar", "Foo", "Bar")); + } + + @Test + void shouldReturnEmptyListWhenNoUsersFound() { + insertUserRecord(handle, "fooBar", "Foo", "Bar"); + + var users = dao.findPagedUsersInSystem(1L, 10, 10); + assertThat(users).isEmpty(); + } + } + + @Nested + class CountUsersInSystem { + + @Test + void shouldReturnCountOfUsers() { + var systemId = insertDeployableSystem(handle, "kiwi"); + var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar"); + insertUserToDeployableSystemLink(handle, userId, systemId, false); + + var count = dao.countUsers(); + assertThat(count).isOne(); + } + + @Test + void shouldReturnZeroWhenNoUsersFound() { + var count = dao.countUsers(); + assertThat(count).isZero(); + } + } + + @Nested + class RemoveUserFromSystem { + + @Test + void shouldRemoveUserFromSystem() { + var systemId = insertDeployableSystem(handle, "kiwi"); + var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar"); + insertUserToDeployableSystemLink(handle, userId, systemId, false); + + var updateCount = dao.removeUserFromSystem(userId, systemId); + assertThat(updateCount).isOne(); + } + } + + @Nested + class MakeUserAdminInSystem { + + @Test + void shouldMakeTheGivenUserAnAdminInTheGivenSystem() { + var systemId = insertDeployableSystem(handle, "kiwi"); + var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar"); + insertUserToDeployableSystemLink(handle, userId, systemId, false); + + var updateCount = dao.makeUserAdminInSystem(userId, systemId); + assertThat(updateCount).isOne(); + } + } + + @Nested + class MakeUserNonAdminInSystem { + + @Test + void shouldMakeTheGivenUserANonAdminInTheGivenSystem() { + var systemId = insertDeployableSystem(handle, "kiwi"); + var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar"); + insertUserToDeployableSystemLink(handle, userId, systemId, true); + + var updateCount = dao.makeUserNonAdminInSystem(userId, systemId); + assertThat(updateCount).isOne(); + } + } + + @Nested + class AddUserToSystem { + + @Test + void shouldAddTheGivenUserToTheGivenSystem() { + var systemId = insertDeployableSystem(handle, "kiwi"); + var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar"); + + var updateCount = dao.addUserToSystem(userId, systemId); + assertThat(updateCount).isOne(); + } + } + } diff --git a/service/src/test/java/org/kiwiproject/champagne/resource/UserResourceTest.java b/service/src/test/java/org/kiwiproject/champagne/resource/UserResourceTest.java index 99f447bf..0c173d8b 100644 --- a/service/src/test/java/org/kiwiproject/champagne/resource/UserResourceTest.java +++ b/service/src/test/java/org/kiwiproject/champagne/resource/UserResourceTest.java @@ -38,11 +38,14 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.kiwiproject.champagne.config.AppConfig; import org.kiwiproject.champagne.dao.AuditRecordDao; +import org.kiwiproject.champagne.dao.DeployableSystemDao; import org.kiwiproject.champagne.dao.UserDao; +import org.kiwiproject.champagne.junit.jupiter.DeployableSystemExtension; import org.kiwiproject.champagne.junit.jupiter.JwtExtension; import org.kiwiproject.champagne.model.AuditRecord; import org.kiwiproject.champagne.model.AuditRecord.Action; import org.kiwiproject.champagne.model.User; +import org.kiwiproject.champagne.model.UserInSystem; import org.kiwiproject.champagne.resource.apps.TestUserApp; import org.kiwiproject.dropwizard.error.dao.ApplicationErrorDao; import org.kiwiproject.dropwizard.error.test.junit.jupiter.ApplicationErrorExtension; @@ -55,9 +58,10 @@ public class UserResourceTest { private static final UserDao USER_DAO = mock(UserDao.class); + private static final DeployableSystemDao SYSTEM_DAO = mock(DeployableSystemDao.class); private static final AuditRecordDao AUDIT_RECORD_DAO = mock(AuditRecordDao.class); private static final ApplicationErrorDao APPLICATION_ERROR_DAO = mock(ApplicationErrorDao.class); - private static final UserResource USER_RESOURCE = new UserResource(USER_DAO, AUDIT_RECORD_DAO, APPLICATION_ERROR_DAO); + private static final UserResource USER_RESOURCE = new UserResource(USER_DAO, SYSTEM_DAO, AUDIT_RECORD_DAO, APPLICATION_ERROR_DAO); private static final ResourceExtension APP = ResourceExtension.builder() .bootstrapLogging(false) @@ -67,10 +71,13 @@ public class UserResourceTest { @RegisterExtension private final JwtExtension jwtExtension = new JwtExtension("bob"); + + @RegisterExtension + public final DeployableSystemExtension deployableSystemExtension = new DeployableSystemExtension(1L, true); @BeforeEach void setUp() { - reset(USER_DAO, AUDIT_RECORD_DAO); + reset(USER_DAO, SYSTEM_DAO, AUDIT_RECORD_DAO); } @Nested @@ -90,7 +97,7 @@ void shouldReturnPagedListOfUsersUsingDefaultPagingParams() { when(USER_DAO.findPagedUsers(0, 25)).thenReturn(List.of(user)); when(USER_DAO.countUsers()).thenReturn(1L); - var response = APP.client().target("/users") + var response = APP.client().target("/users/all") .request() .get(); @@ -104,7 +111,7 @@ void shouldReturnPagedListOfUsersUsingDefaultPagingParams() { var foundUser = first(pagedData.getContent()); assertThat(foundUser) .usingRecursiveComparison() - .ignoringFields("createdAt", "updatedAt") + .ignoringFields("createdAt", "updatedAt", "systems") .isEqualTo(user); verify(USER_DAO).findPagedUsers(0, 25); @@ -127,7 +134,7 @@ void shouldReturnPagedListOfUsersUsingProvidedPagingParams() { when(USER_DAO.findPagedUsers(40, 10)).thenReturn(List.of(user)); when(USER_DAO.countUsers()).thenReturn(41L); - var response = APP.client().target("/users") + var response = APP.client().target("/users/all") .queryParam("pageNumber", 5) .queryParam("pageSize", 10) .request() @@ -143,7 +150,7 @@ void shouldReturnPagedListOfUsersUsingProvidedPagingParams() { var foundUser = first(pagedData.getContent()); assertThat(foundUser) .usingRecursiveComparison() - .ignoringFields("createdAt", "updatedAt") + .ignoringFields("createdAt", "updatedAt", "systems") .isEqualTo(user); verify(USER_DAO).findPagedUsers(40, 10); @@ -153,6 +160,96 @@ void shouldReturnPagedListOfUsersUsingProvidedPagingParams() { } } + @Nested + class ListUsersInSystem { + + @Test + void shouldReturnPagedListOfUsersUsingDefaultPagingParams() { + var user = User.builder() + .id(1L) + .firstName("John") + .lastName("Doe") + .displayName("John Doe") + .createdAt(Instant.now()) + .updatedAt(Instant.now()) + .build(); + + var userInSystem = UserInSystem.builder() + .user(user) + .systemAdmin(false) + .build(); + + when(USER_DAO.findPagedUsersInSystem(1L, 0, 25)).thenReturn(List.of(userInSystem)); + when(USER_DAO.countUsersInSystem(1L)).thenReturn(1L); + + var response = APP.client().target("/users") + .request() + .get(); + + assertOkResponse(response); + + var pagedData = response.readEntity(new GenericType>(){}); + assertThat(pagedData.getNumberOfElements()).isOne(); + assertThat(pagedData.getTotalElements()).isOne(); + assertThat(pagedData.getNumber()).isOne(); + + var foundUser = first(pagedData.getContent()); + assertThat(foundUser) + .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt") + .isEqualTo(user); + + verify(USER_DAO).findPagedUsersInSystem(1L, 0, 25); + verify(USER_DAO).countUsersInSystem(1L); + verifyNoMoreInteractions(USER_DAO); + verifyNoInteractions(AUDIT_RECORD_DAO); + } + + @Test + void shouldReturnPagedListOfUsersUsingProvidedPagingParams() { + var user = User.builder() + .id(1L) + .firstName("John") + .lastName("Doe") + .displayName("John Doe") + .createdAt(Instant.now()) + .updatedAt(Instant.now()) + .build(); + + var userInSystem = UserInSystem.builder() + .user(user) + .systemAdmin(false) + .build(); + + when(USER_DAO.findPagedUsersInSystem(1L, 40, 10)).thenReturn(List.of(userInSystem)); + when(USER_DAO.countUsersInSystem(1L)).thenReturn(41L); + + var response = APP.client().target("/users") + .queryParam("pageNumber", 5) + .queryParam("pageSize", 10) + .request() + .get(); + + assertOkResponse(response); + + var pagedData = response.readEntity(new GenericType>(){}); + assertThat(pagedData.getNumberOfElements()).isOne(); + assertThat(pagedData.getTotalElements()).isEqualTo(41L); + assertThat(pagedData.getNumber()).isEqualTo(5L); + + var foundUser = first(pagedData.getContent()); + assertThat(foundUser) + .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt") + .isEqualTo(user); + + verify(USER_DAO).findPagedUsersInSystem(1L, 40, 10); + verify(USER_DAO).countUsersInSystem(1L); + verifyNoMoreInteractions(USER_DAO); + verifyNoInteractions(AUDIT_RECORD_DAO); + } + } + @Nested class AddUser { @@ -175,7 +272,7 @@ void shouldAddTheGivenUser() { verify(USER_DAO).insertUser(isA(User.class)); - verifyAuditRecorded(1L, Action.CREATED); + verifyAuditRecorded(1L, User.class, Action.CREATED); verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); } @@ -211,7 +308,7 @@ void shouldDeleteGivenUser() { verify(USER_DAO).deleteUser(1L); - verifyAuditRecorded(1L, Action.DELETED); + verifyAuditRecorded(1L, User.class, Action.DELETED); verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); } @@ -234,14 +331,14 @@ void shouldNotAuditIfDeleteDoesNotChangeDB() { } } - private void verifyAuditRecorded(long id, Action action) { + private void verifyAuditRecorded(long id, Class clazz, Action action) { var argCapture = ArgumentCaptor.forClass(AuditRecord.class); verify(AUDIT_RECORD_DAO).insertAuditRecord(argCapture.capture()); var audit = argCapture.getValue(); assertThat(audit.getRecordId()).isEqualTo(id); - assertThat(audit.getRecordType()).isEqualTo(User.class.getSimpleName()); + assertThat(audit.getRecordType()).isEqualTo(clazz.getSimpleName()); assertThat(audit.getAction()).isEqualTo(action); } @@ -350,7 +447,7 @@ void shouldUpdateGivenUser() { verify(USER_DAO).updateUser(any(User.class)); - verifyAuditRecorded(1L, Action.UPDATED); + verifyAuditRecorded(1L, User.class, Action.UPDATED); verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); } @@ -396,4 +493,138 @@ void shouldReturn500WhenMissingId() { verifyNoInteractions(USER_DAO, AUDIT_RECORD_DAO); } } + + @Nested + class RemoveUserFromSystem { + @Test + void shouldRemoveGivenUserFromCurrentSystem() { + when(USER_DAO.removeUserFromSystem(1L, 1L)).thenReturn(1); + + var response = APP.client().target("/users/system/{id}") + .resolveTemplate("id", 1L) + .request() + .delete(); + + assertNoContentResponse(response); + + verify(USER_DAO).removeUserFromSystem(1L, 1L); + + verifyAuditRecorded(1L, UserInSystem.class, Action.DELETED); + + verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); + } + + @Test + void shouldNotAuditIfNothingRemoved() { + when(USER_DAO.removeUserFromSystem(1L, 1L)).thenReturn(0); + + var response = APP.client().target("/users/system/{id}") + .resolveTemplate("id", 1L) + .request() + .delete(); + + assertNoContentResponse(response); + + verify(USER_DAO).removeUserFromSystem(1L, 1L); + + verifyNoInteractions(AUDIT_RECORD_DAO); + verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); + } + } + + @Nested + class UpdateSystemAdminStatus { + @Test + void shouldSetGivenUserToAdminInSystem() { + when(USER_DAO.makeUserAdminInSystem(1L, 1L)).thenReturn(1); + + var response = APP.client().target("/users/system/{id}") + .resolveTemplate("id", 1L) + .request() + .put(json(Map.of("admin", true))); + + assertAcceptedResponse(response); + + verify(USER_DAO).makeUserAdminInSystem(1L, 1L); + + verifyAuditRecorded(1L, UserInSystem.class, Action.UPDATED); + + verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); + } + + @Test + void shouldSetGivenUserToNonAdminInSystem() { + when(USER_DAO.makeUserNonAdminInSystem(1L, 1L)).thenReturn(1); + + var response = APP.client().target("/users/system/{id}") + .resolveTemplate("id", 1L) + .request() + .put(json(Map.of("admin", false))); + + assertAcceptedResponse(response); + + verify(USER_DAO).makeUserNonAdminInSystem(1L, 1L); + + verifyAuditRecorded(1L, UserInSystem.class, Action.UPDATED); + + verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); + } + + @Test + void shouldNotAuditIfNothingUpdated() { + when(USER_DAO.makeUserAdminInSystem(1L, 1L)).thenReturn(0); + + var response = APP.client().target("/users/system/{id}") + .resolveTemplate("id", 1L) + .request() + .put(json(Map.of("admin", true))); + + assertAcceptedResponse(response); + + verify(USER_DAO).makeUserAdminInSystem(1L, 1L); + + verifyNoInteractions(AUDIT_RECORD_DAO); + verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); + } + } + + @Nested + class AddUserToSystem { + @Test + void shouldAddUserToGivenSystem() { + when(USER_DAO.addUserToSystem(1L, 1L)).thenReturn(1); + + var response = APP.client().target("/users/system/{systemId}/{id}") + .resolveTemplate("systemId", 1L) + .resolveTemplate("id", 1L) + .request() + .post(json("")); + + assertAcceptedResponse(response); + + verify(USER_DAO).addUserToSystem(1L, 1L); + + verifyAuditRecorded(1L, User.class, Action.UPDATED); + + verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); + } + + @Test + void shouldNotAuditedIfNothingAdded() { + when(USER_DAO.addUserToSystem(1L, 1L)).thenReturn(0); + + var response = APP.client().target("/users/system/{systemId}/{id}") + .resolveTemplate("systemId", 1L) + .resolveTemplate("id", 1L) + .request() + .post(json("")); + + assertAcceptedResponse(response); + + verify(USER_DAO).addUserToSystem(1L, 1L); + + verifyNoInteractions(AUDIT_RECORD_DAO); + verifyNoMoreInteractions(USER_DAO, AUDIT_RECORD_DAO); + } + } } diff --git a/service/src/test/java/org/kiwiproject/champagne/resource/apps/TestUserApp.java b/service/src/test/java/org/kiwiproject/champagne/resource/apps/TestUserApp.java index f5a06c32..abc05bf3 100644 --- a/service/src/test/java/org/kiwiproject/champagne/resource/apps/TestUserApp.java +++ b/service/src/test/java/org/kiwiproject/champagne/resource/apps/TestUserApp.java @@ -6,6 +6,7 @@ import org.dhatim.dropwizard.jwt.cookie.authentication.JwtCookieAuthBundle; import org.kiwiproject.champagne.config.AppConfig; import org.kiwiproject.champagne.dao.AuditRecordDao; +import org.kiwiproject.champagne.dao.DeployableSystemDao; import org.kiwiproject.champagne.dao.UserDao; import org.kiwiproject.champagne.resource.AuthResource; import org.kiwiproject.champagne.resource.UserResource; @@ -14,6 +15,7 @@ public class TestUserApp extends Application { public static UserDao userDao; + public static DeployableSystemDao deployableSystemDao; public static AuditRecordDao auditRecordDao; public static ApplicationErrorDao errorDao; @@ -25,7 +27,7 @@ public void initialize(Bootstrap bootstrap) { @Override public void run(AppConfig appConfig, Environment environment) { - environment.jersey().register(new UserResource(userDao, auditRecordDao, errorDao)); + environment.jersey().register(new UserResource(userDao, deployableSystemDao, auditRecordDao, errorDao)); environment.jersey().register(new AuthResource(userDao)); } } diff --git a/ui/src/components/Dropdowns/TableActionsDropdown.vue b/ui/src/components/Dropdowns/TableActionsDropdown.vue index 7f5b6415..4e3de70b 100644 --- a/ui/src/components/Dropdowns/TableActionsDropdown.vue +++ b/ui/src/components/Dropdowns/TableActionsDropdown.vue @@ -4,7 +4,7 @@
- + {{ action.label }} diff --git a/ui/src/components/SideBar/SideBar.vue b/ui/src/components/SideBar/SideBar.vue index 4a20a790..5d2201fe 100644 --- a/ui/src/components/SideBar/SideBar.vue +++ b/ui/src/components/SideBar/SideBar.vue @@ -251,6 +251,23 @@