Merge branch 'master' into TP-20240927-importfixes
This commit is contained in:
commit
070e63c7b8
36
Jenkinsfile
vendored
36
Jenkinsfile
vendored
@ -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()
|
||||
}
|
||||
|
55
build.gradle
55
build.gradle
@ -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
124
doc/scenarios/template.html
Normal 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>
|
@ -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.
|
||||
|
@ -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"]
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
|
@ -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);
|
||||
|
@ -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 )
|
||||
""")
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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(
|
||||
|
@ -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")
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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) {
|
||||
return HsOfficeContactRealEntity.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();
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@ -76,7 +75,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
class ListDebitors {
|
||||
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() throws JSONException {
|
||||
void globalAdmin_withoutAssumedRoles_canViewAllDebitors_ifNoCriteriaGiven() {
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
@ -112,7 +111,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
}
|
||||
},
|
||||
"debitorNumber": 1000111,
|
||||
"debitorNumberSuffix": 11,
|
||||
"debitorNumberSuffix": "11",
|
||||
"partner": {
|
||||
"partnerNumber": 10001,
|
||||
"partnerRel": {
|
||||
@ -167,7 +166,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
}
|
||||
},
|
||||
"debitorNumber": 1000212,
|
||||
"debitorNumberSuffix": 12,
|
||||
"debitorNumberSuffix": "12",
|
||||
"partner": {
|
||||
"partnerNumber": 10002,
|
||||
"partnerRel": {
|
||||
@ -201,7 +200,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
}
|
||||
},
|
||||
"debitorNumber": 1000313,
|
||||
"debitorNumberSuffix": 13,
|
||||
"debitorNumberSuffix": "13",
|
||||
"partner": {
|
||||
"partnerNumber": 10003,
|
||||
"partnerRel": {
|
||||
@ -334,7 +333,6 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
.body("""
|
||||
{
|
||||
"debitorRel": {
|
||||
"type": "DEBITOR",
|
||||
"anchorUuid": "%s",
|
||||
"holderUuid": "%s",
|
||||
"contactUuid": "%s"
|
||||
@ -386,7 +384,6 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
.body("""
|
||||
{
|
||||
"debitorRel": {
|
||||
"type": "DEBITOR",
|
||||
"anchorUuid": "%s",
|
||||
"holderUuid": "%s",
|
||||
"contactUuid": "%s"
|
||||
@ -463,13 +460,16 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
"type": "DEBITOR",
|
||||
"contact": {
|
||||
"caption": "first contact",
|
||||
"postalAddress": "Vorname Nachname\\nStraße Hnr\\nPLZ Stadt",
|
||||
"postalAddress": {
|
||||
"country": "Germany"
|
||||
},
|
||||
"emailAddresses": { "main": "contact-admin@firstcontact.example.com" },
|
||||
"phoneNumbers": { "phone_office": "+49 123 1234567" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"debitorNumber": 1000111,
|
||||
"debitorNumberSuffix": 11,
|
||||
"debitorNumberSuffix": "11",
|
||||
"partner": {
|
||||
"partnerNumber": 10001,
|
||||
"partnerRel": {
|
||||
@ -479,10 +479,11 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
"mark": null,
|
||||
"contact": {
|
||||
"caption": "first contact",
|
||||
"postalAddress": "Vorname Nachname\\nStraße Hnr\\nPLZ Stadt",
|
||||
"postalAddress": {
|
||||
"country": "Germany"
|
||||
},
|
||||
"emailAddresses": { "main": "contact-admin@firstcontact.example.com" },
|
||||
"phoneNumbers": { "phone_office": "+49 123 1234567" }
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
"registrationOffice": "Hamburg",
|
||||
@ -581,7 +582,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
"contact": { "caption": "fourth contact" }
|
||||
},
|
||||
"debitorNumber": 10004${debitorNumberSuffix},
|
||||
"debitorNumberSuffix": ${debitorNumberSuffix},
|
||||
"debitorNumberSuffix": "${debitorNumberSuffix}",
|
||||
"partner": {
|
||||
"partnerNumber": 10004,
|
||||
"partnerRel": {
|
||||
|
@ -199,7 +199,9 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
|
||||
"type": "REPRESENTATIVE",
|
||||
"contact": {
|
||||
"caption": "first contact",
|
||||
"postalAddress": "Vorname Nachname\\nStraße Hnr\\nPLZ Stadt",
|
||||
"postalAddress": {
|
||||
"country": "Germany"
|
||||
},
|
||||
"emailAddresses": {
|
||||
"main": "contact-admin@firstcontact.example.com"
|
||||
},
|
||||
|
@ -0,0 +1,339 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.contact.AddPhoneNumberToContactData;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.contact.AmendContactData;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.contact.RemovePhoneNumberFromContactData;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.contact.ReplaceContactData;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateExternalDebitorForPartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSelfDebitorForPartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSepaMandateForDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.FinallyDeleteSepaMandateForDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.InvalidateSepaMandateForDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddOperationsContactToPartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteDebitor;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.DeletePartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddRepresentativeToPartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.RemoveOperationsContactFromPartner;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeToMailinglist;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
@Tag("scenarioTest")
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
classes = { HsadminNgApplication.class, JpaAttempt.class },
|
||||
properties = {
|
||||
"spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///scenariosTC}",
|
||||
"spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}",
|
||||
"spring.datasource.password=${HSADMINNG_POSTGRES_ADMIN_PASSWORD:password}",
|
||||
"hsadminng.superuser=${HSADMINNG_SUPERUSER:superuser-alex@hostsharing.net}"
|
||||
}
|
||||
)
|
||||
@DirtiesContext
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class HsOfficeScenarioTests extends ScenarioTest {
|
||||
|
||||
@Test
|
||||
@Order(1010)
|
||||
@Produces(explicitly = "Partner: Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Hamburg"})
|
||||
void shouldCreateLegalPersonAsPartner() {
|
||||
new CreatePartner(this)
|
||||
.given("partnerNumber", 31010)
|
||||
.given("personType", "LEGAL_PERSON")
|
||||
.given("tradeName", "Test AG")
|
||||
.given("contactCaption", "Test AG - Hamburg")
|
||||
.given("postalAddress", """
|
||||
"firm": "Test AG",
|
||||
"street": "Shanghai-Allee 1",
|
||||
"zipcode": "20123",
|
||||
"city": "Hamburg",
|
||||
"country": "Germany"
|
||||
""")
|
||||
.given("officePhoneNumber", "+49 40 654321-0")
|
||||
.given("emailAddress", "hamburg@test-ag.example.org")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1011)
|
||||
@Produces(explicitly = "Partner: Michelle Matthieu", implicitly = {"Person: Michelle Matthieu", "Contact: Michelle Matthieu"})
|
||||
void shouldCreateNaturalPersonAsPartner() {
|
||||
new CreatePartner(this)
|
||||
.given("partnerNumber", 31011)
|
||||
.given("personType", "NATURAL_PERSON")
|
||||
.given("givenName", "Michelle")
|
||||
.given("familyName", "Matthieu")
|
||||
.given("contactCaption", "Michelle Matthieu")
|
||||
.given("postalAddress", """
|
||||
"name": "Michelle Matthieu",
|
||||
"street": "An der Wandse 34",
|
||||
"zipcode": "22123",
|
||||
"city": "Hamburg",
|
||||
"country": "Germany"
|
||||
""")
|
||||
.given("officePhoneNumber", "+49 40 123456")
|
||||
.given("emailAddress", "michelle.matthieu@example.org")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1020)
|
||||
@Requires("Person: Test AG")
|
||||
@Produces("Representative: Tracy Trust for Test AG")
|
||||
void shouldAddRepresentativeToPartner() {
|
||||
new AddRepresentativeToPartner(this)
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("representativeFamilyName", "Trust")
|
||||
.given("representativeGivenName", "Tracy")
|
||||
.given("representativePostalAddress", """
|
||||
"name": "Michelle Matthieu",
|
||||
"street": "An der Alster 100",
|
||||
"zipcode": "20000",
|
||||
"city": "Hamburg",
|
||||
"country": "Germany"
|
||||
""")
|
||||
.given("representativePhoneNumber", "+49 40 123456")
|
||||
.given("representativeEMailAddress", "tracy.trust@example.org")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1030)
|
||||
@Requires("Person: Test AG")
|
||||
@Produces("Operations-Contact: Dennis Krause for Test AG")
|
||||
void shouldAddOperationsContactToPartner() {
|
||||
new AddOperationsContactToPartner(this)
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("operationsContactFamilyName", "Krause")
|
||||
.given("operationsContactGivenName", "Dennis")
|
||||
.given("operationsContactPhoneNumber", "+49 9932 587741")
|
||||
.given("operationsContactEMailAddress", "dennis.krause@example.org")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1039)
|
||||
@Requires("Operations-Contact: Dennis Krause for Test AG")
|
||||
void shouldRemoveOperationsContactFromPartner() {
|
||||
new RemoveOperationsContactFromPartner(this)
|
||||
.given("operationsContactPerson", "Dennis Krause")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1090)
|
||||
void shouldDeletePartner() {
|
||||
new DeletePartner(this)
|
||||
.given("partnerNumber", 31020)
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1100)
|
||||
@Requires("Partner: Michelle Matthieu")
|
||||
void shouldAmendContactData() {
|
||||
new AmendContactData(this)
|
||||
.given("partnerName", "Matthieu")
|
||||
.given("newEmailAddress", "michelle@matthieu.example.org")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1101)
|
||||
@Requires("Partner: Michelle Matthieu")
|
||||
void shouldAddPhoneNumberToContactData() {
|
||||
new AddPhoneNumberToContactData(this)
|
||||
.given("partnerName", "Matthieu")
|
||||
.given("phoneNumberKeyToAdd", "mobile")
|
||||
.given("phoneNumberToAdd", "+49 152 1234567")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1102)
|
||||
@Requires("Partner: Michelle Matthieu")
|
||||
void shouldRemovePhoneNumberFromContactData() {
|
||||
new RemovePhoneNumberFromContactData(this)
|
||||
.given("partnerName", "Matthieu")
|
||||
.given("phoneNumberKeyToRemove", "office")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1103)
|
||||
@Requires("Partner: Test AG")
|
||||
void shouldReplaceContactData() {
|
||||
new ReplaceContactData(this)
|
||||
.given("partnerName", "Test AG")
|
||||
.given("newContactCaption", "Test AG - China")
|
||||
.given("newPostalAddress", """
|
||||
"firm": "Test AG",
|
||||
"name": "Fi Zhong-Kha",
|
||||
"building": "Thi Chi Koh Building",
|
||||
"street": "No.2 Commercial Second Street",
|
||||
"district": "Niushan Wei Wu",
|
||||
"city": "Dongguan City",
|
||||
"province": "Guangdong Province",
|
||||
"country": "China"
|
||||
""")
|
||||
.given("newOfficePhoneNumber", "++15 999 654321" )
|
||||
.given("newEmailAddress", "norden@test-ag.example.org")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2010)
|
||||
@Requires("Partner: Test AG")
|
||||
@Produces("Debitor: Test AG - main debitor")
|
||||
void shouldCreateSelfDebitorForPartner() {
|
||||
new CreateSelfDebitorForPartner(this, "Debitor: Test AG - main debitor")
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("billingContactCaption", "Test AG - billing department")
|
||||
.given("billingContactEmailAddress", "billing@test-ag.example.org")
|
||||
.given("debitorNumberSuffix", "00") // TODO.impl: could be assigned automatically, but is not yet
|
||||
.given("billable", true)
|
||||
.given("vatId", "VAT123456")
|
||||
.given("vatCountryCode", "DE")
|
||||
.given("vatBusiness", true)
|
||||
.given("vatReverseCharge", false)
|
||||
.given("defaultPrefix", "tst")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2011)
|
||||
@Requires("Person: Test AG")
|
||||
@Produces("Debitor: Billing GmbH")
|
||||
void shouldCreateExternalDebitorForPartner() {
|
||||
new CreateExternalDebitorForPartner(this)
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("billingContactCaption", "Billing GmbH - billing department")
|
||||
.given("billingContactEmailAddress", "billing@test-ag.example.org")
|
||||
.given("debitorNumberSuffix", "01")
|
||||
.given("billable", true)
|
||||
.given("vatId", "VAT123456")
|
||||
.given("vatCountryCode", "DE")
|
||||
.given("vatBusiness", true)
|
||||
.given("vatReverseCharge", false)
|
||||
.given("defaultPrefix", "tsx")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2020)
|
||||
@Requires("Person: Test AG")
|
||||
void shouldDeleteDebitor() {
|
||||
new DeleteDebitor(this)
|
||||
.given("partnerNumber", 31020)
|
||||
.given("debitorSuffix", "02")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2020)
|
||||
@Requires("Debitor: Test AG - main debitor")
|
||||
@Disabled("see TODO.spec in DontDeleteDefaultDebitor")
|
||||
void shouldNotDeleteDefaultDebitor() {
|
||||
new DontDeleteDefaultDebitor(this)
|
||||
.given("partnerNumber", 31020)
|
||||
.given("debitorSuffix", "00")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3100)
|
||||
@Requires("Debitor: Test AG - main debitor")
|
||||
@Produces("SEPA-Mandate: Test AG")
|
||||
void shouldCreateSepaMandateForDebitor() {
|
||||
new CreateSepaMandateForDebitor(this)
|
||||
// existing debitor
|
||||
.given("debitorNumber", "3101000")
|
||||
|
||||
// new sepa-mandate
|
||||
.given("mandateReference", "Test AG - main debitor")
|
||||
.given("mandateAgreement", "2022-10-12")
|
||||
.given("mandateValidFrom", "2024-10-15")
|
||||
|
||||
// new bank-account
|
||||
.given("bankAccountHolder", "Test AG - debit bank account")
|
||||
.given("bankAccountIBAN", "DE02701500000000594937")
|
||||
.given("bankAccountBIC", "SSKMDEMM")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3108)
|
||||
@Requires("SEPA-Mandate: Test AG")
|
||||
void shouldInvalidateSepaMandateForDebitor() {
|
||||
new InvalidateSepaMandateForDebitor(this)
|
||||
.given("bankAccountIBAN", "DE02701500000000594937")
|
||||
.given("mandateValidUntil", "2025-09-30")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3109)
|
||||
@Requires("SEPA-Mandate: Test AG")
|
||||
void shouldFinallyDeleteSepaMandateForDebitor() {
|
||||
new FinallyDeleteSepaMandateForDebitor(this)
|
||||
.given("bankAccountIBAN", "DE02701500000000594937")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4000)
|
||||
@Requires("Partner: Test AG")
|
||||
void shouldCreateMembershipForPartner() {
|
||||
new CreateMembership(this)
|
||||
.given("partnerName", "Test AG")
|
||||
.given("memberNumberSuffix", "00")
|
||||
.given("validFrom", "2024-10-15")
|
||||
.given("membershipFeeBillable", "true")
|
||||
.doRun();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5000)
|
||||
@Requires("Person: Test AG")
|
||||
@Produces("Subscription: Michael Miller to operations-announce")
|
||||
void shouldSubscribeNewPersonAndContactToMailinglist() {
|
||||
new SubscribeToMailinglist(this)
|
||||
// TODO.spec: do we need the personType? or is an operational contact always a natural person? what about distribution lists?
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("subscriberFamilyName", "Miller")
|
||||
.given("subscriberGivenName", "Michael")
|
||||
.given("subscriberEMailAddress", "michael.miller@example.org")
|
||||
.given("mailingList", "operations-announce")
|
||||
.doRun()
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5001)
|
||||
@Requires("Subscription: Michael Miller to operations-announce")
|
||||
void shouldUnsubscribeNewPersonAndContactToMailinglist() {
|
||||
new UnsubscribeFromMailinglist(this)
|
||||
.given("mailingList", "operations-announce")
|
||||
.given("subscriberEMailAddress", "michael.miller@example.org")
|
||||
.doRun();
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase.HttpResponse;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class PathAssertion {
|
||||
|
||||
private final String path;
|
||||
|
||||
public PathAssertion(final String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) {
|
||||
return response -> response.path(path).contains(ScenarioTest.resolve(resolvableValue));
|
||||
}
|
||||
|
||||
public Consumer<HttpResponse> doesNotExist() {
|
||||
return response -> response.path(path).isNull(); // here, null Optional means key not found in JSON
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Target(METHOD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface Produces {
|
||||
String value() default ""; // same as explicitly, makes it possible to omit the property name
|
||||
String explicitly() default ""; // same as value
|
||||
String[] implicitly() default {};
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
# UseCase-Tests
|
||||
|
||||
We define UseCase-tests as test for business-scenarios.
|
||||
They test positive (successful) scenarios by using the REST-API.
|
||||
|
||||
Running these tests also creates test-reports which can be used as documentation about the necessary REST-calls for each scenario.
|
||||
|
||||
Clarification: Acceptance tests also test at the REST-API level but are more technical and also test negative (error-) scenarios.
|
||||
|
||||
## ... extends ScenarioTest
|
||||
|
||||
Each test-method in subclasses of ScenarioTest describes a business-scenario,
|
||||
each utilizing a main-use-case and given example data for the scenario.
|
||||
|
||||
To reduce the number of API-calls, intermediate results can be re-used.
|
||||
This is controlled by two annotations:
|
||||
|
||||
### @Produces(....)
|
||||
|
||||
This annotation tells the test-runner that this scenario produces certain business object for re-use.
|
||||
The UUID of the new business objects are stored in a key-value map using the provided keys.
|
||||
|
||||
There are two variants of this annotation:
|
||||
|
||||
#### A Single Business Object
|
||||
```
|
||||
@Produces("key")
|
||||
```
|
||||
|
||||
This variant is used when there is just a single business-object produced by the use-case.
|
||||
|
||||
#### Multiple Business Objects
|
||||
|
||||
```
|
||||
@Produces(explicitly = "main-key", implicitly = {"other-key", ...})
|
||||
```
|
||||
|
||||
This variant is used when multiple business-objects are produced by the use-case,
|
||||
e.g. a Relation, a Person and a Contact.
|
||||
The UUID of the business-object produced by the main-use-case gets stored as the key after "explicitly",
|
||||
the others are listed after "implicitly";
|
||||
if there is just one, leave out the surrounding braces.
|
||||
|
||||
### @Requires(...)
|
||||
|
||||
This annotation tells the test-runner that which business objects are required before this scenario can run.
|
||||
|
||||
Each subset must be produced by the same producer-method.
|
||||
|
||||
|
||||
## ... extends UseCase
|
||||
|
||||
These classes consist of two parts:
|
||||
|
||||
### Prerequisites of the Use-Case
|
||||
|
||||
The constructor may create prerequisites via `required(...)`.
|
||||
These do not really belong to the use-case itself,
|
||||
e.g. create business objects which, in the context of that use-case, would already exist.
|
||||
|
||||
This is similar to @Requires(...) just that no other test scenario produces this prerequisite.
|
||||
Here, use-cases can be re-used, usually with different data.
|
||||
|
||||
### The Use-Case Itself
|
||||
|
||||
The use-case is implemented by the `run()`-method which contains HTTP-calls.
|
||||
|
||||
Each HTTP-call is wrapped into either `obtain(...)` to keep the result in a placeholder variable,
|
||||
the variable name is also used as a title.
|
||||
Or it's wrapped into a `withTitle(...)` to assign a title.
|
||||
|
||||
The HTTP-call is followed by some assertions, e.g. the HTTP status and JSON-path-expression-matchers.
|
||||
|
||||
Use `${...}` for placeholders which need to be replaced with JSON quotes
|
||||
(e.g. strings are quoted, numbers are not),
|
||||
`%{...}` for placeholders which need to be rendered raw
|
||||
and `&{...}` for placeholders which need to get URI-encoded.
|
||||
|
||||
If `???` is added before the closing brace, the property is optional.
|
||||
This means, if it's not available in the properties, `null` is used.
|
||||
|
||||
Properties with null-values are removed from the JSON.
|
||||
If you need to keep a null-value, e.g. to delete a property,
|
||||
use `NULL` (all caps) in the template (not the variable value).
|
||||
|
||||
A special syntax is the infix `???`-operator like in: `${%{var1???}???%{var2???}%{var3???}}`.
|
||||
In this case the first non-null value is used.
|
||||
|
||||
|
||||
|
||||
### The Use-Case Verification
|
||||
|
||||
The verification-step is implemented by the `verify()`-method which usually contains a HTTP-HTTP-call.
|
||||
|
||||
It can also contain a JSON-path verification to check if a certain value is in the result.
|
||||
|
@ -0,0 +1,13 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Target(METHOD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface Requires {
|
||||
String value();
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.apache.commons.collections4.SetUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.testcontainers.shaded.org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public abstract class ScenarioTest extends ContextBasedTest {
|
||||
|
||||
final static String RUN_AS_USER = "superuser-alex@hostsharing.net"; // TODO.test: use global:AGENT when implemented
|
||||
|
||||
record Alias<T extends UseCase<T>>(Class<T> useCase, UUID uuid) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ObjectUtils.toString(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
private final static Map<String, Alias<?>> aliases = new HashMap<>();
|
||||
private final static Map<String, Object> properties = new HashMap<>();
|
||||
|
||||
public final TestReport testReport = new TestReport(aliases);
|
||||
|
||||
@LocalServerPort
|
||||
Integer port;
|
||||
|
||||
@Autowired
|
||||
HsOfficePersonRepository personRepo;
|
||||
|
||||
@Autowired
|
||||
JpaAttempt jpaAttempt;
|
||||
|
||||
@SneakyThrows
|
||||
@BeforeEach
|
||||
void init(final TestInfo testInfo) {
|
||||
createHostsharingPerson();
|
||||
try {
|
||||
testInfo.getTestMethod().ifPresent(this::callRequiredProducers);
|
||||
testReport.createTestLogMarkdownFile(testInfo);
|
||||
} catch (Exception exc) {
|
||||
throw exc;
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() { // final TestInfo testInfo
|
||||
properties.clear();
|
||||
testReport.close();
|
||||
}
|
||||
|
||||
private void createHostsharingPerson() {
|
||||
jpaAttempt.transacted(() ->
|
||||
{
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
aliases.put(
|
||||
"Person: Hostsharing eG",
|
||||
new Alias<>(
|
||||
null,
|
||||
personRepo.findPersonByOptionalNameLike("Hostsharing eG")
|
||||
.stream()
|
||||
.map(HsOfficePersonEntity::getUuid)
|
||||
.reduce(Reducer::toSingleElement).orElseThrow())
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void callRequiredProducers(final Method currentTestMethod) {
|
||||
final var testMethodRequired = Optional.of(currentTestMethod)
|
||||
.map(m -> m.getAnnotation(Requires.class))
|
||||
.map(Requires::value)
|
||||
.orElse(null);
|
||||
if (testMethodRequired != null) {
|
||||
for (Method potentialProducerMethod : getClass().getDeclaredMethods()) {
|
||||
final var producesAnnot = potentialProducerMethod.getAnnotation(Produces.class);
|
||||
if (producesAnnot != null) {
|
||||
final var testMethodProduces = allOf(
|
||||
producesAnnot.value(),
|
||||
producesAnnot.explicitly(),
|
||||
producesAnnot.implicitly());
|
||||
// @formatter:off
|
||||
if ( // that method can produce something required
|
||||
testMethodProduces.contains(testMethodRequired) &&
|
||||
|
||||
// and it does not produce anything we already have (would cause errors)
|
||||
SetUtils.intersection(testMethodProduces, knowVariables().keySet()).isEmpty()
|
||||
) {
|
||||
// then we recursively produce the pre-requisites of the producer method
|
||||
callRequiredProducers(potentialProducerMethod);
|
||||
|
||||
// and finally we call the producer method
|
||||
potentialProducerMethod.invoke(this);
|
||||
}
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> allOf(final String value, final String explicitly, final String[] implicitly) {
|
||||
final var all = new HashSet<String>();
|
||||
if (!value.isEmpty()) {
|
||||
all.add(value);
|
||||
}
|
||||
if (!explicitly.isEmpty()) {
|
||||
all.add(explicitly);
|
||||
}
|
||||
all.addAll(asList(implicitly));
|
||||
return all;
|
||||
}
|
||||
|
||||
static boolean containsAlias(final String alias) {
|
||||
return aliases.containsKey(alias);
|
||||
}
|
||||
|
||||
static UUID uuid(final String nameWithPlaceholders) {
|
||||
final var resoledName = resolve(nameWithPlaceholders);
|
||||
final UUID alias = ofNullable(knowVariables().get(resoledName)).filter(v -> v instanceof UUID).map(UUID.class::cast).orElse(null);
|
||||
assertThat(alias).as("alias '" + resoledName + "' not found in aliases nor in properties [" +
|
||||
knowVariables().keySet().stream().map(v -> "'" + v + "'").collect(Collectors.joining(", ")) + "]"
|
||||
).isNotNull();
|
||||
return alias;
|
||||
}
|
||||
|
||||
static void putAlias(final String name, final Alias<?> value) {
|
||||
aliases.put(name, value);
|
||||
}
|
||||
|
||||
static void putProperty(final String name, final Object value) {
|
||||
properties.put(name, (value instanceof String string) ? resolveTyped(string) : value);
|
||||
}
|
||||
|
||||
static Map<String, Object> knowVariables() {
|
||||
final var map = new LinkedHashMap<String, Object>();
|
||||
ScenarioTest.aliases.forEach((key, value) -> map.put(key, value.uuid()));
|
||||
map.putAll(ScenarioTest.properties);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static String resolve(final String text) {
|
||||
final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public static Object resolveTyped(final String text) {
|
||||
final var resolved = resolve(text);
|
||||
try {
|
||||
return UUID.fromString(resolved);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// ignore and just use the String value
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TemplateResolver {
|
||||
|
||||
private final static Pattern pattern = Pattern.compile(",(\\s*})", Pattern.MULTILINE);
|
||||
private static final String IF_NOT_FOUND_SYMBOL = "???";
|
||||
|
||||
enum PlaceholderPrefix {
|
||||
RAW('%') {
|
||||
@Override
|
||||
String convert(final Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
},
|
||||
JSON_QUOTED('$'){
|
||||
@Override
|
||||
String convert(final Object value) {
|
||||
return jsonQuoted(value);
|
||||
}
|
||||
},
|
||||
URI_ENCODED('&'){
|
||||
@Override
|
||||
String convert(final Object value) {
|
||||
return value != null ? URLEncoder.encode(value.toString(), StandardCharsets.UTF_8) : "";
|
||||
}
|
||||
};
|
||||
|
||||
private final char prefixChar;
|
||||
|
||||
PlaceholderPrefix(final char prefixChar) {
|
||||
this.prefixChar = prefixChar;
|
||||
}
|
||||
|
||||
static boolean contains(final char givenChar) {
|
||||
return Arrays.stream(values()).anyMatch(p -> p.prefixChar == givenChar);
|
||||
}
|
||||
|
||||
static PlaceholderPrefix ofPrefixChar(final char givenChar) {
|
||||
return Arrays.stream(values()).filter(p -> p.prefixChar == givenChar).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
abstract String convert(final Object value);
|
||||
}
|
||||
|
||||
private final String template;
|
||||
private final Map<String, Object> properties;
|
||||
private final StringBuilder resolved = new StringBuilder();
|
||||
private int position = 0;
|
||||
|
||||
public TemplateResolver(final String template, final Map<String, Object> properties) {
|
||||
this.template = template;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
String resolve() {
|
||||
final var resolved = copy();
|
||||
final var withoutDroppedLines = dropLinesWithNullProperties(resolved);
|
||||
final var result = removeDanglingCommas(withoutDroppedLines);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String removeDanglingCommas(final String withoutDroppedLines) {
|
||||
return pattern.matcher(withoutDroppedLines).replaceAll("$1");
|
||||
}
|
||||
|
||||
private String dropLinesWithNullProperties(final String text) {
|
||||
return Arrays.stream(text.split("\n"))
|
||||
.filter(TemplateResolver::keepLine)
|
||||
.map(TemplateResolver::keptNullValues)
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private static boolean keepLine(final String line) {
|
||||
final var trimmed = line.trim();
|
||||
return !trimmed.endsWith("null,") && !trimmed.endsWith("null");
|
||||
}
|
||||
|
||||
private static String keptNullValues(final String line) {
|
||||
return line.replace(": NULL", ": null");
|
||||
}
|
||||
|
||||
private String copy() {
|
||||
while (hasMoreChars()) {
|
||||
if (PlaceholderPrefix.contains(currentChar()) && nextChar() == '{') {
|
||||
startPlaceholder(currentChar());
|
||||
} else {
|
||||
resolved.append(fetchChar());
|
||||
}
|
||||
}
|
||||
return resolved.toString();
|
||||
}
|
||||
|
||||
private boolean hasMoreChars() {
|
||||
return position < template.length();
|
||||
}
|
||||
|
||||
private void startPlaceholder(final char intro) {
|
||||
skipChars(intro + "{");
|
||||
int nested = 0;
|
||||
final var placeholder = new StringBuilder();
|
||||
while (nested > 0 || currentChar() != '}') {
|
||||
if (currentChar() == '}') {
|
||||
--nested;
|
||||
placeholder.append(fetchChar());
|
||||
} else if (PlaceholderPrefix.contains (currentChar()) && nextChar() == '{') {
|
||||
++nested;
|
||||
placeholder.append(fetchChar());
|
||||
} else {
|
||||
placeholder.append(fetchChar());
|
||||
}
|
||||
}
|
||||
final var name = new TemplateResolver(placeholder.toString(), properties).resolve();
|
||||
final var value = propVal(name);
|
||||
resolved.append(
|
||||
PlaceholderPrefix.ofPrefixChar(intro).convert(value)
|
||||
);
|
||||
skipChar('}');
|
||||
}
|
||||
|
||||
private Object propVal(final String nameExpression) {
|
||||
if (nameExpression.endsWith(IF_NOT_FOUND_SYMBOL)) {
|
||||
final String pureName = nameExpression.substring(0, nameExpression.length() - IF_NOT_FOUND_SYMBOL.length());
|
||||
return properties.get(pureName);
|
||||
} else if (nameExpression.contains(IF_NOT_FOUND_SYMBOL)) {
|
||||
final var parts = StringUtils.split(nameExpression, IF_NOT_FOUND_SYMBOL);
|
||||
return Arrays.stream(parts).filter(Objects::nonNull).findFirst().orElseGet(() -> {
|
||||
if ( parts[parts.length-1].isEmpty() ) {
|
||||
// => whole expression ends with IF_NOT_FOUND_SYMBOL, thus last null element was optional
|
||||
return null;
|
||||
}
|
||||
// => last alternative element in expression was null and not optional
|
||||
throw new IllegalStateException("Missing required value in property-chain: " + nameExpression);
|
||||
});
|
||||
} else {
|
||||
final var val = properties.get(nameExpression);
|
||||
if (val == null) {
|
||||
throw new IllegalStateException("Missing required property: " + nameExpression);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
private void skipChar(final char expectedChar) {
|
||||
if (currentChar() != expectedChar) {
|
||||
throw new IllegalStateException("expected '" + expectedChar + "' but got '" + currentChar() + "'");
|
||||
}
|
||||
++position;
|
||||
}
|
||||
|
||||
private void skipChars(final String expectedChars) {
|
||||
final var nextChars = template.substring(position, position + expectedChars.length());
|
||||
if ( !nextChars.equals(expectedChars) ) {
|
||||
throw new IllegalStateException("expected '" + expectedChars + "' but got '" + nextChars + "'");
|
||||
}
|
||||
position += expectedChars.length();
|
||||
}
|
||||
|
||||
private char fetchChar() {
|
||||
if ((position+1) > template.length()) {
|
||||
throw new IllegalStateException("no more characters. resolved so far: " + resolved);
|
||||
}
|
||||
final var currentChar = currentChar();
|
||||
++position;
|
||||
return currentChar;
|
||||
}
|
||||
|
||||
private char currentChar() {
|
||||
if (position >= template.length()) {
|
||||
throw new IllegalStateException("no more characters, maybe closing bracelet missing in template: '''\n" + template + "\n'''");
|
||||
}
|
||||
return template.charAt(position);
|
||||
}
|
||||
|
||||
private char nextChar() {
|
||||
if ((position+1) >= template.length()) {
|
||||
throw new IllegalStateException("no more characters. resolved so far: " + resolved);
|
||||
}
|
||||
return template.charAt(position+1);
|
||||
}
|
||||
|
||||
private static String jsonQuoted(final Object value) {
|
||||
return switch (value) {
|
||||
case null -> null;
|
||||
case Boolean bool -> bool.toString();
|
||||
case Number number -> number.toString();
|
||||
case String string -> "\"" + string.replace("\n", "\\n") + "\"";
|
||||
default -> "\"" + value + "\"";
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TemplateResolverUnitTest {
|
||||
|
||||
@Test
|
||||
void resolveTemplate() {
|
||||
final var resolved = new TemplateResolver("""
|
||||
with optional JSON quotes:
|
||||
|
||||
${boolean},
|
||||
${numeric},
|
||||
${simple placeholder},
|
||||
${nested %{name}},
|
||||
${with-special-chars}
|
||||
|
||||
and without quotes:
|
||||
|
||||
%{boolean},
|
||||
%{numeric},
|
||||
%{simple placeholder},
|
||||
%{nested %{name}},
|
||||
%{with-special-chars}
|
||||
|
||||
and uri-encoded:
|
||||
|
||||
&{boolean},
|
||||
&{numeric},
|
||||
&{simple placeholder},
|
||||
&{nested %{name}},
|
||||
&{with-special-chars}
|
||||
""",
|
||||
Map.ofEntries(
|
||||
Map.entry("name", "placeholder"),
|
||||
Map.entry("boolean", true),
|
||||
Map.entry("numeric", 42),
|
||||
Map.entry("simple placeholder", "einfach"),
|
||||
Map.entry("nested placeholder", "verschachtelt"),
|
||||
Map.entry("with-special-chars", "3&3 AG")
|
||||
)).resolve();
|
||||
|
||||
assertThat(resolved).isEqualTo("""
|
||||
with optional JSON quotes:
|
||||
|
||||
true,
|
||||
42,
|
||||
"einfach",
|
||||
"verschachtelt",
|
||||
"3&3 AG"
|
||||
|
||||
and without quotes:
|
||||
|
||||
true,
|
||||
42,
|
||||
einfach,
|
||||
verschachtelt,
|
||||
3&3 AG
|
||||
|
||||
and uri-encoded:
|
||||
|
||||
true,
|
||||
42,
|
||||
einfach,
|
||||
verschachtelt,
|
||||
3%263+AG
|
||||
""".trim());
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class TestReport {
|
||||
|
||||
private final Map<String, ?> aliases;
|
||||
private final StringBuilder markdownLog = new StringBuilder(); // records everything for debugging purposes
|
||||
|
||||
private PrintWriter markdownReport;
|
||||
private int silent; // do not print anything to test-report if >0
|
||||
|
||||
public TestReport(final Map<String, ?> aliases) {
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException {
|
||||
final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow();
|
||||
final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow();
|
||||
assertThat(new File("doc/scenarios/").isDirectory() || new File("doc/scenarios/").mkdirs()).as("mkdir doc/scenarios/").isTrue();
|
||||
markdownReport = new PrintWriter(new FileWriter("doc/scenarios/" + testMethodOrder + "-" + testMethodName + ".md"));
|
||||
print("## Scenario #" + testInfo.getTestMethod().map(TestReport::orderNumber).orElseThrow() + ": " +
|
||||
testMethodName.replaceAll("([a-z])([A-Z]+)", "$1 $2"));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void print(final String output) {
|
||||
|
||||
final var outputWithCommentsForUuids = appendUUIDKey(output);
|
||||
|
||||
// for tests executed due to @Requires/@Produces there is no markdownFile yet
|
||||
if (markdownReport != null && silent == 0) {
|
||||
markdownReport.print(outputWithCommentsForUuids);
|
||||
}
|
||||
|
||||
// but the debugLog should contain all output, even if silent
|
||||
markdownLog.append(outputWithCommentsForUuids);
|
||||
}
|
||||
|
||||
public void printLine(final String output) {
|
||||
print(output + "\n");
|
||||
}
|
||||
|
||||
public void printPara(final String output) {
|
||||
printLine("\n" +output + "\n");
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (markdownReport != null) {
|
||||
markdownReport.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static Object orderNumber(final Method method) {
|
||||
return method.getAnnotation(Order.class).value();
|
||||
}
|
||||
|
||||
private String appendUUIDKey(String multilineText) {
|
||||
final var lines = multilineText.split("\\r?\\n");
|
||||
final var result = new StringBuilder();
|
||||
|
||||
for (String line : lines) {
|
||||
for (Map.Entry<String, ?> entry : aliases.entrySet()) {
|
||||
final var uuidString = entry.getValue().toString();
|
||||
if (line.contains(uuidString)) {
|
||||
line = line + " // " + entry.getKey();
|
||||
break; // only add comment for one UUID per row (in our case, there is only one per row)
|
||||
}
|
||||
}
|
||||
result.append(line).append("\n");
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
void silent(final Runnable code) {
|
||||
silent++;
|
||||
code.run();
|
||||
silent--;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,369 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import io.restassured.http.ContentType;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.reflection.AnnotationFinder;
|
||||
import org.apache.commons.collections4.map.LinkedMap;
|
||||
import org.assertj.core.api.OptionalAssert;
|
||||
import org.hibernate.AssertionFailure;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.net.URLEncoder.encode;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.platform.commons.util.StringUtils.isBlank;
|
||||
import static org.junit.platform.commons.util.StringUtils.isNotBlank;
|
||||
|
||||
public abstract class UseCase<T extends UseCase<?>> {
|
||||
|
||||
private static final HttpClient client = HttpClient.newHttpClient();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
protected final ScenarioTest testSuite;
|
||||
private final TestReport testReport;
|
||||
private final Map<String, Function<String, UseCase<?>>> requirements = new LinkedMap<>();
|
||||
private final String resultAlias;
|
||||
private final Map<String, Object> givenProperties = new LinkedHashMap<>();
|
||||
|
||||
private String nextTitle; // just temporary to override resultAlias for sub-use-cases
|
||||
|
||||
public UseCase(final ScenarioTest testSuite) {
|
||||
this(testSuite, getResultAliasFromProducesAnnotationInCallStack());
|
||||
}
|
||||
|
||||
public UseCase(final ScenarioTest testSuite, final String resultAlias) {
|
||||
this.testSuite = testSuite;
|
||||
this.testReport = testSuite.testReport;
|
||||
this.resultAlias = resultAlias;
|
||||
if (resultAlias != null) {
|
||||
testReport.printPara("### UseCase " + title(resultAlias));
|
||||
}
|
||||
}
|
||||
|
||||
public final void requires(final String alias, final Function<String, UseCase<?>> useCaseFactory) {
|
||||
if (!ScenarioTest.containsAlias(alias)) {
|
||||
requirements.put(alias, useCaseFactory);
|
||||
}
|
||||
}
|
||||
|
||||
public final HttpResponse doRun() {
|
||||
testReport.printPara("### Given Properties");
|
||||
testReport.printLine("""
|
||||
| name | value |
|
||||
|------|-------|""");
|
||||
givenProperties.forEach((key, value) ->
|
||||
testReport.printLine("| " + key + " | " + value.toString().replace("\n", "<br>") + " |"));
|
||||
testReport.printLine("");
|
||||
testReport.silent(() ->
|
||||
requirements.forEach((alias, factory) -> {
|
||||
if (!ScenarioTest.containsAlias(alias)) {
|
||||
factory.apply(alias).run().keep();
|
||||
}
|
||||
})
|
||||
);
|
||||
final var response = run();
|
||||
verify();
|
||||
return response;
|
||||
}
|
||||
|
||||
protected abstract HttpResponse run();
|
||||
|
||||
protected void verify() {
|
||||
}
|
||||
|
||||
public final UseCase<T> given(final String propName, final Object propValue) {
|
||||
givenProperties.put(propName, propValue);
|
||||
ScenarioTest.putProperty(propName, propValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
public final JsonTemplate usingJsonBody(final String jsonTemplate) {
|
||||
return new JsonTemplate(jsonTemplate);
|
||||
}
|
||||
|
||||
public final void obtain(
|
||||
final String alias,
|
||||
final Supplier<HttpResponse> http,
|
||||
final Function<HttpResponse, String> extractor,
|
||||
final String... extraInfo) {
|
||||
withTitle(ScenarioTest.resolve(alias), () -> {
|
||||
final var response = http.get().keep(extractor);
|
||||
Arrays.stream(extraInfo).forEach(testReport::printPara);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
public final void obtain(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) {
|
||||
withTitle(ScenarioTest.resolve(alias), () -> {
|
||||
final var response = http.get().keep();
|
||||
Arrays.stream(extraInfo).forEach(testReport::printPara);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
public HttpResponse withTitle(final String title, final Supplier<HttpResponse> code) {
|
||||
this.nextTitle = title;
|
||||
final var response = code.get();
|
||||
this.nextTitle = null;
|
||||
return response;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public final HttpResponse httpGet(final String uriPathWithPlaceholders) {
|
||||
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
|
||||
final var request = HttpRequest.newBuilder()
|
||||
.GET()
|
||||
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
|
||||
.header("current-subject", ScenarioTest.RUN_AS_USER)
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
final var response = client.send(request, BodyHandlers.ofString());
|
||||
return new HttpResponse(HttpMethod.GET, uriPath, null, response);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public final HttpResponse httpPost(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
|
||||
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
|
||||
final var requestBody = bodyJsonTemplate.resolvePlaceholders();
|
||||
final var request = HttpRequest.newBuilder()
|
||||
.POST(BodyPublishers.ofString(requestBody))
|
||||
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("current-subject", ScenarioTest.RUN_AS_USER)
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
final var response = client.send(request, BodyHandlers.ofString());
|
||||
return new HttpResponse(HttpMethod.POST, uriPath, requestBody, response);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public final HttpResponse httpPatch(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
|
||||
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
|
||||
final var requestBody = bodyJsonTemplate.resolvePlaceholders();
|
||||
final var request = HttpRequest.newBuilder()
|
||||
.method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody))
|
||||
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("current-subject", ScenarioTest.RUN_AS_USER)
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
final var response = client.send(request, BodyHandlers.ofString());
|
||||
return new HttpResponse(HttpMethod.PATCH, uriPath, requestBody, response);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public final HttpResponse httpDelete(final String uriPathWithPlaceholders) {
|
||||
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders);
|
||||
final var request = HttpRequest.newBuilder()
|
||||
.DELETE()
|
||||
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("current-subject", ScenarioTest.RUN_AS_USER)
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
final var response = client.send(request, BodyHandlers.ofString());
|
||||
return new HttpResponse(HttpMethod.DELETE, uriPath, null, response);
|
||||
}
|
||||
|
||||
protected PathAssertion path(final String path) {
|
||||
return new PathAssertion(path);
|
||||
}
|
||||
|
||||
protected void verify(
|
||||
final String title,
|
||||
final Supplier<UseCase.HttpResponse> http,
|
||||
final Consumer<UseCase.HttpResponse>... assertions) {
|
||||
withTitle(ScenarioTest.resolve(title), () -> {
|
||||
final var response = http.get();
|
||||
Arrays.stream(assertions).forEach(assertion -> assertion.accept(response));
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
public final UUID uuid(final String alias) {
|
||||
return ScenarioTest.uuid(alias);
|
||||
}
|
||||
|
||||
public String uriEncoded(final String text) {
|
||||
return encode(ScenarioTest.resolve(text), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static class JsonTemplate {
|
||||
|
||||
private final String template;
|
||||
|
||||
private JsonTemplate(final String jsonTemplate) {
|
||||
this.template = jsonTemplate;
|
||||
}
|
||||
|
||||
String resolvePlaceholders() {
|
||||
return ScenarioTest.resolve(template);
|
||||
}
|
||||
}
|
||||
|
||||
public final class HttpResponse {
|
||||
|
||||
@Getter
|
||||
private final java.net.http.HttpResponse<String> response;
|
||||
|
||||
@Getter
|
||||
private final HttpStatus status;
|
||||
|
||||
private UUID locationUuid;
|
||||
|
||||
@SneakyThrows
|
||||
public HttpResponse(
|
||||
final HttpMethod httpMethod,
|
||||
final String uri,
|
||||
final String requestBody,
|
||||
final java.net.http.HttpResponse<String> response
|
||||
) {
|
||||
this.response = response;
|
||||
this.status = HttpStatus.valueOf(response.statusCode());
|
||||
if (this.status == HttpStatus.CREATED) {
|
||||
final var location = response.headers().firstValue("Location").orElseThrow();
|
||||
assertThat(location).startsWith("http://localhost:");
|
||||
locationUuid = UUID.fromString(location.substring(location.lastIndexOf('/') + 1));
|
||||
}
|
||||
|
||||
reportRequestAndResponse(httpMethod, uri, requestBody);
|
||||
}
|
||||
|
||||
public HttpResponse expecting(final HttpStatus httpStatus) {
|
||||
assertThat(HttpStatus.valueOf(response.statusCode())).isEqualTo(httpStatus);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpResponse expecting(final ContentType contentType) {
|
||||
assertThat(response.headers().firstValue("content-type"))
|
||||
.contains(contentType.toString());
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpResponse keep(final Function<HttpResponse, String> extractor) {
|
||||
final var alias = nextTitle != null ? nextTitle : resultAlias;
|
||||
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
|
||||
|
||||
final var value = extractor.apply(this);
|
||||
ScenarioTest.putAlias(
|
||||
alias,
|
||||
new ScenarioTest.Alias<>(UseCase.this.getClass(), UUID.fromString(value)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpResponse keep() {
|
||||
final var alias = nextTitle != null ? nextTitle : resultAlias;
|
||||
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
|
||||
ScenarioTest.putAlias(
|
||||
alias,
|
||||
new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid));
|
||||
return this;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public HttpResponse expectArrayElements(final int expectedElementCount) {
|
||||
final var rootNode = objectMapper.readTree(response.body());
|
||||
assertThat(rootNode.isArray()).as("array expected, but got: " + response.body()).isTrue();
|
||||
|
||||
final var root = (List<?>) objectMapper.readValue(response.body(), new TypeReference<List<Object>>() {
|
||||
});
|
||||
assertThat(root.size()).as("unexpected number of array elements").isEqualTo(expectedElementCount);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public String getFromBody(final String path) {
|
||||
return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Optional<String> getFromBodyAsOptional(final String path) {
|
||||
try {
|
||||
return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path)));
|
||||
} catch (final Exception e) {
|
||||
return null; // means the property did not exist at all, not that it was there with value null
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public OptionalAssert<String> path(final String path) {
|
||||
return assertThat(getFromBodyAsOptional(path));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void reportRequestAndResponse(final HttpMethod httpMethod, final String uri, final String requestBody) {
|
||||
|
||||
// the title
|
||||
if (nextTitle != null) {
|
||||
testReport.printLine("\n### " + nextTitle + "\n");
|
||||
} else if (resultAlias != null) {
|
||||
testReport.printLine("\n### " + resultAlias + "\n");
|
||||
} else {
|
||||
fail("please wrap the http...-call in the UseCase using `withTitle(...)`");
|
||||
}
|
||||
|
||||
// the request
|
||||
testReport.printLine("```");
|
||||
testReport.printLine(httpMethod.name() + " " + uri);
|
||||
testReport.printLine((requestBody != null ? requestBody.trim() : ""));
|
||||
|
||||
// the response
|
||||
testReport.printLine("=> status: " + status + " " + (locationUuid != null ? locationUuid : ""));
|
||||
if (httpMethod == HttpMethod.GET || status.isError()) {
|
||||
final var jsonNode = objectMapper.readTree(response.body());
|
||||
final var prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode);
|
||||
testReport.printLine(prettyJson);
|
||||
}
|
||||
testReport.printLine("```");
|
||||
testReport.printLine("");
|
||||
}
|
||||
}
|
||||
|
||||
protected T self() {
|
||||
//noinspection unchecked
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
private static @Nullable String getResultAliasFromProducesAnnotationInCallStack() {
|
||||
return AnnotationFinder.findCallerAnnotation(Produces.class, Test.class)
|
||||
.map(produces -> oneOf(produces.value(), produces.explicitly()))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static String oneOf(final String one, final String another) {
|
||||
if (isNotBlank(one) && isBlank(another)) {
|
||||
return one;
|
||||
} else if (isBlank(one) && isNotBlank(another)) {
|
||||
return another;
|
||||
}
|
||||
throw new AssertionFailure("exactly one value required, but got '" + one + "' and '" + another + "'");
|
||||
}
|
||||
|
||||
private String title(String resultAlias) {
|
||||
return getClass().getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1 $2") + " => " + resultAlias;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||
|
||||
import static org.assertj.core.api.Assumptions.assumeThat;
|
||||
|
||||
public class UseCaseNotImplementedYet extends UseCase<UseCaseNotImplementedYet> {
|
||||
|
||||
public UseCaseNotImplementedYet(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
assumeThat(false).isTrue(); // makes the test gray
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.contact;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class AddPhoneNumberToContactData extends UseCase<AddPhoneNumberToContactData> {
|
||||
|
||||
public AddPhoneNumberToContactData(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain(
|
||||
"partnerContactUuid",
|
||||
() -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].contact.uuid"),
|
||||
"In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
|
||||
);
|
||||
|
||||
withTitle("Patch the Additional Phone-Number into the Contact", () ->
|
||||
httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
|
||||
{
|
||||
"phoneNumbers": {
|
||||
${phoneNumberKeyToAdd}: ${phoneNumberToAdd}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.OK)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify() {
|
||||
verify(
|
||||
"Verify if the New Phone Number Got Added",
|
||||
() -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
|
||||
.expecting(OK).expecting(JSON).expectArrayElements(1),
|
||||
path("[0].contact.phoneNumbers.%{phoneNumberKeyToAdd}").contains("%{phoneNumberToAdd}")
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.contact;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class AmendContactData extends UseCase<AmendContactData> {
|
||||
|
||||
public AmendContactData(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain("partnerContactUuid", () ->
|
||||
httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].contact.uuid"),
|
||||
"In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
|
||||
);
|
||||
|
||||
withTitle("Patch the New Phone Number Into the Contact", () ->
|
||||
httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
|
||||
{
|
||||
"caption": ${newContactCaption???},
|
||||
"postalAddress": {
|
||||
%{newPostalAddress???}
|
||||
},
|
||||
"phoneNumbers": {
|
||||
"office": ${newOfficePhoneNumber???}
|
||||
},
|
||||
"emailAddresses": {
|
||||
"main": ${newEmailAddress???}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.OK)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.contact;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class RemovePhoneNumberFromContactData extends UseCase<RemovePhoneNumberFromContactData> {
|
||||
|
||||
public RemovePhoneNumberFromContactData(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain(
|
||||
"partnerContactUuid",
|
||||
() -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].contact.uuid"),
|
||||
"In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
|
||||
);
|
||||
|
||||
withTitle("Patch the Additional Phone-Number into the Contact", () ->
|
||||
httpPatch("/api/hs/office/contacts/%{partnerContactUuid}", usingJsonBody("""
|
||||
{
|
||||
"phoneNumbers": {
|
||||
${phoneNumberKeyToRemove}: NULL
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.OK)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify() {
|
||||
verify(
|
||||
"Verify if the New Phone Number Got Added",
|
||||
() -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
|
||||
.expecting(OK).expecting(JSON).expectArrayElements(1),
|
||||
path("[0].contact.phoneNumbers.%{phoneNumberKeyToRemove}").doesNotExist()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.contact;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.CREATED;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class ReplaceContactData extends UseCase<ReplaceContactData> {
|
||||
|
||||
public ReplaceContactData(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain("partnerRelationUuid", () ->
|
||||
httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
|
||||
.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("Contact: %{newContactCaption}", () ->
|
||||
httpPost("/api/hs/office/contacts", usingJsonBody("""
|
||||
{
|
||||
"caption": ${newContactCaption},
|
||||
"postalAddress": {
|
||||
%{newPostalAddress???}
|
||||
},
|
||||
"phoneNumbers": {
|
||||
"phone": ${newOfficePhoneNumber???}
|
||||
},
|
||||
"emailAddresses": {
|
||||
"main": ${newEmailAddress???}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON),
|
||||
"Please check first if that contact already exists, if so, use it's UUID below.",
|
||||
"If any `postalAddress` sub-properties besides those specified in the API " +
|
||||
"(currently `firm`, `name`, `co`, `street`, `zipcode`, `city`, `country`) " +
|
||||
"its values might not appear in external systems.");
|
||||
|
||||
withTitle("Replace the Contact-Reference in the Partner-Relation", () ->
|
||||
httpPatch("/api/hs/office/relations/%{partnerRelationUuid}", usingJsonBody("""
|
||||
{
|
||||
"contactUuid": ${Contact: %{newContactCaption}}
|
||||
}
|
||||
"""))
|
||||
.expecting(OK)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify() {
|
||||
verify(
|
||||
"Verify if the Contact-Relation Got Replaced in the Partner-Relation",
|
||||
() -> httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerName}"))
|
||||
.expecting(OK).expecting(JSON).expectArrayElements(1),
|
||||
path("[0].contact.caption").contains("%{newContactCaption}")
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.person.CreatePerson;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.CREATED;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class CreateExternalDebitorForPartner extends UseCase<CreateExternalDebitorForPartner> {
|
||||
|
||||
public CreateExternalDebitorForPartner(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
|
||||
requires("Person: Billing GmbH", alias -> new CreatePerson(testSuite, alias)
|
||||
.given("personType", "LEGAL_PERSON")
|
||||
.given("tradeName", "Billing GmbH")
|
||||
);
|
||||
}
|
||||
|
||||
@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("BankAccount: Billing GmbH - refund bank account", () ->
|
||||
httpPost("/api/hs/office/bankaccounts", usingJsonBody("""
|
||||
{
|
||||
"holder": "Billing GmbH - refund bank account",
|
||||
"iban": "DE02120300000000202051",
|
||||
"bic": "BYLADEM1001"
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON)
|
||||
);
|
||||
|
||||
obtain("Contact: Billing GmbH - Test AG billing", () ->
|
||||
httpPost("/api/hs/office/contacts", usingJsonBody("""
|
||||
{
|
||||
"caption": "Billing GmbH, billing for Test AG",
|
||||
"emailAddresses": {
|
||||
"main": "test-ag@billing-GmbH.example.com"
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON)
|
||||
);
|
||||
|
||||
return httpPost("/api/hs/office/debitors", usingJsonBody("""
|
||||
{
|
||||
"debitorRel": {
|
||||
"anchorUuid": ${Person: %{partnerPersonTradeName}},
|
||||
"holderUuid": ${Person: Billing GmbH},
|
||||
"contactUuid": ${Contact: Billing GmbH - Test AG billing}
|
||||
},
|
||||
"debitorNumberSuffix": ${debitorNumberSuffix},
|
||||
"billable": ${billable},
|
||||
"vatId": ${vatId},
|
||||
"vatCountryCode": ${vatCountryCode},
|
||||
"vatBusiness": ${vatBusiness},
|
||||
"vatReverseCharge": ${vatReverseCharge},
|
||||
"refundBankAccountUuid": ${BankAccount: Billing GmbH - refund bank account},
|
||||
"defaultPrefix": ${defaultPrefix}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.CREATED;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class CreateSelfDebitorForPartner extends UseCase<CreateSelfDebitorForPartner> {
|
||||
|
||||
public CreateSelfDebitorForPartner(final ScenarioTest testSuite, final String resultAlias) {
|
||||
super(testSuite, resultAlias);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
obtain("partnerPersonUuid", () ->
|
||||
httpGet("/api/hs/office/relations?relationType=PARTNER&personData=" + uriEncoded("%{partnerPersonTradeName}"))
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].holder.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("BankAccount: Test AG - refund bank account", () ->
|
||||
httpPost("/api/hs/office/bankaccounts", usingJsonBody("""
|
||||
{
|
||||
"holder": "Test AG - refund bank account",
|
||||
"iban": "DE88100900001234567892",
|
||||
"bic": "BEVODEBB"
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON)
|
||||
);
|
||||
|
||||
obtain("Contact: Test AG - billing department", () ->
|
||||
httpPost("/api/hs/office/contacts", usingJsonBody("""
|
||||
{
|
||||
"caption": ${billingContactCaption},
|
||||
"emailAddresses": {
|
||||
"main": ${billingContactEmailAddress}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON)
|
||||
);
|
||||
|
||||
return httpPost("/api/hs/office/debitors", usingJsonBody("""
|
||||
{
|
||||
"debitorRel": {
|
||||
"anchorUuid": ${partnerPersonUuid},
|
||||
"holderUuid": ${partnerPersonUuid},
|
||||
"contactUuid": ${Contact: Test AG - billing department}
|
||||
},
|
||||
"debitorNumberSuffix": ${debitorNumberSuffix},
|
||||
"billable": ${billable},
|
||||
"vatId": ${vatId},
|
||||
"vatCountryCode": ${vatCountryCode},
|
||||
"vatBusiness": ${vatBusiness},
|
||||
"vatReverseCharge": ${vatReverseCharge},
|
||||
"refundBankAccountUuid": ${BankAccount: Test AG - refund bank account},
|
||||
"defaultPrefix": ${defaultPrefix}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.CREATED;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class CreateSepaMandateForDebitor extends UseCase<CreateSepaMandateForDebitor> {
|
||||
|
||||
public CreateSepaMandateForDebitor(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain("Debitor: Test AG - main debitor", () ->
|
||||
httpGet("/api/hs/office/debitors?debitorNumber=&{debitorNumber}")
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].uuid")
|
||||
);
|
||||
|
||||
obtain("BankAccount: Test AG - debit bank account", () ->
|
||||
httpPost("/api/hs/office/bankaccounts", usingJsonBody("""
|
||||
{
|
||||
"holder": ${bankAccountHolder},
|
||||
"iban": ${bankAccountIBAN},
|
||||
"bic": ${bankAccountBIC}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON)
|
||||
);
|
||||
|
||||
return httpPost("/api/hs/office/sepamandates", usingJsonBody("""
|
||||
{
|
||||
"debitorUuid": ${Debitor: Test AG - main debitor},
|
||||
"bankAccountUuid": ${BankAccount: Test AG - debit bank account},
|
||||
"reference": ${mandateReference},
|
||||
"agreement": ${mandateAgreement},
|
||||
"validFrom": ${mandateValidFrom}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class DeleteDebitor extends UseCase<DeleteDebitor> {
|
||||
|
||||
public DeleteDebitor(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
|
||||
requires("Debitor: Test AG - delete debitor", alias -> new CreateSelfDebitorForPartner(testSuite, alias)
|
||||
.given("partnerPersonTradeName", "Test AG")
|
||||
.given("billingContactCaption", "Test AG - billing department")
|
||||
.given("billingContactEmailAddress", "billing@test-ag.example.org")
|
||||
.given("debitorNumberSuffix", "%{debitorSuffix}")
|
||||
.given("billable", true)
|
||||
.given("vatId", "VAT123456")
|
||||
.given("vatCountryCode", "DE")
|
||||
.given("vatBusiness", true)
|
||||
.given("vatReverseCharge", false)
|
||||
.given("defaultPrefix", "tsy"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
withTitle("Delete the Debitor using its UUID", () ->
|
||||
httpDelete("/api/hs/office/debitors/&{Debitor: Test AG - delete debitor}")
|
||||
.expecting(HttpStatus.NO_CONTENT)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class DontDeleteDefaultDebitor extends UseCase<DontDeleteDefaultDebitor> {
|
||||
|
||||
public DontDeleteDefaultDebitor(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
httpDelete("/api/hs/office/debitors/&{Debitor: Test AG - main debitor}")
|
||||
// TODO.spec: should be CONFLICT or CLIENT_ERROR for Debitor "00" - but how to delete Partners?
|
||||
.expecting(HttpStatus.NO_CONTENT);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class FinallyDeleteSepaMandateForDebitor extends UseCase<FinallyDeleteSepaMandateForDebitor> {
|
||||
|
||||
public FinallyDeleteSepaMandateForDebitor(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain("SEPA-Mandate: %{bankAccountIBAN}", () ->
|
||||
httpGet("/api/hs/office/sepamandates?iban=&{bankAccountIBAN}")
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
|
||||
"With production data, the bank-account could be used in multiple SEPA-mandates, make sure to use the right one!"
|
||||
);
|
||||
|
||||
// TODO.spec: When to allow actual deletion of SEPA-mandates? Add constraint accordingly.
|
||||
return withTitle("Delete the SEPA-Mandate by its UUID", () -> httpDelete("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}")
|
||||
.expecting(HttpStatus.NO_CONTENT)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class InvalidateSepaMandateForDebitor extends UseCase<InvalidateSepaMandateForDebitor> {
|
||||
|
||||
public InvalidateSepaMandateForDebitor(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain("SEPA-Mandate: %{bankAccountIBAN}", () ->
|
||||
httpGet("/api/hs/office/sepamandates?iban=&{bankAccountIBAN}")
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
|
||||
"With production data, the bank-account could be used in multiple SEPA-mandates, make sure to use the right one!"
|
||||
);
|
||||
|
||||
return withTitle("Patch the End of the Mandate into the SEPA-Mandate", () ->
|
||||
httpPatch("/api/hs/office/sepamandates/&{SEPA-Mandate: %{bankAccountIBAN}}", usingJsonBody("""
|
||||
{
|
||||
"validUntil": ${mandateValidUntil}
|
||||
}
|
||||
"""))
|
||||
.expecting(OK).expecting(JSON)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.membership;
|
||||
|
||||
import io.restassured.http.ContentType;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class CreateMembership extends UseCase<CreateMembership> {
|
||||
|
||||
public CreateMembership(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
obtain("Membership: %{partnerName} 00", () ->
|
||||
httpPost("/api/hs/office/memberships", usingJsonBody("""
|
||||
{
|
||||
"partnerUuid": ${Partner: Test AG},
|
||||
"memberNumberSuffix": ${memberNumberSuffix},
|
||||
"validFrom": ${validFrom},
|
||||
"membershipFeeBillable": ${membershipFeeBillable}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
|
||||
|
||||
import io.restassured.http.ContentType;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.office.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 AddOperationsContactToPartner extends UseCase<AddOperationsContactToPartner> {
|
||||
|
||||
public AddOperationsContactToPartner(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: %{operationsContactGivenName} %{operationsContactFamilyName}", () ->
|
||||
httpPost("/api/hs/office/persons", usingJsonBody("""
|
||||
{
|
||||
"personType": "NATURAL_PERSON",
|
||||
"familyName": ${operationsContactFamilyName},
|
||||
"givenName": ${operationsContactGivenName}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON),
|
||||
"Please check first if that person already exists, if so, use it's UUID below.",
|
||||
"**HINT**: operations contacts are always connected to a partner-person, thus a person which is a holder of a partner-relation."
|
||||
);
|
||||
|
||||
obtain("Contact: %{operationsContactGivenName} %{operationsContactFamilyName}", () ->
|
||||
httpPost("/api/hs/office/contacts", usingJsonBody("""
|
||||
{
|
||||
"caption": "%{operationsContactGivenName} %{operationsContactFamilyName}",
|
||||
"phoneNumbers": {
|
||||
"main": ${operationsContactPhoneNumber}
|
||||
},
|
||||
"emailAddresses": {
|
||||
"main": ${operationsContactEMailAddress}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON),
|
||||
"Please check first if that contact already exists, if so, use it's UUID below."
|
||||
);
|
||||
|
||||
return httpPost("/api/hs/office/relations", usingJsonBody("""
|
||||
{
|
||||
"type": "OPERATIONS",
|
||||
"anchorUuid": ${Person: %{partnerPersonTradeName}},
|
||||
"holderUuid": ${Person: %{operationsContactGivenName} %{operationsContactFamilyName}},
|
||||
"contactUuid": ${Contact: %{operationsContactGivenName} %{operationsContactFamilyName}}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify() {
|
||||
verify(
|
||||
"Verify the New OPERATIONS Relation",
|
||||
() -> httpGet("/api/hs/office/relations?relationType=OPERATIONS&personData=" + uriEncoded(
|
||||
"%{operationsContactFamilyName}"))
|
||||
.expecting(OK).expecting(JSON).expectArrayElements(1),
|
||||
path("[0].contact.caption").contains("%{operationsContactGivenName} %{operationsContactFamilyName}")
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
|
||||
|
||||
import io.restassured.http.ContentType;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.office.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 AddRepresentativeToPartner extends UseCase<AddRepresentativeToPartner> {
|
||||
|
||||
public AddRepresentativeToPartner(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: %{representativeGivenName} %{representativeFamilyName}", () ->
|
||||
httpPost("/api/hs/office/persons", usingJsonBody("""
|
||||
{
|
||||
"personType": "NATURAL_PERSON",
|
||||
"familyName": ${representativeFamilyName},
|
||||
"givenName": ${representativeGivenName}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON),
|
||||
"Please check first if that person already exists, if so, use it's UUID below.",
|
||||
"**HINT**: A representative is always a natural person and represents a non-natural-person."
|
||||
);
|
||||
|
||||
obtain("Contact: %{representativeGivenName} %{representativeFamilyName}", () ->
|
||||
httpPost("/api/hs/office/contacts", usingJsonBody("""
|
||||
{
|
||||
"caption": "%{representativeGivenName} %{representativeFamilyName}",
|
||||
"postalAddress": {
|
||||
%{representativePostalAddress}
|
||||
},
|
||||
"phoneNumbers": {
|
||||
"main": ${representativePhoneNumber}
|
||||
},
|
||||
"emailAddresses": {
|
||||
"main": ${representativeEMailAddress}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON),
|
||||
"Please check first if that contact already exists, if so, use it's UUID below."
|
||||
);
|
||||
|
||||
return httpPost("/api/hs/office/relations", usingJsonBody("""
|
||||
{
|
||||
"type": "REPRESENTATIVE",
|
||||
"anchorUuid": ${Person: %{partnerPersonTradeName}},
|
||||
"holderUuid": ${Person: %{representativeGivenName} %{representativeFamilyName}},
|
||||
"contactUuid": ${Contact: %{representativeGivenName} %{representativeFamilyName}}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify() {
|
||||
verify(
|
||||
"Verify the REPRESENTATIVE Relation Got Removed",
|
||||
() -> httpGet("/api/hs/office/relations?relationType=REPRESENTATIVE&personData=" + uriEncoded("%{representativeFamilyName}"))
|
||||
.expecting(OK).expecting(JSON).expectArrayElements(1),
|
||||
path("[0].contact.caption").contains("%{representativeGivenName} %{representativeFamilyName}")
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
|
||||
|
||||
import io.restassured.http.ContentType;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class CreatePartner extends UseCase<CreatePartner> {
|
||||
|
||||
public CreatePartner(final ScenarioTest testSuite, final String resultAlias) {
|
||||
super(testSuite, resultAlias);
|
||||
}
|
||||
|
||||
public CreatePartner(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain("Person: Hostsharing eG", () ->
|
||||
httpGet("/api/hs/office/persons?name=Hostsharing+eG")
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
|
||||
"Even in production data we expect this query to return just a single result." // TODO.impl: add constraint?
|
||||
);
|
||||
|
||||
obtain("Person: %{%{tradeName???}???%{givenName???} %{familyName???}}", () ->
|
||||
httpPost("/api/hs/office/persons", usingJsonBody("""
|
||||
{
|
||||
"personType": ${personType???},
|
||||
"tradeName": ${tradeName???},
|
||||
"givenName": ${givenName???},
|
||||
"familyName": ${familyName???}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
||||
);
|
||||
|
||||
obtain("Contact: %{contactCaption}", () ->
|
||||
httpPost("/api/hs/office/contacts", usingJsonBody("""
|
||||
{
|
||||
"caption": ${contactCaption},
|
||||
"postalAddress": {
|
||||
%{postalAddress???}
|
||||
},
|
||||
"phoneNumbers": {
|
||||
"office": ${officePhoneNumber???}
|
||||
},
|
||||
"emailAddresses": {
|
||||
"main": ${emailAddress???}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
||||
);
|
||||
|
||||
return httpPost("/api/hs/office/partners", usingJsonBody("""
|
||||
{
|
||||
"partnerNumber": ${partnerNumber},
|
||||
"partnerRel": {
|
||||
"anchorUuid": ${Person: Hostsharing eG},
|
||||
"holderUuid": ${Person: %{%{tradeName???}???%{givenName???} %{familyName???}}},
|
||||
"contactUuid": ${Contact: %{contactCaption}}
|
||||
},
|
||||
"details": {
|
||||
"registrationOffice": "Registergericht Hamburg",
|
||||
"registrationNumber": "1234567"
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify() {
|
||||
verify(
|
||||
"Verify the New Partner Relation",
|
||||
() -> httpGet("/api/hs/office/relations?relationType=PARTNER&contactData=&{contactCaption}")
|
||||
.expecting(OK).expecting(JSON).expectArrayElements(1)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.partner;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class DeletePartner extends UseCase<DeletePartner> {
|
||||
|
||||
public DeletePartner(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
|
||||
requires("Partner: Delete AG", alias -> new CreatePartner(testSuite, alias)
|
||||
.given("personType", "LEGAL_PERSON")
|
||||
.given("tradeName", "Delete AG")
|
||||
.given("contactCaption", "Delete AG - Board of Directors")
|
||||
.given("emailAddress", "board-of-directors@delete-ag.example.org"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
withTitle("Delete Partner by its UUID", () ->
|
||||
httpDelete("/api/hs/office/partners/&{Partner: Delete AG}")
|
||||
.expecting(HttpStatus.NO_CONTENT)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.person;
|
||||
|
||||
import io.restassured.http.ContentType;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class CreatePerson extends UseCase<CreatePerson> {
|
||||
|
||||
public CreatePerson(final ScenarioTest testSuite, final String resultAlias) {
|
||||
super(testSuite, resultAlias);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
return withTitle("Create the Person", () ->
|
||||
httpPost("/api/hs/office/persons", usingJsonBody("""
|
||||
{
|
||||
"personType": ${personType},
|
||||
"tradeName": ${tradeName}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||
import static org.springframework.http.HttpStatus.NO_CONTENT;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class RemoveOperationsContactFromPartner extends UseCase<RemoveOperationsContactFromPartner> {
|
||||
|
||||
public RemoveOperationsContactFromPartner(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain("Operations-Contact: %{operationsContactPerson}",
|
||||
() ->
|
||||
httpGet("/api/hs/office/relations?relationType=OPERATIONS&name=" + uriEncoded(
|
||||
"%{operationsContactPerson}"))
|
||||
.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."
|
||||
);
|
||||
|
||||
return withTitle("Delete the Contact", () ->
|
||||
httpDelete("/api/hs/office/relations/&{Operations-Contact: %{operationsContactPerson}}")
|
||||
.expecting(NO_CONTENT)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify() {
|
||||
verify(
|
||||
"Verify the New OPERATIONS Relation",
|
||||
() -> httpGet("/api/hs/office/relations/&{Operations-Contact: %{operationsContactPerson}}")
|
||||
.expecting(NOT_FOUND)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
|
||||
|
||||
import io.restassured.http.ContentType;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
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 SubscribeToMailinglist extends UseCase<SubscribeToMailinglist> {
|
||||
|
||||
public SubscribeToMailinglist(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}", () ->
|
||||
httpPost("/api/hs/office/persons", usingJsonBody("""
|
||||
{
|
||||
"personType": "NATURAL_PERSON",
|
||||
"familyName": ${subscriberFamilyName},
|
||||
"givenName": ${subscriberGivenName}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
||||
);
|
||||
|
||||
obtain("Contact: %{subscriberGivenName} %{subscriberFamilyName}", () ->
|
||||
httpPost("/api/hs/office/contacts", usingJsonBody("""
|
||||
{
|
||||
"caption": "%{subscriberGivenName} %{subscriberFamilyName}",
|
||||
"emailAddresses": {
|
||||
"main": ${subscriberEMailAddress}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON)
|
||||
);
|
||||
|
||||
return httpPost("/api/hs/office/relations", usingJsonBody("""
|
||||
{
|
||||
"type": "SUBSCRIBER",
|
||||
"mark": ${mailingList},
|
||||
"anchorUuid": ${Person: %{partnerPersonTradeName}},
|
||||
"holderUuid": ${Person: %{subscriberGivenName} %{subscriberFamilyName}},
|
||||
"contactUuid": ${Contact: %{subscriberGivenName} %{subscriberFamilyName}}
|
||||
}
|
||||
"""))
|
||||
.expecting(CREATED).expecting(JSON);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package net.hostsharing.hsadminng.hs.office.scenarios.subscription;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
|
||||
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.NO_CONTENT;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class UnsubscribeFromMailinglist extends UseCase<UnsubscribeFromMailinglist> {
|
||||
|
||||
public UnsubscribeFromMailinglist(final ScenarioTest testSuite) {
|
||||
super(testSuite);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain("Subscription: %{subscriberEMailAddress}", () ->
|
||||
httpGet("/api/hs/office/relations?relationType=SUBSCRIBER" +
|
||||
"&mark=" + uriEncoded("%{mailingList}") +
|
||||
"&contactData=" + uriEncoded("%{subscriberEMailAddress}"))
|
||||
.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."
|
||||
);
|
||||
|
||||
return withTitle("Delete the Subscriber-Relation by its UUID", () ->
|
||||
httpDelete("/api/hs/office/relations/&{Subscription: %{subscriberEMailAddress}}")
|
||||
.expecting(NO_CONTENT)
|
||||
);
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountReposi
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
|
||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@ -58,7 +57,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
|
||||
class ListSepaMandates {
|
||||
|
||||
@Test
|
||||
void globalAdmin_canViewAllSepaMandates_ifNoCriteriaGiven() throws JSONException {
|
||||
void globalAdmin_canViewAllSepaMandates_ifNoCriteriaGiven() {
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
@ -97,6 +96,36 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
|
||||
"""));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalAdmin_canFindSepaMandateByName() {
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-subject", "superuser-alex@hostsharing.net")
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/office/sepamandates?iban=DE02120300000000202051")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.log().all()
|
||||
.body("", lenientlyEquals("""
|
||||
[
|
||||
{
|
||||
"debitor": { "debitorNumber": 1000111 },
|
||||
"bankAccount": {
|
||||
"iban": "DE02120300000000202051",
|
||||
"holder": "First GmbH"
|
||||
},
|
||||
"reference": "ref-10001-11",
|
||||
"validFrom": "2022-10-01",
|
||||
"validTo": "2026-12-31"
|
||||
}
|
||||
]
|
||||
"""));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
@ -0,0 +1,44 @@
|
||||
package net.hostsharing.hsadminng.reflection;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
|
||||
@UtilityClass
|
||||
public class AnnotationFinder {
|
||||
|
||||
@SneakyThrows
|
||||
public static <T extends Annotation> Optional<T> findCallerAnnotation(
|
||||
final Class<T> annotationClassToFind,
|
||||
final Class<? extends Annotation> annotationClassToStopLookup
|
||||
) {
|
||||
for (var element : Thread.currentThread().getStackTrace()) {
|
||||
final var clazz = Class.forName(element.getClassName());
|
||||
final var method = getMethodFromStackElement(clazz, element);
|
||||
|
||||
// Check if the method is annotated with the desired annotation
|
||||
if (method != null) {
|
||||
if (method.isAnnotationPresent(annotationClassToFind)) {
|
||||
return Optional.of(method.getAnnotation(annotationClassToFind));
|
||||
} else if (method.isAnnotationPresent(annotationClassToStopLookup)) {
|
||||
return empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
return empty();
|
||||
}
|
||||
|
||||
private static Method getMethodFromStackElement(Class<?> clazz, StackTraceElement element) {
|
||||
for (var method : clazz.getDeclaredMethods()) {
|
||||
if (method.getName().equals(element.getMethodName())) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user