src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java
New file @@ -0,0 +1,154 @@ package net.hostsharing.hsadminng.hs.office.membership; import com.vladmihalcea.hibernate.type.range.Range; import net.hostsharing.hsadminng.Mapper; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembershipsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource; 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 javax.validation.Valid; import java.time.LocalDate; import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.function.BiConsumer; import static net.hostsharing.hsadminng.Mapper.map; @RestController public class HsOfficeMembershipController implements HsOfficeMembershipsApi { @Autowired private Context context; @Autowired private HsOfficeMembershipRepository membershipRepo; @Autowired private EntityManager em; @Override @Transactional(readOnly = true) public ResponseEntity<List<HsOfficeMembershipResource>> listMemberships( final String currentUser, final String assumedRoles, UUID partnerUuid, Integer memberNumber) { context.define(currentUser, assumedRoles); final var entities = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(partnerUuid, memberNumber); final var resources = Mapper.mapList(entities, HsOfficeMembershipResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(resources); } @Override @Transactional public ResponseEntity<HsOfficeMembershipResource> addMembership( final String currentUser, final String assumedRoles, @Valid final HsOfficeMembershipInsertResource body) { context.define(currentUser, assumedRoles); final var entityToSave = map(body, HsOfficeMembershipEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER); entityToSave.setUuid(UUID.randomUUID()); final var saved = membershipRepo.save(entityToSave); final var uri = MvcUriComponentsBuilder.fromController(getClass()) .path("/api/hs/office/Memberships/{id}") .buildAndExpand(entityToSave.getUuid()) .toUri(); final var mapped = map(saved, HsOfficeMembershipResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.created(uri).body(mapped); } @Override @Transactional(readOnly = true) public ResponseEntity<HsOfficeMembershipResource> getMembershipByUuid( final String currentUser, final String assumedRoles, final UUID membershipUuid) { context.define(currentUser, assumedRoles); final var result = membershipRepo.findByUuid(membershipUuid); if (result.isEmpty()) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(map(result.get(), HsOfficeMembershipResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER)); } @Override @Transactional public ResponseEntity<Void> deleteMembershipByUuid( final String currentUser, final String assumedRoles, final UUID membershipUuid) { context.define(currentUser, assumedRoles); final var result = membershipRepo.deleteByUuid(membershipUuid); if (result == 0) { return ResponseEntity.notFound().build(); } return ResponseEntity.noContent().build(); } @Override @Transactional public ResponseEntity<HsOfficeMembershipResource> patchMembership( final String currentUser, final String assumedRoles, final UUID membershipUuid, final HsOfficeMembershipPatchResource body) { context.define(currentUser, assumedRoles); final var current = membershipRepo.findByUuid(membershipUuid).orElseThrow(); current.setValidity(toPostgresDateRange(current.getValidity().lower(), body.getValidTo())); // current.setReasonForTermination(HsOfficeReasonForTermination.valueOf(body.getReasonForTermination().name())); current.setReasonForTermination( Optional.ofNullable(body.getReasonForTermination()).map(Enum::name).map(HsOfficeReasonForTermination::valueOf).orElse(current.getReasonForTermination()) ); final var saved = membershipRepo.save(current); final var mapped = map(saved, HsOfficeMembershipResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); } private static Range<LocalDate> toPostgresDateRange( final LocalDate validFrom, final LocalDate validTo) { return validTo != null ? Range.closedOpen(validFrom, validTo.plusDays(1)) : Range.closedInfinite(validFrom); } final BiConsumer<HsOfficeMembershipEntity, HsOfficeMembershipResource> SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { resource.setValidFrom(entity.getValidity().lower()); if (entity.getValidity().hasUpperBound()) { resource.setValidTo(entity.getValidity().upper().minusDays(1)); } }; final BiConsumer<HsOfficeMembershipInsertResource, HsOfficeMembershipEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); }; } src/main/resources/api-definition/hs-office/api-mappings.yaml
@@ -28,3 +28,5 @@ null: org.openapitools.jackson.nullable.JsonNullable /api/hs/office/sepamandates/{debitorUUID}: null: org.openapitools.jackson.nullable.JsonNullable /api/hs/office/memberships/{membershipUUID}: null: org.openapitools.jackson.nullable.JsonNullable src/main/resources/api-definition/hs-office/hs-office-membership-schemas.yaml
New file @@ -0,0 +1,75 @@ components: schemas: HsOfficeReasonForTermination: type: string enum: - NONE - CANCELLATION - TRANSFER - DEATH - LIQUIDATION - EXPULSION HsOfficeMembership: type: object properties: uuid: type: string format: uuid partner: $ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner' mainDebitor: $ref: './hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitor' memberNumber: type: integer validFrom: type: string format: date validTo: type: string format: date reasonForTermination: $ref: '#/components/schemas/HsOfficeReasonForTermination' HsOfficeMembershipPatch: type: object properties: validTo: type: string format: date reasonForTermination: $ref: '#/components/schemas/HsOfficeReasonForTermination' additionalProperties: false HsOfficeMembershipInsert: type: object properties: partnerUuid: type: string format: uuid nullable: false mainDebitorUuid: type: string format: uuid nullable: false memberNumber: type: integer nullable: false validFrom: type: string format: date nullable: false validTo: type: string format: date nullable: true reasonForTermination: $ref: '#/components/schemas/HsOfficeReasonForTermination' required: - partnerUuid - debitorUuid - validFrom additionalProperties: false src/main/resources/api-definition/hs-office/hs-office-memberships-with-uuid.yaml
New file @@ -0,0 +1,83 @@ get: tags: - hs-office-memberships description: 'Fetch a single membership by its uuid, if visible for the current subject.' operationId: getMembershipByUuid parameters: - $ref: './auth.yaml#/components/parameters/currentUser' - $ref: './auth.yaml#/components/parameters/assumedRoles' - name: membershipUUID in: path required: true schema: type: string format: uuid description: UUID of the membership to fetch. responses: "200": description: OK content: 'application/json': schema: $ref: './hs-office-membership-schemas.yaml#/components/schemas/HsOfficeMembership' "401": $ref: './error-responses.yaml#/components/responses/Unauthorized' "403": $ref: './error-responses.yaml#/components/responses/Forbidden' patch: tags: - hs-office-memberships description: 'Updates a single membership by its uuid, if permitted for the current subject.' operationId: patchMembership parameters: - $ref: './auth.yaml#/components/parameters/currentUser' - $ref: './auth.yaml#/components/parameters/assumedRoles' - name: membershipUUID in: path required: true schema: type: string format: uuid requestBody: content: 'application/json': schema: $ref: './hs-office-membership-schemas.yaml#/components/schemas/HsOfficeMembershipPatch' responses: "200": description: OK content: 'application/json': schema: $ref: './hs-office-membership-schemas.yaml#/components/schemas/HsOfficeMembership' "401": $ref: './error-responses.yaml#/components/responses/Unauthorized' "403": $ref: './error-responses.yaml#/components/responses/Forbidden' delete: tags: - hs-office-memberships description: 'Delete a single membership by its uuid, if permitted for the current subject.' operationId: deleteMembershipByUuid parameters: - $ref: './auth.yaml#/components/parameters/currentUser' - $ref: './auth.yaml#/components/parameters/assumedRoles' - name: membershipUUID in: path required: true schema: type: string format: uuid description: UUID of the membership 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' src/main/resources/api-definition/hs-office/hs-office-memberships.yaml
New file @@ -0,0 +1,64 @@ get: summary: Returns a list of (optionally filtered) memberships. description: Returns the list of (optionally filtered) memberships which are visible to the current user or any of it's assumed roles. tags: - hs-office-memberships operationId: listMemberships parameters: - $ref: './auth.yaml#/components/parameters/currentUser' - $ref: './auth.yaml#/components/parameters/assumedRoles' - name: partnerUuid in: query required: false schema: type: string format: uuid description: UUID of the business partner. - name: memberNumber in: query required: false schema: type: integer description: Member number. responses: "200": description: OK content: 'application/json': schema: type: array items: $ref: './hs-office-membership-schemas.yaml#/components/schemas/HsOfficeMembership' "401": $ref: './error-responses.yaml#/components/responses/Unauthorized' "403": $ref: './error-responses.yaml#/components/responses/Forbidden' post: summary: Adds a new membership. tags: - hs-office-memberships operationId: addMembership parameters: - $ref: './auth.yaml#/components/parameters/currentUser' - $ref: './auth.yaml#/components/parameters/assumedRoles' requestBody: description: A JSON object describing the new membership. required: true content: application/json: schema: $ref: '/hs-office-membership-schemas.yaml#/components/schemas/HsOfficeMembershipInsert' responses: "201": description: Created content: 'application/json': schema: $ref: './hs-office-membership-schemas.yaml#/components/schemas/HsOfficeMembership' "401": $ref: './error-responses.yaml#/components/responses/Unauthorized' "403": $ref: './error-responses.yaml#/components/responses/Forbidden' "409": $ref: './error-responses.yaml#/components/responses/Conflict' src/main/resources/api-definition/hs-office/hs-office.yaml
@@ -69,3 +69,12 @@ /api/hs/office/sepamandates/{sepaMandateUUID}: $ref: "./hs-office-sepamandates-with-uuid.yaml" # Membership /api/hs/office/memberships: $ref: "./hs-office-memberships.yaml" /api/hs/office/memberships/{membershipUUID}: $ref: "./hs-office-memberships-with-uuid.yaml" src/main/resources/db/changelog/300-hs-office-membership.sql
@@ -13,7 +13,7 @@ uuid uuid unique references RbacObject (uuid) initially deferred, partnerUuid uuid not null references hs_office_partner(uuid), mainDebitorUuid uuid not null references hs_office_debitor(uuid), memberNumber numeric(5) not null, memberNumber numeric(5) not null unique, validity daterange not null, reasonForTermination HsOfficeReasonForTermination not null default 'NONE' ); src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java
New file @@ -0,0 +1,476 @@ package net.hostsharing.hsadminng.hs.office.membership; import com.vladmihalcea.hibernate.type.range.Range; 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.debitor.HsOfficeDebitorRepository; 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.BeforeEach; 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 javax.persistence.EntityManager; import java.time.LocalDate; import java.util.UUID; import static net.hostsharing.hsadminng.hs.office.membership.HsOfficeReasonForTermination.NONE; 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.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { HsadminNgApplication.class, JpaAttempt.class } ) @Transactional class HsOfficeMembershipControllerAcceptanceTest { @LocalServerPort private Integer port; @Autowired Context context; @Autowired Context contextMock; @Autowired HsOfficeMembershipRepository membershipRepo; @Autowired HsOfficeDebitorRepository debitorRepo; @Autowired HsOfficePartnerRepository partnerRepo; @Autowired JpaAttempt jpaAttempt; @Autowired EntityManager em; private static int tempMemberNumber = 20010; @Nested @Accepts({ "Membership:F(Find)" }) class ListMemberships { @Test void globalAdmin_canViewAllMemberships_ifNoCriteriaGiven() throws JSONException { RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() .get("http://localhost/api/hs/office/memberships") .then().log().all().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" [ { "partner": { "person": { "tradeName": "First GmbH" } }, "mainDebitor": { "debitorNumber": 10001 }, "memberNumber": 10001, "validFrom": "2022-10-01", "validTo": null, "reasonForTermination": "NONE" }, { "partner": { "person": { "tradeName": "Second e.K." } }, "mainDebitor": { "debitorNumber": 10002 }, "memberNumber": 10002, "validFrom": "2022-10-01", "validTo": null, "reasonForTermination": "NONE" }, { "partner": { "person": { "tradeName": "Third OHG" } }, "mainDebitor": { "debitorNumber": 10003 }, "memberNumber": 10003, "validFrom": "2022-10-01", "validTo": null, "reasonForTermination": "NONE" } ] """)); // @formatter:on } } @Nested @Accepts({ "Membership:C(Create)" }) class AddMembership { @Test void globalAdmin_canAddMembership() { context.define("superuser-alex@hostsharing.net"); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0); final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("Third").get(0); final var location = RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body(""" { "partnerUuid": "%s", "mainDebitorUuid": "%s", "memberNumber": 20001, "validFrom": "2022-10-13" } """.formatted(givenPartner.getUuid(), givenDebitor.getUuid())) .port(port) .when() .post("http://localhost/api/hs/office/memberships") .then().log().all().assertThat() .statusCode(201) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) .body("mainDebitor.debitorNumber", is(givenDebitor.getDebitorNumber())) .body("partner.person.tradeName", is("Third OHG")) .body("memberNumber", is(20001)) .body("validFrom", is("2022-10-13")) .body("validTo", equalTo(null)) .header("Location", startsWith("http://localhost")) .extract().header("Location"); // @formatter:on // finally, the new membership can be accessed under the generated UUID final var newUserUuid = UUID.fromString( location.substring(location.lastIndexOf('/') + 1)); assertThat(newUserUuid).isNotNull(); assertThat(membershipRepo.findByUuid(newUserUuid)).isPresent(); } // TODO.test: move validation tests to a ...WebMvcTest @Test void globalAdmin_canNotAddMembershipWhenDebitorUuidIsMissing() { } @Test void globalAdmin_canNotAddMembership_ifPartnerDoesNotExist() { } @Test void globalAdmin_canNotAddMembership_ifPersonDoesNotExist() { } } @Nested @Accepts({ "Membership:R(Read)" }) class GetMembership { @Test void globalAdmin_canGetArbitraryMembership() { context.define("superuser-alex@hostsharing.net"); final var givenMembershipUuid = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber( null, 10001) .get(0) .getUuid(); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() .get("http://localhost/api/hs/office/memberships/" + givenMembershipUuid) .then().log().body().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" { "partner": { "person": { "tradeName": "First GmbH" } }, "mainDebitor": { "debitorNumber": 10001 }, "memberNumber": 10001, "validFrom": "2022-10-01", "validTo": null, "reasonForTermination": "NONE" } """)); // @formatter:on } @Test @Accepts({ "Membership:X(Access Control)" }) void normalUser_canNotGetUnrelatedMembership() { context.define("superuser-alex@hostsharing.net"); final var givenMembershipUuid = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber( null, 10001) .get(0) .getUuid(); RestAssured // @formatter:off .given() .header("current-user", "selfregistered-user-drew@hostsharing.org") .port(port) .when() .get("http://localhost/api/hs/office/memberships/" + givenMembershipUuid) .then().log().body().assertThat() .statusCode(404); // @formatter:on } @Test @Accepts({ "Membership:X(Access Control)" }) void debitorAgentUser_canGetRelatedMembership() { context.define("superuser-alex@hostsharing.net"); final var givenMembershipUuid = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber( null, 10003) .get(0) .getUuid(); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .header("assumed-roles", "hs_office_debitor#10003ThirdOHG-thirdcontact.agent") .port(port) .when() .get("http://localhost/api/hs/office/memberships/" + givenMembershipUuid) .then().log().body().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" { "partner": { "person": { "tradeName": "Third OHG" } }, "mainDebitor": { "debitorNumber": 10003, "billingContact": { "label": "third contact" } }, "memberNumber": 10003, "validFrom": "2022-10-01", "validTo": null, "reasonForTermination": "NONE" } """)); // @formatter:on } } @Nested @Accepts({ "Membership:U(Update)" }) class PatchMembership { @Test void globalAdmin_canPatchValidToOfArbitraryMembership() { context.define("superuser-alex@hostsharing.net"); final var givenMembership = givenSomeTemporaryMembershipBessler(); final var location = RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body(""" { "validTo": "2023-12-31", "reasonForTermination": "CANCELLATION" } """) .port(port) .when() .patch("http://localhost/api/hs/office/memberships/" + givenMembership.getUuid()) .then().log().all().assertThat() .statusCode(200) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) .body("partner.person.tradeName", is(givenMembership.getPartner().getPerson().getTradeName())) .body("mainDebitor.debitorNumber", is(givenMembership.getMainDebitor().getDebitorNumber())) .body("memberNumber", is(givenMembership.getMemberNumber())) .body("validFrom", is("2022-11-01")) .body("validTo", is("2023-12-31")) .body("reasonForTermination", is("CANCELLATION")); // @formatter:on // finally, the Membership is actually updated assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get() .matches(mandate -> { assertThat(mandate.getPartner().toShortString()).isEqualTo("First GmbH"); assertThat(mandate.getMainDebitor().toString()).isEqualTo(givenMembership.getMainDebitor().toString()); assertThat(mandate.getMemberNumber()).isEqualTo(givenMembership.getMemberNumber()); assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2024-01-01)"); assertThat(mandate.getReasonForTermination()).isEqualTo(HsOfficeReasonForTermination.CANCELLATION); return true; }); } @Test void globalAdmin_canPatchMainDebitorOfArbitraryMembership() { context.define("superuser-alex@hostsharing.net"); final var givenMembership = givenSomeTemporaryMembershipBessler(); final var givenNewMainDebitor = debitorRepo.findDebitorByDebitorNumber(10003).get(0); final var location = RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body(""" { "mainDebitorUuid": "%s" } """.formatted(givenNewMainDebitor.getUuid())) .port(port) .when() .patch("http://localhost/api/hs/office/memberships/" + givenMembership.getUuid()) .then().log().all().assertThat() .statusCode(200) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) .body("partner.person.tradeName", is(givenMembership.getPartner().getPerson().getTradeName())) // TODO.impl: implement patching the mainDebitor // .body("mainDebitor.debitorNumber", is(10003)) .body("memberNumber", is(givenMembership.getMemberNumber())) .body("validFrom", is("2022-11-01")) .body("validTo", nullValue()) .body("reasonForTermination", is("NONE")); // @formatter:on // finally, the Membership is actually updated assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get() .matches(mandate -> { assertThat(mandate.getPartner().toShortString()).isEqualTo("First GmbH"); // TODO.impl: implement patching the mainDebitor // assertThat(mandate.getMainDebitor().toString()).isEqualTo(givenMembership.getMainDebitor().toString()); assertThat(mandate.getMemberNumber()).isEqualTo(givenMembership.getMemberNumber()); assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,)"); assertThat(mandate.getReasonForTermination()).isEqualTo(NONE); return true; }); } @Test void partnerAgent_canViewButNotPatchValidityOfRelatedMembership() { context.define("superuser-alex@hostsharing.net", "hs_office_partner#FirstGmbH-firstcontact.agent"); final var givenMembership = givenSomeTemporaryMembershipBessler(); final var location = RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .header("assumed-roles", "hs_office_partner#FirstGmbH-firstcontact.agent") .contentType(ContentType.JSON) .body(""" { "validTo": "2023-12-31", "reasonForTermination": "CANCELLATION" } """) .port(port) .when() .patch("http://localhost/api/hs/office/memberships/" + givenMembership.getUuid()) .then().assertThat() .statusCode(403); // @formatter:on // finally, the Membership is actually updated assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get() .matches(mandate -> { assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,)"); assertThat(mandate.getReasonForTermination()).isEqualTo(NONE); return true; }); } } @Nested @Accepts({ "Membership:D(Delete)" }) class DeleteMembership { @Test void globalAdmin_canDeleteArbitraryMembership() { context.define("superuser-alex@hostsharing.net"); final var givenMembership = givenSomeTemporaryMembershipBessler(); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() .delete("http://localhost/api/hs/office/memberships/" + givenMembership.getUuid()) .then().log().body().assertThat() .statusCode(204); // @formatter:on // then the given Membership is gone assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isEmpty(); } @Test @Accepts({ "Membership:X(Access Control)" }) void partnerAgentUser_canNotDeleteRelatedMembership() { context.define("superuser-alex@hostsharing.net"); final var givenMembership = givenSomeTemporaryMembershipBessler(); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .header("assumed-roles", "hs_office_partner#FirstGmbH-firstcontact.agent") .port(port) .when() .delete("http://localhost/api/hs/office/memberships/" + givenMembership.getUuid()) .then().log().body().assertThat() .statusCode(403); // @formatter:on // then the given Membership is still there assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isNotEmpty(); } @Test @Accepts({ "Membership:X(Access Control)" }) void normalUser_canNotDeleteUnrelatedMembership() { context.define("superuser-alex@hostsharing.net"); final var givenMembership = givenSomeTemporaryMembershipBessler(); RestAssured // @formatter:off .given() .header("current-user", "selfregistered-user-drew@hostsharing.org") .port(port) .when() .delete("http://localhost/api/hs/office/memberships/" + givenMembership.getUuid()) .then().log().body().assertThat() .statusCode(404); // @formatter:on // then the given Membership is still there assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isNotEmpty(); } } private HsOfficeMembershipEntity givenSomeTemporaryMembershipBessler() { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); final var newMembership = HsOfficeMembershipEntity.builder() .uuid(UUID.randomUUID()) .partner(givenPartner) .mainDebitor(givenDebitor) .memberNumber(++tempMemberNumber) .validity(Range.closedInfinite(LocalDate.parse("2022-11-01"))) .reasonForTermination(NONE) .build(); return membershipRepo.save(newMembership); }).assertSuccessful().returnedValue(); } @BeforeEach @AfterEach void cleanup() { jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net", null); final var query = em.createQuery("DELETE FROM HsOfficeMembershipEntity m WHERE m.memberNumber >= 20000"); query.executeUpdate(); }); } } src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java
@@ -169,63 +169,49 @@ } @Nested class FindByPartnerUuidMemberships { class ListMemberships { @Test public void globalAdmin_withoutAssumedRole_canViewAllMemberships() { // given context("superuser-alex@hostsharing.net"); // when final var result = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(null, null); // then exactlyTheseMembershipsAreReturned( result, "Membership(10001, First GmbH, 10001, [2022-10-01,), NONE)", "Membership(10002, Second e.K., 10002, [2022-10-01,), NONE)", "Membership(10003, Third OHG, 10003, [2022-10-01,), NONE)"); } @Test public void globalAdmin_withoutAssumedRole_canFindAllMembershipByPartnerUuid() { // given context("superuser-alex@hostsharing.net"); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); // when final var result = membershipRepo.findMembershipsByPartnerUuid(givenPartner.getUuid()); final var result = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber( givenPartner.getUuid(), null); // then allTheseMembershipsAreReturned(result, "Membership(10001, First GmbH, 10001, [2022-10-01,), NONE)"); } @Test public void normalUser_canViewOnlyRelatedMemberships() { // given: context("person-FirstGmbH@example.com"); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); // when: final var result = membershipRepo.findMembershipsByPartnerUuid(givenPartner.getUuid()); // then: exactlyTheseMembershipsAreReturned(result, "Membership(10001, First GmbH, 10001, [2022-10-01,), NONE)"); } } @Nested class FindByOptionalMemberNumber { @Test public void globalAdmin_canViewArbitraryMembership() { public void globalAdmin_withoutAssumedRole_canFindAllMembershipByMemberNumber() { // given context("superuser-alex@hostsharing.net"); // when final var result = membershipRepo.findMembershipByOptionalMemberNumber(10002); final var result = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(null, 10002); // then exactlyTheseMembershipsAreReturned(result, "Membership(10002, Second e.K., 10002, [2022-10-01,), NONE)"); } @Test public void debitorAdmin_canViewRelatedMemberships() { // given // context("person-FirstGmbH@example.com"); context("superuser-alex@hostsharing.net", "hs_office_partner#FirstGmbH-firstcontact.agent"); // context("superuser-alex@hostsharing.net", "hs_office_debitor#10001FirstGmbH-firstcontact.agent"); // context("superuser-alex@hostsharing.net", "hs_office_membership#10001FirstGmbH-firstcontact.admin"); // when final var result = membershipRepo.findMembershipByOptionalMemberNumber(null); // then exactlyTheseMembershipsAreReturned(result, "Membership(10001, First GmbH, 10001, [2022-10-01,), NONE)"); } }