Compare commits
4 Commits
8fc81d8e2b
...
d55fea7851
Author | SHA1 | Date | |
---|---|---|---|
|
d55fea7851 | ||
|
9a8a932570 | ||
|
856bd089a1 | ||
|
47a195ec7a |
@ -90,6 +90,20 @@ Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-cove
|
|||||||
TODO.test: Complete the Acceptance-Tests test concept.
|
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-Tests
|
||||||
|
|
||||||
Performance-critical scenarios have to be identified and a special performance-test has to be implemented.
|
Performance-critical scenarios have to be identified and a special performance-test has to be implemented.
|
||||||
|
@ -77,61 +77,38 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
|||||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
|
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
|
||||||
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
|
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
|
||||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none");
|
"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,
|
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
|
||||||
"ERROR: [400] debitorRel.mark must be null");
|
"ERROR: [400] debitorRel.mark must be null");
|
||||||
|
|
||||||
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
||||||
try {
|
if (body.getDebitorRel() != null) {
|
||||||
if (body.getDebitorRel() != null) {
|
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
||||||
body.getDebitorRel().setType(DEBITOR.name());
|
debitorRel.setType(DEBITOR);
|
||||||
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
||||||
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
||||||
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
||||||
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
|
||||||
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
|
} else {
|
||||||
} else {
|
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
|
||||||
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
|
debitorRelOptional.ifPresentOrElse(
|
||||||
debitorRelOptional.ifPresentOrElse(
|
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
|
||||||
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
|
() -> {
|
||||||
() -> {
|
throw new ValidationException(
|
||||||
throw new ValidationException(
|
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
|
||||||
"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
|
@Override
|
||||||
|
@ -74,7 +74,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
debitorRel:
|
debitorRel:
|
||||||
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
|
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationSubInsert'
|
||||||
debitorRelUuid:
|
debitorRelUuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
@ -41,6 +41,7 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|
||||||
|
# arbitrary relation with explicit type
|
||||||
HsOfficeRelationInsert:
|
HsOfficeRelationInsert:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -64,3 +65,24 @@ components:
|
|||||||
- holderUuid
|
- holderUuid
|
||||||
- type
|
- type
|
||||||
- contactUuid
|
- 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
|
||||||
|
@ -12,7 +12,6 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
|||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
import org.json.JSONException;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
@ -76,7 +75,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
|||||||
class ListDebitors {
|
class ListDebitors {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() throws JSONException {
|
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() {
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@ -334,7 +333,6 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
|||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"debitorRel": {
|
"debitorRel": {
|
||||||
"type": "DEBITOR",
|
|
||||||
"anchorUuid": "%s",
|
"anchorUuid": "%s",
|
||||||
"holderUuid": "%s",
|
"holderUuid": "%s",
|
||||||
"contactUuid": "%s"
|
"contactUuid": "%s"
|
||||||
@ -386,7 +384,6 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
|||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"debitorRel": {
|
"debitorRel": {
|
||||||
"type": "DEBITOR",
|
|
||||||
"anchorUuid": "%s",
|
"anchorUuid": "%s",
|
||||||
"holderUuid": "%s",
|
"holderUuid": "%s",
|
||||||
"contactUuid": "%s"
|
"contactUuid": "%s"
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.jayway.jsonpath.JsonPath;
|
||||||
import io.restassured.http.ContentType;
|
import io.restassured.http.ContentType;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
@ -258,9 +258,7 @@ public abstract class UseCase<T extends UseCase<?>> {
|
|||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public String getFromBody(final String path) {
|
public String getFromBody(final String path) {
|
||||||
// FIXME: use JsonPath: https://www.baeldung.com/guide-to-jayway-jsonpath
|
return JsonPath.parse(response.body()).read(path);
|
||||||
final var rootNode = objectMapper.readTree(response.body());
|
|
||||||
return getPropertyFromJson(rootNode, path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,58 +294,7 @@ public abstract class UseCase<T extends UseCase<?>> {
|
|||||||
throw new AssertionFailure("exactly one value required, but got '" + one + "' and '" + another + "'");
|
throw new AssertionFailure("exactly one value required, but got '" + one + "' and '" + another + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String title(String resultAlias) {
|
private String title(String resultAlias) {
|
||||||
return getClass().getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1 $2") + " => " + 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,6 @@ public class CreateExternalDebitorForPartner extends UseCase<CreateExternalDebit
|
|||||||
return httpPost("/api/hs/office/debitors", usingJsonBody("""
|
return httpPost("/api/hs/office/debitors", usingJsonBody("""
|
||||||
{
|
{
|
||||||
"debitorRel": {
|
"debitorRel": {
|
||||||
"type": "DEBITOR", // FIXME: should be defaulted to DEBITOR
|
|
||||||
"anchorUuid": ${partnerPersonUuid},
|
"anchorUuid": ${partnerPersonUuid},
|
||||||
"holderUuid": ${Person: Billing GmbH},
|
"holderUuid": ${Person: Billing GmbH},
|
||||||
"contactUuid": ${Contact: Billing GmbH - Test AG billing}
|
"contactUuid": ${Contact: Billing GmbH - Test AG billing}
|
||||||
|
@ -49,7 +49,6 @@ public class CreateSelfDebitorForPartner extends UseCase<CreateSelfDebitorForPar
|
|||||||
return httpPost("/api/hs/office/debitors", usingJsonBody("""
|
return httpPost("/api/hs/office/debitors", usingJsonBody("""
|
||||||
{
|
{
|
||||||
"debitorRel": {
|
"debitorRel": {
|
||||||
"type": "DEBITOR", // TODO.impl: should become defaulted to DEBITOR
|
|
||||||
"anchorUuid": ${partnerPersonUuid},
|
"anchorUuid": ${partnerPersonUuid},
|
||||||
"holderUuid": ${partnerPersonUuid},
|
"holderUuid": ${partnerPersonUuid},
|
||||||
"contactUuid": ${Contact: Test AG - billing department}
|
"contactUuid": ${Contact: Test AG - billing department}
|
||||||
|
Loading…
Reference in New Issue
Block a user