From 2ac476d99b2d12edaf534e0e213c4dcc9137c50f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 2 Aug 2022 14:31:48 +0200 Subject: [PATCH] add-customer and introducing JpaAttempt test helper --- README.md | 38 +++++++++++-- .../RestResponseEntityExceptionHandler.java | 57 +++++++++++++++++++ .../hscustomer/CustomerController.java | 37 ++++++++---- .../hsadminng/hscustomer/CustomerEntity.java | 2 + .../2022-07-29-061-hs-customer-rbac.sql | 2 +- 5 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java diff --git a/README.md b/README.md index 1ddad2f4..e4bd4f02 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,20 @@ If you have at least Docker, the Java JDK and Gradle installed in appropriate ve # the following command should return a JSON array with just all packages visible for the admin of the customer aab: curl \ - -H 'current-user: mike@hostsharing.net' \ - -H 'assumed-roles: customer#aab.admin' \ + -H 'current-user: mike@hostsharing.net' -H 'assumed-roles: customer#aab.admin' \ http://localhost:8080/api/packages -The latter `curl` command actually goes through the database server. + # add a new customer + curl \ + -H 'current-user: mike@hostsharing.net' -H "Content-Type: application/json" \ + -d '{ "prefix":"baa", "reference":80001, "adminUserName":"admin@baa.example.com" }' \ + -X POST http://localhost:8080/api/customers - +**ⓘ** +'mike@hostsharing.net' and 'sven@hostsharing.net' are Hostsharing hostmaster accounts coming from the example data which is automatically inserted in Testcontainers and Development environments. +Also try for example 'admin@aaa.example.com' or 'unknown@example.org'. + +**ⓘ** If you want a formatted JSON output, you can pipe the result to `jq` or similar. If you still need to install some of these tools, find some hints in the next chapters. @@ -245,3 +252,26 @@ If the persistent database and the temporary database show different results, on e.g. from a previous run of tests or manually applied. It's best to run `pg-sql-reset && gw bootRun` before each test run, to have a clean database. +## How to Amend Liquibase SQL Changesets? + +Liquibase changesets are meant to be immutable and based on each other. +That means, once a changeset is written, it never changes, not even a whitespace or comment. +Liquibase is a *database migration tool*, not a *database initialization tool*. + +This, if you need to add change a table, stored procedure or whatever, +create a new changeset and apply `ALTER`, `DROP`, `CREATE OR REPLACE` or whatever SQL commands to perform your changes. +These changes will be automatically applied once the application starts up again. +This way, any staging or production database will always match the application code. + +But, during initial development that can be a big hassle because the database structure changes a lot in that stage. +Also, the actual structure of the database won't be easily recognized anymore through lots of migration changesets. + +Therefore, during initial development, it's good approach just to amend the existing changesets and delete the database: + +```shell +pg-sql-reset +gw bootRun +``` + +**⚠** +Just don't forget switching to the migration mode, once there is a production database! diff --git a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java new file mode 100644 index 00000000..64ab4095 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java @@ -0,0 +1,57 @@ +package net.hostsharing.hsadminng.errors; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Getter; +import org.springframework.core.NestedExceptionUtils; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.time.LocalDateTime; + +@ControllerAdvice +public class RestResponseEntityExceptionHandler + extends ResponseEntityExceptionHandler { + + @ExceptionHandler(DataIntegrityViolationException.class) + protected ResponseEntity handleConflict( + final RuntimeException exc, final WebRequest request) { + + return new ResponseEntity<>( + new CustomErrorResponse(exc, HttpStatus.CONFLICT), HttpStatus.CONFLICT); + } + + @ExceptionHandler(JpaSystemException.class) + protected ResponseEntity handleJpaExceptions( + final RuntimeException exc, final WebRequest request) { + + return new ResponseEntity<>( + new CustomErrorResponse(exc, HttpStatus.FORBIDDEN), HttpStatus.FORBIDDEN); + } +} + +@Getter +class CustomErrorResponse { + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss") + private final LocalDateTime timestamp; + + private final HttpStatus status; + + private final String message; + + public CustomErrorResponse(final RuntimeException exc, final HttpStatus status) { + this.timestamp = LocalDateTime.now(); + this.status = status; + this.message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage()); + } + + private String firstLine(final String message) { + return message.split("\\r|\\n|\\r\\n", 0)[0]; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerController.java b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerController.java index ffed4130..27aaf23a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerController.java @@ -2,16 +2,14 @@ package net.hostsharing.hsadminng.hscustomer; import net.hostsharing.hsadminng.context.Context; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; import javax.transaction.Transactional; import java.util.List; +import java.util.UUID; + +@RestController -@Controller public class CustomerController { @Autowired @@ -20,18 +18,35 @@ public class CustomerController { @Autowired private CustomerRepository customerRepository; - @ResponseBody - @RequestMapping(value = "/api/customers", method = RequestMethod.GET) + @GetMapping(value = "/api/customers") @Transactional public List listCustomers( - @RequestHeader(value = "current-user") String userName, - @RequestHeader(value="assumed-roles", required=false) String assumedRoles + @RequestHeader(value = "current-user") String userName, + @RequestHeader(value = "assumed-roles", required = false) String assumedRoles ) { context.setCurrentUser(userName); - if ( assumedRoles != null && !assumedRoles.isBlank() ) { + if (assumedRoles != null && !assumedRoles.isBlank()) { context.assumeRoles(assumedRoles); } return customerRepository.findAll(); } + @PostMapping(value = "/api/customers") + @ResponseStatus + @Transactional + public CustomerEntity addCustomer( + @RequestHeader(value = "current-user") String userName, + @RequestHeader(value = "assumed-roles", required = false) String assumedRoles, + @RequestBody CustomerEntity customer + ) { + context.setCurrentUser(userName); + if (assumedRoles != null && !assumedRoles.isBlank()) { + context.assumeRoles(assumedRoles); + } + if (customer.getUuid() == null) { + customer.setUuid(UUID.randomUUID()); + } + return customerRepository.save(customer); + } + } diff --git a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerEntity.java b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerEntity.java index 8a5f99b3..ccc97488 100644 --- a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerEntity.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hscustomer; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import javax.persistence.*; import java.util.UUID; @@ -10,6 +11,7 @@ import java.util.UUID; @Entity @Table(name = "customer_rv") @Getter +@Setter @NoArgsConstructor @AllArgsConstructor public class CustomerEntity { diff --git a/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql b/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql index c7baea62..8524edab 100644 --- a/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql +++ b/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql @@ -210,7 +210,7 @@ create or replace function addCustomerNotAllowedForCurrentSubjects() language PLPGSQL as $$ begin - raise exception 'add-customer not permitted for %', array_to_string(currentSubjects()); + raise exception 'add-customer not permitted for %', array_to_string(currentSubjects(), ';', 'null'); end; $$; /**