Merge branch 'master' into TP-20240927-importfixes

This commit is contained in:
Dev und Test fuer hsadminng 2024-11-07 06:09:54 +01:00
commit 070e63c7b8
67 changed files with 2931 additions and 126 deletions

36
Jenkinsfile vendored
View File

@ -26,9 +26,35 @@ pipeline {
}
}
stage ('Compile & Test') {
stage ('Compile') {
steps {
sh './gradlew clean check --no-daemon -x pitest -x dependencyCheckAnalyze'
sh './gradlew clean processSpring compileJava compileTestJava --no-daemon'
}
}
stage ('Tests') {
parallel {
stage('Unit-/Integration/Acceptance-Tests') {
steps {
sh './gradlew check --no-daemon -x pitest -x dependencyCheckAnalyze -x importOfficeData -x importHostingAssets'
}
}
stage('Import-Tests') {
steps {
sh './gradlew importOfficeData importHostingAssets --no-daemon'
}
}
stage ('Scenario-Tests') {
steps {
sh './gradlew scenarioTests --no-daemon'
}
}
}
}
stage ('Check') {
steps {
sh './gradlew check -x pitest -x dependencyCheckAnalyze --no-daemon'
}
}
}
@ -45,6 +71,12 @@ pipeline {
sourcePattern: 'src/main/java'
)
// archive scenario-test reports in HTML format
sh '''
./gradlew convertMarkdownToHtml
'''
archiveArtifacts artifacts: 'doc/scenarios/*.html', allowEmptyArchive: true
// cleanup workspace
cleanWs()
}

View File

