HsBookingItemControllerAcceptanceTest passing

This commit is contained in:
Michael Hoennig 2024-09-30 15:21:02 +02:00
parent 7e004d3eed
commit 8ee04ecee3
22 changed files with 386 additions and 117 deletions

View File

@ -17,7 +17,7 @@ public class JsonObjectMapperConfiguration {
public Jackson2ObjectMapperBuilder customObjectMapper() {
return new Jackson2ObjectMapperBuilder()
.modules(new JsonNullableModule(), new JavaTimeModule())
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS)
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS, JsonParser.Feature.ALLOW_COMMENTS)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}

View File

@ -27,7 +27,7 @@ public final class HashGenerator {
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789/.";
private static boolean couldBeHashEnabled; // TODO.impl: remove after legacy data is migrated
private static boolean couldBeHashEnabled; // TODO.legacy: remove after legacy data is migrated
public enum Algorithm {
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),

View File

@ -4,13 +4,57 @@ import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import jakarta.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Getter
public class BookingItemCreatedEvent extends ApplicationEvent {
private final @NotNull HsBookingItem newBookingItem;
public BookingItemCreatedEvent(@NotNull HsBookingItemController source, @NotNull final HsBookingItem newBookingItem) {
private static final Map<UUID, BookingItemCreatedEvent> events = new HashMap<>(); // FIXME: use DB table
static BookingItemCreatedEvent of(final HsBookingItemRealEntity bookingItem) {
return events.get(bookingItem.getUuid());
}
@Getter
public static class Status {
private final String message;
private Status(final String message) {
this.message = message;
}
public static Status finished() {
return new Status(null);
}
public static Status failed(final String errorMessage) {
return new Status(errorMessage);
}
boolean isFinished() {
return message == null;
}
}
private final @NotNull UUID bookingItemUuid;
private final @NotNull String assetJson;
private Status status;
public BookingItemCreatedEvent(
@NotNull final HsBookingItemController source,
@NotNull final HsBookingItem newBookingItem,
final String assetJson) {
super(source);
this.newBookingItem = newBookingItem;
this.bookingItemUuid = newBookingItem.getUuid();
this.assetJson = assetJson;
}
public void setStatus(final Status status) {
this.status = status;
events.put(bookingItemUuid, this);
}
}

View File

@ -1,5 +1,7 @@
package net.hostsharing.hsadminng.hs.booking.item;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
@ -40,6 +42,9 @@ public class HsBookingItemController implements HsBookingItemsApi {
@Autowired
private HsBookingItemRbacRepository bookingItemRepo;
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private EntityManagerWrapper em;
@ -77,7 +82,12 @@ public class HsBookingItemController implements HsBookingItemsApi {
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
.revampProperties();
applicationEventPublisher.publishEvent(new BookingItemCreatedEvent(this, saveProcessor.getEntity()));
try {
applicationEventPublisher.publishEvent(new BookingItemCreatedEvent(
this, saveProcessor.getEntity(), jsonMapper.writeValueAsString(body.getAsset())));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
final var uri =
MvcUriComponentsBuilder.fromController(getClass())

View File

@ -48,7 +48,7 @@ public class BookingItemEntitySaveProcessor {
return this;
}
// TODO.impl: remove once the migration of legacy data is done
// TODO.legacy: remove once the migration of legacy data is done
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
step("validateEntity", "prepareForSave");

View File

@ -1,39 +1,30 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import jakarta.persistence.EntityManager;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRAR_LEVEL_DOMAINS;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
public static final String TARGET_UNIX_USER_PROPERTY_NAME = "targetUnixUser";
public static final String WEBSPACE_NAME_REGEX = "[a-z][a-z0-9]{2}[0-9]{2}";
public static final String TARGET_UNIX_USER_NAME_REGEX = "^"+WEBSPACE_NAME_REGEX+"$|^"+WEBSPACE_NAME_REGEX+"-[a-z0-9\\._-]+$";
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
HsDomainSetupBookingItemValidator() {
super(
// TODO.spec: feels wrong
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
.maxLength(253)
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
.notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name")
.required(),
// TODO.legacy: remove the following property once we give up legacy compatibility
stringProperty(TARGET_UNIX_USER_PROPERTY_NAME).writeOnce()
.maxLength(253)
.matchesRegEx(TARGET_UNIX_USER_NAME_REGEX).describedAs("is not a valid unix-user name")
.writeOnce()
.required(),
stringProperty(VERIFICATION_CODE_PROPERTY_NAME)
.minLength(12)
.maxLength(64)
@ -41,19 +32,6 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
);
}
@Override
public List<String> validateEntity(final HsBookingItem bookingItem) {
final var violations = new ArrayList<String>();
final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class);
if (!bookingItem.isLoaded() &&
domainName.matches("hostsharing.(com|net|org|coop|de)")) {
violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName
+ "' is a forbidden Hostsharing domain name");
}
violations.addAll(super.validateEntity(bookingItem));
return violations;
}
private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) {
final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class);
if (userDefinedVerificationCode != null) {

View File

@ -1,14 +1,31 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent;
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent.Status;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.util.List;
import jakarta.validation.ValidationException;
import java.net.IDN;
import java.util.Map;
import java.util.UUID;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP;
@Component
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedEvent> {
@ -16,41 +33,143 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
@Autowired
private EntityManagerWrapper emw;
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private StandardMapper standardMapper;
@Override
@SneakyThrows
public void onApplicationEvent(final BookingItemCreatedEvent event) {
System.out.println("Received newly created booking item: " + event.getNewBookingItem());
final var newBookingItemRealEntity =
emw.getReference(HsBookingItemRealEntity.class, event.getNewBookingItem().getUuid());
final var newHostingAsset = switch (newBookingItemRealEntity.getType()) {
emw.getReference(HsBookingItemRealEntity.class, event.getBookingItemUuid());
final var asset = jsonMapper.readValue(event.getAssetJson(), HsHostingAssetAutoInsertResource.class);
final var factory = switch (newBookingItemRealEntity.getType()) {
case PRIVATE_CLOUD -> null;
case CLOUD_SERVER -> null;
case MANAGED_SERVER -> null;
case MANAGED_WEBSPACE -> null;
case DOMAIN_SETUP -> createDomainSetupHostingAsset(newBookingItemRealEntity);
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(newBookingItemRealEntity, asset);
};
if (newHostingAsset != null) {
try {
new HostingAssetEntitySaveProcessor(emw, newHostingAsset)
.preprocessEntity()
.validateEntity()
.prepareForSave()
.save()
.validateContext();
} catch (final Exception e) {
// TODO.impl: store status in a separate field, maybe enum+message
newBookingItemRealEntity.getResources().put("status", e.getMessage());
}
if (factory != null) {
event.setStatus(factory.performSaveProcess());
}
}
private HsHostingAsset createDomainSetupHostingAsset(final HsBookingItemRealEntity fromBookingItem) {
return HsHostingAssetRbacEntity.builder()
.bookingItem(fromBookingItem)
.type(HsHostingAssetType.DOMAIN_SETUP)
.identifier(fromBookingItem.getDirectValue("domainName", String.class))
.subHostingAssets(List.of(
// TARGET_UNIX_USER_PROPERTY_NAME
))
.build();
private <T> T ref(final Class<T> entityClass, final UUID uuid) {
return uuid != null ? emw.getReference(entityClass, uuid) : null;
}
@RequiredArgsConstructor
abstract class HostingAssetFactory {
final HsBookingItemRealEntity fromBookingItem;
final HsHostingAssetAutoInsertResource asset;
protected HsHostingAsset create() {
return null;
}
Status performSaveProcess() {
final var newHostingAsset = create();
try {
return persist(newHostingAsset);
} catch (final Exception e) {
return Status.failed(e.getMessage());
}
}
protected Status persist(final HsHostingAsset newHostingAsset) {
new HostingAssetEntitySaveProcessor(emw, newHostingAsset)
.preprocessEntity()
.validateEntity()
.prepareForSave()
.save()
.validateContext();
return Status.finished();
}
}
class DomainSetupHostingAssetFactory extends HostingAssetFactory {
public DomainSetupHostingAssetFactory(
final HsBookingItemRealEntity newBookingItemRealEntity,
final HsHostingAssetAutoInsertResource asset) {
super(newBookingItemRealEntity, asset);
}
@Override
protected HsHostingAsset create() {
final String domainName = asset.getIdentifier();
final var domainSetupAsset = createDomainSetupAsset(domainName);
// TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups
final var subHostingAssetResources = asset.getSubHostingAssets();
final var domainHttpSetupAssetResource = subHostingAssetResources.stream()
.filter(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP)
.findFirst().orElseThrow(() -> new ValidationException(
domainName + ": missing target unix user (assignedToHostingAssetUuid) for DOMAIN_HTTP_SETUP "));
final var domainHttpSetupAsset = domainSetupAsset.getSubHostingAssets().stream().filter(sha -> sha.getType() == DOMAIN_HTTP_SETUP).findFirst().orElseThrow();
domainHttpSetupAsset.setParentAsset(domainSetupAsset);
final HsHostingAssetRealEntity assignedToUnixUserAsset =
emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid());
domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAsset);
if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_DNS_SETUP)) {
domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder()
.type(DOMAIN_DNS_SETUP)
.parentAsset(domainSetupAsset)
.assignedToAsset(assignedToUnixUserAsset.getParentAsset()) // FIXME: why is that needed?
.identifier(domainName + "|DNS")
.config(Map.ofEntries(
// FIXME:
entry("TTL", 21600),
entry("auto-SOA", true)
))
.build());
}
if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_MBOX_SETUP)) {
domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder()
.type(DOMAIN_MBOX_SETUP)
.parentAsset(domainSetupAsset)
.assignedToAsset(assignedToUnixUserAsset.getParentAsset())
.identifier(domainName + "|MBOX")
.caption("HTTP-Setup für " + IDN.toUnicode(domainName))
.build());
}
if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_SMTP_SETUP)) {
domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder()
.type(DOMAIN_SMTP_SETUP)
.parentAsset(domainSetupAsset)
.assignedToAsset(assignedToUnixUserAsset.getParentAsset())
.identifier(domainName + "|SMTP")
.caption("HTTP-Setup für " + IDN.toUnicode(domainName))
.build());
}
return domainSetupAsset;
}
private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) {
return HsHostingAssetRealEntity.builder()
.bookingItem(fromBookingItem)
.type(HsHostingAssetType.DOMAIN_SETUP)
.identifier(domainName)
.caption(asset.getCaption() != null ? asset.getCaption() : domainName)
.parentAsset(ref(HsHostingAssetRealEntity.class, asset.getParentAssetUuid()))
.alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid()))
// FIXME .config(asset.getConfig())
.subHostingAssets(
standardMapper.mapList(asset.getSubHostingAssets(), HsHostingAssetRealEntity.class)
)
.build();
}
@Override
protected Status persist(final HsHostingAsset newHostingAsset) {
final var status = super.persist(newHostingAsset);
newHostingAsset.getSubHostingAssets().forEach(super::persist);
return status;
}
}
}

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -13,6 +14,18 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<H
List<HsHostingAssetRealEntity> findByIdentifier(String assetIdentifier);
default List<HsHostingAssetRealEntity> findByTypeAndIdentifier(@NotNull HsHostingAssetType type, @NotNull String identifier) {
return findByTypeAndIdentifierImpl(type.name(), identifier);
}
@Query("""
select ha
from HsHostingAssetRealEntity ha
where cast(ha.type as String) = :type
and ha.identifier = :identifier
""")
List<HsHostingAssetRealEntity> findByTypeAndIdentifierImpl(@NotNull String type, @NotNull String identifier);
@Query(value = """
select ha.uuid,
ha.alarmcontactuuid,

View File

@ -42,7 +42,7 @@ public class HostingAssetEntitySaveProcessor {
return this;
}
// TODO.impl: remove once the migration of legacy data is done
// TODO.legacy: remove once the migration of legacy data is done
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
step("validateEntity", "prepareForSave");

View File

@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
// TODO.impl: make package private once we've migrated the legacy data
// TODO.legacy: make package private once we've migrated the legacy data
public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
// according to RFC 1035 (section 5) and RFC 1034
@ -33,7 +33,7 @@ public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityVal
RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
public static final String IDENTIFIER_SUFFIX = "|DNS";
private static List<String> zoneFileErrors = null; // TODO.impl: remove once legacy data is migrated
private static List<String> zoneFileErrors = null; // TODO.legacy: remove once legacy data is migrated
HsDomainDnsSetupHostingAssetValidator() {
super(

View File

@ -31,7 +31,7 @@ public class PasswordProperty extends StringProperty<PasswordProperty> {
@Override
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
// TODO.impl: remove after legacy data is migrated
// TODO.legacy: remove after legacy data is migrated
if (HashGenerator.using(hashedUsing).couldBeHash(propValue) && propValue.length() > this.maxLength()) {
// already hashed => do not validate
return;

View File

@ -58,6 +58,17 @@ components:
nullable: false
type:
$ref: '#/components/schemas/HsBookingItemType'
identifier:
type: string
minLength: 3
maxLength: 80
nullable: false
description: only used as a default value for automatically created hosting assets, not part of the booking item
assignedToHostingAssetUuid:
type: string
format: uuid
nullable: false
description: only used as a default value for automatically created hosting assets, not part of the booking item
caption:
type: string
minLength: 3
@ -69,6 +80,8 @@ components:
nullable: true
resources:
$ref: '#/components/schemas/BookingResources'
asset:
$ref: '../hs-hosting/hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetAutoInsert'
required:
- caption
- projectUuid

View File

@ -94,7 +94,68 @@ components:
- type
- identifier
- caption
- config
additionalProperties: false
HsHostingAssetAutoInsert:
type: object
properties:
parentAssetUuid:
type: string
format: uuid
nullable: true
assignedToAssetUuid:
type: string
format: uuid
type:
$ref: '#/components/schemas/HsHostingAssetType'
identifier:
type: string
minLength: 3
maxLength: 80
nullable: false
caption:
type: string
minLength: 3
maxLength: 80
nullable: false
alarmContactUuid:
type: string
format: uuid
nullable: true
config:
$ref: '#/components/schemas/HsHostingAssetConfiguration'
subHostingAssets:
type: array
items:
$ref: '#/components/schemas/HsHostingAssetSubInsert'
required:
- identifier
additionalProperties: false
HsHostingAssetSubInsert:
type: object
properties:
assignedToAssetUuid:
type: string
format: uuid
type:
$ref: '#/components/schemas/HsHostingAssetType'
identifier:
type: string
minLength: 3
maxLength: 80
nullable: false
caption:
type: string
minLength: 3
maxLength: 80
nullable: false
alarmContactUuid:
type: string
format: uuid
nullable: true
config:
$ref: '#/components/schemas/HsHostingAssetConfiguration'
additionalProperties: false
HsHostingAssetConfiguration:

View File

@ -1,6 +1,6 @@
--liquibase formatted sql
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================

View File

@ -1,6 +1,6 @@
--liquibase formatted sql
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================

View File

@ -1,6 +1,6 @@
--liquibase formatted sql
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================

View File

@ -1,6 +1,6 @@
--liquibase formatted sql
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================

View File

@ -1,6 +1,6 @@
--liquibase formatted sql
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================

View File

@ -41,7 +41,7 @@ create table if not exists hs_hosting.asset
config jsonb not null,
alarmContactUuid uuid null references hs_office.contact(uuid) initially deferred,
unique (type, identifier), -- at least as long as we need to be compatible to the legacy system
unique (type, identifier), -- TODO.legacy: at least as long as we need to be compatible to the legacy system
constraint hosting_asset_has_booking_item_or_parent_asset
check (bookingItemUuid is not null or parentAssetUuid is not null or type in ('DOMAIN_SETUP', 'IPV4_NUMBER', 'IPV6_NUMBER'))

View File

@ -1,6 +1,6 @@
--liquibase formatted sql
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================

View File

@ -5,12 +5,14 @@ import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealRepository;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Nested;
@ -31,6 +33,7 @@ import java.util.UUID;
import static java.util.Map.entry;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.matchesRegex;
@ -70,11 +73,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
// given
context("superuser-alex@hostsharing.net");
final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream()
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
.flatMap(List::stream)
.findFirst()
.orElseThrow();
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
RestAssured // @formatter:off
.given()
@ -138,11 +137,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
void globalAdmin_canAddBookingItem() {
context.define("superuser-alex@hostsharing.net");
final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream()
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
.flatMap(List::stream)
.findFirst()
.orElseThrow();
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
final var location = RestAssured // @formatter:off
.given()
@ -189,34 +184,41 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
void projectAgent_canAddBookingItemWithHostingAsset() {
context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT");
final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream()
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
.flatMap(List::stream)
.findFirst()
.orElseThrow();
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
final var givenUnixUser = realHostingAssetRepo.findByTypeAndIdentifier(UNIX_USER, "fir01-web").stream()
.findFirst().orElseThrow();
Dns.fakeResultForDomain("example.org",
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code"));
final var location = RestAssured // @formatter:off
.given()
.header("current-subject", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"projectUuid": "{projectUuid}",
"type": "DOMAIN_SETUP",
"caption": "some new domain-setup booking",
"resources": {
"domainName": "example.org",
"targetUnixUser": "fir01-web",
"verificationCode": "just-a-fake-verification-code"
.header("current-subject", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"projectUuid": "{projectUuid}",
"type": "DOMAIN_SETUP",
"caption": "Domain-Setup for example.org",
"resources": {
"domainName": "example.org",
"verificationCode": "just-a-fake-verification-code"
},
"asset": { // FIXME: rename to hostingAsset
"identifier": "example.org", // also as default for all subAssets
"subHostingAssets": [
{
"type": "DOMAIN_HTTP_SETUP",
"assignedToAssetUuid": "{unixUserUuid}"
}
]
}
}
}
"""
.replace("{projectUuid}", givenProject.getUuid().toString())
)
.port(port)
"""
.replace("{projectUuid}", givenProject.getUuid().toString())
.replace("{unixUserUuid}", givenUnixUser.getUuid().toString())
)
.port(port)
.when()
.post("http://localhost/api/hs/booking/items")
.then().log().all().assertThat()
@ -225,10 +227,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body("", lenientlyEquals("""
{
"type": "DOMAIN_SETUP",
"caption": "some new domain-setup booking",
"caption": "Domain-Setup for example.org",
"validFrom": "{today}",
"validTo": null,
"resources": { "domainName": "example.org", "targetUnixUser": "fir01-web" }
"validTo": null
}
"""
.replace("{today}", LocalDate.now().toString())
@ -240,24 +241,37 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
// then, the new BookingItem can be accessed under the generated UUID
final var newBookingItem = fetchRealBookingItemFromURI(location);
assertThat(newBookingItem)
.extracting(bi -> bi.getDirectValue("domainName", String.class))
.isEqualTo("example.org");
.extracting(HsBookingItem::getCaption)
.isEqualTo("Domain-Setup for example.org");
// and the related HostingAsset also got created
assertThat(realHostingAssetRepo.findByIdentifier("example.org")).isNotEmpty()
// and the related HostingAssets are also got created
final var domainSetupHostingAsset = realHostingAssetRepo.findByIdentifier("example.org");
assertThat(domainSetupHostingAsset).isNotEmpty()
.map(HsHostingAsset::getBookingItem)
.contains(newBookingItem);
assertThat(realHostingAssetRepo.findByIdentifier("example.org|DNS")).isNotEmpty()
.map(HsHostingAsset::getParentAsset)
.isEqualTo(domainSetupHostingAsset);
assertThat(realHostingAssetRepo.findByIdentifier("example.org|HTTP")).isNotEmpty()
.map(HsHostingAsset::getParentAsset)
.isEqualTo(domainSetupHostingAsset);
assertThat(realHostingAssetRepo.findByIdentifier("example.org|MBOX")).isNotEmpty()
.map(HsHostingAsset::getParentAsset)
.isEqualTo(domainSetupHostingAsset);
assertThat(realHostingAssetRepo.findByIdentifier("example.org|SMTP")).isNotEmpty()
.map(HsHostingAsset::getParentAsset)
.isEqualTo(domainSetupHostingAsset);
final var status = BookingItemCreatedEvent.of(newBookingItem);
assertThat(status.getStatus().isFinished());
}
@Test
void projectAgent_canAddBookingItemEvenIfHostingAssetCreationFails() {
context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT");
final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream()
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
.flatMap(List::stream)
.findFirst()
.orElseThrow();
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
final var givenUnixUser = realHostingAssetRepo.findByIdentifier("fir01-web").stream().findFirst().orElseThrow();
Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode
@ -272,12 +286,21 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"caption": "some new domain-setup booking",
"resources": {
"domainName": "example.org",
"targetUnixUser": "fir01-web",
"verificationCode": "just-a-fake-verification-code"
},
"asset": { // FIXME: rename to hostingAsset
"identifier": "example.org", // also as default for all subAssets
"subHostingAssets": [
{
"type": "DOMAIN_HTTP_SETUP",
"assignedToAssetUuid": "{unixUserUuid}"
}
]
}
}
"""
.replace("{projectUuid}", givenProject.getUuid().toString())
.replace("{unixUserUuid}", givenUnixUser.getUuid().toString())
)
.port(port)
.when()
@ -291,7 +314,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"caption": "some new domain-setup booking",
"validFrom": "{today}",
"validTo": null,
"resources": { "domainName": "example.org", "targetUnixUser": "fir01-web" }
"resources": { "domainName": "example.org" }
}
"""
.replace("{today}", LocalDate.now().toString())
@ -305,8 +328,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
assertThat(newBookingItem)
.extracting(bi -> bi.getDirectValue("domainName", String.class))
.isEqualTo("example.org");
assertThat(newBookingItem)
.extracting(bi -> bi.getDirectValue("status", String.class))
final var status = BookingItemCreatedEvent.of(newBookingItem);
assertThat(status.getStatus().getMessage())
.isEqualTo("[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=just-a-fake-verification-code' found for domain name 'example.org' (nor in its super-domain)]");
// but the related HostingAsset did not get created
@ -314,6 +337,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
}
}
private @NotNull HsBookingProjectRealEntity findDefaultProjectOfDebitorNumber(final int debitorNumber) {
return debitorRepo.findByDebitorNumber(debitorNumber).stream()
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
.flatMap(List::stream)
.findFirst()
.orElseThrow();
}
@Nested
@Order(1)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)

View File

@ -502,7 +502,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
// this happens if a natural person is marked as 'contractual' for itself
final var idsToRemove = new HashSet<Integer>();
relations.forEach((id, r) -> {
if (r.getHolder() == r.getAnchor()) {
if (r.getType() == HsOfficeRelationType.REPRESENTATIVE && r.getHolder() == r.getAnchor()) {
idsToRemove.add(id);
}
});