Compare commits

...

4 Commits

Author SHA1 Message Date
Michael Hoennig
d55fea7851 replace own implementation by com.jayway.jsonpath.JsonPath 2024-10-29 10:35:50 +01:00
Michael Hoennig
9a8a932570 remove explicit DEBITOR-type from other test-cases 2024-10-29 10:35:22 +01:00
Michael Hoennig
856bd089a1 add Scenario-Tests to test-concept.md 2024-10-29 10:34:45 +01:00
Michael Hoennig
47a195ec7a always use implicit DEBITOR-type for Debitor relation 2024-10-29 09:54:07 +01:00
8 changed files with 68 additions and 113 deletions

View File

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

View File

@ -77,17 +77,13 @@ 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) {
body.getDebitorRel().setType(DEBITOR.name());
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class); final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
debitorRel.setType(DEBITOR);
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());
@ -102,22 +98,6 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
}); });
} }
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); final var savedEntity = debitorRepo.save(entityToSave);
em.flush(); em.flush();
em.refresh(savedEntity); em.refresh(savedEntity);
@ -129,9 +109,6 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
.toUri(); .toUri();
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class); final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class);
return ResponseEntity.created(uri).body(mapped); return ResponseEntity.created(uri).body(mapped);
} catch (final RuntimeException exc) {
throw exc;
}
} }
@Override @Override

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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