Compare commits

..

No commits in common. "d55fea7851168a1efa1d00fbbc07899b27cdf542" and "8fc81d8e2b3207c22aba4613692714e8dc721b0a" have entirely different histories.

8 changed files with 113 additions and 68 deletions

View File

@ -90,20 +90,6 @@ Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-cove
TODO.test: Complete the Acceptance-Tests test concept.
#### Scenario-Tests
Our Scenario-tests are induced by business use-cases.
They test from the REST API all the way down to the database.
Most scenario-tests are positive tests, they test if business scenarios do work.
But few might be negative tests, which test if specific forbidden data gets rejected.
Our scenario tests also generate test-reports which contain the REST-API calls needed for each scenario.
These reports can be used as examples for the API usage from a business perspective.
There is an extra document regarding scenario-test, see [Scenario-Tests README](../src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md).
#### Performance-Tests
Performance-critical scenarios have to be identified and a special performance-test has to be implemented.

View File

@ -77,38 +77,61 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none");
Validate.isTrue(body.getDebitorRel() == null ||
body.getDebitorRel().getType() == null || DEBITOR.name().equals(body.getDebitorRel().getType()),
"ERROR: [400] debitorRel.type must be '"+DEBITOR.name()+"' or null for default");
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
"ERROR: [400] debitorRel.mark must be null");
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
if (body.getDebitorRel() != null) {
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
debitorRel.setType(DEBITOR);
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
} else {
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse(
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
() -> {
throw new ValidationException(
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
});
try {
if (body.getDebitorRel() != null) {
body.getDebitorRel().setType(DEBITOR.name());
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
} else {
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse(
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
() -> {
throw new ValidationException(
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
});
}
final var partnerRel = em.createNativeQuery("""
SELECT partnerRel.*
FROM hs_office.relation AS partnerRel
JOIN hs_office.relation AS debitorRel
ON debitorRel.type = 'DEBITOR' AND debitorRel.anchorUuid = partnerRel.holderUuid
WHERE partnerRel.type = 'PARTNER'
AND :NEW_DebitorRelUuid = debitorRel.uuid
""").setParameter("NEW_DebitorRelUuid", entityToSave.getDebitorRel().getUuid()).getResultList();
final var debitorRel = em.createNativeQuery("""
SELECT debitorRel.*
FROM hs_office.relation AS debitorRel
WHERE :NEW_DebitorRelUuid = debitorRel.uuid
""").setParameter("NEW_DebitorRelUuid", entityToSave.getDebitorRel().getUuid()).getResultList();
final var savedEntity = debitorRepo.save(entityToSave);
em.flush();
em.refresh(savedEntity);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/office/debitors/{id}")
.buildAndExpand(savedEntity.getUuid())
.toUri();
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class);
return ResponseEntity.created(uri).body(mapped);
} catch (final RuntimeException exc) {
throw exc;
}
final var savedEntity = debitorRepo.save(entityToSave);
em.flush();
em.refresh(savedEntity);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/office/debitors/{id}")
.buildAndExpand(savedEntity.getUuid())
.toUri();
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class);
return ResponseEntity.created(uri).body(mapped);
}
@Override

View File

@ -74,7 +74,7 @@ components:
type: object
properties:
debitorRel:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationSubInsert'
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
debitorRelUuid:
type: string
format: uuid

View File

@ -41,7 +41,6 @@ components:
format: uuid
nullable: true
# arbitrary relation with explicit type
HsOfficeRelationInsert:
type: object
properties:
@ -65,24 +64,3 @@ components:
- holderUuid
- type
- contactUuid
# relation created as a sub-element with implicitly known type
HsOfficeRelationSubInsert:
type: object
properties:
anchorUuid:
type: string
format: uuid
holderUuid:
type: string
format: uuid
mark:
type: string
nullable: true
contactUuid:
type: string
format: uuid
required:
- anchorUuid
- holderUuid
- contactUuid

