diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java index fe08bd4f..64915d31 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; +import static net.hostsharing.hsadminng.errors.Validate.validate; import static net.hostsharing.hsadminng.mapper.KeyValueMap.from; @RestController @@ -38,10 +39,14 @@ public class HsOfficeContactController implements HsOfficeContactsApi { public ResponseEntity> getListOfContacts( final String currentSubject, final String assumedRoles, - final String caption) { + final String caption, + final String emailAddress) { context.define(currentSubject, assumedRoles); - final var entities = contactRepo.findContactByOptionalCaptionLike(caption); + validate("caption, emailAddress").atMaxOne(caption, emailAddress); + final var entities = caption != null + ? contactRepo.findContactByOptionalCaptionLike(caption) + : contactRepo.findContactByEmailAddress(emailAddress); final var resources = mapper.mapList(entities, HsOfficeContactResource.class); return ResponseEntity.ok(resources); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepository.java index f3a88bc0..8f304ce8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepository.java @@ -21,6 +21,17 @@ public interface HsOfficeContactRbacRepository extends Repository findContactByOptionalCaptionLike(String caption); + @Query(value = """ + SELECT c.* FROM hs_office.contact_rv c + WHERE jsonb_path_exists(c.emailaddresses, cast(:emailAddressExpression as jsonpath)) + """, nativeQuery = true) + @Timed("app.office.contacts.repo.findContactByEmailAddressImpl.rbac") + List findContactByEmailAddressImpl(final String emailAddressExpression); + + default List findContactByEmailAddress(final String emailAddress) { + return findContactByEmailAddressImpl("$.** ? (@ == \"" + emailAddress + "\")"); + } + @Timed("app.office.contacts.repo.save.rbac") HsOfficeContactRbacEntity save(final HsOfficeContactRbacEntity entity); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java index 3c39c616..02012820 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java @@ -35,10 +35,10 @@ public class HsOfficePersonController implements HsOfficePersonsApi { public ResponseEntity> getListOfPersons( final String currentSubject, final String assumedRoles, - final String caption) { + final String name) { context.define(currentSubject, assumedRoles); - final var entities = personRepo.findPersonByOptionalNameLike(caption); + final var entities = personRepo.findPersonByOptionalNameLike(name); final var resources = mapper.mapList(entities, HsOfficePersonResource.class); return ResponseEntity.ok(resources); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java index 97e7dac0..a4f17edf 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java @@ -54,15 +54,16 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi { final String assumedRoles, final UUID personUuid, final HsOfficeRelationTypeResource relationType, + final String mark, final String personData, final String contactData) { context.define(currentSubject, assumedRoles); final List entities = - rbacRelationRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData( + rbacRelationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData( personUuid, relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name()), - personData, contactData); + mark, personData, contactData); final var resources = mapper.mapList(entities, HsOfficeRelationResource.class, RELATION_ENTITY_TO_RESOURCE_POSTMAPPER); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java index 0443bc00..0353f02a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java @@ -26,17 +26,20 @@ public interface HsOfficeRelationRbacRepository extends Repository findRelationRelatedToPersonUuidRelationTypePersonAndContactData( + default List findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData( final UUID personUuid, final HsOfficeRelationType relationType, + final String mark, final String personData, final String contactData) { - return findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl( - personUuid, toStringOrNull(relationType), toSqlLikeOperand(personData), toSqlLikeOperand(contactData)); + return findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl( + personUuid, toStringOrNull(relationType), + toSqlLikeOperand(mark), toSqlLikeOperand(personData), toSqlLikeOperand(contactData)); } @Query(value = """ @@ -44,6 +47,7 @@ public interface HsOfficeRelationRbacRepository extends Repository findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl( + @Timed("app.office.relations.repo.findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl.rbac") + List findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl( final UUID personUuid, final String relationType, + final String mark, final String personData, final String contactData); diff --git a/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml b/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml index f7412ddf..4694660b 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-contacts.yaml @@ -7,12 +7,18 @@ get: parameters: - $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/assumedRoles' - - name: name + - name: caption in: query required: false schema: type: string - description: Prefix of caption to filter the results. + description: Beginning of caption to filter the results. + - name: emailAddress + in: query + required: false + schema: + type: string + description: Beginning of email-address to filter the results. responses: "200": description: OK diff --git a/src/main/resources/api-definition/hs-office/hs-office-relations.yaml b/src/main/resources/api-definition/hs-office/hs-office-relations.yaml index 70218c00..35619f07 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-relations.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-relations.yaml @@ -22,6 +22,12 @@ get: schema: $ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType' description: Prefix of name properties from holder or contact to filter the results. + - name: mark + in: query + required: false + schema: + type: string + description: - name: personData in: query required: false diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryIntegrationTest.java index b05b6da7..379c8137 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRbacRepositoryIntegrationTest.java @@ -127,7 +127,7 @@ class HsOfficeContactRbacRepositoryIntegrationTest extends ContextBasedTestWithC } @Nested - class FindAllContacts { + class FindContacts { @Test public void globalAdmin_withoutAssumedRole_canViewAllContacts() { @@ -184,6 +184,22 @@ class HsOfficeContactRbacRepositoryIntegrationTest extends ContextBasedTestWithC } } + @Nested + class FindByEmailAddress { + + @Test + public void globalAdmin_withoutAssumedRole_canViewAllContacts() { + // given + context("superuser-alex@hostsharing.net", null); + + // when + final var result = contactRepo.findContactByEmailAddress("contact-admin@secondcontact.example.com"); + + // then + exactlyTheseContactsAreReturned(result, "second contact"); + } + } + @Nested class DeleteByUuid { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java index b2f50977..e55959c7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java @@ -193,7 +193,7 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea .findFirst().orElseThrow(); // when: - final var result = relationRbacRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData(person.getUuid(), null, null, null); + final var result = relationRbacRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(person.getUuid(), null, null, null, null); // then: exactlyTheseRelationsAreReturned( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java index c7a2236a..d23a4250 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java @@ -31,6 +31,7 @@ import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner; import net.hostsharing.hsadminng.hs.office.scenarios.partner.DeletePartner; import net.hostsharing.hsadminng.hs.office.scenarios.person.ShouldUpdatePersonData; import net.hostsharing.hsadminng.hs.office.scenarios.subscription.RemoveOperationsContactFromPartner; +import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeExistingPersonAndContactToMailinglist; import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeNewPersonAndContactToMailinglist; import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist; import net.hostsharing.hsadminng.hs.scenarios.Produces; @@ -578,7 +579,22 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(5001) @Requires("Subscription: Michael Miller to operations-announce") - void shouldUnsubscribeNewPersonAndContactToMailinglist() { + @Produces("Subscription: Michael Miller to operations-discussion") + void shouldSubscribeExistingPersonAndContactToMailinglist() { + new SubscribeExistingPersonAndContactToMailinglist(scenarioTest) + .given("partnerPersonTradeName", "Test AG") + .given("subscriberFamilyName", "Miller") + .given("subscriberGivenName", "Michael") + .given("subscriberEMailAddress", "michael.miller@example.org") + .given("mailingList", "operations-discussion") + .doRun() + .keep(); + } + + @Test + @Order(5002) + @Requires("Subscription: Michael Miller to operations-announce") + void shouldUnsubscribePersonAndContactFromMailinglist() { new UnsubscribeFromMailinglist(scenarioTest) .given("mailingList", "operations-announce") .given("subscriberEMailAddress", "michael.miller@example.org") diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeExistingPersonAndContactToMailinglist.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeExistingPersonAndContactToMailinglist.java new file mode 100644 index 00000000..f018c454 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/subscription/SubscribeExistingPersonAndContactToMailinglist.java @@ -0,0 +1,54 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.subscription; + +import io.restassured.http.ContentType; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; +import org.springframework.http.HttpStatus; + +import static io.restassured.http.ContentType.JSON; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.OK; + +public class SubscribeExistingPersonAndContactToMailinglist extends UseCase { + + public SubscribeExistingPersonAndContactToMailinglist(final ScenarioTest testSuite) { + super(testSuite); + } + + @Override + protected HttpResponse run() { + + obtain("Person: %{partnerPersonTradeName}", () -> + httpGet("/api/hs/office/persons?name=" + uriEncoded("%{partnerPersonTradeName}")) + .expecting(OK).expecting(JSON), + response -> response.expectArrayElements(1).getFromBody("[0].uuid"), + "In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one." + ); + + obtain( + "Person: %{subscriberGivenName} %{subscriberFamilyName}", () -> + httpGet("/api/hs/office/persons?name=%{subscriberFamilyName}") + .expecting(HttpStatus.OK).expecting(ContentType.JSON), + response -> response.expectArrayElements(1).getFromBody("[0].uuid"), + "In real scenarios there are most likely multiple results and you have to choose the right one." + ); + + obtain("Contact: %{subscriberEMailAddress}", () -> + httpGet("/api/hs/office/contacts?emailAddress=%{subscriberEMailAddress}") + .expecting(HttpStatus.OK).expecting(ContentType.JSON), + response -> response.expectArrayElements(1).getFromBody("[0].uuid"), + "In real scenarios there are most likely multiple results and you have to choose the right one." + ); + + return httpPost("/api/hs/office/relations", usingJsonBody(""" + { + "type": "SUBSCRIBER", + "mark": ${mailingList}, + "anchor.uuid": ${Person: %{partnerPersonTradeName}}, + "holder.uuid": ${Person: %{subscriberGivenName} %{subscriberFamilyName}}, + "contact.uuid": ${Contact: %{subscriberEMailAddress}} + } + """)) + .expecting(CREATED).expecting(JSON); + } +}