Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Hoennig
cbf68f8657 add coop-shares-tests 2024-11-12 20:01:04 +01:00
Michael Hoennig
6177e32051 add M-memberNumber to identifier names 2024-11-11 11:54:40 +01:00
13 changed files with 156 additions and 66 deletions

View File

@ -57,7 +57,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override
@Transactional
public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction(
public ResponseEntity<HsOfficeCoopSharesTransactionResource> postCoopSharesTransaction(
final String currentSubject,
final String assumedRoles,
final HsOfficeCoopSharesTransactionInsertResource requestBody) {
@ -131,9 +131,9 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
}
final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if ( resource.getAdjustedShareTxUuid() != null ) {
entity.setAdjustedShareTx(coopSharesTransactionRepo.findByUuid(resource.getAdjustedShareTxUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] adjustedShareTxUuid %s not found".formatted(resource.getAdjustedShareTxUuid()))));
if ( resource.getRevertedShareTxUuid() != null ) {
entity.setAdjustedShareTx(coopSharesTransactionRepo.findByUuid(resource.getRevertedShareTxUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] adjustedShareTxUuid %s not found".formatted(resource.getRevertedShareTxUuid()))));
}
};
}

View File

@ -14,14 +14,16 @@ public interface HsOfficeMembershipRepository extends Repository<HsOfficeMembers
HsOfficeMembershipEntity save(final HsOfficeMembershipEntity entity);
List<HsOfficeMembershipEntity> findAll();
@Query("""
SELECT membership FROM HsOfficeMembershipEntity membership
WHERE ( CAST(:partnerUuid as org.hibernate.type.UUIDCharType) IS NULL
OR membership.partner.uuid = :partnerUuid )
ORDER BY membership.partner.partnerNumber, membership.memberNumberSuffix
""")
""")
List<HsOfficeMembershipEntity> findMembershipsByOptionalPartnerUuid(UUID partnerUuid);
@Query("""
SELECT membership FROM HsOfficeMembershipEntity membership
WHERE (:partnerNumber = membership.partner.partnerNumber)
@ -31,10 +33,18 @@ public interface HsOfficeMembershipRepository extends Repository<HsOfficeMembers
HsOfficeMembershipEntity findMembershipByPartnerNumberAndSuffix(
@NotNull Integer partnerNumber,
@NotNull String suffix);
default HsOfficeMembershipEntity findMembershipByMemberNumber(Integer memberNumber) {
final var partnerNumber = memberNumber / 100;
final var suffix = memberNumber % 100;
return findMembershipByPartnerNumberAndSuffix(partnerNumber, String.format("%02d", suffix));
final String suffix = String.format("%02d", memberNumber % 100);
final var result = findMembershipByPartnerNumberAndSuffix(partnerNumber, suffix);
// FIXME: remove
if (result == null) {
final var all = findAll();
all.size();
}
return result;
}
long count();

View File

@ -6,7 +6,7 @@ components:
HsOfficeCoopAssetsTransactionType:
type: string
enum:
- ADJUSTMENT
- ADJUSTMENT # FIXME: rename to REVERSAL
- DEPOSIT
- DISBURSAL
- TRANSFER
@ -32,9 +32,9 @@ components:
type: string
comment:
type: string
adjustedAssetTx:
adjustedAssetTx: # FIXME: rename to revertedAssetTx
$ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction'
adjustmentAssetTx:
adjustmentAssetTx: # FIXME: rename to reversalAssetTx
$ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction'
HsOfficeReferencedCoopAssetsTransaction:
@ -80,7 +80,7 @@ components:
maxLength: 48
comment:
type: string
reverseEntry.uuid:
reverseEntry.uuid: # FIXME: rename to revertedAssetTx
type: string
format: uuid
required:

View File

@ -6,7 +6,7 @@ components:
HsOfficeCoopSharesTransactionType:
type: string
enum:
- ADJUSTMENT
- ADJUSTMENT # FIXME: rename to REVERSAL
- SUBSCRIPTION
- CANCELLATION
@ -27,9 +27,9 @@ components:
type: string
comment:
type: string
adjustedShareTx:
revertedShareTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopSharesTransaction'
adjustmentShareTx:
reversalShareTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopSharesTransaction'
HsOfficeReferencedCoopSharesTransaction:
@ -73,7 +73,7 @@ components:
maxLength: 48
comment:
type: string
adjustedShareTx.uuid:
revertedShareTx.uuid:
type: string
format: uuid
required:

View File

@ -46,7 +46,7 @@ post:
summary: Adds a new cooperative share transaction.
tags:
- hs-office-coopShares
operationId: addCoopSharesTransaction
operationId: postCoopSharesTransaction
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'

View File

@ -28,6 +28,9 @@ create table if not exists hs_office.relation
);
--//
-- TODO.impl: unique constraint, to prevent using the same person multiple times as a partner, or better:
-- ( anchorUuid, holderUuid, type)
-- ============================================================================
--changeset michael.hoennig:hs-office-relation-MAIN-TABLE-JOURNAL endDelimiter:--//

View File

@ -12,7 +12,9 @@ import net.hostsharing.hsadminng.hs.office.scenarios.debitor.FinallyDeleteSepaMa
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDebitor;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.InvalidateSepaMandateForDebitor;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CancelMembership;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CoopSharesTransactionUseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateCoopSharesCancellationTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateCoopSharesRevertTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateCoopSharesSubscriptionTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership;
import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddOperationsContactToPartner;
import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner;
@ -262,18 +264,18 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(2020)
@Requires("Debitor: Test AG - main debitor")
@Requires("Debitor: D-3101000 - Test AG - main debitor")
@Disabled("see TODO.spec in DontDeleteDefaultDebitor")
void shouldNotDeleteDefaultDebitor() {
new DontDeleteDefaultDebitor(this)
.given("partnerNumber", 31020)
.given("partnerNumber", 31010)
.given("debitorSuffix", "00")
.doRun();
}
@Test
@Order(3100)
@Requires("Debitor: Test AG - main debitor")
@Requires("Debitor: D-3101000 - Test AG - main debitor")
@Produces("SEPA-Mandate: Test AG")
void shouldCreateSepaMandateForDebitor() {
new CreateSepaMandateForDebitor(this)
@ -315,7 +317,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(4000)
@Requires("Partner: P-31010 - Test AG")
@Produces("Membership: Test AG 00")
@Produces("Membership: M-3101000 - Test AG")
void shouldCreateMembershipForPartner() {
new CreateMembership(this)
.given("partnerName", "Test AG")
@ -328,15 +330,14 @@ class HsOfficeScenarioTests extends ScenarioTest {
}
@Test
@Order(4200)
@Requires("Membership: Test AG 00")
@Order(4201)
@Requires("Membership: M-3101000 - Test AG")
@Produces("Coop-Shares SUBSCRIPTION Transaction")
void testCoopSharesSubscriptionTransaction() {
new CoopSharesTransactionUseCase(this)
void shouldSubscribeCoopShares() {
new CreateCoopSharesSubscriptionTransaction(this)
.given("memberNumber", "3101000")
.given("transactionType", "SUBSCRIPTION")
.given("reference", "sign 2024-01-15")
.given("shareCount", "100")
.given("shareCount", 100)
.given("comment", "Signing the Membership")
.given("transactionDate", "2024-01-15")
.doRun();
@ -344,16 +345,12 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(4201)
@Requires("Coop-Shares SUBSCRIPTION Transaction")
@Produces("Coop-Shares ADJUSTMENT Transaction")
void testCoopSharesAdjustmentTransaction() {
new CoopSharesTransactionUseCase(this)
.given("memberNumber", "3102000")
.given("transactionType", "ADJUSTMENT")
.given("reference", "adjust 2024-01-16")
.given("shareCount", "-90")
.given("comment", "Cancelling 90 Shares, correcting wrong number of digits in subscription")
.given("transactionDate", "2024-01-16")
@Requires("Membership: M-3101000 - Test AG")
void shouldRevertCoopSharesSubscription() {
new CreateCoopSharesRevertTransaction(this)
.given("memberNumber", "3101000")
.given("comment", "reverting some incorrect subscription")
.given("dateOfIncorrectTransaction", "2024-02-15")
.doRun();
}
@ -361,12 +358,11 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Order(4202)
@Requires("Coop-Shares SUBSCRIPTION Transaction")
@Produces("Coop-Shares CANCELLATION Transaction")
void testCoopSharesCancellationTransaction() {
new CoopSharesTransactionUseCase(this)
.given("memberNumber", "3102000")
.given("transactionType", "CANCELLATION")
void shouldCancelCoopSharesSubscription() {
new CreateCoopSharesCancellationTransaction(this)
.given("memberNumber", "3101000")
.given("reference", "cancel 2024-01-15")
.given("shareCount", "8")
.given("sharesToCancel", 8)
.given("comment", "Cancelling 8 Shares")
.given("transactionDate", "2024-02-15")
.doRun();
@ -374,7 +370,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(4900)
@Requires("Membership: Test AG 00")
@Requires("Membership: M-3101000 - Test AG")
void shouldCancelMembershipOfPartner() {
new CancelMembership(this)
.given("memberNumber", "3101000")

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.scenarios;
import lombok.SneakyThrows;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.TestInfo;
@ -15,23 +16,27 @@ import static org.assertj.core.api.Assertions.assertThat;
public class TestReport {
private final Map<String, ?> aliases;
private final StringBuilder markdownLog = new StringBuilder(); // records everything for debugging purposes
private final static File markdownLogFile = new File("doc/scenarios/last-debug-log.md");
private PrintWriter markdownReport;
private final Map<String, ?> aliases;
private final PrintWriter markdownLog; // records everything for debugging purposes
private File markdownReportFile;
private PrintWriter markdownReport; // records only the use-case under test, without its pre-requisites
private int silent; // do not print anything to test-report if >0
@SneakyThrows
public TestReport(final Map<String, ?> aliases) {
this.aliases = aliases;
this.markdownLog = new PrintWriter(new FileWriter(markdownLogFile));
}
public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException {
final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow();
final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow();
assertThat(new File("doc/scenarios/").isDirectory() || new File("doc/scenarios/").mkdirs()).as("mkdir doc/scenarios/").isTrue();
markdownReport = new PrintWriter(new FileWriter("doc/scenarios/" + testMethodOrder + "-" + testMethodName + ".md"));
print("## Scenario #" + testInfo.getTestMethod().map(TestReport::orderNumber).orElseThrow() + ": " +
testMethodName.replaceAll("([a-z])([A-Z]+)", "$1 $2"));
markdownReportFile = new File("doc/scenarios/" + testMethodOrder + "-" + testMethodName + ".md");
markdownReport = new PrintWriter(new FileWriter(markdownReportFile));
print("## Scenario #" + determineScenarioTitle(testInfo));
}
@SneakyThrows
@ -45,7 +50,7 @@ public class TestReport {
}
// but the debugLog should contain all output, even if silent
markdownLog.append(outputWithCommentsForUuids);
markdownLog.print(outputWithCommentsForUuids);
}
public void printLine(final String output) {
@ -59,7 +64,21 @@ public class TestReport {
public void close() {
if (markdownReport != null) {
markdownReport.close();
System.out.println("SCENARIO REPORT: " + asClickableLink(markdownReportFile));
}
markdownLog.close();
System.out.println("DEBUG LOG: " + asClickableLink(markdownLogFile));
}
private static @NotNull String determineScenarioTitle(final TestInfo testInfo) {
final var convertedTestMethodName =
testInfo.getTestMethod().map(TestReport::orderNumber).orElseThrow() + ": " +
testInfo.getTestMethod().map(Method::getName).map(t -> t.replaceAll("([a-z])([A-Z]+)", "$1 $2")).orElseThrow();
return convertedTestMethodName.replaceAll(": should ", ": ");
}
private String asClickableLink(final File file) {
return file.toURI().toString().replace("file:/", "file:///");
}
private static Object orderNumber(final Method method) {
@ -88,5 +107,4 @@ public class TestReport {
code.run();
silent--;
}
}

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import io.restassured.http.ContentType;
import lombok.Getter;
import lombok.SneakyThrows;
@ -81,7 +82,7 @@ public abstract class UseCase<T extends UseCase<?>> {
testReport.silent(() ->
requirements.forEach((alias, factory) -> {
if (!ScenarioTest.containsAlias(alias)) {
factory.apply(alias).run().keep();
factory.apply(alias).run().keepAs(alias);
}
})
);
@ -126,7 +127,7 @@ public abstract class UseCase<T extends UseCase<?>> {
}
public HttpResponse withTitle(final String title, final Supplier<HttpResponse> code) {
this.nextTitle = title;
this.nextTitle = ScenarioTest.resolve(title);
final var response = code.get();
this.nextTitle = null;
return response;
@ -276,15 +277,20 @@ public abstract class UseCase<T extends UseCase<?>> {
return this;
}
public HttpResponse keep() {
final var alias = nextTitle != null ? nextTitle : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
public HttpResponse keepAs(final String alias) {
ScenarioTest.putAlias(
alias,
alias == null ? "unknown alias" : alias, // FIXME
new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid));
return this;
}
public HttpResponse keep() {
final var alias = nextTitle != null ? nextTitle : resultAlias;
// FIXME assertThat(alias).as("cannot keep result, no title or alias found for locationUuid: " + locationUuid).isNotNull();
return keepAs(alias);
}
@SneakyThrows
public HttpResponse expectArrayElements(final int expectedElementCount) {
final var rootNode = objectMapper.readTree(response.body());
@ -305,8 +311,7 @@ public abstract class UseCase<T extends UseCase<?>> {
public <T> Optional<T> getFromBodyAsOptional(final String path) {
try {
return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path)));
} catch (final Exception e) {
// FIXME: catch more precise exception class
} catch (final PathNotFoundException e) {
return null; // means the property did not exist at all, not that it was there with value null
}
}

View File

@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopSharesCancellationTransaction extends CreateCoopSharesTransaction {
public CreateCoopSharesCancellationTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
given("transactionType", "CANCELLATION");
given("shareCount", "-%{sharesToCancel}");
return super.run();
}
}

View File

@ -0,0 +1,27 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopSharesRevertTransaction extends CreateCoopSharesTransaction {
public CreateCoopSharesRevertTransaction(final ScenarioTest testSuite) {
super(testSuite);
requires("CoopShares-Transaction with incorrect shareCount", alias ->
new CreateCoopSharesSubscriptionTransaction(testSuite)
.given("memberNumber", "3101000")
.given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedShareTx
.given("shareCount", 100)
.given("comment", "reverting subscription transaction with wrong share count")
.given("transactionDate", "%{dateOfIncorrectTransaction}")
);
}
@Override
protected HttpResponse run() {
given("transactionType", "ADJUSTMENT");
given("shareCount", -100);
given("revertedShareTx", uuid("CoopShares-Transaction with incorrect shareCount"));
return super.run();
}
}

View File

@ -0,0 +1,12 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopSharesSubscriptionTransaction extends CreateCoopSharesTransaction {
public CreateCoopSharesSubscriptionTransaction(final ScenarioTest testSuite) {
super(testSuite);
given("transactionType", "SUBSCRIPTION");
}
}

View File

@ -8,9 +8,9 @@ import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.OK;
public class CoopSharesTransactionUseCase extends UseCase<CoopSharesTransactionUseCase> {
public abstract class CreateCoopSharesTransaction extends UseCase<CreateCoopSharesTransaction> {
public CoopSharesTransactionUseCase(final ScenarioTest testSuite) {
public CreateCoopSharesTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@ -23,17 +23,20 @@ public class CoopSharesTransactionUseCase extends UseCase<CoopSharesTransactionU
response -> response.getFromBody("$[0].uuid")
);
return httpPost("/api/hs/office/coopsharestransactions", usingJsonBody("""
return withTitle("Create the CoopShares-%{transactionType} Transaction", () ->
httpPost("/api/hs/office/coopsharestransactions", usingJsonBody("""
{
"membership.uuid": ${membershipUuid},
"transactionType": ${transactionType},
"reference": ${reference},
"shareCount": ${shareCount},
"comment": ${comment},
"valueDate": ${transactionDate}
"valueDate": ${transactionDate},
"revertedShareTx.uuid": ${revertedShareTx???}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON);
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
}
@Override
@ -41,8 +44,7 @@ public class CoopSharesTransactionUseCase extends UseCase<CoopSharesTransactionU
verify("Verify Coop-Shares %{transactionType}-Transaction",
() -> httpGet("/api/hs/office/coopsharestransactions/" + response.getLocationUuid())
.expecting(HttpStatus.OK).expecting(ContentType.JSON),
// path("transactionType").contains("%{transactionType}"),
// path("memberNumber").contains("%{memberNumber}"),
path("transactionType").contains("%{transactionType}"),
path("shareCount").contains("%{shareCount}"),
path("comment").contains("%{comment}"),
path("valueDate").contains("%{transactionDate}")