View File

@ -12,6 +12,7 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.json.JSONException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@ -75,7 +76,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
class ListDebitors {
@Test
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() {
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() throws JSONException {
RestAssured // @formatter:off
.given()
@ -333,6 +334,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body("""
{
"debitorRel": {
"type": "DEBITOR",
"anchorUuid": "%s",
"holderUuid": "%s",
"contactUuid": "%s"
@ -384,6 +386,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body("""
{
"debitorRel": {
"type": "DEBITOR",
"anchorUuid": "%s",
"holderUuid": "%s",
"contactUuid": "%s"

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import io.restassured.http.ContentType;
import lombok.Getter;
import lombok.SneakyThrows;
@ -258,7 +258,9 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
public String getFromBody(final String path) {
return JsonPath.parse(response.body()).read(path);
// FIXME: use JsonPath: https://www.baeldung.com/guide-to-jayway-jsonpath
final var rootNode = objectMapper.readTree(response.body());
return getPropertyFromJson(rootNode, path);
}
}
@ -294,7 +296,58 @@ public abstract class UseCase<T extends UseCase<?>> {
throw new AssertionFailure("exactly one value required, but got '" + one + "' and '" + another + "'");
}
private String title(String resultAlias) {
private final String title(String resultAlias) {
return getClass().getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1 $2") + " => " + resultAlias;
}
// FIXME: refactor to own class
/**
* Extracts a property from a JsonNode based on a dotted path.
* Supports array notation like "users[0].address.city" and root arrays like "[0].user.address.city".
*
* @param rootNode the root JsonNode
* @param propertyPath the property path in dot notation (e.g., "[0].user.address.city")
* @return the extracted property value as a String
*/
public static String getPropertyFromJson(final JsonNode rootNode, final String propertyPath) {
final var pathParts = propertyPath.split("\\.");
var currentNode = rootNode;
// Traverse the JSON structure based on the path parts
for (final var part : pathParts) {
// Check if the part contains array notation like "[0]"
if (part.contains("[")) {
String arrayName;
final var arrayIndex = Integer.parseInt(part.substring(part.indexOf("[") + 1, part.indexOf("]")));
if (part.startsWith("[")) {
// This is a root-level array access (e.g., "[0]")
arrayName = null;
} else {
// This is a nested array access (e.g., "users[0]")
arrayName = part.substring(0, part.indexOf("["));
}
// If there's an array name, traverse to it
if (arrayName != null) {
currentNode = currentNode.path(arrayName);
}
// Ensure the current node is an array, then access the element at the index
if (currentNode.isArray()) {
currentNode = currentNode.get(arrayIndex);
}
} else {
// Traverse as a normal field
currentNode = currentNode.path(part);
}
// If at any point, the node is missing, return null
if (currentNode.isMissingNode()) {
return null;
}
}
return currentNode.asText(); // Return the final value as a String
}
}

View File

@ -46,6 +46,7 @@ public class CreateExternalDebitorForPartner extends UseCase<CreateExternalDebit
return httpPost("/api/hs/office/debitors", usingJsonBody("""
{
"debitorRel": {
"type": "DEBITOR", // FIXME: should be defaulted to DEBITOR
"anchorUuid": ${partnerPersonUuid},
"holderUuid": ${Person: Billing GmbH},
"contactUuid": ${Contact: Billing GmbH - Test AG billing}

View File

@ -49,6 +49,7 @@ public class CreateSelfDebitorForPartner extends UseCase<CreateSelfDebitorForPar
return httpPost("/api/hs/office/debitors", usingJsonBody("""
{
"debitorRel": {
"type": "DEBITOR", // TODO.impl: should become defaulted to DEBITOR
"anchorUuid": ${partnerPersonUuid},
"holderUuid": ${partnerPersonUuid},
"contactUuid": ${Contact: Test AG - billing department}