@ -255,7 +255,7 @@ test {
'net.hostsharing.hsadminng.**.generated.**',
]
useJUnitPlatform {
excludeTags 'import'
excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest'
}
}
jacocoTestReport {
@ -344,6 +344,17 @@ tasks.register('importHostingAssets', Test) {
mustRunAfter spotlessJava
}
tasks.register('scenarioTests', Test) {
useJUnitPlatform {
includeTags 'scenarioTest'
}
group 'verification'
description 'run the import jobs as tests'
mustRunAfter spotlessJava
}
// pitest mutation testing
pitest {
targetClasses = ['net.hostsharing.hsadminng.**']
@ -391,3 +402,45 @@ tasks.named("dependencyUpdates").configure {
isNonStable(it.candidate.version)
}
}
// Generate HTML from Markdown scenario-test-reports using Pandoc:
tasks.register('convertMarkdownToHtml') {
description = 'Generates HTML from Markdown scenario-test-reports using Pandoc.'
group = 'Conversion'
// Define the template file and input directory
def templateFile = file('doc/scenarios/template.html')
// Task configuration and execution
doFirst {
// Check if pandoc is installed
try {
exec {
commandLine 'pandoc', '--version'
}
} catch (Exception) {
throw new GradleException("Pandoc is not installed or not found in the system path.")
}
// Check if the template file exists
if (!templateFile.exists()) {
throw new GradleException("Template file 'doc/scenarios/template.html' not found.")
}
}
doLast {
// Gather all Markdown files in the current directory
fileTree(dir: '.', include: 'doc/scenarios/*.md').each { file ->
// Corrected way to create the output file path
def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html'))
// Execute pandoc for each markdown file
exec {
commandLine 'pandoc', file.absolutePath, '--template', templateFile.absolutePath, '-o', outputFile.absolutePath
}
println "Converted ${file.name} to ${outputFile.name}"
}
}
}

124
doc/scenarios/template.html Normal file
View File

@ -0,0 +1,124 @@
<!doctype html>
<html $if(lang)$ lang="$lang$" $endif$>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--[if lt IE 9]>
<script src="http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<!-- <link rel="stylesheet" type="text/css" href="template.css" /> -->
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/template.css" />
<link href="https://vjs.zencdn.net/5.4.4/video-js.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
<!-- <script type='text/javascript' src='menu/js/jquery.cookie.js'></script> -->
<!-- <script type='text/javascript' src='menu/js/jquery.hoverIntent.minified.js'></script> -->
<!-- <script type='text/javascript' src='menu/js/jquery.dcjqaccordion.2.7.min.js'></script> -->
<!-- <link href="menu/css/skins/blue.css" rel="stylesheet" type="text/css" /> -->
<!-- <link href="menu/css/skins/graphite.css" rel="stylesheet" type="text/css" /> -->
<!-- <link href="menu/css/skins/grey.css" rel="stylesheet" type="text/css" /> -->
<!-- <script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> -->
<!-- <script src="script.js"></script> -->
<!-- <script src="jquery.sticky-kit.js "></script> -->
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.cookie.js'></script>
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.hoverIntent.minified.js'></script>
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.dcjqaccordion.2.7.min.js'></script>
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/blue.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/graphite.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/grey.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/gh/ryangrose/easy-pandoc-templates@948e28e5/css/elegant_bootstrap.css" rel="stylesheet" type="text/css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/script.js"></script>
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/jquery.sticky-kit.js"></script>
<meta name="generator" content="pandoc" />
$for(author-meta)$
<meta name="author" content="$author-meta$" />
$endfor$
$if(date-meta)$
<meta name="date" content="$date-meta$" />
$endif$
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
<style type="text/css">code{white-space: pre;}</style>
$if(quotes)$
<style type="text/css">q { quotes: "“" "”" "" ""; }</style>
$endif$
$if(highlighting-css)$
<style type="text/css">
$highlighting-css$
</style>
$endif$
$for(css)$
<link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$/>
$endfor$
$if(math)$
$math$
$endif$
$for(header-includes)$
$header-includes$
$endfor$
</head>
<body>
$if(title)$
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container">
<span class="doc-title">$title$</span>
<ul class="nav pull-right doc-info">
$for(author)$
<li><p class="navbar-text">$author$</p></li>
$endfor$
$if(date)$
<li><p class="navbar-text">$date$</p></li>
$endif$
</ul>
</div>
</div>
</div>
$endif$
<div class="container">
<div class="row">
$if(toc)$
<div id="$idprefix$TOC" class="span3">
<div class="well toc">
$toc$
</div>
</div>
$endif$
<div class="span$if(toc)$9$else$12$endif$">
$if(abstract)$
<H1>$abstract-title$</H1>
$abstract$
$endif$
$for(include-before)$
$include-before$
$endfor$
$body$
$for(include-after)$
$include-after$
$endfor$
</div>
</div>
</div>
<script src="https://vjs.zencdn.net/5.4.4/video.js"></script>
</body>
</html>

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.
#### 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-critical scenarios have to be identified and a special performance-test has to be implemented.

View File

@ -1,10 +1,6 @@
FROM eclipse-temurin:21-jdk
RUN apt-get update && \
apt-get install -y bind9-utils && \
apt-get install -y bind9-utils pandoc && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# RUN mkdir /opt/app
# COPY japp.jar /opt
# CMD ["java", "-jar", "/opt/app/japp.jar"]

View File

@ -54,8 +54,14 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
@Column(name = "caption")
private String caption;
@Builder.Default
@Setter(AccessLevel.NONE)
@Type(JsonType.class)
@Column(name = "postaladdress")
private String postalAddress; // multiline free-format text
private Map<String, String> postalAddress = new HashMap<>();
@Transient
private PatchableMapWrapper<String> postalAddressWrapper;
@Builder.Default
@Setter(AccessLevel.NONE)
@ -75,6 +81,17 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
@Transient
private PatchableMapWrapper<String> phoneNumbersWrapper;
public PatchableMapWrapper<String> getPostalAddress() {
return PatchableMapWrapper.of(
postalAddressWrapper,
(newWrapper) -> {postalAddressWrapper = newWrapper;},
postalAddress);
}
public void putPostalAddress(Map<String, String> newPostalAddress) {
getPostalAddress().assign(newPostalAddress);
}
public PatchableMapWrapper<String> getEmailAddresses() {
return PatchableMapWrapper.of(
emailAddressesWrapper,

View File

@ -18,7 +18,8 @@ class HsOfficeContactEntityPatcher implements EntityPatcher<HsOfficeContactPatch
@Override
public void apply(final HsOfficeContactPatchResource resource) {
OptionalFromJson.of(resource.getCaption()).ifPresent(entity::setCaption);
OptionalFromJson.of(resource.getPostalAddress()).ifPresent(entity::setPostalAddress);
Optional.ofNullable(resource.getPostalAddress())
.ifPresent(r -> entity.getPostalAddress().patch(KeyValueMap.from(resource.getPostalAddress())));
Optional.ofNullable(resource.getEmailAddresses())
.ifPresent(r -> entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses())));
Optional.ofNullable(resource.getPhoneNumbers())

View File

@ -77,16 +77,13 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
"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,
"ERROR: [400] debitorRel.mark must be null");
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
if ( body.getDebitorRel() != null ) {
body.getDebitorRel().setType(DEBITOR.name());
if (body.getDebitorRel() != null) {
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
debitorRel.setType(DEBITOR);
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
@ -95,7 +92,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse(
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
() -> { throw new ValidationException("Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());});
() -> {
throw new ValidationException(
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
});
}
final var savedEntity = debitorRepo.save(entityToSave);

View File

@ -47,7 +47,7 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
OR lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData )
AND ( :contactData IS NULL
OR lower(rel.contact.caption) LIKE :contactData
OR lower(rel.contact.postalAddress) LIKE :contactData
OR lower(CAST(rel.contact.postalAddress AS String)) LIKE :contactData
OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData
OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData )
""")

View File

@ -12,7 +12,7 @@ components:
caption:
type: string
postalAddress:
type: string
$ref: '#/components/schemas/HsOfficeContactPostalAddress'
emailAddresses:
$ref: '#/components/schemas/HsOfficeContactEmailAddresses'
phoneNumbers:
@ -24,7 +24,7 @@ components:
caption:
type: string
postalAddress:
type: string
$ref: '#/components/schemas/HsOfficeContactPostalAddress'
emailAddresses:
$ref: '#/components/schemas/HsOfficeContactEmailAddresses'
phoneNumbers:
@ -39,21 +39,48 @@ components:
type: string
nullable: true
postalAddress:
type: string
nullable: true
$ref: '#/components/schemas/HsOfficeContactPostalAddress'
emailAddresses:
$ref: '#/components/schemas/HsOfficeContactEmailAddresses'
phoneNumbers:
$ref: '#/components/schemas/HsOfficeContactPhoneNumbers'
HsOfficeContactPostalAddress:
# forces generating a java.lang.Object containing a Map, instead of a class with fixed properties
anyOf:
- type: object
properties:
firm:
type: string
nullable: true
name:
type: string
nullable: true
co:
type: string
nullable: true
street:
type: string
nullable: true
zipcode:
type: string
nullable: true
city:
type: string
nullable: true
country:
type: string
nullable: true
additionalProperties: true
HsOfficeContactEmailAddresses:
# forces generating a java.lang.Object containing a Map, instead of class HsOfficeContactEmailAddresses
# forces generating a java.lang.Object containing a Map, instead of a class with fixed properties
anyOf:
- type: object
additionalProperties: true
HsOfficeContactPhoneNumbers:
# forces generating a java.lang.Object containing a Map, instead of class HsOfficeContactEmailAddresses
# forces generating a java.lang.Object containing a Map, instead of a class with fixed properties
anyOf:
- type: object
properties:

View File

@ -17,10 +17,8 @@ components:
minimum: 1000000
maximum: 9999999
debitorNumberSuffix:
type: integer
format: int8
minimum: 00
maximum: 99
type: string
pattern: '^[0-9][0-9]$'
partner:
$ref: 'hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner'
billable:
@ -76,15 +74,13 @@ components:
type: object
properties:
debitorRel:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationSubInsert'
debitorRelUuid:
type: string
format: uuid
debitorNumberSuffix:
type: integer
format: int8
minimum: 00
maximum: 99
type: string
pattern: '^[0-9][0-9]$'
billable:
type: boolean
vatId:

View File

@ -42,6 +42,7 @@ components:
format: uuid
nullable: true
# arbitrary relation with explicit type
HsOfficeRelationInsert:
type: object
properties:
@ -65,3 +66,24 @@ components:
- holderUuid
- type
- 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

@ -7,7 +7,7 @@ get:
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: name
- name: iban
in: query
required: false
schema:

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.customer WHERE uuid = NEW.customerUuid INTO newCustomer;
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s of package', NEW.customerUuid);
perform rbac.defineRoleWithGrants(
@ -102,10 +102,10 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.customer WHERE uuid = OLD.customerUuid INTO oldCustomer;
assert oldCustomer.uuid is not null, format('oldCustomer must not be null for OLD.customerUuid = %s', OLD.customerUuid);
assert oldCustomer.uuid is not null, format('oldCustomer must not be null for OLD.customerUuid = %s of package', OLD.customerUuid);
SELECT * FROM rbactest.customer WHERE uuid = NEW.customerUuid INTO newCustomer;
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s of package', NEW.customerUuid);
if NEW.customerUuid <> OLD.customerUuid then

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.package WHERE uuid = NEW.packageUuid INTO newPackage;
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s of domain', NEW.packageUuid);
perform rbac.defineRoleWithGrants(
@ -98,10 +98,10 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM rbactest.package WHERE uuid = OLD.packageUuid INTO oldPackage;
assert oldPackage.uuid is not null, format('oldPackage must not be null for OLD.packageUuid = %s', OLD.packageUuid);
assert oldPackage.uuid is not null, format('oldPackage must not be null for OLD.packageUuid = %s of domain', OLD.packageUuid);
SELECT * FROM rbactest.package WHERE uuid = NEW.packageUuid INTO newPackage;
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s of domain', NEW.packageUuid);
if NEW.packageUuid <> OLD.packageUuid then

View File

@ -9,7 +9,7 @@ create table if not exists hs_office.contact
uuid uuid unique references rbac.object (uuid) initially deferred,
version int not null default 0,
caption varchar(128) not null,
postalAddress text,
postalAddress jsonb not null,
emailAddresses jsonb not null,
phoneNumbers jsonb not null
);

View File

@ -11,7 +11,6 @@
create or replace procedure hs_office.contact_create_test_data(contCaption varchar)
language plpgsql as $$
declare
postalAddr varchar;
emailAddr varchar;
begin
emailAddr = 'contact-admin@' || base.cleanIdentifier(contCaption) || '.example.com';
@ -19,14 +18,18 @@ begin
perform rbac.create_subject(emailAddr);
call base.defineContext('creating contact test-data', null, emailAddr);
postalAddr := E'Vorname Nachname\nStraße Hnr\nPLZ Stadt';
raise notice 'creating test contact: %', contCaption;
insert
into hs_office.contact (caption, postaladdress, emailaddresses, phonenumbers)
values (
contCaption,
postalAddr,
( '{ ' ||
-- '"name": "' || contCaption || '",' ||
-- '"street": "Somewhere 1",' ||
-- '"zipcode": "12345",' ||
-- '"city": "Where-Ever",' ||
'"country": "Germany"' ||
'}')::jsonb,
('{ "main": "' || emailAddr || '" }')::jsonb,
('{ "phone_office": "+49 123 1234567" }')::jsonb
);

View File

@ -38,13 +38,13 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.person WHERE uuid = NEW.holderUuid INTO newHolderPerson;
assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s', NEW.holderUuid);
assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s of relation', NEW.holderUuid);
SELECT * FROM hs_office.person WHERE uuid = NEW.anchorUuid INTO newAnchorPerson;
assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s', NEW.anchorUuid);
assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s of relation', NEW.anchorUuid);
SELECT * FROM hs_office.contact WHERE uuid = NEW.contactUuid INTO newContact;
assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s', NEW.contactUuid);
assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s of relation', NEW.contactUuid);
perform rbac.defineRoleWithGrants(

View File

@ -37,10 +37,10 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.relation WHERE uuid = NEW.partnerRelUuid INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s of partner', NEW.partnerRelUuid);
SELECT * FROM hs_office.partner_details WHERE uuid = NEW.detailsUuid INTO newPartnerDetails;
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s', NEW.detailsUuid);
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s of partner', NEW.detailsUuid);
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'DELETE'), hs_office.relation_OWNER(newPartnerRel));
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.relation_TENANT(newPartnerRel));
@ -96,16 +96,16 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.relation WHERE uuid = OLD.partnerRelUuid INTO oldPartnerRel;
assert oldPartnerRel.uuid is not null, format('oldPartnerRel must not be null for OLD.partnerRelUuid = %s', OLD.partnerRelUuid);
assert oldPartnerRel.uuid is not null, format('oldPartnerRel must not be null for OLD.partnerRelUuid = %s of partner', OLD.partnerRelUuid);
SELECT * FROM hs_office.relation WHERE uuid = NEW.partnerRelUuid INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s of partner', NEW.partnerRelUuid);
SELECT * FROM hs_office.partner_details WHERE uuid = OLD.detailsUuid INTO oldPartnerDetails;
assert oldPartnerDetails.uuid is not null, format('oldPartnerDetails must not be null for OLD.detailsUuid = %s', OLD.detailsUuid);
assert oldPartnerDetails.uuid is not null, format('oldPartnerDetails must not be null for OLD.detailsUuid = %s of partner', OLD.detailsUuid);
SELECT * FROM hs_office.partner_details WHERE uuid = NEW.detailsUuid INTO newPartnerDetails;
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s', NEW.detailsUuid);
assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s of partner', NEW.detailsUuid);
if NEW.partnerRelUuid <> OLD.partnerRelUuid then

View File

@ -44,10 +44,10 @@ begin
WHERE partnerRel.type = 'PARTNER'
AND NEW.debitorRelUuid = debitorRel.uuid
INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.debitorRelUuid = %s', NEW.debitorRelUuid);
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.debitorRelUuid = %s of debitor', NEW.debitorRelUuid);
SELECT * FROM hs_office.relation WHERE uuid = NEW.debitorRelUuid INTO newDebitorRel;
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorRelUuid = %s', NEW.debitorRelUuid);
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorRelUuid = %s of debitor', NEW.debitorRelUuid);
SELECT * FROM hs_office.bankaccount WHERE uuid = NEW.refundBankAccountUuid INTO newRefundBankAccount;

View File

@ -37,14 +37,14 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.bankaccount WHERE uuid = NEW.bankAccountUuid INTO newBankAccount;
assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s', NEW.bankAccountUuid);
assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s of sepamandate', NEW.bankAccountUuid);
SELECT debitorRel.*
FROM hs_office.relation debitorRel
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid
INTO newDebitorRel;
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s of sepamandate', NEW.debitorUuid);
perform rbac.defineRoleWithGrants(

View File

@ -40,7 +40,7 @@ begin
JOIN hs_office.relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
WHERE partner.uuid = NEW.partnerUuid
INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerUuid = %s', NEW.partnerUuid);
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerUuid = %s of membership', NEW.partnerUuid);
perform rbac.defineRoleWithGrants(

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.membership WHERE uuid = NEW.membershipUuid INTO newMembership;
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s', NEW.membershipUuid);
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s of coopshares', NEW.membershipUuid);
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.membership_AGENT(newMembership));
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership));

View File

@ -36,7 +36,7 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.membership WHERE uuid = NEW.membershipUuid INTO newMembership;
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s', NEW.membershipUuid);
assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s of coopasset', NEW.membershipUuid);
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'SELECT'), hs_office.membership_AGENT(newMembership));
call rbac.grantPermissionToRole(rbac.createPermission(NEW.uuid, 'UPDATE'), hs_office.membership_ADMIN(newMembership));

View File

@ -37,14 +37,14 @@ begin
call rbac.enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office.debitor WHERE uuid = NEW.debitorUuid INTO newDebitor;
assert newDebitor.uuid is not null, format('newDebitor must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
assert newDebitor.uuid is not null, format('newDebitor must not be null for NEW.debitorUuid = %s of project', NEW.debitorUuid);
SELECT debitorRel.*
FROM hs_office.relation debitorRel
JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid
INTO newDebitorRel;
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s or project', NEW.debitorUuid);
perform rbac.defineRoleWithGrants(

View File

@ -46,6 +46,7 @@ public class ArchitectureTest {
"..lambda",
"..generated..",
"..persistence..",
"..reflection",
"..system..",
"..validation..",
"..hs.office.bankaccount",
@ -54,6 +55,7 @@ public class ArchitectureTest {
"..hs.office.coopshares",
"..hs.office.debitor",
"..hs.office.membership",
"..hs.office.scenarios..",
"..hs.migration",
"..hs.office.partner",
"..hs.office.person",
@ -96,7 +98,7 @@ public class ArchitectureTest {
public static final ArchRule testClassesAreProperlyNamed = classes()
.that().haveSimpleNameEndingWith("Test")
.and().doNotHaveModifier(ABSTRACT)
.should().haveNameMatching(".*(UnitTest|RestTest|IntegrationTest|AcceptanceTest|ArchitectureTest)$");
.should().haveNameMatching(".*(UnitTest|RestTest|IntegrationTest|AcceptanceTest|ScenarioTest|ArchitectureTest)$");
@ArchTest
@SuppressWarnings("unused")

View File

@ -110,7 +110,6 @@ public class HsHostingAssetControllerRestTest {
"caption": "some fake cloud-server",
"alarmContact": {
"caption": "some contact",
"postalAddress": "address of some contact",
"emailAddresses": {
"main": "some-contact@example.com"
}
@ -141,7 +140,6 @@ public class HsHostingAssetControllerRestTest {
"caption": "some fake managed-server",
"alarmContact": {
"caption": "some contact",
"postalAddress": "address of some contact",
"emailAddresses": {
"main": "some-contact@example.com"
}

View File

@ -1161,7 +1161,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
contactRecord.getString("last_name"),
contactRecord.getString("firma")));
contact.putEmailAddresses(Map.of("main", contactRecord.getString("email")));
contact.setPostalAddress(toAddress(contactRecord));
contact.putPostalAddress(toAddress(contactRecord));
contact.putPhoneNumbers(toPhoneNumbers(contactRecord));
contacts.put(contactRecord.getInteger("contact_id"), contact);
@ -1181,36 +1181,23 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
return phoneNumbers;
}
private String toAddress(final Record rec) {
final var result = new StringBuilder();
private Map<String, String> toAddress(final Record rec) {
final var result = new LinkedHashMap<String, String>();
final var name = toName(
rec.getString("salut"),
rec.getString("title"),
rec.getString("first_name"),
rec.getString("last_name"));
if (isNotBlank(name))
result.append(name + "\n");
result.put("name", name);
if (isNotBlank(rec.getString("firma")))
result.append(rec.getString("firma") + "\n");
if (isNotBlank(rec.getString("co")))
result.append("c/o " + rec.getString("co") + "\n");
if (isNotBlank(rec.getString("street")))
result.append(rec.getString("street") + "\n");
final var zipcodeAndCity = toZipcodeAndCity(rec);
if (isNotBlank(zipcodeAndCity))
result.append(zipcodeAndCity + "\n");
return result.toString();
}
result.put("firm", name);
private String toZipcodeAndCity(final Record rec) {
final var result = new StringBuilder();
if (isNotBlank(rec.getString("country")))
result.append(rec.getString("country") + " ");
if (isNotBlank(rec.getString("zipcode")))
result.append(rec.getString("zipcode") + " ");
if (isNotBlank(rec.getString("city")))
result.append(rec.getString("city"));
return result.toString();
List.of("co", "street", "zipcode", "city", "country").forEach(key -> {
if (isNotBlank(rec.getString(key)))
result.put(key, rec.getString(key));
});
return result;
}
private String toCaption(

View File

@ -21,10 +21,13 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
@ -214,7 +217,11 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
"emailAddresses": {
"main": "patched@example.org"
},
"postalAddress": "Patched Address",
"postalAddress": {
"extra": "Extra Property",
"co": "P. Patcher",
"street": "Patchstraße 5"
},
"phoneNumbers": {
"phone_office": "+01 100 123456"
}
@ -229,7 +236,10 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body("uuid", isUuidValid())
.body("caption", is("Temp patched contact"))
.body("emailAddresses", is(Map.of("main", "patched@example.org")))
.body("postalAddress", is("Patched Address"))
.body("postalAddress", hasEntry("name", givenContact.getPostalAddress().get("name"))) // unchanged
.body("postalAddress", hasEntry("extra", "Extra Property")) // unchanged
.body("postalAddress", hasEntry("co", "P. Patcher")) // patched
.body("postalAddress", hasEntry("street", "Patchstraße 5")) // patched
.body("phoneNumbers", is(Map.of("phone_office", "+01 100 123456")));
// @formatter:on
@ -239,7 +249,11 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.matches(person -> {
assertThat(person.getCaption()).isEqualTo("Temp patched contact");
assertThat(person.getEmailAddresses()).containsExactlyEntriesOf(Map.of("main", "patched@example.org"));
assertThat(person.getPostalAddress()).isEqualTo("Patched Address");
assertThat(person.getPostalAddress()).containsAllEntriesOf(Map.ofEntries(
entry("name", givenContact.getPostalAddress().get("name")),
entry("co", "P. Patcher"),
entry("street", "Patchstraße 5")
));
assertThat(person.getPhoneNumbers()).containsExactlyEntriesOf(Map.of("phone_office", "+01 100 123456"));
return true;
});
@ -264,7 +278,7 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
"phone_office": "+01 100 123456"
}
}
""")
""")
.port(port)
.when()
.patch("http://localhost/api/hs/office/contacts/" + givenContact.getUuid())
@ -274,7 +288,6 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body("uuid", isUuidValid())
.body("caption", is(givenContact.getCaption()))
.body("emailAddresses", is(Map.of("main", "patched@example.org")))
.body("postalAddress", is(givenContact.getPostalAddress()))
.body("phoneNumbers", is(Map.of("phone_office", "+01 100 123456")));
// @formatter:on
@ -283,12 +296,11 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.matches(person -> {
assertThat(person.getCaption()).isEqualTo(givenContact.getCaption());
assertThat(person.getEmailAddresses()).containsExactlyEntriesOf(Map.of("main", "patched@example.org"));
assertThat(person.getPostalAddress()).isEqualTo(givenContact.getPostalAddress());
assertThat(person.getPostalAddress()).containsExactlyInAnyOrderEntriesOf(givenContact.getPostalAddress());
assertThat(person.getPhoneNumbers()).containsExactlyEntriesOf(Map.of("phone_office", "+01 100 123456"));
return true;
});
}
}
@Nested
@ -361,8 +373,13 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
final var newContact = HsOfficeContactRbacEntity.builder()
.uuid(UUID.randomUUID())
.caption("Temp from " + Context.getCallerMethodNameFromStackFrame(1) )
.postalAddress(Map.ofEntries(
entry("name", RandomStringUtils.randomAlphabetic(6) + " " + RandomStringUtils.randomAlphabetic(10)),
entry("street", RandomStringUtils.randomAlphabetic(10) + randomInt(1, 99)),
entry("zipcode", "D-" + randomInt(10000, 99999)),
entry("city", RandomStringUtils.randomAlphabetic(10))
))
.emailAddresses(Map.of("main", RandomStringUtils.randomAlphabetic(10) + "@example.org"))
.postalAddress("Postal Address " + RandomStringUtils.randomAlphabetic(10))
.phoneNumbers(Map.of("phone_office", "+01 200 " + RandomStringUtils.randomNumeric(8)))
.build();
@ -378,4 +395,8 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
em.createQuery("DELETE FROM HsOfficeContactRbacEntity c WHERE c.caption LIKE 'Temp %'").executeUpdate();
}).assertSuccessful();
}
private int randomInt(final int min, final int max) {
return ThreadLocalRandom.current().nextInt(min, max);
}
}

View File

@ -19,6 +19,19 @@ class HsOfficeContactPatcherUnitTest extends PatchUnitTestBase<
> {
private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID();
private static final Map<String, String> PATCH_POSTAL_ADDRESS = patchMap(
entry("name", "Patty Patch"),
entry("street", "Patchstreet 10"),
entry("zipcode", null),
entry("city", "Hamburg")
);
private static final Map<String, String> PATCHED_POSTAL_ADDRESS = patchMap(
entry("name", "Patty Patch"),
entry("street", "Patchstreet 10"),
entry("city", "Hamburg")
);
private static final Map<String, String> PATCH_EMAIL_ADDRESSES = patchMap(
entry("main", "patched@example.com"),
entry("paul", null),
@ -46,6 +59,11 @@ class HsOfficeContactPatcherUnitTest extends PatchUnitTestBase<
final var entity = new HsOfficeContactRbacEntity();
entity.setUuid(INITIAL_CONTACT_UUID);
entity.setCaption("initial caption");
entity.putPostalAddress(Map.ofEntries(
entry("name", "Ina Initial"),
entry("street", "Initialstraße 50"),
entry("zipcode", "20000"),
entry("city", "Hamburg")));
entity.putEmailAddresses(Map.ofEntries(
entry("main", "initial@example.org"),
entry("paul", "paul@example.com"),
@ -54,7 +72,6 @@ class HsOfficeContactPatcherUnitTest extends PatchUnitTestBase<
entry("phone_office", "+49 40 12345-00"),
entry("phone_mobile", "+49 1555 1234567"),
entry("fax", "+49 40 12345-90")));
entity.setPostalAddress("Initialstraße 50\n20000 Hamburg");
return entity;
}
@ -77,24 +94,26 @@ class HsOfficeContactPatcherUnitTest extends PatchUnitTestBase<
"patched caption",
HsOfficeContactRbacEntity::setCaption),
new SimpleProperty<>(
"resources",
"postalAddress",
HsOfficeContactPatchResource::setPostalAddress,
PATCH_POSTAL_ADDRESS,
HsOfficeContactRbacEntity::putPostalAddress,
PATCHED_POSTAL_ADDRESS)
.notNullable(),
new SimpleProperty<>(
"emailAddresses",
HsOfficeContactPatchResource::setEmailAddresses,
PATCH_EMAIL_ADDRESSES,
HsOfficeContactRbacEntity::putEmailAddresses,
PATCHED_EMAIL_ADDRESSES)
.notNullable(),
new SimpleProperty<>(
"resources",
"phoneNumbers",
HsOfficeContactPatchResource::setPhoneNumbers,
PATCH_PHONE_NUMBERS,
HsOfficeContactRbacEntity::putPhoneNumbers,
PATCHED_PHONE_NUMBERS)
.notNullable(),
new JsonNullableProperty<>(
"patched given name",
HsOfficeContactPatchResource::setPostalAddress,
"patched given name",
HsOfficeContactRbacEntity::setPostalAddress)
.notNullable()
);
}
}

View File

@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.hs.office.contact;
import java.util.Map;
import static java.util.Map.entry;
public class HsOfficeContactRbacTestEntity {
public static final HsOfficeContactRbacEntity TEST_RBAC_CONTACT = hsOfficeContact("some contact", "some-contact@example.com");
@ -9,7 +11,12 @@ public class HsOfficeContactRbacTestEntity {
static public HsOfficeContactRbacEntity hsOfficeContact(final String caption, final String emailAddr) {
return HsOfficeContactRbacEntity.builder()
.caption(caption)
.postalAddress("address of " + caption)
.postalAddress(Map.ofEntries(
entry("name", "M. Meyer"),
entry("street", "Teststraße 11"),
entry("zipcode", "D-12345"),
entry("city", "Berlin")
))
.emailAddresses(Map.of("main", emailAddr))
.build();
}

View File

@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.hs.office.contact;
import java.util.Map;
import static java.util.Map.entry;
public class HsOfficeContactRealTestEntity {
public static final HsOfficeContactRealEntity TEST_REAL_CONTACT = hsOfficeContact("some contact", "some-contact@example.com");
@ -9,7 +11,12 @@ public class HsOfficeContactRealTestEntity {
static public HsOfficeContactRealEntity hsOfficeContact(final String caption, final String emailAddr) {