add-customer and introducing JpaAttempt test helper

This commit is contained in:
Michael Hoennig 2022-08-02 14:31:48 +02:00
parent 03ee2cfd62
commit 2ac476d99b
5 changed files with 120 additions and 16 deletions

View File

@ -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
<big>&#9432;</big>
<big>**&#9432;**</big>
'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'.
<big>**&#9432;**</big>
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
```
<big>**&#9888;**</big>
Just don't forget switching to the migration mode, once there is a production database!

View File

@ -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<CustomErrorResponse> handleConflict(
final RuntimeException exc, final WebRequest request) {
return new ResponseEntity<>(
new CustomErrorResponse(exc, HttpStatus.CONFLICT), HttpStatus.CONFLICT);
}
@ExceptionHandler(JpaSystemException.class)
protected ResponseEntity<CustomErrorResponse> 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];
}
}

View File

@ -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<CustomerEntity> 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);
}
}

View File

@ -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 {

View File

@ -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; $$;
/**