adds HsOfficePartner
This commit is contained in:
parent
d3312c4444
commit
c3195662dd
1
.aliases
1
.aliases
@ -25,3 +25,4 @@ alias pg-sql-reset='pg-sql-stop; pg-sql-remove; pg-sql-run'
|
||||
alias pg-sql-backup='docker exec -i hsadmin-ng-postgres /usr/bin/pg_dump --clean --create -U postgres postgres | gzip -9'
|
||||
alias pg-sql-restore='gunzip --stdout | docker exec -i hsadmin-ng-postgres psql -U postgres -d postgres'
|
||||
|
||||
alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l'
|
||||
|
64
README.md
64
README.md
@ -9,6 +9,7 @@ For architecture consider the files in the `doc` and `adr` folder.
|
||||
- [PostgreSQL Server](#postgresql-server)
|
||||
- [Markdown](#markdown)
|
||||
- [Render Markdown embedded PlantUML](#render-markdown-embedded-plantuml)
|
||||
- [Render Markdown Embedded Mermaid Diagrams](#render-markdown-embedded-mermaid-diagrams)
|
||||
- [IDE Specific Settings](#ide-specific-settings)
|
||||
- [IntelliJ IDEA](#intellij-idea)
|
||||
- [Other Tools](#other-tools)
|
||||
@ -27,6 +28,7 @@ For architecture consider the files in the `doc` and `adr` folder.
|
||||
- [Dependency-License-Compatibility](#dependency-license-compatibility)
|
||||
- [Dependency Version Upgrade](#dependency-version-upgrade)
|
||||
- [How To ...](#how-to-...)
|
||||
- [How to Configure .pgpass for the Default PostgreSQL Database?](#how-to-configure-.pgpass-for-the-default-postgresql-database?)
|
||||
- [How to Run the Tests Against a Local User-Space Podman Daemon?](#how-to-run-the-tests-against-a-local-user-space-podman-daemon?)
|
||||
- [Install and Run Podman](#install-and-run-podman)
|
||||
- [Use the Command Line to Run the Tests Against the Podman Daemon ](#use-the-command-line-to-run-the-tests-against-the-podman-daemon-)
|
||||
@ -35,7 +37,8 @@ For architecture consider the files in the `doc` and `adr` folder.
|
||||
- [How to Run the Tests Against a Remote Podman or Docker Daemon?](#how-to-run-the-tests-against-a-remote-podman-or-docker-daemon?)
|
||||
- [How to Run the Application on a Different Port?](#how-to-run-the-application-on-a-different-port?)
|
||||
- [How to Use a Persistent Database for Integration Tests?](#how-to-use-a-persistent-database-for-integration-tests?)
|
||||
- [How to Amend Liquibase SQL Changesets?](#how-to-amend-liquibase-sql-changesets?)
|
||||
- [How to Amend Liquibase SQL Changesets?](#how-to-amend-liquibase-sql-changesets?)
|
||||
- [How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?](#how-to-re-generate-spring-controller-interfaces-from-openapi-specs?)
|
||||
- [Further Documentation](#further-documentation)
|
||||
<!-- generated TOC end. -->
|
||||
|
||||
@ -190,6 +193,12 @@ Again, given the container is running, to restore the backup from ~/backup, run:
|
||||
To generate the TOC (Table of Contents), a little bash script from a
|
||||
[Blog Article](https://medium.com/@acrodriguez/one-liner-to-generate-a-markdown-toc-f5292112fd14) was used.
|
||||
|
||||
Given this is in PATH as `md-toc`, use:
|
||||
|
||||
```shell
|
||||
md-toc <README.md 2 4 | sed -e 's/^ //g'
|
||||
```
|
||||
|
||||
To render the Markdown files, especially to watch embedded PlantUML diagrams, you can use one of the following methods:
|
||||
|
||||
#### Render Markdown embedded PlantUML
|
||||
@ -233,6 +242,39 @@ pandoc --filter pandoc-plantuml rbac.md -o rbac.pdf
|
||||
|
||||
If you have figured out how it works, please add instructions above this section.
|
||||
|
||||
#### Render Markdown Embedded Mermaid Diagrams
|
||||
|
||||
The source of RBAC role diagrams are much easier to read with Mermaid than with PlantUML or GraphViz, that's the main reason Mermaid ist used too.
|
||||
|
||||
Can you see the following diagram right in your IDE?
|
||||
I mean a real graphic diagram, not just some markup code.
|
||||
@startuml
|
||||
me -> you: Can you see this diagram?
|
||||
you -> me: Sorry, I don't :-(
|
||||
me -> you: Install some tooling!
|
||||
@enduml
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
A[Can you see this diagram?];
|
||||
A --> yes;
|
||||
A --> no;
|
||||
no --> F[Follow the instructions below!]
|
||||
F --> yes
|
||||
yes --> E[Then everything is fine.]
|
||||
```
|
||||
|
||||
If not, you need to install some tooling.
|
||||
|
||||
##### for IntelliJ IDEA (or derived products)
|
||||
|
||||
You just need the bundled Markdown plugin enabled and install and activate the Mermaid plugin in its [settings](jetbrains://idea/settings?name=Languages+%26+Frameworks--Markdown).
|
||||
|
||||
|
||||
##### for other IDEs / command-line / operating systems
|
||||
|
||||
If you have figured out how it works, please add instructions above this section.
|
||||
|
||||
### IDE Specific Settings
|
||||
|
||||
#### IntelliJ IDEA
|
||||
@ -643,8 +685,8 @@ If the persistent database and the temporary database show different results, on
|
||||
2. You might have changes in the database which interfere with the tests,
|
||||
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?
|
||||
|
||||
### 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.
|
||||
@ -668,6 +710,22 @@ gw bootRun
|
||||
<big>**⚠**</big>
|
||||
Just don't forget switching to the migration mode, once there is a production database!
|
||||
|
||||
### How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?
|
||||
|
||||
The API is described as OpenAPI specifications in `src/main/resources/api-definition/`.
|
||||
|
||||
Once generated, the interfaces for the Spring-Controllers can be found in `build/generated/sources/openapi`.
|
||||
|
||||
These interfaces have to be implemented by subclasses named `*Controller`.
|
||||
|
||||
All gradle tasks which need the generated interfaces depend on the Gradle task `processSpring` which controls the code generation.
|
||||
It can also be executed directly:
|
||||
|
||||
```shell
|
||||
gw processSpring
|
||||
```
|
||||
|
||||
|
||||
## Further Documentation
|
||||
|
||||
- the `doc` directory contains architecture concepts and a glossary
|
||||
|
@ -0,0 +1,12 @@
|
||||
package net.hostsharing.hsadminng.errors;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface DisplayName {
|
||||
String value() default "";
|
||||
}
|
@ -6,15 +6,18 @@ 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.JpaObjectRetrievalFailureException;
|
||||
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 javax.persistence.EntityNotFoundException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ControllerAdvice
|
||||
public class RestResponseEntityExceptionHandler
|
||||
@ -42,6 +45,41 @@ public class RestResponseEntityExceptionHandler
|
||||
return errorResponse(request, HttpStatus.NOT_FOUND, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler({ JpaObjectRetrievalFailureException.class, EntityNotFoundException.class })
|
||||
protected ResponseEntity<CustomErrorResponse> handleJpaObjectRetrievalFailureException(
|
||||
final RuntimeException exc, final WebRequest request) {
|
||||
final var message =
|
||||
userReadableEntityClassName(
|
||||
firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage()));
|
||||
return errorResponse(request, HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
private String userReadableEntityClassName(final String exceptionMessage) {
|
||||
final var regex = "(net.hostsharing.hsadminng.[a-z0-9_.]*.[A-Za-z0-9_$]*Entity) ";
|
||||
final var pattern = Pattern.compile(regex);
|
||||
final var matcher = pattern.matcher(exceptionMessage);
|
||||
if (matcher.find()) {
|
||||
final var entityName = matcher.group(1);
|
||||
final var entityClass = resolveClassOrNull(entityName);
|
||||
if (entityClass != null ) {
|
||||
return (entityClass.isAnnotationPresent(DisplayName.class)
|
||||
? exceptionMessage.replace(entityName, entityClass.getAnnotation(DisplayName.class).value())
|
||||
: exceptionMessage.replace(entityName, entityClass.getSimpleName()))
|
||||
.replace(" with id ", " with uuid ");
|
||||
}
|
||||
|
||||
}
|
||||
return exceptionMessage;
|
||||
}
|
||||
|
||||
private static Class<?> resolveClassOrNull(final String entityName) {
|
||||
try {
|
||||
return ClassLoader.getSystemClassLoader().loadClass(entityName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(Throwable.class)
|
||||
protected ResponseEntity<CustomErrorResponse> handleOtherExceptions(
|
||||
final Throwable exc, final WebRequest request) {
|
||||
|
@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.Stringify;
|
||||
import net.hostsharing.hsadminng.Stringifyable;
|
||||
|
||||
@ -21,6 +22,7 @@ import static net.hostsharing.hsadminng.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayName("Contact")
|
||||
public class HsOfficeContactEntity implements Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact")
|
||||
|
@ -0,0 +1,150 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.Mapper;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitorsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntityPatcher;
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.Mapper.map;
|
||||
|
||||
@RestController
|
||||
|
||||
public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
|
||||
@Autowired
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeDebitorRepository debitorRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePartnerRepository partnerRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeContactRepository contactRepo;
|
||||
|
||||
@Autowired
|
||||
private EntityManager em;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<List<HsOfficeDebitorResource>> listDebitors(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final String name,
|
||||
final Integer debitorNumber) {
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var entities = debitorNumber != null
|
||||
? debitorRepo.findDebitorByDebitorNumber(debitorNumber)
|
||||
: debitorRepo.findDebitorByOptionalNameLike(name);
|
||||
|
||||
final var resources = Mapper.mapList(entities, HsOfficeDebitorResource.class,
|
||||
DEBITOR_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResponseEntity<HsOfficeDebitorResource> addDebitor(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final HsOfficeDebitorInsertResource body) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var entityToSave = map(body, HsOfficeDebitorEntity.class, DEBITOR_RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
entityToSave.setUuid(UUID.randomUUID());
|
||||
|
||||
final var saved = debitorRepo.save(entityToSave);
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
.path("/api/hs/office/debitors/{id}")
|
||||
.buildAndExpand(entityToSave.getUuid())
|
||||
.toUri();
|
||||
final var mapped = map(saved, HsOfficeDebitorResource.class,
|
||||
DEBITOR_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<HsOfficeDebitorResource> getDebitorByUuid(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final UUID debitorUuid) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var result = debitorRepo.findByUuid(debitorUuid);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(map(result.get(), HsOfficeDebitorResource.class, DEBITOR_ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResponseEntity<Void> deleteDebitorByUuid(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final UUID debitorUuid) {
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var result = debitorRepo.deleteByUuid(debitorUuid);
|
||||
if (result == 0) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResponseEntity<HsOfficeDebitorResource> patchDebitor(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final UUID debitorUuid,
|
||||
final HsOfficeDebitorPatchResource body) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow();
|
||||
|
||||
new HsOfficeDebitorEntityPatcher(em, current).apply(body);
|
||||
|
||||
final var saved = debitorRepo.save(current);
|
||||
final var mapped = map(saved, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
|
||||
final BiConsumer<HsOfficeDebitorEntity, HsOfficeDebitorResource> DEBITOR_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setPartner(map(entity.getPartner(), HsOfficePartnerResource.class));
|
||||
resource.setBillingContact(map(entity.getBillingContact(), HsOfficeContactResource.class));
|
||||
};
|
||||
|
||||
final BiConsumer<HsOfficeDebitorInsertResource, HsOfficeDebitorEntity> DEBITOR_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.setPartner(em.getReference(HsOfficePartnerEntity.class, resource.getPartnerUuid()));
|
||||
entity.setBillingContact(em.getReference(HsOfficeContactEntity.class, resource.getBillingContactUuid()));
|
||||
};
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.Stringify;
|
||||
import net.hostsharing.hsadminng.Stringifyable;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(name = "hs_office_debitor_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("Debitor")
|
||||
public class HsOfficeDebitorEntity implements Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficeDebitorEntity> stringify =
|
||||
stringify(HsOfficeDebitorEntity.class, "debitor")
|
||||
.withProp(HsOfficeDebitorEntity::getDebitorNumber)
|
||||
.withProp(HsOfficeDebitorEntity::getPartner)
|
||||
.withSeparator(": ")
|
||||
.quotedValues(false);
|
||||
|
||||
private @Id UUID uuid;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "partneruuid")
|
||||
private HsOfficePartnerEntity partner;
|
||||
|
||||
private @Column(name = "debitornumber") Integer debitorNumber;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "billingcontactuuid")
|
||||
private HsOfficeContactEntity billingContact;
|
||||
|
||||
private @Column(name = "vatid") String vatId;
|
||||
private @Column(name = "vatcountrycode") String vatCountryCode;
|
||||
private @Column(name = "vatbusiness") boolean vatBusiness;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return debitorNumber.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.OptionalFromJson;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
|
||||
class HsOfficeDebitorEntityPatcher implements EntityPatcher<HsOfficeDebitorPatchResource> {
|
||||
|
||||
private final EntityManager em;
|
||||
private final HsOfficeDebitorEntity entity;
|
||||
|
||||
HsOfficeDebitorEntityPatcher(
|
||||
final EntityManager em,
|
||||
final HsOfficeDebitorEntity entity) {
|
||||
this.em = em;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(final HsOfficeDebitorPatchResource resource) {
|
||||
OptionalFromJson.of(resource.getBillingContactUuid()).ifPresent(newValue -> {
|
||||
verifyNotNull(newValue, "billingContact");
|
||||
entity.setBillingContact(em.getReference(HsOfficeContactEntity.class, newValue));
|
||||
});
|
||||
OptionalFromJson.of(resource.getVatId()).ifPresent(entity::setVatId);
|
||||
OptionalFromJson.of(resource.getVatCountryCode()).ifPresent(entity::setVatCountryCode);
|
||||
OptionalFromJson.of(resource.getVatBusiness()).ifPresent(newValue -> {
|
||||
verifyNotNull(newValue, "vatBusiness");
|
||||
entity.setVatBusiness(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
private void verifyNotNull(final Object newValue, final String propertyName) {
|
||||
if (newValue == null) {
|
||||
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEntity, UUID> {
|
||||
|
||||
Optional<HsOfficeDebitorEntity> findByUuid(UUID id);
|
||||
|
||||
@Query("""
|
||||
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
||||
WHERE debitor.debitorNumber = :debitorNumber
|
||||
""")
|
||||
List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int debitorNumber);
|
||||
|
||||
@Query("""
|
||||
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
||||
JOIN HsOfficePartnerEntity partner ON partner.uuid = debitor.partner
|
||||
JOIN HsOfficePersonEntity person ON person.uuid = partner.person
|
||||
JOIN HsOfficeContactEntity contact ON contact.uuid = debitor.billingContact
|
||||
WHERE :name is null
|
||||
OR partner.birthName like concat(:name, '%')
|
||||
OR person.tradeName like concat(:name, '%')
|
||||
OR person.familyName like concat(:name, '%')
|
||||
OR person.givenName like concat(:name, '%')
|
||||
OR contact.label like concat(:name, '%')
|
||||
""")
|
||||
List<HsOfficeDebitorEntity> findDebitorByOptionalNameLike(String name);
|
||||
|
||||
HsOfficeDebitorEntity save(final HsOfficeDebitorEntity entity);
|
||||
|
||||
long count();
|
||||
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
@ -65,6 +65,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
|
||||
final var entityToSave = mapToHsOfficePartnerEntity(body);
|
||||
entityToSave.setUuid(UUID.randomUUID());
|
||||
// TODO.impl: use getReference
|
||||
entityToSave.setContact(contactRepo.findByUuid(body.getContactUuid()).orElseThrow(
|
||||
() -> new NoSuchElementException("cannot find contact uuid " + body.getContactUuid())
|
||||
));
|
||||
@ -141,6 +142,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
resource.setContact(map(entity.getContact(), HsOfficeContactResource.class));
|
||||
};
|
||||
|
||||
// TODO.impl: user postmapper + getReference
|
||||
private HsOfficePartnerEntity mapToHsOfficePartnerEntity(final HsOfficePartnerInsertResource resource) {
|
||||
final var entity = new HsOfficePartnerEntity();
|
||||
entity.setBirthday(resource.getBirthday());
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.Stringify;
|
||||
import net.hostsharing.hsadminng.Stringifyable;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
@ -19,6 +20,7 @@ import static net.hostsharing.hsadminng.Stringify.stringify;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("Partner")
|
||||
public class HsOfficePartnerEntity implements Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficePartnerEntity> stringify = stringify(HsOfficePartnerEntity.class, "partner")
|
||||
|
@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.person;
|
||||
import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.Stringify;
|
||||
import net.hostsharing.hsadminng.Stringifyable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@ -26,6 +27,7 @@ import static net.hostsharing.hsadminng.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayName("Person")
|
||||
public class HsOfficePersonEntity implements Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
|
||||
@ -50,10 +52,6 @@ public class HsOfficePersonEntity implements Stringifyable {
|
||||
@Column(name = "givenname")
|
||||
private String givenName;
|
||||
|
||||
public String getDisplayName() {
|
||||
return toShortString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString.apply(this);
|
||||
|
@ -20,3 +20,5 @@ map:
|
||||
null: org.openapitools.jackson.nullable.JsonNullable
|
||||
/api/hs/office/relationships/{relationshipUUID}:
|
||||
null: org.openapitools.jackson.nullable.JsonNullable
|
||||
/api/hs/office/debitors/{debitorUUID}:
|
||||
null: org.openapitools.jackson.nullable.JsonNullable
|
||||
|
@ -0,0 +1,70 @@
|
||||
|
||||
components:
|
||||
|
||||
schemas:
|
||||
|
||||
HsOfficeDebitor:
|
||||
type: object
|
||||
properties:
|
||||
uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
debitorNumber:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 10000
|
||||
maximum: 99999
|
||||
partner:
|
||||
$ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner'
|
||||
billingContact:
|
||||
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
|
||||
vatId:
|
||||
type: string
|
||||
vatCountryCode:
|
||||
type: string
|
||||
pattern: '^[A_Z][A-Z]$'
|
||||
vatBusiness:
|
||||
type: boolean
|
||||
|
||||
HsOfficeDebitorPatch:
|
||||
type: object
|
||||
properties:
|
||||
billingContactUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
vatId:
|
||||
type: string
|
||||
nullable: true
|
||||
vatCountryCode:
|
||||
type: string
|
||||
pattern: '^[A_Z][A-Z]$'
|
||||
nullable: true
|
||||
vatBusiness:
|
||||
type: boolean
|
||||
nullable: true
|
||||
|
||||
HsOfficeDebitorInsert:
|
||||
type: object
|
||||
properties:
|
||||
partnerUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
billingContactUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
debitorNumber:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 10000
|
||||
maximum: 99999
|
||||
vatId:
|
||||
type: string
|
||||
vatCountryCode:
|
||||
type: string
|
||||
pattern: '^[A_Z][A-Z]$'
|
||||
vatBusiness:
|
||||
type: boolean
|
||||
required:
|
||||
- partnerUuid
|
||||
- billingContactUuid
|
@ -0,0 +1,83 @@
|
||||
get:
|
||||
tags:
|
||||
- hs-office-debitors
|
||||
description: 'Fetch a single debitor by its uuid, if visible for the current subject.'
|
||||
operationId: getDebitorByUuid
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: debitorUUID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUID of the debitor to fetch.
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitor'
|
||||
|
||||
"401":
|
||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||
"403":
|
||||
$ref: './error-responses.yaml#/components/responses/Forbidden'
|
||||
|
||||
patch:
|
||||
tags:
|
||||
- hs-office-debitors
|
||||
description: 'Updates a single debitor by its uuid, if permitted for the current subject.'
|
||||
operationId: patchDebitor
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: debitorUUID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitorPatch'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitor'
|
||||
"401":
|
||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||
"403":
|
||||
$ref: './error-responses.yaml#/components/responses/Forbidden'
|
||||
|
||||
delete:
|
||||
tags:
|
||||
- hs-office-debitors
|
||||
description: 'Delete a single debitor by its uuid, if permitted for the current subject.'
|
||||
operationId: deleteDebitorByUuid
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: debitorUUID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUID of the debitor to delete.
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||
"403":
|
||||
$ref: './error-responses.yaml#/components/responses/Forbidden'
|
||||
"404":
|
||||
$ref: './error-responses.yaml#/components/responses/NotFound'
|
@ -0,0 +1,62 @@
|
||||
get:
|
||||
summary: Returns a list of (optionally filtered) debitors.
|
||||
description: Returns the list of (optionally filtered) debitors which are visible to the current user or any of it's assumed roles.
|
||||
tags:
|
||||
- hs-office-debitors
|
||||
operationId: listDebitors
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: name
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: Prefix of name properties from person or contact to filter the results.
|
||||
- name: debitorNumber
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
description: Debitor number of the requested debitor.
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: './hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitor'
|
||||
"401":
|
||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||
"403":
|
||||
$ref: './error-responses.yaml#/components/responses/Forbidden'
|
||||
|
||||
post:
|
||||
summary: Adds a new debitor.
|
||||
tags:
|
||||
- hs-office-debitors
|
||||
operationId: addDebitor
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
requestBody:
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitorInsert'
|
||||
required: true
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitor'
|
||||
"401":
|
||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||
"403":
|
||||
$ref: './error-responses.yaml#/components/responses/Forbidden'
|
||||
"409":
|
||||
$ref: './error-responses.yaml#/components/responses/Conflict'
|
@ -35,7 +35,6 @@ paths:
|
||||
$ref: "./hs-office-persons-with-uuid.yaml"
|
||||
|
||||
|
||||
|
||||
# Relationships
|
||||
|
||||
/api/hs/office/relationships:
|
||||
@ -44,3 +43,11 @@ paths:
|
||||
/api/hs/office/relationships/{relationshipUUID}:
|
||||
$ref: "./hs-office-relationships-with-uuid.yaml"
|
||||
|
||||
|
||||
# Debitors
|
||||
|
||||
/api/hs/office/debitors:
|
||||
$ref: "./hs-office-debitors.yaml"
|
||||
|
||||
/api/hs/office/debitors/{debitorUUID}:
|
||||
$ref: "./hs-office-debitors-with-uuid.yaml"
|
||||
|
@ -63,6 +63,7 @@ do language plpgsql $$
|
||||
call createHsOfficePersonTestData('NATURAL', null, 'Smith', 'Peter');
|
||||
call createHsOfficePersonTestData('LEGAL', 'Second e.K.', 'Sandra', 'Miller');
|
||||
call createHsOfficePersonTestData('SOLE_REPRESENTATION', 'Third OHG');
|
||||
call createHsOfficePersonTestData('SOLE_REPRESENTATION', 'Fourth e.G.');
|
||||
call createHsOfficePersonTestData('JOINT_REPRESENTATION', 'Erben Bessler', 'Mel', 'Bessler');
|
||||
call createHsOfficePersonTestData('NATURAL', null, 'Bessler', 'Anita');
|
||||
call createHsOfficePersonTestData('NATURAL', null, 'Winkler', 'Paul');
|
||||
|
@ -64,10 +64,9 @@ end; $$;
|
||||
do language plpgsql $$
|
||||
begin
|
||||
call createHsOfficePartnerTestData('First GmbH', 'first contact');
|
||||
|
||||
call createHsOfficePartnerTestData('Second e.K.', 'second contact');
|
||||
|
||||
call createHsOfficePartnerTestData('Third OHG', 'third contact');
|
||||
call createHsOfficePartnerTestData('Fourth e.G.', 'forth contact');
|
||||
end;
|
||||
$$;
|
||||
--//
|
||||
|
18
src/main/resources/db/changelog/270-hs-office-debitor.sql
Normal file
18
src/main/resources/db/changelog/270-hs-office-debitor.sql
Normal file
@ -0,0 +1,18 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-MAIN-TABLE:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create table hs_office_debitor
|
||||
(
|
||||
uuid uuid unique references RbacObject (uuid) initially deferred,
|
||||
partnerUuid uuid not null references hs_office_partner(uuid),
|
||||
debitorNumber numeric(5) not null,
|
||||
billingContactUuid uuid not null references hs_office_contact(uuid),
|
||||
vatId varchar(24), -- TODO.spec: here or in person?
|
||||
vatCountryCode varchar(2),
|
||||
vatBusiness boolean not null -- TODO.spec: more of such?
|
||||
-- TODO.impl: SEPA-mandate + bank account
|
||||
);
|
||||
--//
|
@ -0,0 +1,24 @@
|
||||
### hs_office_debitor RBAC Roles
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
%% role:debitor.owner
|
||||
role:debitor.owner --> perm:debitor.*;
|
||||
role:global.admin --> role:debitor.owner;
|
||||
|
||||
%% role:debitor.admin
|
||||
role:debitor.admin --> perm:debitor.edit;
|
||||
role:debitor.owner --> role:debitor.admin;
|
||||
|
||||
%% role:debitor.tenant
|
||||
role:debitor.tenant --> perm:debitor.view;
|
||||
%% super-roles
|
||||
role:debitor.admin --> role:debitor.tenant;
|
||||
role:partner.admin --> role:debitor.tenant;
|
||||
role:person.admin --> role:debitor.tenant;
|
||||
role:contact.admin --> role:debitor.tenant;
|
||||
%% sub-roles
|
||||
role:debitor.tenant --> role:partner.tenant;
|
||||
role:debitor.tenant --> role:person.tenant;
|
||||
role:debitor.tenant --> role:contact.tenant;
|
||||
```
|
192
src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql
Normal file
192
src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql
Normal file
@ -0,0 +1,192 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-rbac-OBJECT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRelatedRbacObject('hs_office_debitor');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRoleDescriptors('hsOfficeDebitor', 'hs_office_debitor');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-rbac-ROLES-CREATION:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates and updates the roles and their assignments for debitor entities.
|
||||
*/
|
||||
|
||||
create or replace function hsOfficeDebitorRbacRolesTrigger()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
declare
|
||||
hsOfficeDebitorTenant RbacRoleDescriptor;
|
||||
ownerRole uuid;
|
||||
adminRole uuid;
|
||||
oldPartner hs_office_partner;
|
||||
newPartner hs_office_partner;
|
||||
newPerson hs_office_person;
|
||||
oldContact hs_office_contact;
|
||||
newContact hs_office_contact;
|
||||
begin
|
||||
|
||||
hsOfficeDebitorTenant := hsOfficeDebitorTenant(NEW);
|
||||
|
||||
select * from hs_office_partner as p where p.uuid = NEW.partnerUuid into newPartner;
|
||||
select * from hs_office_person as p where p.uuid = newPartner.personUuid into newPerson;
|
||||
select * from hs_office_contact as c where c.uuid = NEW.billingContactUuid into newContact;
|
||||
|
||||
if TG_OP = 'INSERT' then
|
||||
|
||||
-- the owner role with full access for the global admins
|
||||
ownerRole = createRole(
|
||||
hsOfficeDebitorOwner(NEW),
|
||||
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['*']),
|
||||
beneathRole(globalAdmin())
|
||||
);
|
||||
|
||||
-- the admin role with full access for owner
|
||||
adminRole = createRole(
|
||||
hsOfficeDebitorAdmin(NEW),
|
||||
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['edit']),
|
||||
beneathRole(ownerRole)
|
||||
);
|
||||
|
||||
-- the tenant role for those related users who can view the data
|
||||
perform createRole(
|
||||
hsOfficeDebitorTenant,
|
||||
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']),
|
||||
beneathRoles(array[
|
||||
hsOfficeDebitorAdmin(NEW),
|
||||
hsOfficePartnerAdmin(newPartner),
|
||||
hsOfficePersonAdmin(newPerson),
|
||||
hsOfficeContactAdmin(newContact)]),
|
||||
withSubRoles(array[
|
||||
hsOfficePartnerTenant(newPartner),
|
||||
hsOfficePersonTenant(newPerson),
|
||||
hsOfficeContactTenant(newContact)])
|
||||
);
|
||||
|
||||
elsif TG_OP = 'UPDATE' then
|
||||
|
||||
if OLD.partnerUuid <> NEW.partnerUuid then
|
||||
select * from hs_office_partner as p where p.uuid = OLD.partnerUuid into oldPartner;
|
||||
|
||||
call revokeRoleFromRole( hsOfficeDebitorTenant, hsOfficePartnerAdmin(oldPartner) );
|
||||
call grantRoleToRole( hsOfficeDebitorTenant, hsOfficePartnerAdmin(newPartner) );
|
||||
|
||||
call revokeRoleFromRole( hsOfficePartnerTenant(oldPartner), hsOfficeDebitorTenant );
|
||||
call grantRoleToRole( hsOfficePartnerTenant(newPartner), hsOfficeDebitorTenant );
|
||||
end if;
|
||||
|
||||
if OLD.billingContactUuid <> NEW.billingContactUuid then
|
||||
select * from hs_office_contact as c where c.uuid = OLD.billingContactUuid into oldContact;
|
||||
|
||||
call revokeRoleFromRole( hsOfficeDebitorTenant, hsOfficeContactAdmin(oldContact) );
|
||||
call grantRoleToRole( hsOfficeDebitorTenant, hsOfficeContactAdmin(newContact) );
|
||||
|
||||
call revokeRoleFromRole( hsOfficeContactTenant(oldContact), hsOfficeDebitorTenant );
|
||||
call grantRoleToRole( hsOfficeContactTenant(newContact), hsOfficeDebitorTenant );
|
||||
end if;
|
||||
else
|
||||
raise exception 'invalid usage of TRIGGER';
|
||||
end if;
|
||||
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
An AFTER INSERT TRIGGER which creates the role structure for a new debitor.
|
||||
*/
|
||||
create trigger createRbacRolesForHsOfficeDebitor_Trigger
|
||||
after insert
|
||||
on hs_office_debitor
|
||||
for each row
|
||||
execute procedure hsOfficeDebitorRbacRolesTrigger();
|
||||
|
||||
/*
|
||||
An AFTER UPDATE TRIGGER which updates the role structure of a debitor.
|
||||
*/
|
||||
create trigger updateRbacRolesForHsOfficeDebitor_Trigger
|
||||
after update
|
||||
on hs_office_debitor
|
||||
for each row
|
||||
execute procedure hsOfficeDebitorRbacRolesTrigger();
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_debitor', $idName$
|
||||
'#' || debitorNumber || ':' ||
|
||||
(select idName from hs_office_partner_iv p where p.uuid = target.partnerUuid)
|
||||
$idName$);
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRestrictedView('hs_office_debitor',
|
||||
'target.debitorNumber',
|
||||
$updates$
|
||||
billingContactUuid = new.billingContactUuid,
|
||||
vatId = new.vatId,
|
||||
vatCountryCode = new.vatCountryCode,
|
||||
vatBusiness = new.vatBusiness
|
||||
$updates$);
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-rbac-NEW-DEBITOR:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
/*
|
||||
Creates a global permission for new-debitor and assigns it to the hostsharing admins role.
|
||||
*/
|
||||
do language plpgsql $$
|
||||
declare
|
||||
addDebitorPermissions uuid[];
|
||||
globalObjectUuid uuid;
|
||||
globalAdminRoleUuid uuid ;
|
||||
begin
|
||||
call defineContext('granting global new-debitor permission to global admin role', null, null, null);
|
||||
|
||||
globalAdminRoleUuid := findRoleId(globalAdmin());
|
||||
globalObjectUuid := (select uuid from global);
|
||||
addDebitorPermissions := createPermissions(globalObjectUuid, array ['new-debitor']);
|
||||
call grantPermissionsToRole(globalAdminRoleUuid, addDebitorPermissions);
|
||||
end;
|
||||
$$;
|
||||
|
||||
/**
|
||||
Used by the trigger to prevent the add-debitor to current user respectively assumed roles.
|
||||
*/
|
||||
create or replace function addHsOfficeDebitorNotAllowedForCurrentSubjects()
|
||||
returns trigger
|
||||
language PLPGSQL
|
||||
as $$
|
||||
begin
|
||||
raise exception '[403] new-debitor not permitted for %',
|
||||
array_to_string(currentSubjects(), ';', 'null');
|
||||
end; $$;
|
||||
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to create a new debitor.
|
||||
*/
|
||||
create trigger hs_office_debitor_insert_trigger
|
||||
before insert
|
||||
on hs_office_debitor
|
||||
for each row
|
||||
-- TODO.spec: who is allowed to create new debitors
|
||||
when ( not hasAssumedRole() )
|
||||
execute procedure addHsOfficeDebitorNotAllowedForCurrentSubjects();
|
||||
--//
|
||||
|
@ -0,0 +1,52 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-TEST-DATA-GENERATOR:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates a single debitor test record.
|
||||
*/
|
||||
create or replace procedure createHsOfficeDebitorTestData( partnerTradeName varchar, billingContactLabel varchar )
|
||||
language plpgsql as $$
|
||||
declare
|
||||
currentTask varchar;
|
||||
idName varchar;
|
||||
relatedPartner hs_office_partner;
|
||||
relatedContact hs_office_contact;
|
||||
newDebitorNumber numeric(6);
|
||||
begin
|
||||
idName := cleanIdentifier( partnerTradeName|| '-' || billingContactLabel);
|
||||
currentTask := 'creating RBAC test debitor ' || idName;
|
||||
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
|
||||
execute format('set local hsadminng.currentTask to %L', currentTask);
|
||||
|
||||
select partner.* from hs_office_partner partner
|
||||
join hs_office_person person on person.uuid = partner.personUuid
|
||||
where person.tradeName = partnerTradeName into relatedPartner;
|
||||
select c.* from hs_office_contact c where c.label = billingContactLabel into relatedContact;
|
||||
select coalesce(max(debitorNumber)+1, 10001) from hs_office_debitor into newDebitorNumber;
|
||||
|
||||
raise notice 'creating test debitor: % (#%)', idName, newDebitorNumber;
|
||||
raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner;
|
||||
raise notice '- using billingContact (%): %', relatedContact.uuid, relatedContact;
|
||||
insert
|
||||
into hs_office_debitor (uuid, partneruuid, debitornumber, billingcontactuuid, vatbusiness)
|
||||
values (uuid_generate_v4(), relatedPartner.uuid, newDebitorNumber, relatedContact.uuid, true);
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
do language plpgsql $$
|
||||
begin
|
||||
call createHsOfficeDebitorTestData('First GmbH', 'first contact');
|
||||
call createHsOfficeDebitorTestData('Second e.K.', 'second contact');
|
||||
call createHsOfficeDebitorTestData('Third OHG', 'third contact');
|
||||
end;
|
||||
$$;
|
||||
--//
|
@ -71,3 +71,9 @@ databaseChangeLog:
|
||||
file: db/changelog/233-hs-office-relationship-rbac.sql
|
||||
- include:
|
||||
file: db/changelog/238-hs-office-relationship-test-data.sql
|
||||
- include:
|
||||
file: db/changelog/270-hs-office-debitor.sql
|
||||
- include:
|
||||
file: db/changelog/273-hs-office-debitor-rbac.sql
|
||||
- include:
|
||||
file: db/changelog/278-hs-office-debitor-test-data.sql
|
||||
|
@ -132,7 +132,7 @@ public abstract class PatchUnitTestBase<R, E> {
|
||||
|
||||
protected abstract EntityPatcher<R> createPatcher(final E entity);
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@SuppressWarnings("types")
|
||||
protected abstract Stream<Property> propertyTestDescriptors();
|
||||
|
||||
private Stream<Arguments> propertyTestCases() {
|
||||
|
@ -17,10 +17,16 @@ public class ArchitectureTest {
|
||||
public static final String NET_HOSTSHARING_HSADMINNG = "net.hostsharing.hsadminng";
|
||||
|
||||
@ArchTest
|
||||
@SuppressWarnings("unused")
|
||||
public static final ArchRule doNotUseJUnit4 = noClasses()
|
||||
.should().accessClassesThat()
|
||||
.resideInAPackage("org.junit");
|
||||
|
||||
@ArchTest
|
||||
@SuppressWarnings("unused")
|
||||
public static final ArchRule dontUseImplSuffix = noClasses()
|
||||
.should().haveSimpleNameEndingWith("Impl");
|
||||
|
||||
@ArchTest
|
||||
@SuppressWarnings("unused")
|
||||
public static final ArchRule contextPackageRule = classes()
|
||||
@ -68,7 +74,7 @@ public class ArchitectureTest {
|
||||
public static final ArchRule HsOfficePartnerPackageRule = classes()
|
||||
.that().resideInAPackage("..hs.office.partner..")
|
||||
.should().onlyBeAccessed().byClassesThat()
|
||||
.resideInAnyPackage("..hs.office.partner..");
|
||||
.resideInAnyPackage("..hs.office.partner..", "..hs.office.debitor..");
|
||||
|
||||
@ArchTest
|
||||
@SuppressWarnings("unused")
|
||||
|
@ -4,9 +4,14 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
|
||||
import javax.persistence.EntityNotFoundException;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@ -44,6 +49,75 @@ class RestResponseEntityExceptionHandlerUnitTest {
|
||||
assertThat(errorResponse.getBody().getMessage()).isEqualTo("ERROR: [401] First Line");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleJpaObjectRetrievalFailureExceptionWithDisplayName() {
|
||||
// given
|
||||
final var givenException = new JpaObjectRetrievalFailureException(
|
||||
new EntityNotFoundException(
|
||||
"Unable to find net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity with id 12345-123454")
|
||||
);
|
||||
final var givenWebRequest = mock(WebRequest.class);
|
||||
|
||||
// when
|
||||
final var errorResponse = exceptionHandler.handleJpaObjectRetrievalFailureException(givenException, givenWebRequest);
|
||||
|
||||
// then
|
||||
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(400);
|
||||
assertThat(errorResponse.getBody().getMessage()).isEqualTo("Unable to find Partner with uuid 12345-123454");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleJpaObjectRetrievalFailureExceptionIfEntityClassCannotBeDetermined() {
|
||||
// given
|
||||
final var givenException = new JpaObjectRetrievalFailureException(
|
||||
new EntityNotFoundException(
|
||||
"Unable to find net.hostsharing.hsadminng.WhateverEntity with id 12345-123454")
|
||||
);
|
||||
final var givenWebRequest = mock(WebRequest.class);
|
||||
|
||||
// when
|
||||
final var errorResponse = exceptionHandler.handleJpaObjectRetrievalFailureException(givenException, givenWebRequest);
|
||||
|
||||
// then
|
||||
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(400);
|
||||
assertThat(errorResponse.getBody().getMessage()).isEqualTo(
|
||||
"Unable to find net.hostsharing.hsadminng.WhateverEntity with id 12345-123454");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleJpaObjectRetrievalFailureExceptionIfPatternDoesNotMatch() {
|
||||
// given
|
||||
final var givenException = new JpaObjectRetrievalFailureException(
|
||||
new EntityNotFoundException("whatever error message")
|
||||
);
|
||||
final var givenWebRequest = mock(WebRequest.class);
|
||||
|
||||
// when
|
||||
final var errorResponse = exceptionHandler.handleJpaObjectRetrievalFailureException(givenException, givenWebRequest);
|
||||
|
||||
// then
|
||||
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(400);
|
||||
assertThat(errorResponse.getBody().getMessage()).isEqualTo("whatever error message");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleJpaObjectRetrievalFailureExceptionWithEntityName() {
|
||||
// given
|
||||
final var givenException = new JpaObjectRetrievalFailureException(
|
||||
new EntityNotFoundException("Unable to find "
|
||||
+ NoDisplayNameEntity.class.getTypeName()
|
||||
+ " with id 12345-123454")
|
||||
);
|
||||
final var givenWebRequest = mock(WebRequest.class);
|
||||
|
||||
// when
|
||||
final var errorResponse = exceptionHandler.handleJpaObjectRetrievalFailureException(givenException, givenWebRequest);
|
||||
|
||||
// then
|
||||
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(400);
|
||||
assertThat(errorResponse.getBody().getMessage()).isEqualTo("Unable to find NoDisplayNameEntity with uuid 12345-123454");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jpaExceptionWithUnknownErrorCode() {
|
||||
// given
|
||||
@ -59,6 +133,20 @@ class RestResponseEntityExceptionHandlerUnitTest {
|
||||
assertThat(errorResponse.getBody().getMessage()).isEqualTo("ERROR: [999] First Line");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleNoSuchElementException() {
|
||||
// given
|
||||
final var givenException = new NoSuchElementException("some error message");
|
||||
final var givenWebRequest = mock(WebRequest.class);
|
||||
|
||||
// when
|
||||
final var errorResponse = exceptionHandler.handleNoSuchElementException(givenException, givenWebRequest);
|
||||
|
||||
// then
|
||||
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(404);
|
||||
assertThat(errorResponse.getBody().getMessage()).isEqualTo("some error message");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleOtherExceptionsWithoutErrorCode() {
|
||||
// given
|
||||
@ -87,4 +175,8 @@ class RestResponseEntityExceptionHandlerUnitTest {
|
||||
assertThat(errorResponse.getBody().getMessage()).isEqualTo("ERROR: [418] First Line");
|
||||
}
|
||||
|
||||
public static class NoDisplayNameEntity {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,505 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import net.hostsharing.hsadminng.Accepts;
|
||||
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository;
|
||||
import net.hostsharing.test.JpaAttempt;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid;
|
||||
import static net.hostsharing.test.JsonMatcher.lenientlyEquals;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assumptions.assumeThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
classes = { HsadminNgApplication.class, JpaAttempt.class }
|
||||
)
|
||||
@Transactional
|
||||
class HsOfficeDebitorControllerAcceptanceTest {
|
||||
|
||||
private static int nextDebitorNumber = 20001;
|
||||
|
||||
@LocalServerPort
|
||||
private Integer port;
|
||||
|
||||
@Autowired
|
||||
Context context;
|
||||
|
||||
@Autowired
|
||||
Context contextMock;
|
||||
|
||||
@Autowired
|
||||
HsOfficeDebitorRepository debitorRepo;
|
||||
|
||||
@Autowired
|
||||
HsOfficePartnerRepository partnerRepo;
|
||||
|
||||
@Autowired
|
||||
HsOfficeContactRepository contactRepo;
|
||||
|
||||
@Autowired
|
||||
JpaAttempt jpaAttempt;
|
||||
|
||||
Set<UUID> tempDebitorUuids = new HashSet<>();
|
||||
|
||||
@Nested
|
||||
@Accepts({ "Debitor:F(Find)" })
|
||||
class ListDebitors {
|
||||
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() throws JSONException {
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/office/debitors")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.body("", lenientlyEquals("""
|
||||
[
|
||||
{
|
||||
"debitorNumber": 10001,
|
||||
"partner": { "person": { "personType": "LEGAL" } },
|
||||
"billingContact": { "label": "first contact" },
|
||||
"vatId": null,
|
||||
"vatCountryCode": null,
|
||||
"vatBusiness": true
|
||||
},
|
||||
{
|
||||
"debitorNumber": 10002,
|
||||
"partner": { "person": { "tradeName": "Second e.K." } },
|
||||
"billingContact": { "label": "second contact" },
|
||||
"vatId": null,
|
||||
"vatCountryCode": null,
|
||||
"vatBusiness": true
|
||||
},
|
||||
{
|
||||
"debitorNumber": 10003,
|
||||
"partner": { "person": { "tradeName": "Third OHG" } },
|
||||
"billingContact": { "label": "third contact" },
|
||||
"vatId": null,
|
||||
"vatCountryCode": null,
|
||||
"vatBusiness": true
|
||||
}
|
||||
]
|
||||
"""));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRoles_canFindDebitorDebitorByDebitorNumber() throws JSONException {
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/office/debitors?debitorNumber=10002")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.body("", lenientlyEquals("""
|
||||
[
|
||||
{
|
||||
"debitorNumber": 10002,
|
||||
"partner": { "person": { "tradeName": "Second e.K." } },
|
||||
"billingContact": { "label": "second contact" },
|
||||
"vatId": null,
|
||||
"vatCountryCode": null,
|
||||
"vatBusiness": true
|
||||
}
|
||||
]
|
||||
"""));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Accepts({ "Debitor:C(Create)" })
|
||||
class AddDebitor {
|
||||
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRole_canAddDebitor() {
|
||||
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0);
|
||||
final var givenContact = contactRepo.findContactByOptionalLabelLike("forth").get(0);
|
||||
|
||||
final var location = RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"partnerUuid": "%s",
|
||||
"billingContactUuid": "%s",
|
||||
"debitorNumber": "%s",
|
||||
"vatId": "VAT123456",
|
||||
"vatCountryCode": "DE",
|
||||
"vatBusiness": true
|
||||
}
|
||||
""".formatted( givenPartner.getUuid(), givenContact.getUuid(), nextDebitorNumber++))
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/office/debitors")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(201)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("uuid", isUuidValid())
|
||||
.body("vatId", is("VAT123456"))
|
||||
.body("billingContact.label", is(givenContact.getLabel()))
|
||||
.body("partner.person.tradeName", is(givenPartner.getPerson().getTradeName()))
|
||||
.header("Location", startsWith("http://localhost"))
|
||||
.extract().header("Location"); // @formatter:on
|
||||
|
||||
// finally, the new debitor can be accessed under the generated UUID
|
||||
final var newUserUuid = toCleanup(UUID.fromString(
|
||||
location.substring(location.lastIndexOf('/') + 1)));
|
||||
assertThat(newUserUuid).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalAdmin_canNotAddDebitor_ifContactDoesNotExist() {
|
||||
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0);
|
||||
final var givenContactUuid = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6");
|
||||
|
||||
final var location = RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"partnerUuid": "%s",
|
||||
"billingContactUuid": "%s",
|
||||
"debitorNumber": "%s",
|
||||
"vatId": "VAT123456",
|
||||
"vatCountryCode": "DE",
|
||||
"vatBusiness": true
|
||||
}
|
||||
""".formatted( givenPartner.getUuid(), givenContactUuid, nextDebitorNumber++))
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/office/debitors")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(400)
|
||||
.body("message", is("Unable to find Contact with uuid 3fa85f64-5717-4562-b3fc-2c963f66afa6"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalAdmin_canNotAddDebitor_ifPartnerDoesNotExist() {
|
||||
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenPartnerUuid = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6");
|
||||
final var givenContact = contactRepo.findContactByOptionalLabelLike("forth").get(0);
|
||||
|
||||
final var location = RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"partnerUuid": "%s",
|
||||
"billingContactUuid": "%s",
|
||||
"debitorNumber": "%s",
|
||||
"vatId": "VAT123456",
|
||||
"vatCountryCode": "DE",
|
||||
"vatBusiness": true
|
||||
}
|
||||
""".formatted( givenPartnerUuid, givenContact.getUuid(), nextDebitorNumber++))
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/office/debitors")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(400)
|
||||
.body("message", is("Unable to find Partner with uuid 3fa85f64-5717-4562-b3fc-2c963f66afa6"));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Accepts({ "Debitor:R(Read)" })
|
||||
class GetDebitor {
|
||||
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRole_canGetArbitraryDebitor() {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenDebitorUuid = debitorRepo.findDebitorByOptionalNameLike("First").get(0).getUuid();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/office/debitors/" + givenDebitorUuid)
|
||||
.then().log().body().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.body("", lenientlyEquals("""
|
||||
{
|
||||
"partner": { person: { "tradeName": "First GmbH" } },
|
||||
"billingContact": { "label": "first contact" }
|
||||
}
|
||||
""")); // @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@Accepts({ "Debitor:X(Access Control)" })
|
||||
void normalUser_canNotGetUnrelatedDebitor() {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenDebitorUuid = debitorRepo.findDebitorByOptionalNameLike("First").get(0).getUuid();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "selfregistered-user-drew@hostsharing.org")
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/office/debitors/" + givenDebitorUuid)
|
||||
.then().log().body().assertThat()
|
||||
.statusCode(404); // @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@Accepts({ "Debitor:X(Access Control)" })
|
||||
void contactAdminUser_canGetRelatedDebitor() {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenDebitorUuid = debitorRepo.findDebitorByOptionalNameLike("first contact").get(0).getUuid();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "contact-admin@firstcontact.example.com")
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/office/debitors/" + givenDebitorUuid)
|
||||
.then().log().body().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.body("", lenientlyEquals("""
|
||||
{
|
||||
"partner": { person: { "tradeName": "First GmbH" } },
|
||||
"billingContact": { "label": "first contact" }
|
||||
}
|
||||
""")); // @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Accepts({ "Debitor:U(Update)" })
|
||||
class PatchDebitor {
|
||||
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRole_canPatchAllPropertiesOfArbitraryDebitor() {
|
||||
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenDebitor = givenSomeTemporaryDebitor();
|
||||
final var givenContact = contactRepo.findContactByOptionalLabelLike("forth").get(0);
|
||||
|
||||
final var location = RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"contactUuid": "%s",
|
||||
"vatId": "VAT222222",
|
||||
"vatCountryCode": "AA",
|
||||
"vatBusiness": true
|
||||
}
|
||||
""".formatted(givenContact.getUuid()))
|
||||
.port(port)
|
||||
.when()
|
||||
.patch("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid())
|
||||
.then().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("uuid", isUuidValid())
|
||||
.body("vatId", is("VAT222222"))
|
||||
.body("vatCountryCode", is("AA"))
|
||||
.body("vatBusiness", is(true))
|
||||
.body("billingContact.label", is(givenContact.getLabel()))
|
||||
.body("partner.person.tradeName", is(givenDebitor.getPartner().getPerson().getTradeName()));
|
||||
// @formatter:on
|
||||
|
||||
// finally, the debitor is actually updated
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent().get()
|
||||
.matches(partner -> {
|
||||
assertThat(partner.getPartner().getPerson().getTradeName()).isEqualTo(givenDebitor.getPartner()
|
||||
.getPerson()
|
||||
.getTradeName());
|
||||
assertThat(partner.getBillingContact().getLabel()).isEqualTo("forth contact");
|
||||
assertThat(partner.getVatId()).isEqualTo("VAT222222");
|
||||
assertThat(partner.getVatCountryCode()).isEqualTo("AA");
|
||||
assertThat(partner.isVatBusiness()).isEqualTo(true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRole_canPatchPartialPropertiesOfArbitraryDebitor() {
|
||||
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenDebitor = givenSomeTemporaryDebitor();
|
||||
final var newBillingContact = contactRepo.findContactByOptionalLabelLike("sixth").get(0);
|
||||
|
||||
final var location = RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"billingContactUuid": "%s",
|
||||
"vatId": "VAT999999"
|
||||
}
|
||||
""".formatted(newBillingContact.getUuid()))
|
||||
.port(port)
|
||||
.when()
|
||||
.patch("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid())
|
||||
.then().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("uuid", isUuidValid())
|
||||
.body("billingContact.label", is("sixth contact"))
|
||||
.body("vatId", is("VAT999999"))
|
||||
.body("vatCountryCode", is(givenDebitor.getVatCountryCode()))
|
||||
.body("vatBusiness", is(givenDebitor.isVatBusiness()));
|
||||
// @formatter:on
|
||||
|
||||
// finally, the debitor is actually updated
|
||||
assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent().get()
|
||||
.matches(partner -> {
|
||||
assertThat(partner.getPartner().getPerson().getTradeName()).isEqualTo(givenDebitor.getPartner()
|
||||
.getPerson()
|
||||
.getTradeName());
|
||||
assertThat(partner.getBillingContact().getLabel()).isEqualTo("sixth contact");
|
||||
assertThat(partner.getVatId()).isEqualTo("VAT999999");
|
||||
assertThat(partner.getVatCountryCode()).isEqualTo(givenDebitor.getVatCountryCode());
|
||||
assertThat(partner.isVatBusiness()).isEqualTo(givenDebitor.isVatBusiness());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Accepts({ "Debitor:D(Delete)" })
|
||||
class DeleteDebitor {
|
||||
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRole_canDeleteArbitraryDebitor() {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenDebitor = givenSomeTemporaryDebitor();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.port(port)
|
||||
.when()
|
||||
.delete("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid())
|
||||
.then().log().body().assertThat()
|
||||
.statusCode(204); // @formatter:on
|
||||
|
||||
// then the given debitor is gone
|
||||
assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Accepts({ "Debitor:X(Access Control)" })
|
||||
void contactAdminUser_canNotDeleteRelatedDebitor() {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenDebitor = givenSomeTemporaryDebitor();
|
||||
assumeThat(givenDebitor.getBillingContact().getLabel()).isEqualTo("forth contact");
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "contact-admin@forthcontact.example.com")
|
||||
.port(port)
|
||||
.when()
|
||||
.delete("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid())
|
||||
.then().log().body().assertThat()
|
||||
.statusCode(403); // @formatter:on
|
||||
|
||||
// then the given debitor is still there
|
||||
assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Accepts({ "Debitor:X(Access Control)" })
|
||||
void normalUser_canNotDeleteUnrelatedDebitor() {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenDebitor = givenSomeTemporaryDebitor();
|
||||
assumeThat(givenDebitor.getBillingContact().getLabel()).isEqualTo("forth contact");
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "selfregistered-user-drew@hostsharing.org")
|
||||
.port(port)
|
||||
.when()
|
||||
.delete("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid())
|
||||
.then().log().body().assertThat()
|
||||
.statusCode(404); // @formatter:on
|
||||
|
||||
// then the given debitor is still there
|
||||
assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private HsOfficeDebitorEntity givenSomeTemporaryDebitor() {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Fourth").get(0);
|
||||
final var givenContact = contactRepo.findContactByOptionalLabelLike("forth contact").get(0);
|
||||
final var newDebitor = HsOfficeDebitorEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.debitorNumber(nextDebitorNumber++)
|
||||
.partner(givenPartner)
|
||||
.billingContact(givenContact)
|
||||
.build();
|
||||
|
||||
toCleanup(newDebitor.getUuid());
|
||||
|
||||
return debitorRepo.save(newDebitor);
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
private UUID toCleanup(final UUID tempDebitorUuid) {
|
||||
tempDebitorUuids.add(tempDebitorUuid);
|
||||
return tempDebitorUuid;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
tempDebitorUuids.forEach(uuid -> {
|
||||
jpaAttempt.transacted(() -> {
|
||||
context.define("superuser-alex@hostsharing.net", null);
|
||||
System.out.println("DELETING temporary debitor: " + uuid);
|
||||
final var count = debitorRepo.deleteByUuid(uuid);
|
||||
System.out.println("DELETED temporary debitor: " + uuid + (count > 0 ? " successful" : " failed"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.PatchUnitTestBase;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
@TestInstance(PER_CLASS)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HsOfficeDebitorEntityPatcherUnitTest extends PatchUnitTestBase<
|
||||
HsOfficeDebitorPatchResource,
|
||||
HsOfficeDebitorEntity
|
||||
> {
|
||||
|
||||
private static final UUID INITIAL_DEBITOR_UUID = UUID.randomUUID();
|
||||
private static final UUID INITIAL_PARTNER_UUID = UUID.randomUUID();
|
||||
private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID();
|
||||
private static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID();
|
||||
|
||||
private static final String PATCHED_VAT_COUNTRY_CODE = "ZZ";
|
||||
|
||||
private static final boolean PATCHED_VAT_BUSINESS = false;
|
||||
|
||||
private final HsOfficePartnerEntity givenInitialPartner = HsOfficePartnerEntity.builder()
|
||||
.uuid(INITIAL_PARTNER_UUID)
|
||||
.build();
|
||||
|
||||
private final HsOfficeContactEntity givenInitialContact = HsOfficeContactEntity.builder()
|
||||
.uuid(INITIAL_CONTACT_UUID)
|
||||
.build();
|
||||
@Mock
|
||||
private EntityManager em;
|
||||
|
||||
@BeforeEach
|
||||
void initMocks() {
|
||||
lenient().when(em.getReference(eq(HsOfficeContactEntity.class), any())).thenAnswer(invocation ->
|
||||
HsOfficeContactEntity.builder().uuid(invocation.getArgument(1)).build());
|
||||
lenient().when(em.getReference(eq(HsOfficeContactEntity.class), any())).thenAnswer(invocation ->
|
||||
HsOfficeContactEntity.builder().uuid(invocation.getArgument(1)).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsOfficeDebitorEntity newInitialEntity() {
|
||||
final var entity = new HsOfficeDebitorEntity();
|
||||
entity.setUuid(INITIAL_DEBITOR_UUID);
|
||||
entity.setPartner(givenInitialPartner);
|
||||
entity.setBillingContact(givenInitialContact);
|
||||
entity.setVatId("initial VAT-ID");
|
||||
entity.setVatCountryCode("AA");
|
||||
entity.setVatBusiness(true);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsOfficeDebitorPatchResource newPatchResource() {
|
||||
return new HsOfficeDebitorPatchResource();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsOfficeDebitorEntityPatcher createPatcher(final HsOfficeDebitorEntity debitor) {
|
||||
return new HsOfficeDebitorEntityPatcher(em, debitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Property> propertyTestDescriptors() {
|
||||
return Stream.of(
|
||||
new JsonNullableProperty<>(
|
||||
"billingContact",
|
||||
HsOfficeDebitorPatchResource::setBillingContactUuid,
|
||||
PATCHED_CONTACT_UUID,
|
||||
HsOfficeDebitorEntity::setBillingContact,
|
||||
newBillingContact(PATCHED_CONTACT_UUID))
|
||||
.notNullable(),
|
||||
new JsonNullableProperty<>(
|
||||
"vatId",
|
||||
HsOfficeDebitorPatchResource::setVatId,
|
||||
"patched VAT-ID",
|
||||
HsOfficeDebitorEntity::setVatId),
|
||||
new JsonNullableProperty<>(
|
||||
"vatCountryCode",
|
||||
HsOfficeDebitorPatchResource::setVatCountryCode,
|
||||
PATCHED_VAT_COUNTRY_CODE,
|
||||
HsOfficeDebitorEntity::setVatCountryCode),
|
||||
new JsonNullableProperty<>(
|
||||
"vatBusiness",
|
||||
HsOfficeDebitorPatchResource::setVatBusiness,
|
||||
PATCHED_VAT_BUSINESS,
|
||||
HsOfficeDebitorEntity::setVatBusiness)
|
||||
.notNullable()
|
||||
);
|
||||
}
|
||||
|
||||
private HsOfficeContactEntity newBillingContact(final UUID uuid) {
|
||||
final var newContact = new HsOfficeContactEntity();
|
||||
newContact.setUuid(uuid);
|
||||
return newContact;
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HsOfficeDebitorEntityTest {
|
||||
|
||||
@Test
|
||||
void toStringContainsPartnerAndContact() {
|
||||
final var given = HsOfficeDebitorEntity.builder()
|
||||
.debitorNumber(123456)
|
||||
.partner(HsOfficePartnerEntity.builder()
|
||||
.person(HsOfficePersonEntity.builder()
|
||||
.tradeName("some trade name")
|
||||
.build())
|
||||
.birthName("some birth name")
|
||||
.build())
|
||||
.billingContact(HsOfficeContactEntity.builder().label("some label").build())
|
||||
.build();
|
||||
|
||||
final var result = given.toString();
|
||||
|
||||
assertThat(result).isEqualTo("debitor(123456: some trade name)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toShortStringContainsPartnerAndContact() {
|
||||
final var given = HsOfficeDebitorEntity.builder()
|
||||
.debitorNumber(123456)
|
||||
.build();
|
||||
|
||||
final var result = given.toShortString();
|
||||
|
||||
assertThat(result).isEqualTo("123456");
|
||||
}
|
||||
}
|
@ -0,0 +1,464 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.context.ContextBasedTest;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository;
|
||||
import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
|
||||
import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
|
||||
import net.hostsharing.test.Array;
|
||||
import net.hostsharing.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.grantDisplaysOf;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.roleNamesOf;
|
||||
import static net.hostsharing.test.JpaAttempt.attempt;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DataJpaTest
|
||||
@ComponentScan(basePackageClasses = { HsOfficeDebitorRepository.class, Context.class, JpaAttempt.class })
|
||||
@DirtiesContext
|
||||
class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTest {
|
||||
|
||||
@Autowired
|
||||
HsOfficeDebitorRepository debitorRepo;
|
||||
|
||||
@Autowired
|
||||
HsOfficePartnerRepository partnerRepo;
|
||||
|
||||
@Autowired
|
||||
HsOfficeContactRepository contactRepo;
|
||||
|
||||
@Autowired
|
||||
RawRbacRoleRepository rawRoleRepo;
|
||||
|
||||
@Autowired
|
||||
RawRbacGrantRepository rawGrantRepo;
|
||||
|
||||
@Autowired
|
||||
EntityManager em;
|
||||
|
||||
@Autowired
|
||||
JpaAttempt jpaAttempt;
|
||||
|
||||
@MockBean
|
||||
HttpServletRequest request;
|
||||
|
||||
Set<HsOfficeDebitorEntity> tempDebitors = new HashSet<>();
|
||||
|
||||
@Nested
|
||||
class CreateDebitor {
|
||||
|
||||
@Test
|
||||
public void testHostsharingAdmin_withoutAssumedRole_canCreateNewDebitor() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var count = debitorRepo.count();
|
||||
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First GmbH").get(0);
|
||||
final var givenContact = contactRepo.findContactByOptionalLabelLike("first contact").get(0);
|
||||
|
||||
// when
|
||||
final var result = attempt(em, () -> {
|
||||
final var newDebitor = toCleanup(HsOfficeDebitorEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.debitorNumber(20001)
|
||||
.partner(rawReference(givenPartner))
|
||||
.billingContact(rawReference(givenContact))
|
||||
.build());
|
||||
return debitorRepo.save(newDebitor);
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertSuccessful();
|
||||
assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeDebitorEntity::getUuid).isNotNull();
|
||||
assertThatDebitorIsPersisted(result.returnedValue());
|
||||
assertThat(debitorRepo.count()).isEqualTo(count + 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createsAndGrantsRoles() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var initialRoleNames = roleNamesOf(rawRoleRepo.findAll());
|
||||
final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll());
|
||||
|
||||
// when
|
||||
attempt(em, () -> {
|
||||
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Fourth").get(0);
|
||||
final var givenContact = contactRepo.findContactByOptionalLabelLike("forth contact").get(0);
|
||||
final var newDebitor = toCleanup(HsOfficeDebitorEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.debitorNumber(20002)
|
||||
.partner(rawReference(givenPartner))
|
||||
.billingContact(rawReference(givenContact))
|
||||
.build());
|
||||
return debitorRepo.save(newDebitor);
|
||||
}).assertSuccessful();
|
||||
|
||||
// then
|
||||
assertThat(roleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(Array.from(
|
||||
initialRoleNames,
|
||||
"hs_office_debitor#20002Fourthe.G.-forthcontact.admin",
|
||||
"hs_office_debitor#20002Fourthe.G.-forthcontact.owner",
|
||||
"hs_office_debitor#20002Fourthe.G.-forthcontact.tenant"));
|
||||
assertThat(grantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromSkippingNull(
|
||||
initialGrantNames,
|
||||
|
||||
"{ grant perm * on hs_office_debitor#20002Fourthe.G.-forthcontact to role hs_office_debitor#20002Fourthe.G.-forthcontact.owner by system and assume }",
|
||||
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.owner to role global#global.admin by system and assume }",
|
||||
|
||||
"{ grant perm edit on hs_office_debitor#20002Fourthe.G.-forthcontact to role hs_office_debitor#20002Fourthe.G.-forthcontact.admin by system and assume }",
|
||||
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.admin to role hs_office_debitor#20002Fourthe.G.-forthcontact.owner by system and assume }",
|
||||
|
||||
"{ grant perm view on hs_office_debitor#20002Fourthe.G.-forthcontact to role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant by system and assume }",
|
||||
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant to role hs_office_contact#forthcontact.admin by system and assume }",
|
||||
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.admin by system and assume }",
|
||||
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant to role hs_office_partner#Fourthe.G.-forthcontact.admin by system and assume }",
|
||||
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant to role hs_office_person#Fourthe.G..admin by system and assume }",
|
||||
"{ grant role hs_office_partner#Fourthe.G.-forthcontact.tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant by system and assume }",
|
||||
"{ grant role hs_office_contact#forthcontact.tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant by system and assume }",
|
||||
"{ grant role hs_office_person#Fourthe.G..tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant by system and assume }",
|
||||
|
||||
null));
|
||||
}
|
||||
|
||||
private void assertThatDebitorIsPersisted(final HsOfficeDebitorEntity saved) {
|
||||
final var found = debitorRepo.findByUuid(saved.getUuid());
|
||||
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FindByOptionalName {
|
||||
|
||||
@Test
|
||||
public void globalAdmin_withoutAssumedRole_canViewAllDebitors() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
|
||||
// when
|
||||
final var result = debitorRepo.findDebitorByOptionalNameLike(null);
|
||||
|
||||
// then
|
||||
allTheseDebitorsAreReturned(
|
||||
result,
|
||||
"debitor(10001: First GmbH)",
|
||||
"debitor(10002: Second e.K.)",
|
||||
"debitor(10003: Third OHG)");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"hs_office_partner#FirstGmbH-firstcontact.admin",
|
||||
"hs_office_person#FirstGmbH.admin",
|
||||
"hs_office_contact#firstcontact.admin",
|
||||
})
|
||||
public void relatedPersonAdmin_canViewRelatedDebitors(final String assumedRole) {
|
||||
// given:
|
||||
context("superuser-alex@hostsharing.net", assumedRole);
|
||||
|
||||
// when:
|
||||
final var result = debitorRepo.findDebitorByOptionalNameLike(null);
|
||||
|
||||
// then:
|
||||
exactlyTheseDebitorsAreReturned(result, "debitor(10001: First GmbH)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedUser_canNotViewAnyDebitor() {
|
||||
// given:
|
||||
context("selfregistered-test-user@hostsharing.org");
|
||||
|
||||
// when:
|
||||
final var result = debitorRepo.findDebitorByOptionalNameLike(null);
|
||||
|
||||
// then:
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FindByDebitorNumberLike {
|
||||
|
||||
@Test
|
||||
public void globalAdmin_withoutAssumedRole_canViewAllDebitors() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
|
||||
// when
|
||||
final var result = debitorRepo.findDebitorByDebitorNumber(10003);
|
||||
|
||||
// then
|
||||
exactlyTheseDebitorsAreReturned(result, "debitor(10003: Third OHG)");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FindByNameLike {
|
||||
|
||||
@Test
|
||||
public void globalAdmin_withoutAssumedRole_canViewAllDebitors() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
|
||||
// when
|
||||
final var result = debitorRepo.findDebitorByOptionalNameLike("third contact");
|
||||
|
||||
// then
|
||||
exactlyTheseDebitorsAreReturned(result, "debitor(10003: Third OHG)");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class UpdateDebitor {
|
||||
|
||||
@Test
|
||||
public void hostsharingAdmin_withoutAssumedRole_canUpdateArbitraryDebitor() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "fifth contact");
|
||||
assertThatDebitorIsVisibleForUserWithRole(
|
||||
givenDebitor,
|
||||
"hs_office_partner#Fourthe.G.-forthcontact.admin");
|
||||
assertThatDebitorActuallyInDatabase(givenDebitor);
|
||||
final var givenNewContact = contactRepo.findContactByOptionalLabelLike("sixth contact").get(0);
|
||||
final String givenNewVatId = "NEW-VAT-ID";
|
||||
final String givenNewVatCountryCode = "NC";
|
||||
final boolean givenNewVatBusiness = !givenDebitor.isVatBusiness();
|
||||
|
||||
// when
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net");
|
||||
givenDebitor.setBillingContact(rawReference(givenNewContact));
|
||||
givenDebitor.setVatId(givenNewVatId);
|
||||
givenDebitor.setVatCountryCode(givenNewVatCountryCode);
|
||||
givenDebitor.setVatBusiness(givenNewVatBusiness);
|
||||
return toCleanup(debitorRepo.save(givenDebitor));
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertSuccessful();
|
||||
assertThatDebitorIsVisibleForUserWithRole(
|
||||
result.returnedValue(),
|
||||
"global#global.admin");
|
||||
assertThatDebitorIsVisibleForUserWithRole(
|
||||
result.returnedValue(),
|
||||
"hs_office_contact#sixthcontact.admin");
|
||||
assertThatDebitorIsNotVisibleForUserWithRole(
|
||||
result.returnedValue(),
|
||||
"hs_office_contact#fifthcontact.admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partnerAdmin_canNotUpdateRelatedDebitor() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "eighth");
|
||||
assertThatDebitorIsVisibleForUserWithRole(
|
||||
givenDebitor,
|
||||
"hs_office_partner#Fourthe.G.-forthcontact.admin");
|
||||
assertThatDebitorActuallyInDatabase(givenDebitor);
|
||||
|
||||
// when
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net", "hs_office_partner#Fourthe.G.-forthcontact.admin");
|
||||
givenDebitor.setVatId("NEW-VAT-ID");
|
||||
return debitorRepo.save(givenDebitor);
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertExceptionWithRootCauseMessage(JpaSystemException.class,
|
||||
"[403] Subject ", " is not allowed to update hs_office_debitor uuid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contactAdmin_canNotUpdateRelatedDebitor() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "ninth");
|
||||
assertThatDebitorIsVisibleForUserWithRole(
|
||||
givenDebitor,
|
||||
"hs_office_contact#ninthcontact.admin");
|
||||
assertThatDebitorActuallyInDatabase(givenDebitor);
|
||||
|
||||
// when
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net", "hs_office_contact#ninthcontact.admin");
|
||||
givenDebitor.setVatId("NEW-VAT-ID");
|
||||
return debitorRepo.save(givenDebitor);
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertExceptionWithRootCauseMessage(JpaSystemException.class,
|
||||
"[403] Subject ", " is not allowed to update hs_office_debitor uuid");
|
||||
}
|
||||
|
||||
private void assertThatDebitorActuallyInDatabase(final HsOfficeDebitorEntity saved) {
|
||||
final var found = debitorRepo.findByUuid(saved.getUuid());
|
||||
assertThat(found).isNotEmpty().get().isNotSameAs(saved).usingRecursiveComparison().isEqualTo(saved);
|
||||
}
|
||||
|
||||
private void assertThatDebitorIsVisibleForUserWithRole(
|
||||
final HsOfficeDebitorEntity entity,
|
||||
final String assumedRoles) {
|
||||
jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net", assumedRoles);
|
||||
assertThatDebitorActuallyInDatabase(entity);
|
||||
}).assertSuccessful();
|
||||
}
|
||||
|
||||
private void assertThatDebitorIsNotVisibleForUserWithRole(
|
||||
final HsOfficeDebitorEntity entity,
|
||||
final String assumedRoles) {
|
||||
jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net", assumedRoles);
|
||||
final var found = debitorRepo.findByUuid(entity.getUuid());
|
||||
assertThat(found).isEmpty();
|
||||
}).assertSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DeleteByUuid {
|
||||
|
||||
@Test
|
||||
public void globalAdmin_withoutAssumedRole_canDeleteAnyDebitor() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net", null);
|
||||
final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "tenth");
|
||||
|
||||
// when
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net");
|
||||
debitorRepo.deleteByUuid(givenDebitor.getUuid());
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertSuccessful();
|
||||
assertThat(jpaAttempt.transacted(() -> {
|
||||
context("superuser-fran@hostsharing.net", null);
|
||||
return debitorRepo.findByUuid(givenDebitor.getUuid());
|
||||
}).assertSuccessful().returnedValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void relatedPerson_canNotDeleteTheirRelatedDebitor() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net", null);
|
||||
final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "eleventh");
|
||||
|
||||
// when
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context("person-Fourthe.G.@example.com");
|
||||
assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent();
|
||||
|
||||
debitorRepo.deleteByUuid(givenDebitor.getUuid());
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertExceptionWithRootCauseMessage(
|
||||
JpaSystemException.class,
|
||||
"[403] Subject ", " not allowed to delete hs_office_debitor");
|
||||
assertThat(jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net");
|
||||
return debitorRepo.findByUuid(givenDebitor.getUuid());
|
||||
}).assertSuccessful().returnedValue()).isPresent(); // still there
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deletingADebitorAlsoDeletesRelatedRolesAndGrants() {
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var initialRoleNames = Array.from(roleNamesOf(rawRoleRepo.findAll()));
|
||||
final var initialGrantNames = Array.from(grantDisplaysOf(rawGrantRepo.findAll()));
|
||||
final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "twelfth");
|
||||
assertThat(rawRoleRepo.findAll().size()).as("precondition failed: unexpected number of roles created")
|
||||
.isEqualTo(initialRoleNames.length + 3);
|
||||
assertThat(rawGrantRepo.findAll().size()).as("precondition failed: unexpected number of grants created")
|
||||
.isEqualTo(initialGrantNames.length + 12);
|
||||
|
||||
// when
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net");
|
||||
return debitorRepo.deleteByUuid(givenDebitor.getUuid());
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertSuccessful();
|
||||
assertThat(result.returnedValue()).isEqualTo(1);
|
||||
assertThat(roleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(initialRoleNames);
|
||||
assertThat(grantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(initialGrantNames);
|
||||
}
|
||||
}
|
||||
|
||||
private HsOfficePartnerEntity rawReference(final HsOfficePartnerEntity givenPartner) {
|
||||
return em.getReference(HsOfficePartnerEntity.class, givenPartner.getUuid());
|
||||
}
|
||||
|
||||
private HsOfficeContactEntity rawReference(final HsOfficeContactEntity givenContact) {
|
||||
return em.getReference(HsOfficeContactEntity.class, givenContact.getUuid());
|
||||
}
|
||||
|
||||
private HsOfficeDebitorEntity givenSomeTemporaryDebitor(final String partner, final String contact) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike(partner).get(0);
|
||||
final var givenContact = contactRepo.findContactByOptionalLabelLike(contact).get(0);
|
||||
final var newDebitor = HsOfficeDebitorEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.debitorNumber(20000)
|
||||
.partner(rawReference(givenPartner))
|
||||
.billingContact(rawReference(givenContact))
|
||||
.build();
|
||||
|
||||
toCleanup(newDebitor);
|
||||
|
||||
return debitorRepo.save(newDebitor);
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
private HsOfficeDebitorEntity toCleanup(final HsOfficeDebitorEntity tempDebitor) {
|
||||
tempDebitors.add(tempDebitor);
|
||||
return tempDebitor;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
context("superuser-alex@hostsharing.net", null);
|
||||
tempDebitors.forEach(tempDebitor -> {
|
||||
System.out.println("DELETING temporary debitor: " + tempDebitor.toString());
|
||||
debitorRepo.deleteByUuid(tempDebitor.getUuid());
|
||||
});
|
||||
}
|
||||
|
||||
void exactlyTheseDebitorsAreReturned(final List<HsOfficeDebitorEntity> actualResult, final String... debitorNames) {
|
||||
assertThat(actualResult)
|
||||
.extracting(HsOfficeDebitorEntity::toString)
|
||||
.containsExactlyInAnyOrder(debitorNames);
|
||||
}
|
||||
|
||||
void allTheseDebitorsAreReturned(final List<HsOfficeDebitorEntity> actualResult, final String... debitorNames) {
|
||||
assertThat(actualResult)
|
||||
.extracting(HsOfficeDebitorEntity::toString)
|
||||
.contains(debitorNames);
|
||||
}
|
||||
}
|
@ -87,6 +87,10 @@ class HsOfficePartnerControllerAcceptanceTest {
|
||||
{
|
||||
"person": { "tradeName": "Second e.K." },
|
||||
"contact": { "label": "second contact" }
|
||||
},
|
||||
{
|
||||
"person": { "personType": "SOLE_REPRESENTATION" },
|
||||
"contact": { "label": "forth contact" }
|
||||
}
|
||||
]
|
||||
"""));
|
||||
|
@ -70,6 +70,30 @@ class HsOfficePersonControllerAcceptanceTest {
|
||||
.body("", lenientlyEquals("""
|
||||
[
|
||||
{
|
||||
"personType": "LEGAL",
|
||||
"tradeName": "First GmbH",
|
||||
"givenName": null,
|
||||
"familyName": null
|
||||
},
|
||||
{
|
||||
"personType": "LEGAL",
|
||||
"tradeName": "Second e.K.",
|
||||
"givenName": "Miller",
|
||||
"familyName": "Sandra"
|
||||
},
|
||||
{
|
||||
"personType": "SOLE_REPRESENTATION",
|
||||
"tradeName": "Third OHG",
|
||||
"givenName": null,
|
||||
"familyName": null
|
||||
},
|
||||
{
|
||||
"personType": "SOLE_REPRESENTATION",
|
||||
"tradeName": "Fourth e.G.",
|
||||
"givenName": null,
|
||||
"familyName": null
|
||||
},
|
||||
{
|
||||
"personType": "NATURAL",
|
||||
"tradeName": null,
|
||||
"givenName": "Anita",
|
||||
@ -81,24 +105,6 @@ class HsOfficePersonControllerAcceptanceTest {
|
||||
"givenName": "Bessler",
|
||||
"familyName": "Mel"
|
||||
},
|
||||
{
|
||||
"personType": "LEGAL",
|
||||
"tradeName": "First GmbH",
|
||||
"givenName": null,
|
||||
"familyName": null
|
||||
},
|
||||
{
|
||||
"personType": "SOLE_REPRESENTATION",
|
||||
"tradeName": "Third OHG",
|
||||
"givenName": null,
|
||||
"familyName": null
|
||||
},
|
||||
{
|
||||
"personType": "LEGAL",
|
||||
"tradeName": "Second e.K.",
|
||||
"givenName": "Miller",
|
||||
"familyName": "Sandra"
|
||||
},
|
||||
{
|
||||
"personType": "NATURAL",
|
||||
"tradeName": null,
|
||||
@ -406,7 +412,7 @@ class HsOfficePersonControllerAcceptanceTest {
|
||||
final var entity = personRepo.findByUuid(uuid);
|
||||
final var count = personRepo.deleteByUuid(uuid);
|
||||
System.out.println("DELETED temporary person: " + uuid + (count > 0 ? " successful" : " failed") +
|
||||
(" (" + entity.map(HsOfficePersonEntity::getDisplayName).orElse("null") + ")"));
|
||||
(" (" + entity.map(hsOfficePersonEntity -> hsOfficePersonEntity.toShortString()).orElse("null") + ")"));
|
||||
}).assertSuccessful();
|
||||
});
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class HsOfficePersonEntityUnitTest {
|
||||
.tradeName("some trade name")
|
||||
.build();
|
||||
|
||||
final var actualDisplay = givenPersonEntity.getDisplayName();
|
||||
final var actualDisplay = givenPersonEntity.toShortString();
|
||||
|
||||
assertThat(actualDisplay).isEqualTo("some trade name");
|
||||
}
|
||||
@ -26,7 +26,7 @@ class HsOfficePersonEntityUnitTest {
|
||||
.givenName("some given name")
|
||||
.build();
|
||||
|
||||
final var actualDisplay = givenPersonEntity.getDisplayName();
|
||||
final var actualDisplay = givenPersonEntity.toShortString();
|
||||
|
||||
assertThat(actualDisplay).isEqualTo("some family name, some given name");
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ package net.hostsharing.hsadminng.hs.office.person;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.context.ContextBasedTest;
|
||||
import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
|
||||
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantRepository;
|
||||
import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
|
||||
import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository;
|
||||
import net.hostsharing.test.Array;
|
||||
import net.hostsharing.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@ -266,7 +268,7 @@ class HsOfficePersonRepositoryIntegrationTest extends ContextBasedTest {
|
||||
context("superuser-alex@hostsharing.net", null);
|
||||
final var result = personRepo.findPersonByOptionalNameLike("some temporary person");
|
||||
result.forEach(tempPerson -> {
|
||||
System.out.println("DELETING temporary person: " + tempPerson.getDisplayName());
|
||||
System.out.println("DELETING temporary person: " + tempPerson.toShortString());
|
||||
personRepo.deleteByUuid(tempPerson.getUuid());
|
||||
});
|
||||
}
|
||||
@ -293,7 +295,7 @@ class HsOfficePersonRepositoryIntegrationTest extends ContextBasedTest {
|
||||
|
||||
void allThesePersonsAreReturned(final List<HsOfficePersonEntity> actualResult, final String... personLabels) {
|
||||
assertThat(actualResult)
|
||||
.extracting(HsOfficePersonEntity::getDisplayName)
|
||||
.extracting(hsOfficePersonEntity -> hsOfficePersonEntity.toShortString())
|
||||
.contains(personLabels);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user