create relation with holder- and contact-data, and search for contact emailAddress + relation mark #136

Merged
hsh-michaelhoennig merged 12 commits from feature/create-relation-with-holder-and-contact-data into master 2024-12-13 14:09:03 +01:00
11 changed files with 136 additions and 16 deletions
Showing only changes of commit 5bd635a4cd - Show all commits

View File

@ -17,6 +17,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.errors.Validate.validate;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from; import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController @RestController
@ -38,10 +39,14 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
public ResponseEntity<List<HsOfficeContactResource>> getListOfContacts( public ResponseEntity<List<HsOfficeContactResource>> getListOfContacts(
final String currentSubject, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final String caption) { final String caption,
final String emailAddress) {
context.define(currentSubject, assumedRoles); 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); final var resources = mapper.mapList(entities, HsOfficeContactResource.class);
return ResponseEntity.ok(resources); return ResponseEntity.ok(resources);

View File

@ -21,6 +21,17 @@ public interface HsOfficeContactRbacRepository extends Repository<HsOfficeContac
@Timed("app.office.contacts.repo.findContactByOptionalCaptionLike.rbac") @Timed("app.office.contacts.repo.findContactByOptionalCaptionLike.rbac")
List<HsOfficeContactRbacEntity> findContactByOptionalCaptionLike(String caption); List<HsOfficeContactRbacEntity> 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<HsOfficeContactRbacEntity> findContactByEmailAddressImpl(final String emailAddressExpression);
default List<HsOfficeContactRbacEntity> findContactByEmailAddress(final String emailAddress) {
return findContactByEmailAddressImpl("$.** ? (@ == \"" + emailAddress + "\")");
}
@Timed("app.office.contacts.repo.save.rbac") @Timed("app.office.contacts.repo.save.rbac")
HsOfficeContactRbacEntity save(final HsOfficeContactRbacEntity entity); HsOfficeContactRbacEntity save(final HsOfficeContactRbacEntity entity);

View File

@ -35,10 +35,10 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
public ResponseEntity<List<HsOfficePersonResource>> getListOfPersons( public ResponseEntity<List<HsOfficePersonResource>> getListOfPersons(
final String currentSubject, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final String caption) { final String name) {
context.define(currentSubject, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = personRepo.findPersonByOptionalNameLike(caption); final var entities = personRepo.findPersonByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficePersonResource.class); final var resources = mapper.mapList(entities, HsOfficePersonResource.class);
return ResponseEntity.ok(resources); return ResponseEntity.ok(resources);

View File

@ -54,15 +54,16 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
final String assumedRoles, final String assumedRoles,
final UUID personUuid, final UUID personUuid,
final HsOfficeRelationTypeResource relationType, final HsOfficeRelationTypeResource relationType,
final String mark,
final String personData, final String personData,
final String contactData) { final String contactData) {
context.define(currentSubject, assumedRoles); context.define(currentSubject, assumedRoles);
final List<HsOfficeRelationRbacEntity> entities = final List<HsOfficeRelationRbacEntity> entities =
rbacRelationRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData( rbacRelationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(
personUuid, personUuid,
relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name()), relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name()),
personData, contactData); mark, personData, contactData);
final var resources = mapper.mapList(entities, HsOfficeRelationResource.class, final var resources = mapper.mapList(entities, HsOfficeRelationResource.class,
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER); RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);

View File

@ -26,17 +26,20 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
* * * *
* @param personUuid the optional UUID of the anchorPerson or holderPerson * @param personUuid the optional UUID of the anchorPerson or holderPerson
* @param relationType the type of the relation * @param relationType the type of the relation
* @param mark the mark (use '%' for wildcard), case ignored
* @param personData a string to match the persons tradeName, familyName or givenName (use '%' for wildcard), case ignored * @param personData a string to match the persons tradeName, familyName or givenName (use '%' for wildcard), case ignored
* @param contactData a string to match the contacts caption, postalAddress, emailAddresses or phoneNumbers (use '%' for wildcard), case ignored * @param contactData a string to match the contacts caption, postalAddress, emailAddresses or phoneNumbers (use '%' for wildcard), case ignored
* @return a list of (accessible) relations which match all given criteria * @return a list of (accessible) relations which match all given criteria
*/ */
default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypePersonAndContactData( default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(
final UUID personUuid, final UUID personUuid,
final HsOfficeRelationType relationType, final HsOfficeRelationType relationType,
final String mark,
final String personData, final String personData,
final String contactData) { final String contactData) {
return findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl( return findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(
personUuid, toStringOrNull(relationType), toSqlLikeOperand(personData), toSqlLikeOperand(contactData)); personUuid, toStringOrNull(relationType),
toSqlLikeOperand(mark), toSqlLikeOperand(personData), toSqlLikeOperand(contactData));
} }
@Query(value = """ @Query(value = """
@ -44,6 +47,7 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType) WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType)
AND ( :personUuid IS NULL AND ( :personUuid IS NULL
OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid ) OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid )
AND ( :mark IS NULL OR lower(rel.mark) LIKE :mark )
AND ( :personData IS NULL AND ( :personData IS NULL
OR lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData OR lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData
OR lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData OR lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData
@ -54,10 +58,11 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData
OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData ) OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData )
""") """)
@Timed("app.office.relations.repo.findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl.rbac") @Timed("app.office.relations.repo.findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl.rbac")
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl( List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(
final UUID personUuid, final UUID personUuid,
final String relationType, final String relationType,
final String mark,
final String personData, final String personData,
final String contactData); final String contactData);

View File

@ -7,12 +7,18 @@ get:
parameters: parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles' - $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: name - name: caption
in: query in: query
required: false required: false
schema: schema:
type: string 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: responses:
"200": "200":
description: OK description: OK

View File

@ -22,6 +22,12 @@ get:
schema: schema:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType' $ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType'
description: Prefix of name properties from holder or contact to filter the results. 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 - name: personData
in: query in: query
required: false required: false

View File

@ -127,7 +127,7 @@ class HsOfficeContactRbacRepositoryIntegrationTest extends ContextBasedTestWithC
} }
@Nested @Nested
class FindAllContacts { class FindContacts {
@Test @Test
public void globalAdmin_withoutAssumedRole_canViewAllContacts() { 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 @Nested
class DeleteByUuid { class DeleteByUuid {

View File

@ -193,7 +193,7 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
.findFirst().orElseThrow(); .findFirst().orElseThrow();
// when: // when:
final var result = relationRbacRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData(person.getUuid(), null, null, null); final var result = relationRbacRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(person.getUuid(), null, null, null, null);
// then: // then:
exactlyTheseRelationsAreReturned( exactlyTheseRelationsAreReturned(

View File

@ -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.partner.DeletePartner;
import net.hostsharing.hsadminng.hs.office.scenarios.person.ShouldUpdatePersonData; 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.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.SubscribeNewPersonAndContactToMailinglist;
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist; import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist;
import net.hostsharing.hsadminng.hs.scenarios.Produces; import net.hostsharing.hsadminng.hs.scenarios.Produces;
@ -578,7 +579,22 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(5001) @Order(5001)
@Requires("Subscription: Michael Miller to operations-announce") @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) new UnsubscribeFromMailinglist(scenarioTest)
.given("mailingList", "operations-announce") .given("mailingList", "operations-announce")
.given("subscriberEMailAddress", "michael.miller@example.org") .given("subscriberEMailAddress", "michael.miller@example.org")

View File

@ -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<SubscribeExistingPersonAndContactToMailinglist> {
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);
}
}