add DomainSetup-HostingAssets for new BookingItem via created-event (#111)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: #111 Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
parent
cc2b04472f
commit
60341bf644
@ -0,0 +1,119 @@
|
||||
# Handling Automatic Creation of Hosting Assets for New Booking Items
|
||||
|
||||
**Status:**
|
||||
- [x] proposed by (Michael Hönnig)
|
||||
- [ ] accepted by (Participants)
|
||||
- [ ] rejected by (Participants)
|
||||
- [ ] superseded by (superseding ADR)
|
||||
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
When a customer creates a new booking item (e.g., `MANAGED_WEBSPACE`), the system must automatically create the related hosting asset.
|
||||
This process can sometimes fail or require additional data from the user, e.g. installing a DNS verification key, or a hostmaster, e.g. the target server to use.
|
||||
|
||||
The challenge is how to handle this automatic creation process while dealing with missing data, asynchronicity and failures while ensuring system consistency and proper user notification.
|
||||
|
||||
|
||||
### Technical Background
|
||||
|
||||
The creation of hosting assets can occur synchronously (in simple cases) or asynchronously (when additional steps like manual verification are needed).
|
||||
For example, a `DOMAIN_SETUP` hosting asset may require DNS verification from the user, and until this is provided, the related domain cannot be fully set up.
|
||||
|
||||
Additionally, not all data needed for creating the hosting asset is stored in the booking item.
|
||||
It's part of the HTTP request and later stored in the hosting asset, but we also need to store it before the hosting asset can be created asynchronously.
|
||||
|
||||
Current system behavior involves returning HTTP 201 upon booking item creation, but the automatic hosting asset creation might fail due to missing information.
|
||||
The system needs to manage the creation process in a way that ensures valid hosting assets are created and informs the user of any actions required while still returning a 201 HTTP code, not an error code.
|
||||
|
||||
|
||||
## Considered Options
|
||||
|
||||
For storing the data needed for the hosting-asset creation:
|
||||
|
||||
* STORAGE-1: Store temporary asset data in the `BookingItemEntity`, e.g. a JSON column.
|
||||
And delete the value of that column, once the hosting assets got successfully created.
|
||||
* STORAGE-2: Create hosting assets immediately, even if invalid, but mark them as "inactive" until completed and fully validated.
|
||||
* STORAGE-3: Store the asset data in a kind of event- or job-queue, which get deleted once the hosting-asset got successfully created.
|
||||
|
||||
For the user-notification status:
|
||||
|
||||
* STATUS-1: Introduce a status field in the booking-items.
|
||||
* STATUS-2: Store the status in the event-/job-queue entries.
|
||||
|
||||
### STORAGE-1: Temporary Data Storage in `BookingItemEntity`
|
||||
|
||||
Store asset-related data (e.g., domain name) in a temporary column or JSON field in the `BookingItemEntity` until the hosting assets are successfully created.
|
||||
Once assets are created, the temporary data is deleted to avoid inconsistencies.
|
||||
|
||||
#### Advantages
|
||||
- Easy to implement.
|
||||
|
||||
#### Disadvantages
|
||||
- Needs either a separate map of properties in the booking-item.
|
||||
- Or, if stored as a JSON field in the booking-item-resources, these are misused.
|
||||
- Requires additional cleanup logic to remove stale data.
|
||||
|
||||
### STORAGE-2: Inactive Hosting Assets Until Validation
|
||||
|
||||
Create the hosting assets immediately upon booking item creation but mark them as "inactive" until all required information (e.g., verification code) is provided and validation is complete.
|
||||
|
||||
#### Advantages
|
||||
- Avoids temporary external data storage for the hosting-assets.
|
||||
|
||||
#### Disadvantages
|
||||
- Validation becomes more complex as some properties need to be validated, others not.
|
||||
And some properties even need special treatment for new entities, which then becomes vague.
|
||||
- Inactive assets have to be filtered from operational assets.
|
||||
- Potential risk of incomplete or inconsistent assets being created, which may require correction.
|
||||
- Difficult to write tests for all possible combinations of validations.
|
||||
|
||||
### STORAGE-3: Event-Based Approach
|
||||
|
||||
The hosting asset data required for creation us passed to the API and stored in a `BookingItemCreatedEvent`.
|
||||
If hosting asset creation cannot happen synchronously, the event is stored and processed asynchronously in batches, retrying failed asset creation as needed.
|
||||
|
||||
#### Advantages
|
||||
- Clean-data-structure (separation of concerns).
|
||||
- Clear separation between booking item creation and hosting asset creation.
|
||||
- Only valid assets in the database.
|
||||
- Can handle complex asynchronous processes (like waiting for external verification) in a clean and structured manner.
|
||||
- Easier to manage retries and failures in asset creation without complicating the booking item structure.
|
||||
|
||||
#### Disadvantages
|
||||
- At the Spring controller level, the whole JSON is already converted into Java objects,
|
||||
but for storing the asset data in the even, we need JSON again.
|
||||
This could is not just a performance-overhead but could also lead to inconsistencies.
|
||||
|
||||
### STATUS-1: Store hosting-asset-creation-status in the `BookingItemEntity`
|
||||
|
||||
A status field would be added to booking-items to track the creation state of related hosting assets.
|
||||
The users could check their booking-items for the status of the hosting-asset creation, error messages and further instructions.
|
||||
|
||||
#### Advantages
|
||||
- Easy to implement.
|
||||
|
||||
#### Disadvantages
|
||||
- Adds a field to the booking-item which is makes no sense anymore once the related hosting asset is created.
|
||||
|
||||
|
||||
### Status-2: Store hosting-asset-creation-status in the `BookingItemCreateEvent`
|
||||
|
||||
A status field would be added to the booking-item-created event and get updated with the latest messages any time we try to create the hosting-asset.
|
||||
|
||||
#### Advantages
|
||||
- Clean-data-structure (separation of concerns)
|
||||
|
||||
#### Disadvantages
|
||||
- Accessing the status requires querying the event queue.
|
||||
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
**Chosen Option: STORAGE-3 with STATUS-2 (Event-Based Approach with `BookingItemCreatedEvent`)**
|
||||
|
||||
The event-based approach was selected as the best solution for handling automatic hosting asset creation. This option provides a clear separation between booking item creation and hosting asset creation, ensuring that no invalid or incomplete assets are created. The asynchronous nature of the event system allows for retries and external validation steps (such as user-entered verification codes) without disrupting the overall flow.
|
||||
|
||||
By using `BookingItemCreatedEvent` to store the hosting-asset data and the status,
|
||||
we don't need to misuse other data structures for temporary data
|
||||
and therefore hava a clean separation of concerns.
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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"),
|
||||
|
@ -0,0 +1,20 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Getter
|
||||
public class BookingItemCreatedAppEvent extends ApplicationEvent {
|
||||
|
||||
private BookingItemCreatedEventEntity entity;
|
||||
|
||||
public BookingItemCreatedAppEvent(
|
||||
@NotNull final Object source,
|
||||
@NotNull final HsBookingItemRealEntity newBookingItem,
|
||||
final String assetJson) {
|
||||
super(source);
|
||||
this.entity = new BookingItemCreatedEventEntity(newBookingItem, assetJson);
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Getter
|
||||
public class BookingItemCreatedEvent extends ApplicationEvent {
|
||||
private final @NotNull HsBookingItem newBookingItem;
|
||||
|
||||
public BookingItemCreatedEvent(@NotNull HsBookingItemController source, @NotNull final HsBookingItem newBookingItem) {
|
||||
super(source);
|
||||
this.newBookingItem = newBookingItem;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapsId;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_booking", name = "item_created_event")
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
public class BookingItemCreatedEventEntity implements BaseEntity {
|
||||
@Id
|
||||
@Column(name="bookingitemuuid")
|
||||
private UUID uuid;
|
||||
|
||||
@MapsId
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "bookingitemuuid", nullable = false)
|
||||
private HsBookingItemRealEntity bookingItem;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "assetjson")
|
||||
private String assetJson;
|
||||
|
||||
@Setter
|
||||
@Column(name = "statusmessage")
|
||||
private String statusMessage;
|
||||
|
||||
public BookingItemCreatedEventEntity(
|
||||
@NotNull final HsBookingItemRealEntity newBookingItem,
|
||||
final String assetJson) {
|
||||
this.bookingItem = newBookingItem;
|
||||
this.assetJson = assetJson;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
|
||||
|
||||
BookingItemCreatedEventEntity save(HsBookingItemRealEntity current);
|
||||
|
||||
BookingItemCreatedEventEntity findByBookingItem(HsBookingItemRealEntity newBookingItem);
|
||||
}
|
@ -14,7 +14,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
@ -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;
|
||||
@ -23,6 +25,7 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||
|
||||
@RestController
|
||||
@ -40,6 +43,9 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
@Autowired
|
||||
private HsBookingItemRbacRepository bookingItemRepo;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper jsonMapper;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper em;
|
||||
|
||||
@ -76,8 +82,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
.validateContext()
|
||||
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
|
||||
.revampProperties();
|
||||
|
||||
applicationEventPublisher.publishEvent(new BookingItemCreatedEvent(this, saveProcessor.getEntity()));
|
||||
publishSavedEvent(saveProcessor, body);
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
@ -137,6 +142,16 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
private void publishSavedEvent(final BookingItemEntitySaveProcessor saveProcessor, final HsBookingItemInsertResource body) {
|
||||
try {
|
||||
final var bookingItemRealEntity = em.getReference(HsBookingItemRealEntity.class, saveProcessor.getEntity().getUuid());
|
||||
applicationEventPublisher.publishEvent(new BookingItemCreatedAppEvent(
|
||||
this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getHostingAsset())));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
final BiConsumer<HsBookingItem, HsBookingItemResource> ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setValidFrom(entity.getValidity().lower());
|
||||
if (entity.getValidity().hasUpperBound()) {
|
||||
@ -148,6 +163,9 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.setProject(em.find(HsBookingProjectRealEntity.class, resource.getProjectUuid()));
|
||||
ofNullable(resource.getParentItemUuid())
|
||||
.map(parentItemUuid -> em.find(HsBookingItemRealEntity.class, parentItemUuid))
|
||||
.ifPresent(entity::setParentItem);
|
||||
entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
|
||||
entity.putResources(KeyValueMap.from(resource.getResources()));
|
||||
};
|
||||
|
@ -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");
|
||||
|
@ -13,27 +13,21 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRA
|
||||
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)
|
||||
|
@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
|
@ -1,56 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
||||
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;
|
||||
|
||||
@Component
|
||||
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedEvent> {
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper emw;
|
||||
|
||||
@Override
|
||||
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()) {
|
||||
case PRIVATE_CLOUD -> null;
|
||||
case CLOUD_SERVER -> null;
|
||||
case MANAGED_SERVER -> null;
|
||||
case MANAGED_WEBSPACE -> null;
|
||||
case DOMAIN_SETUP -> createDomainSetupHostingAsset(newBookingItemRealEntity);
|
||||
};
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
@ -89,10 +89,9 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
|
||||
@JoinColumn(name = "alarmcontactuuid")
|
||||
private HsOfficeContactRealEntity alarmContact;
|
||||
|
||||
@Builder.Default
|
||||
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
|
||||
private List<HsHostingAssetRealEntity> subHostingAssets = new ArrayList<>();
|
||||
private List<HsHostingAssetRealEntity> subHostingAssets;
|
||||
|
||||
@Column(name = "identifier")
|
||||
private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc
|
||||
@ -125,6 +124,13 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
|
||||
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig);
|
||||
}
|
||||
|
||||
public List<HsHostingAssetRealEntity> getSubHostingAssets() {
|
||||
if (subHostingAssets == null) {
|
||||
subHostingAssets = new ArrayList<>();
|
||||
}
|
||||
return subHostingAssets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PatchableMapWrapper<Object> directProps() {
|
||||
return getConfig();
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1,153 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetSubInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.net.IDN;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
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;
|
||||
|
||||
public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
|
||||
|
||||
public DomainSetupHostingAssetFactory(
|
||||
final EntityManagerWrapper emw,
|
||||
final HsBookingItemRealEntity newBookingItemRealEntity,
|
||||
final HsHostingAssetAutoInsertResource asset,
|
||||
final StandardMapper standardMapper) {
|
||||
super(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsHostingAsset create() {
|
||||
final var domainSetupAsset = createDomainSetupAsset(getDomainName());
|
||||
final var subHostingAssets = domainSetupAsset.getSubHostingAssets();
|
||||
|
||||
// TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups
|
||||
|
||||
final var domainHttpSetupAssetResource = findSubHostingAssetResource(HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP);
|
||||
final var assignedToUnixUserAssetEntity = domainHttpSetupAssetResource
|
||||
.map(HsHostingAssetSubInsertResource::getAssignedToAssetUuid)
|
||||
.map(uuid -> emw.find(HsHostingAssetRealEntity.class, uuid))
|
||||
.orElseThrow(() -> new ValidationException("DOMAIN_HTTP_SETUP subAsset with assignedToAssetUuid required in compatibility mode"));
|
||||
|
||||
subHostingAssets.add(
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_HTTP_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity)
|
||||
.identifier(getDomainName() + "|HTTP")
|
||||
.caption("HTTP-Setup für " + IDN.toUnicode(getDomainName())))
|
||||
);
|
||||
|
||||
// Do not add to subHostingAssets in compatibility mode, in this case, DNS setup works via file system.
|
||||
// The entity is created just for validation purposes.
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_DNS_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||
.identifier(getDomainName() + "|DNS")
|
||||
.caption("DNS-Setup für " + IDN.toUnicode(getDomainName())));
|
||||
|
||||
subHostingAssets.add(
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_MBOX_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||
.identifier(getDomainName() + "|MBOX")
|
||||
.caption("MBOX-Setup für " + IDN.toUnicode(getDomainName())))
|
||||
);
|
||||
|
||||
subHostingAssets.add(
|
||||
createDomainSubSetupAssetEntity(
|
||||
domainSetupAsset,
|
||||
DOMAIN_SMTP_SETUP,
|
||||
builder -> builder
|
||||
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||
.identifier(getDomainName() + "|SMTP")
|
||||
.caption("SMTP-Setup für " + IDN.toUnicode(getDomainName())))
|
||||
);
|
||||
|
||||
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)
|
||||
.alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid()))
|
||||
// the sub-hosting-assets get added later
|
||||
.build();
|
||||
}
|
||||
|
||||
private HsHostingAssetRealEntity createDomainSubSetupAssetEntity(
|
||||
final HsHostingAssetRealEntity domainSetupAsset,
|
||||
final HsHostingAssetType subAssetType,
|
||||
final Function<HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>> builderTransformer) {
|
||||
final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name());
|
||||
|
||||
final var subAssetResourceOptional = findSubHostingAssetResource(resourceType);
|
||||
|
||||
subAssetResourceOptional.ifPresentOrElse(
|
||||
subAssetResource -> verifyNotOverspecified(subAssetResource),
|
||||
() -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); }
|
||||
);
|
||||
|
||||
return builderTransformer.apply(
|
||||
HsHostingAssetRealEntity.builder()
|
||||
.type(subAssetType)
|
||||
.parentAsset(domainSetupAsset))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Optional<HsHostingAssetSubInsertResource> findSubHostingAssetResource(final HsHostingAssetTypeResource resourceType) {
|
||||
return getSubHostingAssetResources().stream()
|
||||
.filter(ha -> ha.getType() == resourceType)
|
||||
.reduce(Reducer::toSingleElement);
|
||||
}
|
||||
|
||||
// TODO.legacy: while we need to stay compatible, only default values can be used, thus only the type can be specified
|
||||
private void verifyNotOverspecified(final HsHostingAssetSubInsertResource givenSubAssetResource) {
|
||||
final var convert = new ToStringConverter().ignoring("assignedToAssetUuid");
|
||||
final var expectedSubAssetResource = new HsHostingAssetSubInsertResource();
|
||||
expectedSubAssetResource.setType(givenSubAssetResource.getType());
|
||||
if ( !convert.from(givenSubAssetResource).equals(convert.from(expectedSubAssetResource)) ) {
|
||||
throw new ValidationException("sub asset " + givenSubAssetResource.getType() + " is over-specified, in compatibility mode, only default values allowed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getDomainName() {
|
||||
return asset.getIdentifier();
|
||||
}
|
||||
|
||||
private List<HsHostingAssetSubInsertResource> getSubHostingAssetResources() {
|
||||
return asset.getSubHostingAssets();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persist(final HsHostingAsset newHostingAsset) {
|
||||
super.persist(newHostingAsset);
|
||||
newHostingAsset.getSubHostingAssets().forEach(super::persist);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
abstract class HostingAssetFactory {
|
||||
|
||||
final EntityManagerWrapper emw;
|
||||
final HsBookingItemRealEntity fromBookingItem;
|
||||
final HsHostingAssetAutoInsertResource asset;
|
||||
final StandardMapper standardMapper;
|
||||
|
||||
protected abstract HsHostingAsset create();
|
||||
|
||||
public String performSaveProcess() {
|
||||
try {
|
||||
final var newHostingAsset = create();
|
||||
persist(newHostingAsset);
|
||||
return null;
|
||||
} catch (final Exception e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
protected void persist(final HsHostingAsset newHostingAsset) {
|
||||
new HostingAssetEntitySaveProcessor(emw, newHostingAsset)
|
||||
.preprocessEntity()
|
||||
.validateEntity()
|
||||
.prepareForSave()
|
||||
.save()
|
||||
.validateContext();
|
||||
}
|
||||
|
||||
protected <T> T ref(final Class<T> entityClass, final UUID uuid) {
|
||||
return uuid != null ? emw.getReference(entityClass, uuid) : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
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;
|
||||
|
||||
|
||||
@Component
|
||||
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper emw;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper jsonMapper;
|
||||
|
||||
@Autowired
|
||||
private StandardMapper standardMapper;
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void onApplicationEvent(final BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
|
||||
if (containsAssetJson(bookingItemCreatedAppEvent)) {
|
||||
createRelatedHostingAsset(bookingItemCreatedAppEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsAssetJson(final BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
|
||||
return bookingItemCreatedAppEvent.getEntity().getAssetJson() != null;
|
||||
}
|
||||
|
||||
private void createRelatedHostingAsset(final BookingItemCreatedAppEvent event) throws JsonProcessingException {
|
||||
final var newBookingItemRealEntity = event.getEntity().getBookingItem();
|
||||
final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class);
|
||||
final var factory = switch (newBookingItemRealEntity.getType()) {
|
||||
case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER ->
|
||||
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
};
|
||||
if (factory != null) {
|
||||
final var statusMessage = factory.performSaveProcess();
|
||||
// TODO.impl: once we implement retry, we need to amend this code (persist/merge/delete)
|
||||
if (statusMessage != null) {
|
||||
event.getEntity().setStatusMessage(statusMessage);
|
||||
emw.persist(event.getEntity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HostingAssetFactory forNowNoAutomaticHostingAssetCreationPossible(
|
||||
final EntityManagerWrapper emw,
|
||||
final HsBookingItemRealEntity fromBookingItem,
|
||||
final HsHostingAssetAutoInsertResource asset,
|
||||
final StandardMapper standardMapper
|
||||
) {
|
||||
return new HostingAssetFactory(emw, fromBookingItem, asset, standardMapper) {
|
||||
|
||||
@Override
|
||||
protected HsHostingAsset create() {
|
||||
// TODO.impl: we should validate the asset JSON, but some violations are un-avoidable at that stage
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String performSaveProcess() {
|
||||
return "waiting for manual setup of hosting asset for booking item of type " + fromBookingItem.getType();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
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.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
|
||||
|
||||
public ManagedWebspaceHostingAssetFactory(
|
||||
final EntityManagerWrapper emw,
|
||||
final HsBookingItemRealEntity newBookingItemRealEntity,
|
||||
final HsHostingAssetAutoInsertResource asset,
|
||||
final StandardMapper standardMapper) {
|
||||
super(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsHostingAsset create() {
|
||||
if (asset.getType() != HsHostingAssetTypeResource.MANAGED_WEBSPACE) {
|
||||
throw new ValidationException("requires MANAGED_WEBSPACE hosting asset, but got " +
|
||||
Optional.of(asset)
|
||||
.map(HsHostingAssetAutoInsertResource::getType)
|
||||
.map(Enum::name)
|
||||
.orElse(null));
|
||||
}
|
||||
final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class);
|
||||
managedWebspaceHostingAsset.setBookingItem(fromBookingItem);
|
||||
emw.createQuery(
|
||||
"SELECT asset FROM HsHostingAssetRealEntity asset WHERE asset.bookingItem.uuid=:bookingItemUuid",
|
||||
HsHostingAssetRealEntity.class)
|
||||
.setParameter("bookingItemUuid", fromBookingItem.getParentItem().getUuid())
|
||||
.getResultStream().findFirst()
|
||||
.ifPresent(managedWebspaceHostingAsset::setParentAsset);
|
||||
|
||||
return managedWebspaceHostingAsset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persist(final HsHostingAsset newManagedWebspaceHostingAsset) {
|
||||
super.persist(newManagedWebspaceHostingAsset);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
public class ToStringConverter {
|
||||
|
||||
final public Set<String> ignoredFields = new HashSet<>();
|
||||
|
||||
public ToStringConverter ignoring(final String fieldName) {
|
||||
ignoredFields.add(fieldName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String from(Object obj) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
return "{ " +
|
||||
Arrays.stream(obj.getClass().getDeclaredFields())
|
||||
.filter(f -> !ignoredFields.contains(f.getName()))
|
||||
.map(field -> {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
return field.getName() + ": " + field.get(obj);
|
||||
} catch (IllegalAccessException e) {
|
||||
// ignore inaccessible fields
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(joining(", "))
|
||||
+ " }";
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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(
|
||||
|
@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
@ -11,7 +11,7 @@ import lombok.experimental.FieldNameConstants;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
@ -8,7 +8,7 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
@ -8,7 +8,7 @@ import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
|
@ -9,7 +9,7 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
|
@ -13,7 +13,7 @@ import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
|
@ -10,7 +10,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
|
@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.person;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
|
@ -5,7 +5,7 @@ import lombok.experimental.FieldNameConstants;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
|
@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,8 @@
|
||||
package net.hostsharing.hsadminng.lambda;
|
||||
|
||||
public class Reducer {
|
||||
public static <T> T toSingleElement(T last, T next) {
|
||||
throw new AssertionError("only a single entity expected");
|
||||
}
|
||||
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
@ -21,10 +21,10 @@ import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||
*/
|
||||
abstract class Mapper extends ModelMapper {
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager em;
|
||||
EntityManagerWrapper em;
|
||||
|
||||
Mapper() {
|
||||
Mapper(@Autowired final EntityManagerWrapper em) {
|
||||
this.em = em;
|
||||
getConfiguration().setAmbiguityIgnored(true);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@ -8,7 +10,8 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class StandardMapper extends Mapper {
|
||||
|
||||
public StandardMapper() {
|
||||
public StandardMapper(@Autowired final EntityManagerWrapper em) {
|
||||
super(em);
|
||||
getConfiguration().setAmbiguityIgnored(true);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static org.modelmapper.convention.MatchingStrategies.STRICT;
|
||||
@ -13,7 +15,8 @@ import static org.modelmapper.convention.MatchingStrategies.STRICT;
|
||||
@Component
|
||||
public class StrictMapper extends Mapper {
|
||||
|
||||
public StrictMapper() {
|
||||
public StrictMapper(@Autowired final EntityManagerWrapper em) {
|
||||
super(em);
|
||||
getConfiguration().setMatchingStrategy(STRICT);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
package net.hostsharing.hsadminng.rbac.object;
|
||||
package net.hostsharing.hsadminng.persistence;
|
||||
|
||||
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
// TODO.impl: this class does not really belong into this package, but there is no right place yet
|
||||
public interface BaseEntity<T extends BaseEntity<?>> {
|
||||
UUID getUuid();
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.hostsharing.hsadminng.persistence;
|
||||
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.generator;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import org.reflections.Reflections;
|
||||
import org.reflections.scanners.TypeAnnotationsScanner;
|
||||
|
||||
|
@ -5,7 +5,7 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
|
||||
|
@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.test.pac.TestPackageEntity;
|
||||
|
@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerEntity;
|
||||
|
@ -56,6 +56,10 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: false
|
||||
parentItemUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: false
|
||||
type:
|
||||
$ref: '#/components/schemas/HsBookingItemType'
|
||||
caption:
|
||||
@ -69,6 +73,8 @@ components:
|
||||
nullable: true
|
||||
resources:
|
||||
$ref: '#/components/schemas/BookingResources'
|
||||
hostingAsset:
|
||||
$ref: '../hs-hosting/hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetAutoInsert'
|
||||
required:
|
||||
- caption
|
||||
- projectUuid
|
||||
|
@ -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:
|
||||
type:
|
||||
$ref: '#/components/schemas/HsHostingAssetType'
|
||||
identifier:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 80
|
||||
nullable: false
|
||||
caption:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 80
|
||||
nullable: false
|
||||
assignedToAssetUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
alarmContactUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
config:
|
||||
$ref: '#/components/schemas/HsHostingAssetConfiguration'
|
||||
additionalProperties: false
|
||||
|
||||
HsHostingAssetConfiguration:
|
||||
|
@ -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.
|
||||
|
||||
-- ============================================================================
|
||||
|
@ -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.
|
||||
|
||||
-- ============================================================================
|
||||
|
@ -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.
|
||||
|
||||
-- ============================================================================
|
||||
|
@ -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.
|
||||
|
||||
-- ============================================================================
|
||||
|
@ -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.
|
||||
|
||||
-- ============================================================================
|
||||
|
@ -31,6 +31,20 @@ create table if not exists hs_booking.item
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset michael.hoennig:hs-booking-item-EVENT-TABLE endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create table if not exists hs_booking.item_created_event
|
||||
(
|
||||
bookingItemUuid uuid unique references hs_booking.item (uuid),
|
||||
version int not null default 0,
|
||||
assetJson text,
|
||||
statusMessage text
|
||||
);
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset michael.hoennig:hs-booking-item-MAIN-TABLE-JOURNAL endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
@ -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'))
|
||||
|
@ -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.
|
||||
|
||||
-- ============================================================================
|
||||
|
@ -43,6 +43,7 @@ public class ArchitectureTest {
|
||||
"..test.dom",
|
||||
"..context",
|
||||
"..hash",
|
||||
"..lambda",
|
||||
"..generated..",
|
||||
"..persistence..",
|
||||
"..system..",
|
||||
@ -64,6 +65,7 @@ public class ArchitectureTest {
|
||||
"..hs.booking.item.validators",
|
||||
"..hs.hosting.asset",
|
||||
"..hs.hosting.asset.validators",
|
||||
"..hs.hosting.asset.factories",
|
||||
"..errors",
|
||||
"..mapper",
|
||||
"..ping",
|
||||
|
@ -5,12 +5,15 @@ 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.AfterEach;
|
||||
import org.junit.jupiter.api.ClassOrderer;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@ -31,6 +34,8 @@ 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.MANAGED_SERVER;
|
||||
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;
|
||||
@ -58,6 +63,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
@Autowired
|
||||
HsHostingAssetRealRepository realHostingAssetRepo;
|
||||
|
||||
@Autowired
|
||||
BookingItemCreatedEventRepository bookingItemCreationEventRepo;
|
||||
|
||||
@Autowired
|
||||
JpaAttempt jpaAttempt;
|
||||
|
||||
@ -70,11 +78,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 +142,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()
|
||||
@ -186,37 +186,121 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
}
|
||||
|
||||
@Test
|
||||
void projectAgent_canAddBookingItemWithHostingAsset() {
|
||||
void projectAgent_canAddManagedWebspaceBookingItemWithHostingAsset() {
|
||||
|
||||
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();
|
||||
|
||||
Dns.fakeResultForDomain("example.org",
|
||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code"));
|
||||
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
|
||||
final var givenManagedServer = realHostingAssetRepo.findByTypeAndIdentifier(MANAGED_SERVER, "vm1011").stream()
|
||||
.map(HsHostingAsset::getBookingItem)
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
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"
|
||||
{
|
||||
"projectUuid": "{projectUuid}",
|
||||
"parentItemUuid": "{managedServerUuid}",
|
||||
"type": "MANAGED_WEBSPACE",
|
||||
"caption": "some managed webspace",
|
||||
"resources": {
|
||||
"SSD": 25,
|
||||
"Traffic": 250
|
||||
},
|
||||
"hostingAsset": {
|
||||
"type": "MANAGED_WEBSPACE",
|
||||
"identifier": "fir00"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
"""
|
||||
.replace("{projectUuid}", givenProject.getUuid().toString())
|
||||
.replace("{managedServerUuid}", givenManagedServer.getUuid().toString())
|
||||
)
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/booking/items")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(201)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("", lenientlyEquals("""
|
||||
{
|
||||
"type": "MANAGED_WEBSPACE",
|
||||
"caption": "some managed webspace",
|
||||
"validFrom": "{today}",
|
||||
"validTo": null
|
||||
}
|
||||
"""
|
||||
.replace("{today}", LocalDate.now().toString())
|
||||
.replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString()))
|
||||
)
|
||||
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/booking/items/[^/]*"))
|
||||
.extract().header("Location"); // @formatter:on
|
||||
|
||||
// then, the new BookingItem can be accessed under the generated UUID
|
||||
final var newBookingItem = fetchRealBookingItemFromURI(location);
|
||||
assertThat(newBookingItem)
|
||||
.extracting(HsBookingItem::getCaption)
|
||||
.isEqualTo("some managed webspace");
|
||||
|
||||
// and the related HostingAssets are also got created
|
||||
final var domainSetupHostingAsset = realHostingAssetRepo.findByIdentifier("fir00");
|
||||
assertThat(domainSetupHostingAsset).isNotEmpty()
|
||||
.map(HsHostingAsset::getBookingItem)
|
||||
.contains(newBookingItem);
|
||||
final var event = bookingItemCreationEventRepo.findByBookingItem(newBookingItem);
|
||||
assertThat(event).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void projectAgent_canAddDomainSetupBookingItemWithHostingAsset() {
|
||||
|
||||
context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT");
|
||||
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
|
||||
// TODO.impl: "sec01-web" should not work, but does
|
||||
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": "Domain-Setup for example.org",
|
||||
"resources": {
|
||||
"domainName": "example.org",
|
||||
"verificationCode": "just-a-fake-verification-code"
|
||||
},
|
||||
"hostingAsset": {
|
||||
"identifier": "example.org", // also as default for all subAssets
|
||||
"subHostingAssets": [
|
||||
{
|
||||
"type": "DOMAIN_DNS_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_HTTP_SETUP",
|
||||
"assignedToAssetUuid": "{unixUserUuid}"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_MBOX_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_SMTP_SETUP"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
.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 +309,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 +323,34 @@ 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);
|
||||
// TODO.legacy: add check for example.org|DNS
|
||||
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 event = bookingItemCreationEventRepo.findByBookingItem(newBookingItem);
|
||||
assertThat(event).isNull();
|
||||
}
|
||||
|
||||
@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 +365,30 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
"caption": "some new domain-setup booking",
|
||||
"resources": {
|
||||
"domainName": "example.org",
|
||||
"targetUnixUser": "fir01-web",
|
||||
"verificationCode": "just-a-fake-verification-code"
|
||||
},
|
||||
"hostingAsset": {
|
||||
"identifier": "example.org", // also as default for all subAssets
|
||||
"subHostingAssets": [
|
||||
{
|
||||
"type": "DOMAIN_DNS_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_HTTP_SETUP",
|
||||
"assignedToAssetUuid": "{unixUserUuid}"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_MBOX_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_SMTP_SETUP"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
.replace("{projectUuid}", givenProject.getUuid().toString())
|
||||
.replace("{unixUserUuid}", givenUnixUser.getUuid().toString())
|
||||
)
|
||||
.port(port)
|
||||
.when()
|
||||
@ -291,7 +402,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 +416,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 event = bookingItemCreationEventRepo.findByBookingItem(newBookingItem);
|
||||
assertThat(event.getStatusMessage())
|
||||
.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 +425,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)
|
||||
@ -534,6 +653,13 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanupEventEntities() {
|
||||
jpaAttempt.transacted(() -> {
|
||||
em.createQuery("delete from BookingItemCreatedEventEntity").executeUpdate();
|
||||
}).assertSuccessful();
|
||||
}
|
||||
|
||||
private Map.Entry<String, Object> resource(final String key, final Object value) {
|
||||
return entry(key, value);
|
||||
}
|
||||
|
@ -36,8 +36,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.org"),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
entry("domainName", "example.org")
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -59,7 +58,6 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.org"),
|
||||
entry("targetUnixUser", "xyz00"),
|
||||
entry("verificationCode", "1234-5678-9100")
|
||||
))
|
||||
.build();
|
||||
@ -80,8 +78,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253)),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253))
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -99,8 +96,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254)),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254))
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -118,8 +114,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.com"),
|
||||
entry("targetUnixUser", "xyz00-test")
|
||||
entry("domainName", "example.com")
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -130,25 +125,6 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidUnixUser() {
|
||||
final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.com"),
|
||||
entry("targetUnixUser", "xyz00test")
|
||||
))
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).contains("'D-12345:Test-Project:Test-Domain.resources.targetUnixUser' = 'xyz00test' is not a valid unix-user name");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"de", "com", "net", "org", "actually-any-top-level-domain",
|
||||
@ -196,8 +172,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", secondLevelRegistrarDomain),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
entry("domainName", secondLevelRegistrarDomain)
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -219,7 +194,6 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
// then
|
||||
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
|
||||
"{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}], matchesRegExDescription=is not a (non-top-level) fully qualified domain name, notMatchesRegEx=[[^.]+, (co|org|gov|ac|sch)\\.uk, (com|net|org|edu|gov|asn|id)\\.au, (co|ne|or|ac|go)\\.jp, (com|net|org|gov|edu|ac)\\.cn, (com|net|org|gov|edu|mil|art)\\.br, (co|net|org|gen|firm|ind)\\.in, (com|net|org|gob|edu)\\.mx, (gov|edu)\\.it, (co|net|org|govt|ac|school|geek|kiwi)\\.nz, (co|ne|or|go|re|pe)\\.kr], notMatchesRegExDescription=is a forbidden registrar-level domain name, maxLength=253, required=true, writeOnce=true}",
|
||||
"{type=string, propertyName=targetUnixUser, matchesRegEx=[^[a-z][a-z0-9]{2}[0-9]{2}$|^[a-z][a-z0-9]{2}[0-9]{2}-[a-z0-9\\._-]+$], matchesRegExDescription=is not a valid unix-user name, maxLength=253, required=true, writeOnce=true}",
|
||||
"{type=string, propertyName=verificationCode, minLength=12, maxLength=64, computed=IN_INIT}");
|
||||
}
|
||||
}
|
||||
|
@ -157,6 +157,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
)
|
||||
);
|
||||
final var givenParentAsset = givenParentAsset(MANAGED_SERVER, "vm1011");
|
||||
final var expectedUnixUserId = nextUnixUserId();
|
||||
|
||||
final var location = RestAssured // @formatter:off
|
||||
.given()
|
||||
@ -184,10 +185,12 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
"identifier": "fir10",
|
||||
"caption": "some separate ManagedWebspace HA",
|
||||
"config": {
|
||||
"groupid": 1000000
|
||||
"groupid": {lastUnixUserId}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
"""
|
||||
.replace("{lastUnixUserId}", expectedUnixUserId.toString())
|
||||
))
|
||||
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
|
||||
.extract().header("Location"); // @formatter:on
|
||||
|
||||
@ -205,9 +208,11 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
.isEqualTo("""
|
||||
HsHostingAsset(UNIX_USER, fir10, fir10 webspace user, MANAGED_WEBSPACE:fir10, {
|
||||
"password" : null,
|
||||
"userid" : 1000000
|
||||
"userid" : {lastUnixUserId}
|
||||
})
|
||||
""".trim());
|
||||
"""
|
||||
.replace("{lastUnixUserId}", expectedUnixUserId.toString())
|
||||
.trim());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -777,4 +782,11 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
}).returnedValue();
|
||||
}
|
||||
|
||||
|
||||
private Integer nextUnixUserId() {
|
||||
final Object result = em.createNativeQuery("SELECT nextval('hs_hosting.asset_unixuser_system_id_seq')", Integer.class)
|
||||
.getSingleResult();
|
||||
return (Integer) result + 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,259 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEventEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapperFake;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
|
||||
import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// Tests the DomainSetupHostingAssetFactory through a HsBookingItemCreatedListener instance.
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DomainSetupHostingAssetFactoryUnitTest {
|
||||
|
||||
private final HsHostingAssetRealEntity managedWebspaceHostingAsset = HsHostingAssetRealEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type(MANAGED_WEBSPACE)
|
||||
.identifier("one00")
|
||||
.build();
|
||||
|
||||
private final HsHostingAssetRealEntity unixUserHostingAsset = HsHostingAssetRealEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type(UNIX_USER)
|
||||
.identifier("one00-web")
|
||||
.parentAsset(managedWebspaceHostingAsset)
|
||||
.build();
|
||||
|
||||
private final HsHostingAssetRealEntity anotherManagedWebspaceHostingAsset = HsHostingAssetRealEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type(MANAGED_WEBSPACE)
|
||||
.identifier("two00")
|
||||
.build();
|
||||
|
||||
private EntityManagerWrapperFake emwFake = new EntityManagerWrapperFake();
|
||||
|
||||
@Spy
|
||||
private EntityManagerWrapper emw = emwFake;
|
||||
|
||||
@Spy
|
||||
private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build();
|
||||
|
||||
@Spy
|
||||
private StandardMapper standardMapper = new StandardMapper(emw);
|
||||
|
||||
@InjectMocks
|
||||
private HsBookingItemCreatedListener listener;
|
||||
|
||||
@BeforeEach
|
||||
void initMocks() {
|
||||
emwFake.persist(managedWebspaceHostingAsset);
|
||||
emwFake.persist(unixUserHostingAsset);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotPersistEventEntityWithoutValidationErrors() {
|
||||
// given
|
||||
final var givenBookingItem = createBookingItemFromResources(
|
||||
entry("domainName", "example.org"),
|
||||
entry("verificationCode", "just-a-fake-verification-code")
|
||||
);
|
||||
final var givenAssetJson = """
|
||||
{
|
||||
"identifier": "example.org", // also as default for all subAssets
|
||||
"subHostingAssets": [
|
||||
{
|
||||
"type": "DOMAIN_HTTP_SETUP",
|
||||
"assignedToAssetUuid": "{unixUserHostingAssetUuid}"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_DNS_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_MBOX_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_SMTP_SETUP"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
.replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString());
|
||||
Dns.fakeResultForDomain("example.org",
|
||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code"));
|
||||
|
||||
// when
|
||||
listener.onApplicationEvent(
|
||||
new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson)
|
||||
);
|
||||
|
||||
// then
|
||||
assertThat(emwFake.stream(BookingItemCreatedEventEntity.class))
|
||||
.as("the event should not have been persisted, but got persisted")
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistsEventEntityIfDomainSetupVerificationFails() {
|
||||
// given
|
||||
final var givenBookingItem = createBookingItemFromResources(
|
||||
entry("domainName", "example.org")
|
||||
);
|
||||
final var givenAssetJson = """
|
||||
{
|
||||
"identifier": "example.org", // also as default for all subAssets
|
||||
"subHostingAssets": [
|
||||
{
|
||||
"type": "DOMAIN_HTTP_SETUP",
|
||||
"assignedToAssetUuid": "{unixUserHostingAssetUuid}"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_DNS_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_MBOX_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_SMTP_SETUP"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
.replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString());
|
||||
Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode
|
||||
|
||||
// when
|
||||
listener.onApplicationEvent(
|
||||
new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson)
|
||||
);
|
||||
|
||||
// then
|
||||
assertEventStatus(givenBookingItem, givenAssetJson,
|
||||
"[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=null' found for domain name 'example.org' (nor in its super-domain)]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistsEventEntityIfDomainDnsSetupIsSupplied() {
|
||||
// given
|
||||
final var givenBookingItem = createBookingItemFromResources(
|
||||
entry("domainName", "example.org"),
|
||||
entry("verificationCode", "just-a-fake-verification-code")
|
||||
);
|
||||
final var givenAssetJson = """
|
||||
{
|
||||
"identifier": "example.org", // also as default for all subAssets
|
||||
"subHostingAssets": [
|
||||
{
|
||||
"type": "DOMAIN_HTTP_SETUP",
|
||||
"assignedToAssetUuid": "{unixUserHostingAssetUuid}"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_DNS_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_MBOX_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_SMTP_SETUP"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
.replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString())
|
||||
.replace("{managedWebspaceHostingAssetUuid}", managedWebspaceHostingAsset.getUuid().toString());
|
||||
Dns.fakeResultForDomain("example.org",
|
||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code"));
|
||||
|
||||
// when
|
||||
listener.onApplicationEvent(
|
||||
new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson)
|
||||
);
|
||||
|
||||
// then
|
||||
assertEventStatus(givenBookingItem, givenAssetJson,
|
||||
"domain DNS setup not allowed for legacy compatibility");
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistsEventEntityIfSuppliedDomainUnixUserAndSmtpSetupWebspaceDontMatch() {
|
||||
// given
|
||||
final var givenBookingItem = createBookingItemFromResources(
|
||||
entry("domainName", "example.org"),
|
||||
entry("verificationCode", "just-a-fake-verification-code")
|
||||
);
|
||||
final var givenAssetJson = """
|
||||
{
|
||||
"identifier": "example.org", // also as default for all subAssets
|
||||
"subHostingAssets": [
|
||||
{
|
||||
"type": "DOMAIN_HTTP_SETUP",
|
||||
"assignedToAssetUuid": "{unixUserHostingAssetUuid}"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_DNS_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_MBOX_SETUP"
|
||||
},
|
||||
{
|
||||
"type": "DOMAIN_SMTP_SETUP"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
.replace("{unixUserHostingAssetUuid}", unixUserHostingAsset.getUuid().toString())
|
||||
.replace("{managedWebspaceHostingAssetUuid}", anotherManagedWebspaceHostingAsset.getUuid().toString());
|
||||
Dns.fakeResultForDomain("example.org",
|
||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code"));
|
||||
|
||||
// when
|
||||
listener.onApplicationEvent(
|
||||
new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson)
|
||||
);
|
||||
|
||||
// then
|
||||
assertEventStatus(givenBookingItem, givenAssetJson,
|
||||
"domain DNS setup not allowed for legacy compatibility");
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static HsBookingItemRealEntity createBookingItemFromResources(final Map.Entry<String, String>... givenResources) {
|
||||
return HsBookingItemRealEntity.builder()
|
||||
.type(HsBookingItemType.DOMAIN_SETUP)
|
||||
.resources(Map.ofEntries(givenResources))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void assertEventStatus(
|
||||
final HsBookingItemRealEntity givenBookingItem,
|
||||
final String givenAssetJson,
|
||||
final String expectedErrorMessage) {
|
||||
emwFake.stream(BookingItemCreatedEventEntity.class)
|
||||
.reduce(Reducer::toSingleElement)
|
||||
.map(eventEntity -> {
|
||||
assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem);
|
||||
assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson);
|
||||
assertThat(eventEntity.getStatusMessage()).isEqualTo(expectedErrorMessage);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEventEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapperFake;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.DOMAIN_SETUP;
|
||||
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HsBookingItemCreatedListenerUnitTest {
|
||||
|
||||
final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder()
|
||||
.debitorNumber(12345)
|
||||
.defaultPrefix("xyz")
|
||||
.build();
|
||||
|
||||
private EntityManagerWrapperFake emwFake = new EntityManagerWrapperFake();
|
||||
|
||||
@Spy
|
||||
private EntityManagerWrapper emw = emwFake;
|
||||
|
||||
@Spy
|
||||
private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build();
|
||||
|
||||
@Spy
|
||||
private StandardMapper standardMapper = new StandardMapper(emw);
|
||||
|
||||
@InjectMocks
|
||||
private HsBookingItemCreatedListener listener;
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("bookingItemTypesWithoutAutomaticAssetCreation")
|
||||
void persistsEventEntityIfBookingItemTypeDoesNotSupportAutomaticHostingAssetCreation(final HsBookingItemType bookingItemType) {
|
||||
// given
|
||||
final var givenBookingItem = createBookingItemFromResources(bookingItemType);
|
||||
final var givenAssetJson = """
|
||||
{
|
||||
// anything should be rejected
|
||||
}
|
||||
""";
|
||||
|
||||
// when
|
||||
listener.onApplicationEvent(
|
||||
new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson)
|
||||
);
|
||||
|
||||
// then
|
||||
assertEventStatus(givenBookingItem, givenAssetJson,
|
||||
"waiting for manual setup of hosting asset for booking item of type " + bookingItemType);
|
||||
}
|
||||
|
||||
static List<HsBookingItemType> bookingItemTypesWithoutAutomaticAssetCreation() {
|
||||
return Arrays.stream(HsBookingItemType.values())
|
||||
.filter(v -> v != MANAGED_WEBSPACE && v != DOMAIN_SETUP)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static HsBookingItemRealEntity createBookingItemFromResources(
|
||||
final HsBookingItemType bookingItemType
|
||||
) {
|
||||
return HsBookingItemRealEntity.builder()
|
||||
.type(bookingItemType)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void assertEventStatus(
|
||||
final HsBookingItemRealEntity givenBookingItem,
|
||||
final String givenAssetJson,
|
||||
final String expectedErrorMessage) {
|
||||
emwFake.stream(BookingItemCreatedEventEntity.class)
|
||||
.reduce(Reducer::toSingleElement)
|
||||
.map(eventEntity -> {
|
||||
assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem);
|
||||
assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson);
|
||||
assertThat(eventEntity.getStatusMessage()).isEqualTo(expectedErrorMessage);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEventEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapperFake;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// Tests the DomainSetupHostingAssetFactory through a HsBookingItemCreatedListener instance.
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ManagedWebspaceHostingAssetFactoryUnitTest {
|
||||
|
||||
final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder()
|
||||
.debitorNumber(12345)
|
||||
.defaultPrefix("xyz")
|
||||
.build();
|
||||
final HsBookingProjectRealEntity project = HsBookingProjectRealEntity.builder()
|
||||
.debitor(debitor)
|
||||
.caption("Test-Project")
|
||||
.build();
|
||||
final HsOfficeContact alarmContact = HsOfficeContactRealEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.caption("Alarm Contact xyz")
|
||||
.build();
|
||||
|
||||
private EntityManagerWrapperFake emwFake = new EntityManagerWrapperFake();
|
||||
|
||||
@Spy
|
||||
private EntityManagerWrapper emw = emwFake;
|
||||
|
||||
@Spy
|
||||
private ObjectMapper jsonMapper = new JsonObjectMapperConfiguration().customObjectMapper().build();
|
||||
|
||||
@Spy
|
||||
private StandardMapper standardMapper = new StandardMapper(emw);
|
||||
|
||||
@InjectMocks
|
||||
private HsBookingItemCreatedListener listener;
|
||||
|
||||
@BeforeEach
|
||||
void initMocks() {
|
||||
emwFake.persist(alarmContact);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotPersistAnyEntityWithoutHostingAssetWithoutValidationErrors() {
|
||||
// given
|
||||
final var givenBookingItem = HsBookingItemRealEntity.builder()
|
||||
.type(HsBookingItemType.MANAGED_WEBSPACE)
|
||||
.project(project)
|
||||
.caption("Test Managed-Webspace")
|
||||
.resources(Map.ofEntries(
|
||||
Map.entry("RAM", 25),
|
||||
Map.entry("Traffic", 250)
|
||||
))
|
||||
.build();
|
||||
|
||||
// when
|
||||
listener.onApplicationEvent(
|
||||
new BookingItemCreatedAppEvent(this, givenBookingItem, null)
|
||||
);
|
||||
|
||||
// then
|
||||
assertThat(emwFake.stream(BookingItemCreatedEventEntity.class).findAny().isEmpty())
|
||||
.as("the event should not have been persisted, but got persisted").isTrue();
|
||||
assertThat(emwFake.stream(HsHostingAssetRealEntity.class).findAny().isEmpty())
|
||||
.as("the hosting asset should not have been persisted, but got persisted").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistsEventEntityIfDomainSetupVerificationFails() {
|
||||
// given
|
||||
final var givenBookingItem = createBookingItemFromResources(
|
||||
entry("domainName", "example.org")
|
||||
);
|
||||
final var givenAssetJson = """
|
||||
{
|
||||
"identifier": "xyz00"
|
||||
}
|
||||
""";
|
||||
Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode
|
||||
|
||||
// when
|
||||
listener.onApplicationEvent(
|
||||
new BookingItemCreatedAppEvent(this, givenBookingItem, givenAssetJson)
|
||||
);
|
||||
|
||||
// then
|
||||
assertEventStatus(givenBookingItem, givenAssetJson,
|
||||
"requires MANAGED_WEBSPACE hosting asset, but got null");
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static HsBookingItemRealEntity createBookingItemFromResources(final Map.Entry<String, String>... givenResources) {
|
||||
return HsBookingItemRealEntity.builder()
|
||||
.type(HsBookingItemType.MANAGED_WEBSPACE)
|
||||
.resources(Map.ofEntries(givenResources))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void assertEventStatus(
|
||||
final HsBookingItemRealEntity givenBookingItem,
|
||||
final String givenAssetJson,
|
||||
final String expectedErrorMessage) {
|
||||
emwFake.stream(BookingItemCreatedEventEntity.class)
|
||||
.reduce(Reducer::toSingleElement)
|
||||
.map(eventEntity -> {
|
||||
assertThat(eventEntity.getBookingItem()).isSameAs(givenBookingItem);
|
||||
assertThat(eventEntity.getAssetJson()).isEqualTo(givenAssetJson);
|
||||
assertThat(eventEntity.getStatusMessage()).isEqualTo(expectedErrorMessage);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
@ -42,8 +42,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
.project(project)
|
||||
.type(HsBookingItemType.DOMAIN_SETUP)
|
||||
.resources(new HashMap<>(ofEntries(
|
||||
entry("domainName", domainName),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
entry("domainName", domainName)
|
||||
))));
|
||||
HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem);
|
||||
return HsHostingAssetRbacEntity.builder()
|
||||
|
@ -6,7 +6,7 @@ import com.opencsv.CSVReaderBuilder;
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
@ -1680,18 +1680,13 @@ public class ImportHostingAssets extends BaseOfficeDataImport {
|
||||
final var relatedProject = domainSetup.getSubHostingAssets().stream()
|
||||
.map(ha -> ha.getAssignedToAsset() != null ? ha.getAssignedToAsset().getRelatedProject() : null)
|
||||
.findAny().orElseThrow();
|
||||
final var targetUnixUser = domainSetup.getSubHostingAssets().stream()
|
||||
.filter(subAsset -> subAsset.getType() == DOMAIN_HTTP_SETUP)
|
||||
.map(domainHttpSetup -> domainHttpSetup.getAssignedToAsset().getIdentifier())
|
||||
.findAny().orElse(null);
|
||||
final var bookingItem = HsBookingItemRealEntity.builder()
|
||||
.type(HsBookingItemType.DOMAIN_SETUP)
|
||||
.caption("BI " + domainSetup.getIdentifier())
|
||||
.project((HsBookingProjectRealEntity) relatedProject)
|
||||
//.validity(toPostgresDateRange(created, cancelled))
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", domainSetup.getIdentifier()),
|
||||
entry("targetUnixUser", targetUnixUser)
|
||||
entry("domainName", domainSetup.getIdentifier())
|
||||
))
|
||||
.build();
|
||||
domainSetup.setBookingItem(bookingItem);
|
||||
|
@ -4,12 +4,11 @@ import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
@ -18,15 +17,10 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.SynchronizationType;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@ -47,19 +41,8 @@ public class HsOfficeMembershipControllerRestTest {
|
||||
@MockBean
|
||||
HsOfficeMembershipRepository membershipRepo;
|
||||
|
||||
@Mock
|
||||
EntityManager em;
|
||||
|
||||
@MockBean
|
||||
EntityManagerFactory emf;
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
when(emf.createEntityManager()).thenReturn(em);
|
||||
when(emf.createEntityManager(any(Map.class))).thenReturn(em);
|
||||
when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em);
|
||||
when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em);
|
||||
}
|
||||
EntityManagerWrapper em;
|
||||
|
||||
@Nested
|
||||
class AddMembership {
|
||||
|
@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipStatusResource;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
@ -12,7 +13,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
@ -38,9 +38,9 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase<
|
||||
private static final Boolean PATCHED_MEMBERSHIP_FEE_BILLABLE = false;
|
||||
|
||||
@Mock
|
||||
private EntityManager em;
|
||||
private EntityManagerWrapper em;
|
||||
|
||||
private StandardMapper mapper = new StandardMapper();
|
||||
private StandardMapper mapper = new StandardMapper(em);
|
||||
|
||||
@BeforeEach
|
||||
void initMocks() {
|
||||
|
@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -18,7 +19,6 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.persistence.SynchronizationType;
|
||||
@ -57,7 +57,7 @@ class HsOfficePartnerControllerRestTest {
|
||||
HsOfficeRelationRealRepository relationRepo;
|
||||
|
||||
@MockBean
|
||||
EntityManager em;
|
||||
EntityManagerWrapper em;
|
||||
|
||||
@MockBean
|
||||
EntityManagerFactory emf;
|
||||
|
@ -0,0 +1,94 @@
|
||||
package net.hostsharing.hsadminng.persistence;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import jakarta.persistence.Id;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
||||
public class EntityManagerWrapperFake extends EntityManagerWrapper {
|
||||
|
||||
private Map<Class<?>, Map<Object, Object>> entityClasses = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public boolean contains(final Object entity) {
|
||||
final var id = getEntityId(entity);
|
||||
return find(entity.getClass(), id) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getReference(final Class<T> entityClass, final Object primaryKey) {
|
||||
return find(entityClass, primaryKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T find(final Class<T> entityClass, final Object primaryKey) {
|
||||
if (entityClasses.containsKey(entityClass)) {
|
||||
final var entities = entityClasses.get(entityClass);
|
||||
//noinspection unchecked
|
||||
return entities.keySet().stream()
|
||||
.filter(key -> key.equals(primaryKey))
|
||||
.map(key -> (T) entities.get(key))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persist(final Object entity) {
|
||||
if (!entityClasses.containsKey(entity.getClass())) {
|
||||
entityClasses.put(entity.getClass(), new HashMap<>());
|
||||
}
|
||||
final var id = getEntityId(entity).orElseGet(() -> setEntityId(entity, UUID.randomUUID()));
|
||||
entityClasses.get(entity.getClass()).put(id, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
}
|
||||
|
||||
public Stream<Object> stream() {
|
||||
return entityClasses.values().stream().flatMap(entitiesPerClass -> entitiesPerClass.values().stream());
|
||||
}
|
||||
|
||||
public <T> Stream<T> stream(final Class<T> entityClass) {
|
||||
if (entityClasses.containsKey(entityClass)) {
|
||||
//noinspection unchecked
|
||||
return (Stream<T>) entityClasses.get(entityClass).values().stream();
|
||||
}
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static Optional<Object> getEntityId(final Object entity) {
|
||||
for (Class<?> currentClass = entity.getClass(); currentClass != null; currentClass = currentClass.getSuperclass()) {
|
||||
for (Field field : currentClass.getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(Id.class)) {
|
||||
field.setAccessible(true);
|
||||
return Optional.ofNullable(field.get(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName());
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static Object setEntityId(final Object entity, final Object id) {
|
||||
for (Class<?> currentClass = entity.getClass(); currentClass != null; currentClass = currentClass.getSuperclass()) {
|
||||
for (Field field : currentClass.getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(Id.class)) {
|
||||
field.setAccessible(true);
|
||||
field.set(entity, id);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No @Id field found in entity class: " + entity.getClass().getName());
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package net.hostsharing.hsadminng.persistence;
|
||||
|
||||
import net.hostsharing.hsadminng.mapper.Array;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
@ -53,9 +52,9 @@ class EntityManagerWrapperUnitTest {
|
||||
if (type == double.class) return 0.0;
|
||||
if (type == char.class) return '\0';
|
||||
if (type == String.class) return "dummy";
|
||||
if (type == String[].class) return Array.of("dummy");
|
||||
if (type == String[].class) return new String[]{"dummy"};
|
||||
if (type == Class.class) return String.class;
|
||||
if (type == Class[].class) return Array.of(String.class);
|
||||
if (type == Class[].class) return new Class[0];
|
||||
return mock(type);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.context;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.Array;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -17,7 +18,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DataJpaTest
|
||||
@ComponentScan(basePackageClasses = { Context.class, JpaAttempt.class, StandardMapper.class })
|
||||
@ComponentScan(basePackageClasses = { Context.class, JpaAttempt.class, EntityManagerWrapper.class, StandardMapper.class })
|
||||
@DirtiesContext
|
||||
class ContextIntegrationTests {
|
||||
|
||||
|
@ -2,10 +2,10 @@ package net.hostsharing.hsadminng.rbac.role;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
@ -15,7 +15,6 @@ import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.SynchronizationType;
|
||||
import java.util.Map;
|
||||
@ -43,8 +42,8 @@ class RbacRoleControllerRestTest {
|
||||
@MockBean
|
||||
RbacRoleRepository rbacRoleRepository;
|
||||
|
||||
@Mock
|
||||
EntityManager em;
|
||||
@MockBean
|
||||
EntityManagerWrapper em;
|
||||
|
||||
@MockBean
|
||||
EntityManagerFactory emf;
|
||||
|
@ -2,10 +2,9 @@ package net.hostsharing.hsadminng.rbac.subject;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
@ -15,18 +14,12 @@ import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.SynchronizationType;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@ -44,19 +37,9 @@ class RbacSubjectControllerRestTest {
|
||||
@MockBean
|
||||
RbacSubjectRepository rbacSubjectRepository;
|
||||
|
||||
@Mock
|
||||
EntityManager em;
|
||||
|
||||
@MockBean
|
||||
EntityManagerFactory emf;
|
||||
EntityManagerWrapper em;
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
when(emf.createEntityManager()).thenReturn(em);
|
||||
when(emf.createEntityManager(any(Map.class))).thenReturn(em);
|
||||
when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em);
|
||||
when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSubjectUsesGivenUuid() throws Exception {
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.hostsharing.hsadminng.rbac.test;
|
||||
|
||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.rbac.grant.RbacGrantEntity;
|
||||
import net.hostsharing.hsadminng.rbac.grant.RbacGrantRepository;
|
||||
import net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService;
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.hostsharing.hsadminng.rbac.test;
|
||||
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -3,13 +3,13 @@ package net.hostsharing.hsadminng.rbac.test;
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.util.List;
|
||||
@ -24,7 +24,7 @@ import static org.mockito.Mockito.when;
|
||||
class MapperUnitTest {
|
||||
|
||||
@Mock
|
||||
EntityManager em;
|
||||
EntityManagerWrapper em;
|
||||
|
||||
@InjectMocks
|
||||
StandardMapper mapper;
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.hostsharing.hsadminng.rbac.test;
|
||||
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import org.junit.jupiter.api.Named;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
Loading…
Reference in New Issue
Block a user