Abstract RepositoryManagement test (#2631)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-08-28 12:56:16 +03:00
parent 42b0bc06a9
commit 4f0a8893c7
52 changed files with 1901 additions and 3042 deletions

View File

@@ -239,11 +239,10 @@ public interface ControllerManagement {
*
* @param actionId to the handle status for
* @param message for the status
* @return the update action in case the status has been changed to {@link Status#RETRIEVED}
* @throws EntityNotFoundException if action with given ID does not exist
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
Action registerRetrieved(long actionId, String message);
void registerRetrieved(long actionId, String message);
/**
* Updates attributes of the controller according to the given {@link UpdateMode}.
@@ -260,8 +259,7 @@ public interface ControllerManagement {
Target updateControllerAttributes(@NotEmpty String controllerId, @NotNull Map<String, String> attributes, UpdateMode mode);
/**
* Finds {@link Target} based on given controller ID returns found Target without details, i.e.
* NO {@link Target#getTags()} and {@link Target#getActions()} possible.
* Finds {@link Target} based on given controller ID returns found Target without details
*
* @param controllerId to look for.
* @return {@link Target} or {@code null} if it does not exist
@@ -271,9 +269,7 @@ public interface ControllerManagement {
Optional<Target> findByControllerId(@NotEmpty String controllerId);
/**
* Finds {@link Target} based on given ID returns found Target without details, i.e.
* NO {@link Target#getTags()} and {@link Target#getActions()}
* possible.
* Finds {@link Target} based on given ID returns found Target without details
*
* @param targetId to look for.
* @return {@link Target} or {@code null} if it does not exist

View File

@@ -242,16 +242,6 @@ public interface DeploymentManagement extends PermissionSupport {
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<ActionStatus> findActionStatusByAction(long actionId, @NotNull Pageable pageable);
/**
* Counts all the {@link ActionStatus} entries of the given {@link Action}.
*
* @param actionId to be filtered on
* @return count of {@link ActionStatus} entries
* @throws EntityNotFoundException if action with given ID does not exist
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
long countActionStatusByAction(long actionId);
/**
* Retrieves all messages for an {@link ActionStatus}.<p/>
* No entity based access control applied.

View File

@@ -13,7 +13,6 @@ import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.im.authentication.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidationCount;
import org.springframework.security.access.prepost.PreAuthorize;
/**

View File

@@ -9,9 +9,6 @@
*/
package org.eclipse.hawkbit.repository;
import java.util.Optional;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@@ -20,17 +17,9 @@ import lombok.Getter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.im.authentication.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.Tag;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetTag;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
/**
* Management service for {@link DistributionSetTag}s.

View File

@@ -88,11 +88,9 @@ public final class MaintenanceScheduleHelper {
if (allNotEmpty(cronSchedule, duration, timezone)) {
validateCronSchedule(cronSchedule);
validateDuration(duration);
// check if there is a window currently active or available in
// future.
if (!getNextMaintenanceWindow(cronSchedule, duration, timezone).isPresent()) {
throw new InvalidMaintenanceScheduleException(
"No valid maintenance window available after current time");
// check if there is a window currently active or available in the future.
if (getNextMaintenanceWindow(cronSchedule, duration, timezone).isEmpty()) {
throw new InvalidMaintenanceScheduleException("No valid maintenance window available after current time");
}
} else if (atLeastOneNotEmpty(cronSchedule, duration, timezone)) {
throw new InvalidMaintenanceScheduleException("All of schedule, duration and timezone should either be null or non empty.");

View File

@@ -18,11 +18,9 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
/**
* An implementation of the {@link PageRequest} which is offset based by means
* the offset is given and not the page number as in the original
* {@link PageRequest} implementation where the offset is generated. Due that
* the REST-API is working with {@code offset} and {@code limit} parameter we
* need an offset based page request.
* An implementation of the {@link PageRequest} which is offset based by means the offset is given and not the page number as in the original
* {@link PageRequest} implementation where the offset is generated. Due to the REST-API is working with {@code offset} and {@code limit}
* parameter we need an offset based page request.
*/
@Getter
@EqualsAndHashCode(callSuper = true)

View File

@@ -20,8 +20,7 @@ import org.eclipse.hawkbit.repository.model.RolloutGroup;
public interface QuotaManagement {
/**
* @return maximum number of {@link ActionStatus} entries that the
* controller can report for an {@link Action}.
* @return maximum number of {@link ActionStatus} entries that the controller can report for an {@link Action}.
*/
int getMaxStatusEntriesPerAction();
@@ -31,15 +30,12 @@ public interface QuotaManagement {
int getMaxAttributeEntriesPerTarget();
/**
* @return maximum number of allowed {@link RolloutGroup}s per
* {@link Rollout}.
* @return maximum number of allowed {@link RolloutGroup}s per {@link Rollout}.
*/
int getMaxRolloutGroupsPerRollout();
/**
* @return maximum number of
* {@link ControllerManagement#getActionHistoryMessages(Long, int)}
* for an individual {@link ActionStatus}.
* @return maximum number of action (history) messages per actions status.
*/
int getMaxMessagesPerActionStatus();
@@ -49,7 +45,7 @@ public interface QuotaManagement {
int getMaxMetaDataEntriesPerSoftwareModule();
/**
* @return maximum number of meta data entries per distribution set
* @return maximum number of metadata entries per distribution set
*/
int getMaxMetaDataEntriesPerDistributionSet();
@@ -80,14 +76,12 @@ public interface QuotaManagement {
int getMaxTargetsPerRolloutGroup();
/**
* @return the maximum number of target distribution set assignments
* resulting from a manual assignment
* @return the maximum number of target distribution set assignments resulting from a manual assignment
*/
int getMaxTargetDistributionSetAssignmentsPerManualAssignment();
/**
* @return the maximum number of targets for an automatic distribution set
* assignment
* @return the maximum number of targets for an automatic distribution set assignment
*/
int getMaxTargetsPerAutoAssignment();
@@ -110,5 +104,4 @@ public interface QuotaManagement {
* @return the maximum number of distribution set types per target type
*/
int getMaxDistributionSetTypesPerTargetType();
}

View File

@@ -1,68 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Collection of regular expression characters to check strings
*/
public class RegexCharacterCollection {
private final EnumSet<RegexChar> characters;
private final Pattern findAnyCharacter;
public RegexCharacterCollection(final RegexChar... characters) {
this.characters = EnumSet.copyOf(Arrays.asList(characters));
this.findAnyCharacter = getPatternFindAnyCharacter();
}
public static boolean stringContainsCharacter(final String stringToCheck,
final RegexCharacterCollection regexCharacterCollection) {
return regexCharacterCollection.findAnyCharacter.matcher(stringToCheck).matches();
}
private Pattern getPatternFindAnyCharacter() {
final String regexCharacters = characters.stream().map(RegexChar::getRegExp)
.collect(Collectors.joining());
final String regularExpression = String.format(".*[%s]+.*", regexCharacters);
return Pattern.compile(regularExpression);
}
public enum RegexChar {
WHITESPACE("\\s", "character.whitespace"), DIGITS("0-9", "character.digits"), QUOTATION_MARKS("'\"",
"character.quotationMarks"), SLASHES("\\/\\\\", "character.slashes"), GREATER_THAN(
">"), LESS_THAN("<"), EQUALS_SYMBOL("="), EXCLAMATION_MARK("!"), QUESTION_MARK("?"), COLON(":");
private final String regExp;
private final String l18nReferenceDescription;
RegexChar(final String character) {
this(character, null);
}
RegexChar(final String regExp, final String l18nReferenceDescription) {
this.regExp = regExp;
this.l18nReferenceDescription = l18nReferenceDescription;
}
public String getRegExp() {
return regExp;
}
public Optional<String> getL18nReferenceDescription() {
return Optional.ofNullable(l18nReferenceDescription);
}
}
}

View File

@@ -12,7 +12,6 @@ package org.eclipse.hawkbit.repository;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
@@ -23,15 +22,12 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule;
public final class RepositoryConstants {
/**
* Prefix that the server puts in front of
* {@link ActionStatus#getMessages()} if the message is generated by the
* server.
* Prefix that the server puts in front of action status messages if the message is generated by the server.
*/
public static final String SERVER_MESSAGE_PREFIX = "Update Server: ";
/**
* Number of {@link DistributionSetType}s that are generated as part of
* default tenant setup.
* Number of {@link DistributionSetType}s that are generated as part of default tenant setup.
*/
public static final int DEFAULT_DS_TYPES_IN_TENANT = 3;
@@ -41,8 +37,7 @@ public final class RepositoryConstants {
public static final int MAX_ACTION_COUNT = 100;
/**
* Maximum number of messages that can be retrieved by a controller for an
* {@link Action}.
* Maximum number of messages that can be retrieved by a controller for an {@link Action}.
*/
public static final int MAX_ACTION_HISTORY_MSG_COUNT = 100;

View File

@@ -10,8 +10,9 @@
package org.eclipse.hawkbit.repository;
import org.eclipse.hawkbit.im.authentication.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.Target;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -22,32 +23,25 @@ import org.springframework.security.access.prepost.PreAuthorize;
public interface RolloutExecutor {
/**
* This execution should only be triggered by the system as a background job and
* not available via API.
*
* This execution should only be triggered by the system as a background job and not available via API.
* <p/>
* Process rollout based on its current {@link Rollout#getStatus()}.
*
* For {@link RolloutStatus#CREATING} that means creating the
* {@link RolloutGroup}s with {@link Target}s and when finished switch to
* {@link RolloutStatus#READY}.
*
* For {@link RolloutStatus#READY} that means switching to
* {@link RolloutStatus#STARTING} if the {@link Rollout#getStartAt()} is set and
* time of calling this method is beyond this point in time. This auto start
* mechanism is optional. Call {@link #start(Long)} otherwise.
*
* For {@link RolloutStatus#STARTING} that means starting the first
* {@link RolloutGroup}s in line and when finished switch to
* <p/>
* For {@link RolloutStatus#CREATING} that means creating the {@link org.eclipse.hawkbit.repository.model.RolloutGroup}s
* with {@link Target}s and when finished switch to {@link RolloutStatus#READY}.
* <p/>
* For {@link RolloutStatus#READY} that means switching to {@link RolloutStatus#STARTING} if the {@link Rollout#getStartAt()} is set and
* time of calling this method is beyond this point in time. This auto start mechanism is optional.
* Call {@link RolloutManagement#start(long)} otherwise.
* <p/>
* For {@link RolloutStatus#STARTING} that means starting the first {@link RolloutGroup}s in line and when finished switch to
* {@link RolloutStatus#RUNNING}.
*
* For {@link RolloutStatus#RUNNING} that means checking to activate further
* groups based on the defined thresholds. Switched to
* <p/>
* For {@link RolloutStatus#RUNNING} that means checking to activate further groups based on the defined thresholds. Switched to
* {@link RolloutStatus#FINISHED} is all groups are finished.
*
* For {@link RolloutStatus#DELETING} that means either soft delete in case
* rollout was already {@link RolloutStatus#RUNNING} which results in status
* change {@link RolloutStatus#DELETED} or hard delete from the persistence
* otherwise.
* <p/>
* For {@link RolloutStatus#DELETING} that means either soft delete in case rollout was already {@link RolloutStatus#RUNNING}
* which results in status change {@link RolloutStatus#DELETED} or hard delete from the persistence otherwise.
*/
@PreAuthorize(SpringEvalExpressions.IS_SYSTEM_CODE)
void execute(Rollout rollout);

View File

@@ -95,15 +95,6 @@ public interface RolloutGroupManagement extends PermissionSupport {
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<RolloutGroup> findByRollout(long rolloutId, @NotNull Pageable pageable);
/**
* Retrieves a page of {@link RolloutGroup}s filtered by a given {@link Rollout}.
*
* @param rolloutId the ID of the rollout to filter the {@link RolloutGroup}s
* @return a page of found {@link RolloutGroup}s
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
long countByRollout(long rolloutId);
/**
* Get targets of specified rollout group.
*

View File

@@ -24,27 +24,22 @@ public interface RolloutHandler {
/**
* Process rollout based on its current {@link Rollout#getStatus()}.
*
* For {@link Rollout.RolloutStatus#CREATING} that means creating the
* {@link RolloutGroup}s with {@link Target}s and when finished switch to
* <p/>
* For {@link Rollout.RolloutStatus#CREATING} that means creating the {@link RolloutGroup}s with {@link Target}s and when finished switch to
* {@link Rollout.RolloutStatus#READY}.
*
* For {@link Rollout.RolloutStatus#READY} that means switching to
* {@link Rollout.RolloutStatus#STARTING} if the {@link Rollout#getStartAt()} is
* set and time of calling this method is beyond this point in time. This auto
* <p/>
* For {@link Rollout.RolloutStatus#READY} that means switching to {@link Rollout.RolloutStatus#STARTING} if the
* {@link Rollout#getStartAt()} is set and time of calling this method is beyond this point in time. This auto
* start mechanism is optional. Call {@link RolloutManagement#start(long)} otherwise.
*
* For {@link Rollout.RolloutStatus#STARTING} that means starting the first
* {@link RolloutGroup}s in line and when finished switch to
* <p/>
* For {@link Rollout.RolloutStatus#STARTING} that means starting the first {@link RolloutGroup}s in line and when finished switch to
* {@link Rollout.RolloutStatus#RUNNING}.
*
* For {@link Rollout.RolloutStatus#RUNNING} that means checking to activate
* further groups based on the defined thresholds. Switched to
* <p/>
* For {@link Rollout.RolloutStatus#RUNNING} that means checking to activate further groups based on the defined thresholds. Switched to
* {@link Rollout.RolloutStatus#FINISHED} is all groups are finished.
*
* For {@link Rollout.RolloutStatus#DELETING} that means either soft delete in
* case rollout was already {@link Rollout.RolloutStatus#RUNNING} which results
* in status change {@link Rollout.RolloutStatus#DELETED} or hard delete from
* <p/>
* For {@link Rollout.RolloutStatus#DELETING} that means either soft delete in case rollout was already
* {@link Rollout.RolloutStatus#RUNNING} which results in status change {@link Rollout.RolloutStatus#DELETED} or hard delete from
* the persistence otherwise.
*/
@PreAuthorize(SpringEvalExpressions.IS_SYSTEM_CODE)

View File

@@ -86,25 +86,19 @@ public interface RolloutManagement extends PermissionSupport {
long countByDistributionSetIdAndRolloutIsStoppable(long setId);
/**
* Persists a new rollout entity. The filter within the
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets which
* are effected by this rollout to create. The amount of groups will be defined
* as equally sized.
*
* The rollout is not started. Only the preparation of the rollout is done,
* creating and persisting all the necessary groups. The Rollout and the groups
* are persisted in {@link RolloutStatus#CREATING} and
* {@link RolloutGroupStatus#CREATING}.
*
* The RolloutScheduler will start to assign targets to the groups. Once all
* targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with .
* Persists a new rollout entity. The filter within the {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets which
* are effected by this rollout to create. The amount of groups will be defined as equally sized.
* <p/>
* The rollout is not started. Only the preparation of the rollout is done, creating and persisting all the necessary groups. The Rollout
* and the groups are persisted in {@link RolloutStatus#CREATING} and {@link RolloutGroupStatus#CREATING}.
* <p/>
* The RolloutScheduler will start to assign targets to the groups. Once all targets have been assigned to the groups, the rollout status
* is changed to {@link RolloutStatus#READY} so it can be started with .
*
* @param create the rollout entity to create
* @param amountGroup the amount of groups to split the rollout into
* @param confirmationRequired if a confirmation is required by the device group(s) of the rollout
* @param conditions the rolloutgroup conditions and actions which should be applied
* for each {@link RolloutGroup}
* @param conditions the rolloutgroup conditions and actions which should be applied for each {@link RolloutGroup}
* @param dynamicRolloutGroupTemplate the template for dynamic rollout groups
* @return the persisted rollout.
* @throws EntityNotFoundException if given {@link DistributionSet} does not exist
@@ -118,61 +112,45 @@ public interface RolloutManagement extends PermissionSupport {
@NotNull RolloutGroupConditions conditions, DynamicRolloutGroupTemplate dynamicRolloutGroupTemplate);
/**
* Persists a new rollout entity. The filter within the
* {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets which
* are effected by this rollout to create. The amount of groups will be defined
* as equally sized.
*
* The rollout is not started. Only the preparation of the rollout is done,
* creating and persisting all the necessary groups. The Rollout and the groups
* are persisted in {@link RolloutStatus#CREATING} and
* {@link RolloutGroupStatus#CREATING}.
*
* The RolloutScheduler will start to assign targets to the groups. Once all
* targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with .
* Persists a new rollout entity. The filter within the {@link Rollout#getTargetFilterQuery()} is used to retrieve the targets which
* are effected by this rollout to create. The amount of groups will be defined as equally sized.
* <p/>
* The rollout is not started. Only the preparation of the rollout is done, creating and persisting all the necessary groups. The Rollout
* and the groups are persisted in {@link RolloutStatus#CREATING} and {@link RolloutGroupStatus#CREATING}.
* <p/>
* The RolloutScheduler will start to assign targets to the groups. Once all targets have been assigned to the groups, the rollout status is
* changed to {@link RolloutStatus#READY} so it can be started with .
*
* @param create the rollout entity to create
* @param amountGroup the amount of groups to split the rollout into
* @param confirmationRequired if a confirmation is required by the device group(s) of the rollout
* @param conditions the rolloutgroup conditions and actions which should be applied
* for each {@link RolloutGroup}
* @param conditions the rolloutgroup conditions and actions which should be applied for each {@link RolloutGroup}
* @return the persisted rollout.
* @throws EntityNotFoundException if given {@link DistributionSet} does not exist
* @throws ConstraintViolationException if rollout or group parameters are invalid.
* @throws AssignmentQuotaExceededException if the maximum number of allowed targets per rollout group is
* exceeded.
* @throws AssignmentQuotaExceededException if the maximum number of allowed targets per rollout group is exceeded.
*/
@PreAuthorize(SpringEvalExpressions.HAS_CREATE_REPOSITORY)
Rollout create(@NotNull @Valid Create create, int amountGroup, boolean confirmationRequired,
@NotNull RolloutGroupConditions conditions);
Rollout create(@NotNull @Valid Create create, int amountGroup, boolean confirmationRequired, @NotNull RolloutGroupConditions conditions);
/**
* Persists a new rollout entity. The filter within the
* {@link Rollout#getTargetFilterQuery()} is used to filter the targets which
* are affected by this rollout. The given groups will be used to create the
* groups.
*
* The rollout is not started. Only the preparation of the rollout is done,
* creating and persisting all the necessary groups. The Rollout and the groups
* are persisted in {@link RolloutStatus#CREATING} and
* {@link RolloutGroupStatus#CREATING}.
*
* The RolloutScheduler will start to assign targets to the groups. Once all
* targets have been assigned to the groups, the rollout status is changed to
* {@link RolloutStatus#READY} so it can be started with
* {@link #start(long)}.
* Persists a new rollout entity. The filter within the {@link Rollout#getTargetFilterQuery()} is used to filter the targets which
* are affected by this rollout. The given groups will be used to create the groups.
* <p/>
* The rollout is not started. Only the preparation of the rollout is done, creating and persisting all the necessary groups. The Rollout
* and the groups are persisted in {@link RolloutStatus#CREATING} and {@link RolloutGroupStatus#CREATING}.
* <p/>
* The RolloutScheduler will start to assign targets to the groups. Once all targets have been assigned to the groups, the rollout status is
* changed to {@link RolloutStatus#READY} so it can be started with {@link #start(long)}.
*
* @param rollout the rollout entity to create
* @param groups optional definition of groups
* @param conditions the rollout group conditions and actions which should be applied
* for each {@link RolloutGroup} if not defined by the RolloutGroup
* itself
* @param conditions the rollout group conditions and actions which should be applied for each {@link RolloutGroup} if not defined by the
* RolloutGroup itself
* @return the persisted rollout.
* @throws EntityNotFoundException if given {@link DistributionSet} does not exist
* @throws ConstraintViolationException if rollout or group parameters are invalid
* @throws AssignmentQuotaExceededException if the maximum number of allowed targets per rollout group is
* exceeded.
* @throws AssignmentQuotaExceededException if the maximum number of allowed targets per rollout group is exceeded.
*/
@PreAuthorize(SpringEvalExpressions.HAS_CREATE_REPOSITORY)
Rollout create(@Valid @NotNull Create rollout, @NotNull @Valid List<GroupCreate> groups, RolloutGroupConditions conditions);
@@ -192,8 +170,7 @@ public interface RolloutManagement extends PermissionSupport {
*
* @param deleted flag if deleted rollouts should be included
* @param pageable the page request to sort and limit the result
* @return a list of rollouts with details of targets count for different
* statuses
* @return a list of rollouts with details of targets count for different statuses
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<Rollout> findAllWithDetailedStatus(boolean deleted, @NotNull Pageable pageable);
@@ -218,7 +195,7 @@ public interface RolloutManagement extends PermissionSupport {
* @param rsql search text which matches name or description of rollout
* @param deleted flag if deleted rollouts should be included
* @param pageable the page request to sort and limit the result
* @return the founded rollout or {@code null} if rollout with given ID does not exists
* @return the founded rollout or {@code null} if rollout with given ID does not exist
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<Rollout> findByRsqlWithDetailedStatus(@NotEmpty String rsql, boolean deleted, @NotNull Pageable pageable);
@@ -356,6 +333,7 @@ public interface RolloutManagement extends PermissionSupport {
/**
* Stop a rollout
*
* @param rolloutId of the rollout to be stopped
* @return stopped rollout
*/

View File

@@ -11,10 +11,7 @@ package org.eclipse.hawkbit.repository;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@@ -25,8 +22,6 @@ import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.im.authentication.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.NamedEntity;
@@ -36,7 +31,6 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule.MetadataValue;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.security.access.prepost.PreAuthorize;
/**
@@ -90,6 +84,7 @@ public interface SoftwareModuleManagement<T extends SoftwareModule>
@ToString(callSuper = true)
final class Create extends UpdateCreate {
private SoftwareModuleType type;
private boolean encrypted;
}
@@ -123,6 +118,5 @@ public interface SoftwareModuleManagement<T extends SoftwareModule>
@ValidString
@Size(max = SoftwareModule.VENDOR_MAX_SIZE)
private String vendor;
private SoftwareModuleType type;
}
}

View File

@@ -115,7 +115,4 @@ public interface SystemManagement {
*/
@PreAuthorize("hasAuthority('UPDATE_" + SpPermission.TENANT_CONFIGURATION + "')")
TenantMetaData updateTenantMetadata(long defaultDsType);
@PreAuthorize(SpringEvalExpressions.IS_SYSTEM_CODE)
TenantMetaData getTenantMetadata(long tenantId);
}

View File

@@ -56,13 +56,12 @@ public interface TargetFilterQueryManagement<T extends TargetFilterQuery>
* Verifies the provided filter syntax.
*
* @param query to verify
* @return <code>true</code> if syntax is valid
* @throws RSQLParameterUnsupportedFieldException if a field in the RSQL string is used but not provided by the
* given {@code fieldNameProvider}
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
boolean verifyTargetFilterQuerySyntax(@NotNull String query);
void verifyTargetFilterQuerySyntax(@NotNull String query);
/**
* Counts all target filters that have a given auto assign distribution set

View File

@@ -60,7 +60,6 @@ public interface TargetManagement<T extends Target>
String HAS_READ_TARGET_AND_READ_ROLLOUT = HAS_READ_REPOSITORY + " and hasAuthority('READ_" + SpPermission.ROLLOUT + "')";
String HAS_READ_TARGET_AND_READ_DISTRIBUTION_SET = HAS_READ_REPOSITORY + " and hasAuthority('READ_" + SpPermission.DISTRIBUTION_SET + "')";
String DETAILS_BASE = "base";
String DETAILS_AUTO_CONFIRMATION_STATUS = "autoConfirmationStatus";
String DETAILS_TAGS = "tags";
@@ -111,11 +110,6 @@ public interface TargetManagement<T extends Target>
@PreAuthorize(HAS_READ_REPOSITORY)
Target getWithDetails(@NotEmpty String controllerId, String detailsKey);
@PreAuthorize(HAS_READ_REPOSITORY)
default Target getWithDetails(@NotEmpty String controllerId) {
return getWithDetails(controllerId, DETAILS_BASE);
}
@PreAuthorize(HAS_READ_REPOSITORY)
default Target getWithAutoConfigurationStatus(@NotEmpty String controllerId) {
return getWithDetails(controllerId, DETAILS_AUTO_CONFIRMATION_STATUS);

View File

@@ -59,8 +59,7 @@ public interface TenantConfigurationManagement extends PermissionSupport {
<T extends Serializable> Map<String, TenantConfigurationValue<T>> addOrUpdateConfiguration(Map<String, T> configurations);
/**
* Deletes a specific configuration for the current tenant. Does nothing in
* case there is no tenant specific configuration value.
* Deletes a specific configuration for the current tenant. Does nothing in case there is no tenant specific configuration value.
*
* @param configurationKey the configuration key to be deleted
*/
@@ -68,59 +67,46 @@ public interface TenantConfigurationManagement extends PermissionSupport {
void deleteConfiguration(String configurationKey);
/**
* Retrieves a configuration value from the e.g. tenant overwritten
* configuration values or in case the tenant does not a have a specific
* Retrieves a configuration value from the e.g. tenant overwritten configuration values or in case the tenant does not a have a specific
* configuration the global default value hold in the {@link Environment}.
*
* @param configurationKeyName the key of the configuration
* @return the converted configuration value either from the tenant specific
* configuration stored or from the fall back default values or
* {@code null} in case key has not been configured and not default
* value exists
* @return the converted configuration value either from the tenant specific configuration stored or from the fallback default values or
* {@code null} in case key has not been configured and not default value exists
* @throws TenantConfigurationValidatorException if the {@code propertyType} and the value in general does not
* match the expected type and format defined by the Key
* @throws ConversionFailedException if the property cannot be converted to the given
* {@code propertyType}
* @throws ConversionFailedException if the property cannot be converted to the given {@code propertyType}
*/
@PreAuthorize(value = SpringEvalExpressions.HAS_READ_REPOSITORY)
<T extends Serializable> TenantConfigurationValue<T> getConfigurationValue(String configurationKeyName);
/**
* Retrieves a configuration value from the e.g. tenant overwritten
* configuration values or in case the tenant does not a have a specific
* Retrieves a configuration value from the e.g. tenant overwritten configuration values or in case the tenant does not a have a specific
* configuration the global default value hold in the {@link Environment}.
*
* @param <T> the type of the configuration value
* @param configurationKeyName the key of the configuration
* @param propertyType the type of the configuration value, e.g. {@code String.class}
* , {@code Integer.class}, etc
* @return the converted configuration value either from the tenant specific
* configuration stored or from the fallback default values or
* {@code null} in case key has not been configured and not default
* value exists
* @param propertyType the type of the configuration value, e.g. {@code String.class}, {@code Integer.class}, etc
* @return the converted configuration value either from the tenant specific configuration stored or from the fallback default values or
* {@code null} in case key has not been configured and not default value exists
* @throws TenantConfigurationValidatorException if the {@code propertyType} and the value in general does not
* match the expected type and format defined by the Key
* @throws ConversionFailedException if the property cannot be converted to the given
* {@code propertyType}
* @throws ConversionFailedException if the property cannot be converted to the given {@code propertyType}
*/
@PreAuthorize(value = SpringEvalExpressions.HAS_READ_REPOSITORY)
<T extends Serializable> TenantConfigurationValue<T> getConfigurationValue(String configurationKeyName,
Class<T> propertyType);
/**
* returns the global configuration property either defined in the property
* file or an default value otherwise.
* returns the global configuration property either defined in the property file or a default value otherwise.
*
* @param <T> the type of the configuration value
* @param configurationKeyName the key of the configuration
* @param propertyType the type of the configuration value, e.g. {@code String.class}
* , {@code Integer.class}, etc
* @param propertyType the type of the configuration value, e.g. {@code String.class}, {@code Integer.class}, etc
* @return the global configured value
* @throws TenantConfigurationValidatorException if the {@code propertyType} and the value in the property
* file or the default value does not match the expected type
* and format defined by the Key
* @throws ConversionFailedException if the property cannot be converted to the given
* {@code propertyType}
* @throws TenantConfigurationValidatorException if the {@code propertyType} and the value in the property file or the default value
* does not match the expected type and format defined by the Key
* @throws ConversionFailedException if the property cannot be converted to the given {@code propertyType}
*/
@PreAuthorize(value = SpringEvalExpressions.HAS_READ_REPOSITORY)
<T> T getGlobalConfigurationValue(String configurationKeyName, Class<T> propertyType);

View File

@@ -25,7 +25,8 @@ public interface TenantStatsManagement {
*
* @return collected statistics
*/
@PreAuthorize("hasAuthority('" + SpRole.TENANT_ADMIN + "')" + " or " +
@PreAuthorize(
"hasAuthority('" + SpRole.TENANT_ADMIN + "')" + " or " +
SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN + " or " +
SpringEvalExpressions.IS_SYSTEM_CODE)
TenantUsage getStatsOfTenant();

View File

@@ -13,8 +13,7 @@ import java.util.Arrays;
import java.util.Optional;
/**
* Enumerates the supported update modes. Each mode represents an attribute
* update strategy.
* Enumerates the supported update modes. Each mode represents an attribute update strategy.
*
* @see ControllerManagement
*/
@@ -34,9 +33,4 @@ public enum UpdateMode {
* Removal update strategy
*/
REMOVE;
public static Optional<UpdateMode> valueOfIgnoreCase(final String name) {
return Arrays.stream(values()).filter(mode -> mode.name().equalsIgnoreCase(name)).findAny();
}
}

View File

@@ -46,7 +46,7 @@ public class ValidStringValidator implements ConstraintValidator<ValidString, St
try {
return cleaner.isValid(stringToDocument(value));
} catch (final Exception ex) {
log.error(String.format("There was an exception during bean field value (%s) validation", value), ex);
log.error("There was an exception during bean field value ({}) validation", value, ex);
return false;
}
}

View File

@@ -1,31 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.model;
import java.util.Collection;
import lombok.Builder;
import lombok.Data;
/**
* Holds distribution set filter parameters.
*/
@Data
@Builder
public final class DistributionSetFilter {
private final Boolean isDeleted;
private final Boolean isComplete;
private final Boolean isValid;
private final Long typeId;
private final String searchText;
private final Boolean selectDSWithNoTag;
private final Collection<String> tagNames;
}

View File

@@ -14,9 +14,8 @@ import java.time.LocalDateTime;
import lombok.Data;
/**
* The poll time object which holds all the necessary information around the
* target poll time, e.g. the last poll time, the next poll time and the overdue
* poll time.
* The poll time object which holds all the necessary information around the target poll time, e.g. the last poll time, the next poll time and
* the overdue poll time.
*/
@Data
public class PollStatus {
@@ -26,7 +25,8 @@ public class PollStatus {
private final LocalDateTime overdueDate;
private final LocalDateTime currentDate;
public PollStatus(final LocalDateTime lastPollDate, final LocalDateTime nextPollDate,
public PollStatus(
final LocalDateTime lastPollDate, final LocalDateTime nextPollDate,
final LocalDateTime overdueDate, final LocalDateTime currentDate) {
this.lastPollDate = lastPollDate;
this.nextPollDate = nextPollDate;
@@ -35,11 +35,9 @@ public class PollStatus {
}
/**
* calculates if the target poll time is overdue and the target has not been
* polled in the configured poll time interval.
* Calculates if the target poll time is overdue and the target has not been polled in the configured poll time interval.
*
* @return {@code true} if the current time is after the poll time overdue
* date otherwise {@code false}.
* @return {@code true} if the current time is after the poll time overdue date otherwise {@code false}.
*/
public boolean isOverdue() {
return currentDate.isAfter(overdueDate);

View File

@@ -1,111 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository;
import static org.assertj.core.api.Assertions.assertThat;
import org.eclipse.hawkbit.repository.RegexCharacterCollection.RegexChar;
import org.junit.jupiter.api.Test;
/**
* Feature: Unit Tests - Repository<br/>
* Story: Regular expression helper
*/
class RegexCharTest {
private static final int INDEX_FIRST_PRINTABLE_ASCII_CHAR = 32;
private static final int INDEX_LAST_PRINTABLE_ASCII_CHAR = 127;
private static final String TEST_STRING = getPrintableAsciiCharacters();
/**
* Verifies every RegexChar can be used to exclusively find the desired characters in a String.
*/
@Test
void allRegexCharsOnlyFindExpectedChars() {
for (final RegexChar character : RegexChar.values()) {
switch (character) {
case DIGITS:
assertRegexCharExclusivelyFindsGivenCharacters(character, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
break;
case WHITESPACE:
assertRegexCharExclusivelyFindsGivenCharacters(character, " ", "\t");
break;
case SLASHES:
assertRegexCharExclusivelyFindsGivenCharacters(character, "/", "\\");
break;
case QUOTATION_MARKS:
assertRegexCharExclusivelyFindsGivenCharacters(character, "\"", "'");
break;
default:
assertRegexCharExclusivelyFindsGivenCharacters(character, character.getRegExp());
break;
}
}
}
/**
* Verifies that combinations of RegexChars can be used to find the desired characters in a String.
*/
@Test
void combinedRegexCharsFindExpectedChars() {
final RegexCharacterCollection greaterAndLessThan = new RegexCharacterCollection(RegexChar.GREATER_THAN,
RegexChar.LESS_THAN);
final RegexCharacterCollection equalsAndQuestionMark = new RegexCharacterCollection(RegexChar.EQUALS_SYMBOL,
RegexChar.QUESTION_MARK);
final RegexCharacterCollection colonAndWhitespace = new RegexCharacterCollection(RegexChar.COLON,
RegexChar.WHITESPACE);
assertRegexCharsExclusivelyFindsGivenCharacters(greaterAndLessThan, "<", ">");
assertRegexCharsExclusivelyFindsGivenCharacters(equalsAndQuestionMark, "=", "?");
assertRegexCharsExclusivelyFindsGivenCharacters(colonAndWhitespace, ":", " ", "\t");
}
private static String getPrintableAsciiCharacters() {
final StringBuilder stringBuilder = new StringBuilder();
for (int i = INDEX_FIRST_PRINTABLE_ASCII_CHAR; i < INDEX_LAST_PRINTABLE_ASCII_CHAR; i++) {
stringBuilder.append((char) i);
}
stringBuilder.append("\t");
return stringBuilder.toString();
}
private static String insertStringIntoString(final String baseString, final String stringToInsert,
final int position) {
final StringBuilder stringBuilder = new StringBuilder(baseString);
return stringBuilder.insert(position, stringToInsert).toString();
}
private void assertRegexCharExclusivelyFindsGivenCharacters(final RegexChar characterToVerify,
final String... charactersExpectedToBeFoundByRegex) {
assertRegexCharsExclusivelyFindsGivenCharacters(new RegexCharacterCollection(characterToVerify),
charactersExpectedToBeFoundByRegex);
}
private void assertRegexCharsExclusivelyFindsGivenCharacters(final RegexCharacterCollection regexToVerify,
final String... charactersExpectedToBeFoundByRegex) {
String notMatchingString = TEST_STRING;
for (final String character : charactersExpectedToBeFoundByRegex) {
notMatchingString = notMatchingString.replace(character, "");
}
for (final String character : charactersExpectedToBeFoundByRegex) {
assertThat(RegexCharacterCollection.stringContainsCharacter("", regexToVerify)).isFalse();
assertThat(RegexCharacterCollection.stringContainsCharacter(notMatchingString, regexToVerify)).isFalse();
assertThat(RegexCharacterCollection.stringContainsCharacter(character, regexToVerify)).isTrue();
assertThat(RegexCharacterCollection
.stringContainsCharacter(insertStringIntoString(notMatchingString, character, 0), regexToVerify))
.isTrue();
assertThat(RegexCharacterCollection.stringContainsCharacter(
insertStringIntoString(notMatchingString, character, notMatchingString.length()), regexToVerify)).isTrue();
assertThat(RegexCharacterCollection.stringContainsCharacter(
insertStringIntoString(notMatchingString, character, notMatchingString.length() / 2), regexToVerify)).isTrue();
}
}
}

View File

@@ -156,7 +156,7 @@ public class JpaRepositoryConfiguration {
* @return the {@link MethodValidationPostProcessor}
*/
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
public static MethodValidationPostProcessor methodValidationPostProcessor() {
final MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
// ValidatorFactory shall NOT be closed because after closing the generated Validator
// methods shall not be called - we need the validator in future
@@ -170,7 +170,7 @@ public class JpaRepositoryConfiguration {
}
@Bean
public BeanPostProcessor entityManagerBeanPostProcessor(
public static BeanPostProcessor entityManagerBeanPostProcessor(
@Autowired(required = false) final AccessController<JpaArtifact> artifactAccessController,
@Autowired(required = false) final AccessController<JpaSoftwareModuleType> softwareModuleTypeAccessController,
@Autowired(required = false) final AccessController<JpaSoftwareModule> softwareModuleAccessController,

View File

@@ -431,8 +431,8 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
@Transactional
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public Action registerRetrieved(final long actionId, final String message) {
return handleRegisterRetrieved(actionId, message);
public void registerRetrieved(final long actionId, final String message) {
handleRegisterRetrieved(actionId, message);
}
@Override
@@ -895,10 +895,8 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
*
* @param actionId to the handle status for
* @param message for the status
* @return the updated action in case the status has been changed to
* {@link Status#RETRIEVED}
*/
private Action handleRegisterRetrieved(final Long actionId, final String message) {
private void handleRegisterRetrieved(final Long actionId, final String message) {
final JpaAction action = actionRepository.getById(actionId);
// do a manual query with CriteriaBuilder to avoid unnecessary field queries and an extra
// count query made by spring-data when using pageable requests, we don't need an extra count
@@ -910,8 +908,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
.multiselect(actionStatusRoot.get(AbstractJpaBaseEntity_.id), actionStatusRoot.get(JpaActionStatus_.status))
.where(cb.equal(actionStatusRoot.get(JpaActionStatus_.action).get(AbstractJpaBaseEntity_.id), actionId))
.orderBy(cb.desc(actionStatusRoot.get(AbstractJpaBaseEntity_.id)));
final List<Object[]> resultList = entityManager.createQuery(query).setFirstResult(0).setMaxResults(1)
.getResultList();
final List<Object[]> resultList = entityManager.createQuery(query).setFirstResult(0).setMaxResults(1).getResultList();
// if the latest status is not in retrieve state then we add a retrieved state again, we want
// to document a deployment retrieved status and a cancel retrieved status, but multiple
@@ -919,8 +916,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
// case controller retrieves a action multiple times.
if (resultList.isEmpty() || (Status.RETRIEVED != resultList.get(0)[1])) {
// document that the status has been retrieved
actionStatusRepository
.save(new JpaActionStatus(action, Status.RETRIEVED, System.currentTimeMillis(), message));
actionStatusRepository.save(new JpaActionStatus(action, Status.RETRIEVED, System.currentTimeMillis(), message));
// don't change the action status itself in case the action is in
// canceling state otherwise
@@ -928,10 +924,8 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
// cancel job anymore.
if (!action.isCancelingOrCanceled()) {
action.setStatus(Status.RETRIEVED);
return actionRepository.save(action);
}
}
return action;
}
private void cancelAssignDistributionSetEvent(final Action action) {

View File

@@ -316,16 +316,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
return actionStatusRepository.findByActionId(pageable, actionId);
}
@Override
public long countActionStatusByAction(final long actionId) {
assertActionExistsAndAccessible(actionId);
return actionStatusRepository.countByActionId(actionId);
}
// action is already got and there are checked read permissions - do not check
// permissions
// and UI which is to be removed
// action is already got and there are checked read permissions - do not check permissions and UI which is to be removed
@Override
public Page<String> findMessagesByActionStatusId(final long actionStatusId, final Pageable pageable) {
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();

View File

@@ -153,13 +153,6 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement {
return JpaManagementHelper.convertPage(rolloutGroupRepository.findByRolloutId(rolloutId, pageable), pageable);
}
@Override
public long countByRollout(final long rolloutId) {
throwEntityNotFoundExceptionIfRolloutDoesNotExist(rolloutId);
return rolloutGroupRepository.countByRolloutId(rolloutId);
}
@Override
public Page<Target> findTargetsOfRolloutGroup(final long rolloutGroupId, final Pageable page) {
final JpaRolloutGroup rolloutGroup = rolloutGroupRepository.getById(rolloutGroupId);

View File

@@ -271,11 +271,6 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst
return tenantMetaDataRepository.save(data);
}
@Override
public TenantMetaData getTenantMetadata(final long tenantId) {
return tenantMetaDataRepository.findById(tenantId).orElseThrow(() -> new EntityNotFoundException(TenantMetaData.class, tenantId));
}
private static boolean isPostgreSql(final JpaProperties properties) {
return Database.POSTGRESQL == properties.getDatabase();
}

View File

@@ -30,6 +30,7 @@ import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException;
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
@@ -114,13 +115,12 @@ class JpaTargetFilterQueryManagement
}
@Override
public boolean verifyTargetFilterQuerySyntax(final String query) {
public void verifyTargetFilterQuerySyntax(final String query) {
try {
RsqlUtility.getInstance().validateRsqlFor(query, TargetFields.class, JpaTarget.class);
return true;
} catch (final RSQLParserException | RSQLParameterUnsupportedFieldException e) {
log.debug("The RSQL query '{}}' is invalid.", query, e);
return false;
throw new RSQLParameterSyntaxException("Cannot create a Rollout with an empty target query filter!");
}
}

View File

@@ -464,7 +464,7 @@ public class JpaTargetManagement
// get the modifiable metadata map
final Map<String, String> metadata = target.getMetadata();
if (!metadata.containsKey(key)) {
throw new EntityNotFoundException("Target metadata", controllerId + ":" + key);
assertMetadataQuota(target.getId(), metadata.size() + 1);
}
metadata.put(key, value);

View File

@@ -11,6 +11,7 @@ package org.eclipse.hawkbit.repository.jpa.model;
import java.io.Serial;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
@@ -119,7 +120,7 @@ public class JpaDistributionSet
foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_ds_metadata_ds"))
@MapKeyColumn(name = "meta_key", length = DistributionSet.METADATA_MAX_KEY_SIZE)
@Column(name = "meta_value", length = DistributionSet.METADATA_MAX_VALUE_SIZE)
private Map<String, String> metadata;
private Map<String, String> metadata = new HashMap<>();
@Column(name = "complete")
private boolean complete;

View File

@@ -119,22 +119,9 @@ public class JpaSoftwareModule
@ManyToMany(mappedBy = "modules", targetEntity = JpaDistributionSet.class, fetch = FetchType.LAZY)
private List<DistributionSet> assignedTo;
/**
* Parameterized constructor.
*/
public JpaSoftwareModule(final SoftwareModuleType type, final String name, final String version) {
this(type, name, version, null, null, false);
}
/**
* Parameterized constructor.
*/
public JpaSoftwareModule(final SoftwareModuleType type, final String name, final String version,
final String description, final String vendor, final boolean encrypted) {
super(name, version, description);
this.vendor = vendor;
super(name, version, null);
this.type = (JpaSoftwareModuleType) type;
this.encrypted = encrypted;
}
@Override

View File

@@ -20,7 +20,6 @@ import java.util.stream.StreamSupport;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity_;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
@@ -48,7 +47,6 @@ import org.eclipse.hawkbit.repository.jpa.specifications.DistributionSetSpecific
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionStatusCreate;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetFilter;
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
@@ -167,11 +165,18 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest
targets.stream().map(Target::getControllerId).toList(), tag.getId());
}
protected List<? extends DistributionSet> assignTag(final Collection<? extends DistributionSet> sets,
final DistributionSetTag tag) {
protected List<? extends DistributionSet> assignTag(final Collection<? extends DistributionSet> sets, final DistributionSetTag tag) {
return distributionSetManagement.assignTag(sets.stream().map(DistributionSet::getId).toList(), tag.getId());
}
protected List<? extends DistributionSet> assignTags(final Collection<? extends DistributionSet> sets, final DistributionSetTag... tags) {
List<? extends DistributionSet> result = null;
for (DistributionSetTag tag : tags) {
result = assignTag(sets, tag);
}
return result;
}
protected List<? extends DistributionSet> unassignTag(
final Collection<DistributionSet> sets, final DistributionSetTag tag) {
return distributionSetManagement.unassignTag(sets.stream().map(DistributionSet::getId).toList(), tag.getId());
@@ -181,8 +186,8 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest
targets.stream().map(Target::getControllerId).forEach(id -> targetManagement.assignType(id, type.getId()));
}
protected void assertRollout(final Rollout rollout, final boolean dynamic, final Rollout.RolloutStatus status, final int groupCreated,
final long totalTargets) {
protected void assertRollout(
final Rollout rollout, final boolean dynamic, final Rollout.RolloutStatus status, final int groupCreated, final long totalTargets) {
final Rollout refreshed = refresh(rollout);
assertThat(refreshed.isDynamic()).as("Is dynamic").isEqualTo(dynamic);
assertThat(refreshed.getStatus()).as("Status").isEqualTo(status);
@@ -229,43 +234,4 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest
private JpaRollout refresh(final Rollout rollout) {
return rolloutRepository.findById(rollout.getId()).get();
}
protected Slice<JpaDistributionSet> findDsByDistributionSetFilter(final DistributionSetFilter distributionSetFilter, final Pageable pageable) {
final List<Specification<JpaDistributionSet>> specList = buildDistributionSetSpecifications(distributionSetFilter);
return JpaManagementHelper.findAllWithoutCountBySpec(distributionSetRepository, specList, pageable);
}
private static List<Specification<JpaDistributionSet>> buildDistributionSetSpecifications(
final DistributionSetFilter distributionSetFilter) {
final List<Specification<JpaDistributionSet>> specList = new ArrayList<>(10);
if (distributionSetFilter.getIsComplete() != null) {
specList.add(DistributionSetSpecification.isCompleted(distributionSetFilter.getIsComplete()));
}
if (distributionSetFilter.getIsDeleted() != null) {
specList.add(DistributionSetSpecification.isDeleted(distributionSetFilter.getIsDeleted()));
}
if (distributionSetFilter.getIsValid() != null) {
specList.add(DistributionSetSpecification.isValid(distributionSetFilter.getIsValid()));
}
if (distributionSetFilter.getTypeId() != null) {
specList.add(DistributionSetSpecification.byType(distributionSetFilter.getTypeId()));
}
if (!ObjectUtils.isEmpty(distributionSetFilter.getSearchText())) {
final String[] dsFilterNameAndVersionEntries = JpaManagementHelper
.getFilterNameAndVersionEntries(distributionSetFilter.getSearchText().trim());
specList.add(DistributionSetSpecification.likeNameAndVersion(dsFilterNameAndVersionEntries[0], dsFilterNameAndVersionEntries[1]));
}
if (hasTagsFilterActive(distributionSetFilter)) {
specList.add(DistributionSetSpecification.hasTags(
distributionSetFilter.getTagNames(), distributionSetFilter.getSelectDSWithNoTag()));
}
return specList;
}
private static boolean hasTagsFilterActive(final DistributionSetFilter distributionSetFilter) {
final boolean isNoTagActive = Boolean.TRUE.equals(distributionSetFilter.getSelectDSWithNoTag());
final boolean isAtLeastOneTagActive = !CollectionUtils.isEmpty(distributionSetFilter.getTagNames());
return isNoTagActive || isAtLeastOneTagActive;
}
}

View File

@@ -0,0 +1,662 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.management;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.RepositoryManagement;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.eclipse.hawkbit.repository.test.matcher.EventVerifier;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.eclipse.hawkbit.utils.ObjectCopyUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;
import org.springframework.util.ObjectUtils;
@Slf4j
public abstract class AbstractRepositoryManagementTest<T extends BaseEntity, C, U extends Identifiable<Long>>
extends AbstractJpaIntegrationTest {
protected RepositoryManagement<T, C, U> repositoryManagement;
@Getter(AccessLevel.PROTECTED)
private Class<T> entityType; // T
private Class<C> createType; // C
private Class<U> updateType; // U
private Function<Class<?>, ?> entityFactory;
@BeforeEach
void setup() {
synchronized (AbstractRepositoryManagementTest.class) {
if (repositoryManagement == null) {
final RepoMan<T, C, U> repoMan = resolveRepositoryManagement();
repositoryManagement = repoMan.repo;
entityType = repoMan.entityType();
createType = repoMan.createType();
updateType = repoMan.updateType();
entityFactory = repoMan.entityFactory;
}
}
expectedEvents.clear();
EventVerifier.DYNAMIC_EXPECTATIONS.set(() -> expectedEvents.entrySet().stream()
.map(e -> new EventVerifier.DynamicExpect(e.getKey(), e.getValue().get()))
.toList()
.toArray(new Expect[0]));
}
// also test get/find(Long)
@ExpectEvents
@Test
void create() {
final T instance = instance();
assertEquals(repositoryManagement.get(instance.getId()), instance);
assertThat(repositoryManagement.find(instance.getId()))
.isPresent()
.hasValueSatisfying(get -> assertEquals(get, instance));
}
// also test get/find(Collection<Long>)
@ExpectEvents
@Test
void creates() {
final List<T> instances = instances();
final List<Long> instanceIds = instances.stream().map(Identifiable::getId).toList();
assertEquals(repositoryManagement.get(instanceIds), instances);
assertThat(repositoryManagement.find(instanceIds)).hasSize(instances.size());
}
@Test
void findCountAll() {
// some entities as distribution set types has pre-initialized entities
final int alreadyExisting = repositoryManagement.findAll(UNPAGED).getContent().size();
final int count = 5;
final List<T> instances = instances(count); // create 'count' instances
assertThat(repositoryManagement.findAll(UNPAGED).getContent()).as("Wrong size of all entities").hasSize(count + alreadyExisting);
assertThat(repositoryManagement.count()).as("Wrong size of all entities").isEqualTo(count + alreadyExisting);
repositoryManagement.delete(instances.get(0).getId());
assertThat(repositoryManagement.findAll(UNPAGED).getContent()).as("Wrong size of all entities").hasSize(count + alreadyExisting - 1);
assertThat(repositoryManagement.count()).as("Wrong size of all entities").isEqualTo(count + alreadyExisting - 1);
}
@ExpectEvents
@Test
void update() {
final T instance = instance();
final U update = forBuildableTypeRe(updateType, instance.getId());
incrementEvents(entityType, EventType.UPDATED);
final T instanceUpdated = repositoryManagement.update(update);
assertNotNull(instanceUpdated);
assertEquals(instanceUpdated, update);
final T get = repositoryManagement.get(instance.getId());
assertEquals(get, update);
assertEquals(get, ObjectCopyUtil.copy(update, instance, false, UnaryOperator.identity()));
}
/**
* Calling update without changing fields results in no recorded change in the repository including unchanged audit fields.
*/
@SneakyThrows
@Test
void updateNothingDontChangeRepository() {
final T instance = instance();
final U emptyUpdate = forBuildableType(updateType, instance.getId(), (name, type) -> {
try {
try {
final Method getter = entityType.getMethod("get" + Character.toUpperCase(name.charAt(0)) + name.substring(1));
if (getter.getReturnType() == type) {
return getter.invoke(instance);
}
} catch (final NoSuchMethodException e) {
if (type == boolean.class || type == Boolean.class) {
final Method getter = entityType.getMethod("is" + Character.toUpperCase(name.charAt(0)) + name.substring(1));
if (getter.getReturnType() == boolean.class || getter.getReturnType() == Boolean.class) {
return getter.invoke(instance);
}
}
}
} catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException e2) {
log.debug("Could not invoke getter for {} in {}", name, instance, e2);
}
return null;
});
final T updated = repositoryManagement.update(emptyUpdate);
assertThat(updated.getOptLockRevision())
.as("Expected version number of updated entity to be equal to created version")
.isEqualTo(instance.getOptLockRevision());
}
@ExpectEvents
@Test
void delete() {
final Long instanceId = instance().getId();
incrementEvents(entityType, EventType.DELETED);
repositoryManagement.delete(instanceId);
assertThat(repositoryManagement.find(instanceId)).isEmpty();
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> repositoryManagement.get(instanceId));
}
@ExpectEvents
@Test
void deletes() {
final List<Long> instanceIds = instances().stream().map(Identifiable::getId).toList();
final int count = instanceIds.size();
assertThat(repositoryManagement.get(instanceIds)).hasSize(count);
assertThat(repositoryManagement.find(instanceIds)).hasSize(count);
incrementEvents(entityType, EventType.DELETED, count);
repositoryManagement.delete(instanceIds);
for (final Long instanceId : instanceIds) {
assertThat(repositoryManagement.find(instanceId)).isEmpty();
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> repositoryManagement.get(instanceId));
}
}
@Test
void failToReadUpdateDeleteNotExisting() {
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> repositoryManagement.get(NOT_EXIST_IDL));
assertThat(repositoryManagement.find(-1L)).isEmpty();
final U notExistUpdate = forBuildableTypeRe(updateType, NOT_EXIST_IDL);
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> repositoryManagement.update(notExistUpdate));
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> repositoryManagement.delete(NOT_EXIST_IDL));
}
@Test
void failToDuplicate() {
final C create = forType(createType);
incrementEvents(entityType, EventType.CREATED);
repositoryManagement.create(create);
assertThatExceptionOfType(EntityAlreadyExistsException.class).isThrownBy(() -> repositoryManagement.create(create));
}
protected T instance() {
final C create = forType(createType);
incrementEvents(entityType, EventType.CREATED);
final T instance = repositoryManagement.create(create);
assertNotNull(instance);
assertEquals(instance, create);
final Long instanceId = instance.getId();
assertNotNull(repositoryManagement.get(instanceId));
assertThat(repositoryManagement.find(instanceId)).isPresent();
return instance;
}
protected List<T> instances() {
return instances(1 + RND.nextInt(5));
}
protected List<T> instances(final int count) {
final List<C> creates = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
creates.add(forType(createType));
}
incrementEvents(entityType, EventType.CREATED, count);
final List<T> instances = repositoryManagement.create(creates);
assertNotNull(instances);
assertThat(instances).hasSize(count);
assertEquals(instances, creates);
return instances;
}
// asserts that all expected's fields (getters) are equal to the actual's fields
// does a deep compare for fields which are not primitive or String
protected void assertEquals(final Object actual, final Object expected) {
final Deque<String> stack = new ArrayDeque<>();
try {
assertEquals(actual, expected, stack);
} catch (final AssertionError e) {
throw new AssertionError(
String.format(
"Comparison failed at path: %s%n\tActual: %s,%n\tExpected: %s",
String.join("->", stack), actual, expected),
e);
}
}
private final Map<Class<?>, AtomicInteger> expectedEvents = new ConcurrentHashMap<>();
protected void incrementEvents(final Class<?> entityType, final EventType eventType) {
incrementEvents(entityType, eventType, 1);
}
protected void incrementEvents(final Class<?> entityType, final EventType eventType, final int count) {
expectedEvents.computeIfAbsent(eventType.getEventType(entityType), k -> new AtomicInteger(0)).addAndGet(count);
}
protected static Type[] genericTypes(final Class<?> clazz, final Class<?> targetSuperClass) {
return findGenericSuperType(ResolvableType.forType(clazz), targetSuperClass)
.map(ResolvableType::resolveGenerics)
.orElseThrow(() ->
new IllegalStateException("No generic type found for " + targetSuperClass.getName() + " in " + clazz.getName()));
}
protected final AtomicLong counter = new AtomicLong();
@SuppressWarnings("unchecked")
protected <O> O forType(final Class<O> type) {
if (type == boolean.class || type == Boolean.class) {
return (O) (counter.incrementAndGet() % 2 == 0 ? Boolean.TRUE : Boolean.FALSE);
} else if (type == Integer.class || type == int.class) {
return (O) Integer.valueOf((int) counter.incrementAndGet());
} else if (type == Long.class || type == long.class) {
return (O) Long.valueOf(counter.incrementAndGet());
} else if (type == Float.class || type == float.class) {
return (O) Float.valueOf(counter.incrementAndGet());
} else if (type == Double.class || type == double.class) {
return (O) Double.valueOf(counter.incrementAndGet());
} else if (type == String.class) {
return (O) ("test-" + counter.incrementAndGet());
} else if (type == Set.class) {
final Set<?> set = new HashSet<>();
// set.add(forType(createType));
return (O) set;
} else if (type.isEnum()) {
final O[] constants = type.getEnumConstants();
return constants[(int) (counter.incrementAndGet() % constants.length)];
} else {
// entity classes for created events are root level classes
if ("org.eclipse.hawkbit.repository.model".equals(type.getPackageName()) && BaseEntity.class.isAssignableFrom(type)) {
try {
incrementEvents(type, EventType.CREATED);
return (O) entityFactory.apply(type);
} catch (final Exception e) {
log.error("Could not create instance of {} using entity factory", type.getName(), e);
}
}
try { // try with constructor
final Constructor<?>[] constructors = type.getConstructors();
if (ObjectUtils.isEmpty(constructors)) {
throw new NoSuchMethodException("No public constructor found for " + type);
}
// prefer empty constructor
for (final Constructor<?> constructor : constructors) {
if (constructor.getParameterCount() == 0) {
return (O) constructor.newInstance();
}
}
constructors[0].setAccessible(true);
return (O) constructors[0].newInstance(Stream.of(constructors[0].getParameterTypes())
.map(this::forType)
.toArray());
} catch (final ReflectiveOperationException e) { // try with builder
// try builder pattern
try {
return forBuildableType(type, null);
} catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException e1) {
log.debug("{} is not a builder. Throws could not instantiate", type.getName());
}
log.error("Could not instantiate {}", type.getName(), e);
throw new IllegalStateException("Could not create instance of " + createType.getName(), e);
}
}
}
protected Object builderParameterValue(final Method builderSetter) {
return forType(builderSetter.getParameterTypes()[0]);
}
private <O> O forBuildableTypeRe(final Class<O> type, final Long id) {
try {
return forBuildableType(type, id);
} catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.debug("{} is not a buildable type", type.getName());
throw new IllegalStateException("Could not create instance of " + createType.getName(), e);
}
}
private <O> O forBuildableType(final Class<O> type, final Long id)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return forBuildableType(type, id, null);
}
@SuppressWarnings("unchecked")
private <O> O forBuildableType(final Class<O> type, final Long id, final BiFunction<String, Class<?>, Object> propertyValueSupplier)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
final Object builder = type.getMethod("builder").invoke(null);
Arrays.stream(builder.getClass().getMethods())
.filter(method -> method.getParameterCount() == 1)
.filter(method -> method.getDeclaringClass() != Object.class)
.forEach(method -> {
try {
if (id != null && "id".equals(method.getName()) && method.getParameterTypes()[0] == Long.class) {
method.invoke(builder, id);
} else {
if (propertyValueSupplier == null) {
method.invoke(builder, builderParameterValue(method));
} else {
final Object propertyValue = propertyValueSupplier.apply(method.getName(), method.getParameterTypes()[0]);
if (propertyValue != null) {
method.invoke(builder, propertyValue);
}
}
}
} catch (final IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch (final InvocationTargetException ex) {
throw new RuntimeException(ex.getTargetException() == null ? ex : ex.getTargetException());
}
});
final Method build = builder.getClass().getDeclaredMethod("build");
build.setAccessible(true);
return (O) build.invoke(builder);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private RepoMan<T, C, U> resolveRepositoryManagement() {
final Type[] abstractTestTypeArgs = genericTypes(getClass(), AbstractRepositoryManagementTest.class);
final List<Field> fields = fields(getClass(), new ArrayList<>()).stream()
.peek(field -> field.setAccessible(true))
.filter(field -> RepositoryManagement.class.isAssignableFrom(field.getType()))
.filter(field -> field.getDeclaringClass() != AbstractRepositoryManagementTest.class ||
!"repositoryManagement".equals(field.getName()))
.toList();
final Map<Class<?>, Supplier<?>> entityCreators = new HashMap<>();
fields.forEach(field -> {
try {
field.setAccessible(true);
final RepositoryManagement entityManager = (RepositoryManagement) field.get(this);
final Type[] fieldGenericTypes = genericTypes(field.getType(), RepositoryManagement.class);
entityCreators.put((Class<?>) fieldGenericTypes[0], () -> entityManager.create(forType((Class<?>) fieldGenericTypes[1])));
} catch (final IllegalAccessException e) {
throw new IllegalStateException("Could not access field " + field.getName(), e);
}
});
return new RepoMan<T, C, U>(
fields.stream()
.peek(field -> System.out.println("Found RepositoryManagement field: " + field.getName() + "-> " + Arrays.toString(
genericTypes(field.getType(), RepositoryManagement.class))))
.filter(field -> Arrays.equals(genericTypes(field.getType(), RepositoryManagement.class), abstractTestTypeArgs))
.map(field -> {
field.setAccessible(true);
try {
return field.get(this);
} catch (final IllegalAccessException e) {
throw new IllegalStateException("Could not access field " + field.getName(), e);
}
})
.map(RepositoryManagement.class::cast)
.findAny()
.orElseThrow(() -> new IllegalStateException(
"No field matching the generic types found: " + Arrays.toString(abstractTestTypeArgs))),
(Class<T>) abstractTestTypeArgs[0],
(Class<C>) abstractTestTypeArgs[1],
(Class<U>) abstractTestTypeArgs[2],
entityClass -> {
final Supplier<?> entitySupplier = entityCreators.get(entityClass);
if (entitySupplier == null) {
throw new IllegalStateException("No entity creator found for " + entityClass.getName());
}
return entitySupplier.get();
});
}
private List<Field> fields(final Class<?> clazz, final List<Field> fields) {
Collections.addAll(fields, clazz.getDeclaredFields());
final Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
fields(superClass, fields);
}
return fields;
}
private static Optional<ResolvableType> findGenericSuperType(final ResolvableType resolvableType, final Class<?> targetSuperClass) {
if (resolvableType.getRawClass() == targetSuperClass) {
return Optional.of(resolvableType);
}
final Optional<ResolvableType> inInterfaces = Arrays.stream(resolvableType.getInterfaces())
.filter(superInterface -> superInterface.getRawClass() == targetSuperClass)
.findAny();
if (inInterfaces.isPresent()) {
return inInterfaces;
} else if (resolvableType.getSuperType() != ResolvableType.NONE) {
return findGenericSuperType(resolvableType.getSuperType(), targetSuperClass);
} else {
return Optional.empty();
}
}
private void assertEquals(final Object actual, final Object expected, final Deque<String> path) {
if (actual == expected) {
return; // same reference
}
if (expected == null || actual == null) {
throw new AssertionError("One of the expected and actual is null the other is not");
}
if (expected.equals(actual)) {
return; // equal
}
final Class<?> expectedClass = expected.getClass();
final Class<?> actualClass = actual.getClass();
if (actualClass == expectedClass) {
if (actual.equals(expected)) {
if (actualClass == Boolean.class ||
actualClass == Integer.class || actualClass == Long.class ||
actualClass == Float.class || actualClass == Double.class ||
actualClass == String.class) {
return;
}
} else if (!(Collection.class.isAssignableFrom(actualClass))) {
throw new AssertionError("Expected:\n\t" + expected + "but got:\n\t" + actual);
}
}
if (expected instanceof Collection<?> expectedCollection) {
if (actual instanceof Collection<?> actualCollection) {
if (actualCollection.size() != expectedCollection.size()) {
throw new AssertionError("Expected collection size " + expectedCollection.size() + " but got " + actualCollection.size());
}
final AtomicInteger index = new AtomicInteger(0);
expectedCollection.iterator().forEachRemaining(expectedElement -> {
index.incrementAndGet();
actualCollection.stream()
.filter(actualElement -> {
try {
assertEquals(actualElement, expectedElement, path);
return true;
} catch (final AssertionError e) {
return false;
}
})
.findFirst()
.orElseThrow(
() -> {
path.addLast("[" + index.get() + ", not found]");
return new AssertionError("Expected collection to contain " + expectedElement + " but it does not");
});
});
return;
} else {
throw new AssertionError("Expected a collection but got " + actual.getClass().getName());
}
} else if (expectedClass.isArray()) {
if (actual.getClass().isArray()) {
final int expectedLength = Array.getLength(expected);
final int actualLength = Array.getLength(actual);
if (expectedLength != actualLength) {
throw new AssertionError("Expected array length " + expectedLength + " but got " + actualLength);
}
for (int i = 0; i < expectedLength; i++) {
final Object expectedElement = Array.get(expected, i);
path.addLast("[" + i + ", not found]");
assertEquals(Array.get(actual, i), expectedElement, path);
path.removeLast();
}
return;
} else {
throw new AssertionError("Expected an array but got " + actual.getClass().getName());
}
} else if (expected instanceof Optional<?> expectedOptional) {
if (actual instanceof Optional<?> actualOptional) {
if (expectedOptional.isPresent() && actualOptional.isPresent()) {
path.addLast(".get()");
assertEquals(actualOptional.get(), expectedOptional.get(), path);
} else if (expectedOptional.isEmpty() && actualOptional.isEmpty()) {
return; // both are empty
} else {
throw new AssertionError(
"Expected optional to be " + (expectedOptional.isPresent() ? "present" : "empty") +
" but got " + (actualOptional.isPresent() ? "present" : "empty"));
}
return;
} else {
throw new AssertionError("Expected an Optional but got " + actual.getClass().getName());
}
} else if (expected instanceof Map<?, ?> expectedMap) {
if (actual instanceof Map<?, ?> actualMap) {
if (actualMap.size() != expectedMap.size()) {
throw new AssertionError("Expected map size " + expectedMap.size() + " but got " + actualMap.size());
}
expectedMap.forEach((key, expectedValue) -> {
path.add("[" + key + "]");
assertEquals(actualMap.get(key), expectedValue, path);
path.removeLast();
});
return;
} else {
throw new AssertionError("Expected a map but got " + actual.getClass().getName());
}
}
// compare fields
for (final Method expectedGetter : expectedClass.getMethods()) {
if (expectedGetter.getDeclaringClass() != Object.class &&
expectedGetter.getParameterCount() == 0 &&
(expectedGetter.getName().startsWith("get") || expectedGetter.getName().startsWith("is"))) {
final Object expectedValue;
try {
expectedValue = expectedGetter.invoke(expected);
} catch (final IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Could not invoke expectedGetter " + expectedGetter.getName(), e);
}
var actualGetter = getActualGetter(expectedGetter, expectedClass, actualClass);
final Object actualValue;
try {
actualValue = actualGetter.invoke(actual);
} catch (final IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Could not invoke expectedGetter " + expectedGetter.getName(), e);
}
log.debug("Comparing actual={} vs expected={}: expectedGetter {}", actualValue, expectedValue, expectedGetter.getName());
path.addLast("." + expectedGetter.getName() + "()");
assertEquals(actualValue, expectedValue, path);
path.removeLast();
}
}
}
private static Method getActualGetter(final Method expectedGetter, final Class<?> expectedClass, final Class<?> actualClass) {
Method actualGetter;
if (expectedClass == actualClass) {
actualGetter = expectedGetter;
} else {
try {
actualGetter = actualClass.getMethod(expectedGetter.getName());
} catch (final NoSuchMethodException e) {
try {
actualGetter = actualClass.getMethod(expectedGetter.getName().startsWith("get")
? expectedGetter.getName().replaceFirst("get", "is")
: expectedGetter.getName().replaceFirst("is", "get"));
} catch (final NoSuchMethodException nop) {
throw new RuntimeException(
"Could not find expectedGetter " + expectedGetter.getName() + " in " + actualClass.getName(), e);
}
}
}
return actualGetter;
}
protected enum EventType {
CREATED("CreatedEvent"),
UPDATED("UpdatedEvent"),
DELETED("DeletedEvent");
private static final Map<EventTypeKey, Class<?>> EVENT_TYPE_MAP = new ConcurrentHashMap<>();
private final String suffix;
EventType(String suffix) {
this.suffix = suffix;
}
private Class<?> getEventType(final Class<?> entityType) {
return EVENT_TYPE_MAP.computeIfAbsent(
new EventTypeKey(entityType, this),
key -> {
try {
return Class.forName(
"org.eclipse.hawkbit.repository.event.remote." + (this == DELETED ? "" : "entity.") +
entityType.getSimpleName() +
suffix);
} catch (final ClassNotFoundException e) {
throw new IllegalStateException(
"Could not find event class for " + entityType.getName() + " and event type " + this, e);
}
});
}
private record EventTypeKey(Class<?> entityClass, EventType eventType) {}
}
private record RepoMan<T extends BaseEntity, C, U extends Identifiable<Long>>(
RepositoryManagement<T, C, U> repo,
Class<T> entityType,
Class<C> createType,
Class<U> updateType,
Function<Class<?>, ?> entityFactory) {}
}

View File

@@ -0,0 +1,163 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.management;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import lombok.SneakyThrows;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.MetadataSupport;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@SuppressWarnings("java:S119") // java:S119 - better self explainable
public abstract class AbstractRepositoryManagementWithMetadataTest<T extends BaseEntity, C, U extends Identifiable<Long>, MV, MVI extends MV>
extends AbstractRepositoryManagementTest<T, C, U> {
protected MetadataSupport<MV> metadataSupport; // repository management, just casted to MetadataSupport
private Class<MVI> metadataValueImplType;
private int maxMetaDataEntries;
@SneakyThrows
@SuppressWarnings("unchecked")
@BeforeEach
@Override
void setup() {
super.setup();
synchronized (AbstractRepositoryManagementWithMetadataTest.class) {
if (metadataSupport == null) {
setMetadataSupport();
final Type[] genericTypes = genericTypes(getClass(), AbstractRepositoryManagementWithMetadataTest.class);
metadataValueImplType = (Class<MVI>) genericTypes[4];
maxMetaDataEntries = (Integer)QuotaManagement.class.getMethod("getMaxMetaDataEntriesPer" + getEntityType().getSimpleName()).invoke(quotaManagement);
}
}
}
@SuppressWarnings("unchecked")
protected void setMetadataSupport() {
metadataSupport = (MetadataSupport<MV>) repositoryManagement;
}
@Test
void createMetadata() {
final String key = forType(String.class);
final MV createValue = getCreateValue();
// since based on counter, forType returns always different values (unless boolean), so this is different value
final MV updateValue = getCreateValue();
// create an entity
final T instance = instance();
// initial opt lock revision must be 1
assertThat(instance.getOptLockRevision()).isEqualTo(1);
// create an entity meta data entry
waitNextMillis(); // wait that last modified at is different so the opt lock revision is increased
metadataSupport.createMetadata(instance.getId(), key, createValue);
final T createResult = repositoryManagement.get(instance.getId());
assertThat(createResult.getOptLockRevision()).isEqualTo(2);
assertThat(createResult.getLastModifiedAt()).isPositive();
T reloaded = repositoryManagement.get(instance.getId());
assertThat(reloaded.getOptLockRevision()).isEqualTo(2);
assertThat(reloaded.getLastModifiedAt()).isPositive();
// update the entity metadata
waitNextMillis(); // wait that last modified at is different so the opt lock revision is increased
metadataSupport.createMetadata(instance.getId(), key, updateValue);
reloaded = repositoryManagement.get(instance.getId());
assertThat(reloaded.getOptLockRevision()).isEqualTo(3);
assertThat(reloaded.getLastModifiedAt()).isPositive();
// verify updated metadata is with the updated value
assertEquals(metadataSupport.getMetadata(instance.getId()).get(key), updateValue);
}
@Test
void getMetadata() {
final T instance = instance();
for (int i = 0; i < maxMetaDataEntries; i++) {
metadataSupport.createMetadata(instance.getId(), "key" + i, getCreateValue());
}
final T instance2 = instance();
for (int i = 0; i < maxMetaDataEntries - 1; i++) {
metadataSupport.createMetadata(instance2.getId(), "key" + i, getCreateValue());
}
assertThat(metadataSupport.getMetadata(instance.getId())).hasSize(maxMetaDataEntries);
assertThat(metadataSupport.getMetadata(instance2.getId())).hasSize(maxMetaDataEntries - 1);
}
@Test
void deleteMetadata() {
final T instance = instance();
for (int i = 0; i < maxMetaDataEntries; i++) {
metadataSupport.createMetadata(instance.getId(), "key" + i, getCreateValue());
}
assertThat(metadataSupport.getMetadata(instance.getId())).hasSize(maxMetaDataEntries);
for (int i = 0; i < 2; i++) {
metadataSupport.deleteMetadata(instance.getId(), "key" + i);
}
assertThat(metadataSupport.getMetadata(instance.getId())).hasSize(maxMetaDataEntries - 2);
}
@Test
void failIfMetadataQuotaExceeded() {
// add metadata one by one
final Long id1 = instance().getId();
for (int i = 0; i < maxMetaDataEntries; ++i) {
metadataSupport.createMetadata(id1, "k" + i, getCreateValue());
}
// verify quota exceeded exception
final MV exceedValue = getCreateValue();
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> metadataSupport.createMetadata(id1, "k" + maxMetaDataEntries, exceedValue));
// add multiple meta data entries at once
final T instance2 = instance();
final Map<String, MV> metaData2 = new HashMap<>();
for (int i = 0; i < maxMetaDataEntries + 1; ++i) {
metaData2.put("k" + i, getCreateValue());
}
// verify quota exceeded exception
final Long id2 = instance2.getId();
assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(() -> metadataSupport.createMetadata(id2, metaData2));
// add some meta data entries
final int firstHalf = Math.round((maxMetaDataEntries) / 2.f);
final Long id3 = instance().getId();
for (int i = 0; i < firstHalf; ++i) {
metadataSupport.createMetadata(id3, "k" + i, getCreateValue());
}
// add too many data entries
final int secondHalf = maxMetaDataEntries - firstHalf;
final Map<String, MV> metaData3 = new HashMap<>();
for (int i = 0; i < secondHalf + 1; ++i) {
metaData3.put("kk" + i, getCreateValue());
}
// verify quota is exceeded
assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(() -> metadataSupport.createMetadata(id3, metaData3));
}
private MVI getCreateValue() {
return forType(metadataValueImplType);
}
}

View File

@@ -16,23 +16,21 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import jakarta.validation.ConstraintViolationException;
import org.assertj.core.api.Condition;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement.Create;
import org.eclipse.hawkbit.repository.DistributionSetManagement.Update;
import org.eclipse.hawkbit.repository.DistributionSetTagManagement;
import org.eclipse.hawkbit.repository.DistributionSetTypeManagement;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
@@ -47,16 +45,12 @@ import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetExcepti
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
import org.eclipse.hawkbit.repository.exception.LockedException;
import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.ActionCancellationType;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetFilter;
import org.eclipse.hawkbit.repository.model.DistributionSetFilter.DistributionSetFilterBuilder;
import org.eclipse.hawkbit.repository.model.DistributionSetInvalidation;
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.NamedVersionedEntity;
import org.eclipse.hawkbit.repository.model.Rollout;
@@ -65,7 +59,6 @@ import org.eclipse.hawkbit.repository.model.Statistic;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -75,12 +68,12 @@ import org.springframework.beans.factory.annotation.Autowired;
* Feature: Component Tests - Repository<br/>
* Story: DistributionSet Management
*/
class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
class DistributionSetManagementTest extends AbstractRepositoryManagementWithMetadataTest<DistributionSet, Create, Update, String, String> {
private static final String TAG1_NAME = "Tag1";
@Autowired
RepositoryProperties repositoryProperties;
private RepositoryProperties repositoryProperties;
/**
* Verifies that management get access react as specified on calls for non existing entities by means of Optional not present.
@@ -145,7 +138,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
verifyThrownExceptionBy(() -> distributionSetManagement.getMetadata(NOT_EXIST_IDL), "DistributionSet");
verifyThrownExceptionBy(
() -> distributionSetManagement.update(DistributionSetManagement.Update.builder().id(NOT_EXIST_IDL).build()),
() -> distributionSetManagement.update(Update.builder().id(NOT_EXIST_IDL).build()),
"DistributionSet");
verifyThrownExceptionBy(() -> distributionSetManagement.createMetadata(NOT_EXIST_IDL, "xxx", "xxx"), "DistributionSet");
@@ -186,7 +179,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
@Test
void createDistributionSetWithImplicitType() {
final DistributionSet set = distributionSetManagement
.create(DistributionSetManagement.Create.builder().type(defaultDsType()).name("newtypesoft").version("1").build());
.create(Create.builder().type(defaultDsType()).name("newtypesoft").version("1").build());
assertThat(set.getType())
.as("Type should be equal to default type of tenant")
@@ -199,8 +192,8 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
*/
@Test
void createDistributionSetWithDuplicateNameAndVersionFails() {
final DistributionSetManagement.Create distributionSetCreate =
DistributionSetManagement.Create.builder().type(defaultDsType()).name("newtypesoft").version("1").build();
final Create distributionSetCreate =
Create.builder().type(defaultDsType()).name("newtypesoft").version("1").build();
distributionSetManagement.create(distributionSetCreate);
assertThatExceptionOfType(EntityAlreadyExistsException.class).isThrownBy(() -> distributionSetManagement.create(distributionSetCreate));
@@ -211,9 +204,9 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
*/
@Test
void createMultipleDistributionSetsWithImplicitType() {
final List<DistributionSetManagement.Create> creates = new ArrayList<>(10);
final List<Create> creates = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
creates.add(DistributionSetManagement.Create.builder().type(defaultDsType()).name("newtypesoft" + i).version("1" + i).build());
creates.add(Create.builder().type(defaultDsType()).name("newtypesoft" + i).version("1" + i).build());
}
assertThat(distributionSetManagement.create(creates))
@@ -227,52 +220,6 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
});
}
/**
* Verifies the enforcement of the metadata quota per distribution set.
*/
@Test
void createMetadataUntilQuotaIsExceeded() {
// add meta data one by one
final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1");
final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerDistributionSet();
for (int i = 0; i < maxMetaData; ++i) {
insertMetadata("k" + i, "v" + i, ds1);
}
// quota exceeded
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> insertMetadata("k" + maxMetaData, "v" + maxMetaData, ds1));
// add multiple meta data entries at once
final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2");
final Map<String, String> metaData2 = new HashMap<>();
for (int i = 0; i < maxMetaData + 1; ++i) {
metaData2.put("k" + i, "v" + i);
}
// verify quota is exceeded
final Long ds2Id = ds2.getId();
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> distributionSetManagement.createMetadata(ds2Id, metaData2));
// add some meta data entries
final DistributionSet ds3 = testdataFactory.createDistributionSet("ds3");
final int firstHalf = Math.round((maxMetaData) / 2.f);
final Long ds3Id = ds3.getId();
for (int i = 0; i < firstHalf; ++i) {
insertMetadata("k" + i, "v" + i, ds3);
}
// add too many data entries
final int secondHalf = maxMetaData - firstHalf;
final Map<String, String> metaData3 = new HashMap<>();
for (int i = 0; i < secondHalf + 1; ++i) {
metaData3.put("kk" + i, "vv" + i);
}
// verify quota is exceeded
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> distributionSetManagement.createMetadata(ds3Id, metaData3));
}
/**
* Ensures that distribution sets can assigned and unassigned to a distribution set tag.
*/
@@ -292,7 +239,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
.as("ds has wrong tag size")
.hasSize(1));
final DistributionSetTag findDistributionSetTag = getOrThrow(distributionSetTagManagement.find(tag.getId()));
final DistributionSetTag findDistributionSetTag = distributionSetTagManagement.get(tag.getId());
assertThat(assignedDS)
.as("assigned ds has wrong size")
@@ -330,7 +277,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
// assign target
assignDistributionSet(ds.getId(), target.getControllerId());
ds = getOrThrow(distributionSetManagement.getWithDetails(ds.getId()));
ds = distributionSetManagement.getWithDetails(ds.getId()).orElseThrow();
final Long dsId = ds.getId();
// not allowed as it is assigned now
@@ -338,7 +285,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
.isInstanceOf(EntityReadOnlyException.class);
// not allowed as it is assigned now
final Long appId = getOrThrow(findFirstModuleByType(ds, appType)).getId();
final Long appId = findFirstModuleByType(ds, appType).map(Identifiable::getId).orElseThrow();
assertThatThrownBy(() -> distributionSetManagement.unassignSoftwareModule(dsId, appId))
.isInstanceOf(EntityReadOnlyException.class);
}
@@ -349,7 +296,7 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
@Test
void updateDistributionSetUnsupportedModuleFails() {
final Long setId = distributionSetManagement.create(
DistributionSetManagement.Create.builder()
Create.builder()
.type(distributionSetTypeManagement.create(
DistributionSetTypeManagement.Create.builder()
.key("test")
@@ -375,45 +322,62 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
* Legal updates of a DS, e.g. name or description and module addition, removal while still unassigned.
*/
@Test
void updateDistributionSet() {
void assignAndUnassignSm() {
// prepare data
DistributionSet ds = testdataFactory.createDistributionSet("");
final DistributionSet ds = testdataFactory.createDistributionSet("");
final SoftwareModule os = testdataFactory.createSoftwareModuleOs();
// update data
// legal update of module addition
// legal update of module - addition
distributionSetManagement.assignSoftwareModules(ds.getId(), Set.of(os.getId()));
ds = getOrThrow(distributionSetManagement.getWithDetails(ds.getId()));
assertThat(getOrThrow(findFirstModuleByType(ds, osType))).isEqualTo(os);
assertThat(findFirstModuleByType(distributionSetManagement.get(ds.getId()), osType)).hasValue(os);
// legal update of module removal
distributionSetManagement.unassignSoftwareModule(ds.getId(),
getOrThrow(findFirstModuleByType(ds, appType)).getId());
ds = getOrThrow(distributionSetManagement.getWithDetails(ds.getId()));
assertThat(findFirstModuleByType(ds, appType)).isNotPresent();
// Update description
distributionSetManagement.update(DistributionSetManagement.Update.builder().id(ds.getId()).name("a new name")
.description("a new description").version("a new version").requiredMigrationStep(true).build());
ds = getOrThrow(distributionSetManagement.getWithDetails(ds.getId()));
assertThat(ds.getDescription()).isEqualTo("a new description");
assertThat(ds.getName()).isEqualTo("a new name");
assertThat(ds.getVersion()).isEqualTo("a new version");
assertThat(ds.isRequiredMigrationStep()).isTrue();
// legal update of module - removal
distributionSetManagement.unassignSoftwareModule(ds.getId(), findFirstModuleByType(ds, appType).map(Identifiable::getId).orElseThrow());
assertThat(findFirstModuleByType(distributionSetManagement.get(ds.getId()), appType)).isNotPresent();
}
/**
* Verifies that an exception is thrown when trying to update an invalid distribution set
*/
@Test
void updateInvalidDistributionSet() {
void failToUpdateInvalidDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet();
final DistributionSetManagement.Update update =
DistributionSetManagement.Update.builder().id(distributionSet.getId()).name("new_name").build();
final Update update =
Update.builder().id(distributionSet.getId()).name("new_name").build();
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Invalid distributionSet should throw an exception").isThrownBy(() -> distributionSetManagement.update(update));
}
@Test
void failToModifyMetadataForInvalidDistributionSet() {
final String key = forType(String.class);
final Long instanceId = instance().getId();
distributionSetManagement.createMetadata(instanceId, Map.of(key, forType(String.class)));
distributionSetManagement.invalidate(distributionSetManagement.get(instanceId));
// assert that no new metadata can be created
final String key2 = forType(String.class);
final String createValue = forType(String.class);
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Create metadata of an invalid entity should throw an exception")
.isThrownBy(() -> distributionSetManagement.createMetadata(instanceId, key2, createValue));
final Map<String, String> createMetadata = Map.of(key2, createValue);
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Create metadata of an invalid entity should throw an exception")
.isThrownBy(() -> distributionSetManagement.createMetadata(instanceId, createMetadata));
// assert that an existing metadata can not be updated
final String updateValue = forType(String.class);
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Update metadata of an invalid entity should throw an exception")
.isThrownBy(() -> distributionSetManagement.createMetadata(instanceId, key, updateValue));
final Map<String, String> updateMetadata = Map.of(key, updateValue);
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Update metadata of an invalid entity should throw an exception")
.isThrownBy(() -> distributionSetManagement.createMetadata(instanceId, updateMetadata));
}
/**
* Verifies the enforcement of the software module quota per distribution set.
@@ -489,111 +453,6 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
.unassignSoftwareModule(distributionSetId, softwareModuleId));
}
/**
* Checks that metadata for a distribution set can be updated.
*/
@Test
@WithUser(allSpPermissions = true)
void createMetadata() {
final String knownKey = "myKnownKey";
final String knownValue = "myKnownValue";
final String knownUpdateValue = "myNewUpdatedValue";
// create a DS
final DistributionSet ds = testdataFactory.createDistributionSet("testDs");
// initial opt lock revision must be zero
assertThat(ds.getOptLockRevision()).isEqualTo(1);
waitNextMillis();
// create an DS meta data entry
insertMetadata(knownKey, knownValue, ds);
final DistributionSet changedLockRevisionDS = getOrThrow(distributionSetManagement.find(ds.getId()));
assertThat(changedLockRevisionDS.getOptLockRevision()).isEqualTo(2);
waitNextMillis();
// update the DS metadata
distributionSetManagement.createMetadata(ds.getId(), knownKey, knownUpdateValue);
// we are updating the sw metadata so also modifying the base software
// module so opt lock revision must be three
final DistributionSet reloadedDS = getOrThrow(distributionSetManagement.find(ds.getId()));
assertThat(reloadedDS.getOptLockRevision()).isEqualTo(3);
assertThat(reloadedDS.getLastModifiedAt()).isPositive();
// verify updated meta data is the updated value
assertThat(distributionSetManagement.getMetadata(ds.getId())).containsEntry(knownKey, knownUpdateValue);
}
/**
* searches for distribution sets based on the various filter options, e.g. name, version, desc., tags.
*/
@Test
void searchDistributionSetsOnFilters() {
DistributionSetTag dsTagA = distributionSetTagManagement
.create(DistributionSetTagManagement.Create.builder().name("DistributionSetTag-A").build());
final DistributionSetTag dsTagB = distributionSetTagManagement
.create(DistributionSetTagManagement.Create.builder().name("DistributionSetTag-B").build());
final DistributionSetTag dsTagC = distributionSetTagManagement
.create(DistributionSetTagManagement.Create.builder().name("DistributionSetTag-C").build());
distributionSetTagManagement.create(DistributionSetTagManagement.Create.builder().name("DistributionSetTag-D").build());
List<? extends DistributionSet> dsGroup1 = testdataFactory.createDistributionSets("", 5);
final String dsGroup2Prefix = "test";
List<? extends DistributionSet> dsGroup2 = testdataFactory.createDistributionSets(dsGroup2Prefix, 5);
DistributionSet dsDeleted = testdataFactory.createDistributionSet("testDeleted");
final DistributionSet dsInComplete = distributionSetManagement.create(
DistributionSetManagement.Create.builder()
.name("notcomplete").version("1").type(standardDsType).build());
DistributionSetType newType = distributionSetTypeManagement
.create(DistributionSetTypeManagement.Create.builder().key("foo").name("bar").description("test").build());
distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(newType.getId(),
singletonList(osType.getId()));
newType = distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(newType.getId(),
Arrays.asList(appType.getId(), runtimeType.getId()));
final DistributionSet dsNewType = distributionSetManagement.create(
DistributionSetManagement.Create.builder()
.type(newType)
.name("newtype").version("1")
.modules(new HashSet<>(dsDeleted.getModules()))
.build());
assignDistributionSet(dsDeleted, testdataFactory.createTargets(5));
distributionSetManagement.delete(dsDeleted.getId());
dsDeleted = getOrThrow(distributionSetManagement.find(dsDeleted.getId()));
dsGroup1 = assignTag(dsGroup1, dsTagA);
dsTagA = getOrThrow(distributionSetTagRepository.findById(dsTagA.getId()));
dsGroup1 = assignTag(dsGroup1, dsTagB);
dsTagA = getOrThrow(distributionSetTagRepository.findById(dsTagA.getId()));
dsGroup2 = assignTag(dsGroup2, dsTagA);
dsTagA = getOrThrow(distributionSetTagRepository.findById(dsTagA.getId()));
final List<? extends DistributionSet> allDistributionSets = Stream
.of(dsGroup1, dsGroup2, Arrays.asList(dsDeleted, dsInComplete, dsNewType)).flatMap(Collection::stream)
.toList();
final List<? extends DistributionSet> dsGroup1WithGroup2 = Stream.of(dsGroup1, dsGroup2).flatMap(Collection::stream)
.toList();
final int sizeOfAllDistributionSets = allDistributionSets.size();
// check setup
assertThat(distributionSetRepository.findAll()).hasSize(sizeOfAllDistributionSets);
validateFindAll(allDistributionSets);
validateDeleted(dsDeleted, sizeOfAllDistributionSets - 1);
validateCompleted(dsInComplete, sizeOfAllDistributionSets - 1);
validateType(newType, dsNewType, sizeOfAllDistributionSets - 1);
validateSearchText(allDistributionSets, dsGroup2Prefix);
validateTags(dsTagA, dsTagB, dsTagC, dsGroup1WithGroup2, dsGroup1);
validateDeletedAndCompleted(dsGroup1WithGroup2, dsNewType, dsDeleted);
validateDeletedAndCompletedAndType(dsGroup1WithGroup2, dsDeleted, newType, dsNewType);
validateDeletedAndCompletedAndTypeAndSearchText(dsGroup2, newType, dsGroup2Prefix);
validateDeletedAndCompletedAndTypeAndSearchText(dsGroup1WithGroup2, dsDeleted, dsInComplete, dsNewType, newType, ":1");
validateDeletedAndCompletedAndTypeAndSearchTextAndTag(dsGroup2, dsTagA, dsGroup2Prefix);
}
/**
* Locks a DS.
*/
@@ -688,10 +547,9 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
/**
* Test implicit locks for a DS and skip tags.
*/
@SuppressWarnings("rawtypes")
@Test
void shouldLockImplicitlyForDistributionSet() {
final JpaDistributionSetManagement distributionSetManagement = (JpaDistributionSetManagement) (DistributionSetManagement) this.distributionSetManagement;
final JpaDistributionSetManagement distributionSetManagement = (JpaDistributionSetManagement) this.distributionSetManagement;
final DistributionSet distributionSet = testdataFactory.createDistributionSet("ds-non-skip");
// assert that implicit lock is applicable for non skip tags
assertThat(distributionSetManagement.shouldLockImplicitly(distributionSet)).isTrue();
@@ -703,7 +561,8 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
// remove same in case-insensitive terms tags
// in of case-insensitive db's it will end up as same names and constraint violation (?)
.distinct()
.map(skipTag -> (DistributionSetTagManagement.Create)DistributionSetTagManagement.Create.builder().name(skipTag).build())
.map(skipTag -> (DistributionSetTagManagement.Create) DistributionSetTagManagement.Create.builder().name(skipTag)
.build())
.toList());
// assert that implicit lock locks for every skip tag
skipTags.forEach(skipTag -> {
@@ -729,21 +588,6 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
.isFalse();
}
/**
* Deletes a DS that is no in use. Expected behaviour is a hard delete on the database.
*/
@Test
void deleteUnassignedDistributionSet() {
final DistributionSet ds1 = testdataFactory.createDistributionSet("ds-1");
testdataFactory.createDistributionSet("ds-2");
// delete a ds
assertThat(distributionSetRepository.findAll()).hasSize(2);
distributionSetManagement.delete(ds1.getId());
// not assigned so not marked as deleted but fully deleted
assertThat(distributionSetRepository.findAll()).hasSize(1);
}
/**
* Deletes an invalid distribution set
*/
@@ -766,27 +610,6 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
assertThat(distributionSetRepository.findById(set.getId())).isEmpty();
}
/**
* Queries and loads the metadata related to a given distribution set.
*/
@Test
void getMetadata() {
// create a DS
final DistributionSet ds1 = testdataFactory.createDistributionSet("testDs1");
final DistributionSet ds2 = testdataFactory.createDistributionSet("testDs2");
for (int index = 0; index < quotaManagement.getMaxMetaDataEntriesPerDistributionSet(); index++) {
insertMetadata("key" + index, "value" + index, ds1);
}
for (int index = 0; index <= quotaManagement.getMaxMetaDataEntriesPerDistributionSet() - 2; index++) {
insertMetadata("key" + index, "value" + index, ds2);
}
assertThat(distributionSetManagement.getMetadata(ds1.getId())).hasSize(quotaManagement.getMaxMetaDataEntriesPerDistributionSet());
assertThat(distributionSetManagement.getMetadata(ds2.getId())).hasSize(quotaManagement.getMaxMetaDataEntriesPerDistributionSet() - 1);
}
/**
* Deletes a DS that is in use by either target assignment or rollout. Expected behaviour is a soft delete on the database, i.e. only marked as
* deleted, kept as reference but unavailable for future use..
@@ -862,33 +685,6 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
.isThrownBy(() -> distributionSetManagement.getValidAndComplete(distributionSetId));
}
/**
* Verify that an exception is thrown when trying to create or update metadata for an invalid distribution set.
*/
@Test
void createMetadataForInvalidDistributionSet() {
final String knownKey1 = "myKnownKey1";
final String knownKey2 = "myKnownKey2";
final String knownValue = "myKnownValue";
final String knownUpdateValue = "knownUpdateValue";
final Long dsId = testdataFactory.createDistributionSet().getId();
distributionSetManagement.createMetadata(dsId, Map.of(knownKey1, knownValue));
distributionSetInvalidationManagement.invalidateDistributionSet(
new DistributionSetInvalidation(singletonList(dsId), ActionCancellationType.NONE));
// assert that no new metadata can be created
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Invalid distributionSet should throw an exception")
.isThrownBy(() -> distributionSetManagement.createMetadata(dsId, Map.of(knownKey2, knownValue)));
// assert that an existing metadata can not be updated
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Invalid distributionSet should throw an exception")
.isThrownBy(() -> distributionSetManagement.createMetadata(dsId, knownKey1, knownUpdateValue));
}
/**
* Get the Rollouts count by status statistics for a specific Distribution Set
*/
@@ -964,286 +760,86 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest {
}
private void createAndUpdateDistributionSetWithInvalidDescription(final DistributionSet set) {
final DistributionSetManagement.Create distributionSetCreate =
DistributionSetManagement.Create.builder().name("a").version("a").description(randomString(513)).build();
final Create distributionSetCreate =
Create.builder().name("a").version("a").description(randomString(513)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too long description should not be created")
.isThrownBy(() -> distributionSetManagement.create(distributionSetCreate));
final DistributionSetManagement.Create distributionSetCreate2 =
DistributionSetManagement.Create.builder().name("a").version("a").description(INVALID_TEXT_HTML).build();
final Create distributionSetCreate2 =
Create.builder().name("a").version("a").description(INVALID_TEXT_HTML).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with invalid description should not be created")
.isThrownBy(() -> distributionSetManagement.create(distributionSetCreate2));
final DistributionSetManagement.Update distributionSetUpdate =
DistributionSetManagement.Update.builder().id(set.getId()).description(randomString(513)).build();
final Update distributionSetUpdate =
Update.builder().id(set.getId()).description(randomString(513)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too long description should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate));
final DistributionSetManagement.Update distributionSetUpdate2 =
DistributionSetManagement.Update.builder().id(set.getId()).description(INVALID_TEXT_HTML).build();
final Update distributionSetUpdate2 =
Update.builder().id(set.getId()).description(INVALID_TEXT_HTML).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with invalid characters should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate2));
}
private void createAndUpdateDistributionSetWithInvalidName(final DistributionSet set) {
final DistributionSetManagement.Create distributionSetCreate = DistributionSetManagement.Create.builder()
final Create distributionSetCreate = Create.builder()
.version("a").name(randomString(NamedEntity.NAME_MAX_SIZE + 1)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too long name should not be created")
.isThrownBy(() -> distributionSetManagement.create(distributionSetCreate));
final DistributionSetManagement.Create distributionSetCreate2 = DistributionSetManagement.Create.builder().version("a").name("").build();
final Create distributionSetCreate2 = Create.builder().version("a").name("").build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too short name should not be created")
.isThrownBy(() -> distributionSetManagement.create(distributionSetCreate2));
final DistributionSetManagement.Create distributionSetCreate3 = DistributionSetManagement.Create.builder().version("a").name(INVALID_TEXT_HTML).build();
final Create distributionSetCreate3 = Create.builder().version("a").name(INVALID_TEXT_HTML).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with invalid characters in name should not be created")
.isThrownBy(() -> distributionSetManagement.create(distributionSetCreate3));
final DistributionSetManagement.Update distributionSetUpdate = DistributionSetManagement.Update.builder().id(set.getId())
final Update distributionSetUpdate = Update.builder().id(set.getId())
.name(randomString(NamedEntity.NAME_MAX_SIZE + 1)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too long name should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate));
final DistributionSetManagement.Update distributionSetUpdate2 = DistributionSetManagement.Update.builder().id(set.getId()).name(INVALID_TEXT_HTML).build();
final Update distributionSetUpdate2 = Update.builder().id(set.getId()).name(INVALID_TEXT_HTML).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with invalid characters should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate2));
final DistributionSetManagement.Update distributionSetUpdate3 = DistributionSetManagement.Update.builder().id(set.getId()).name("").build();
final Update distributionSetUpdate3 = Update.builder().id(set.getId()).name("").build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too short name should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate3));
}
private void createAndUpdateDistributionSetWithInvalidVersion(final DistributionSet set) {
final DistributionSetManagement.Create distributionSetCreate = DistributionSetManagement.Create.builder()
final Create distributionSetCreate = Create.builder()
.name("a").version(randomString(NamedVersionedEntity.VERSION_MAX_SIZE + 1)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too long version should not be created")
.isThrownBy(() -> distributionSetManagement.create(distributionSetCreate));
final DistributionSetManagement.Create distributionSetCreate2 = DistributionSetManagement.Create.builder().name("a").version("").build();
final Create distributionSetCreate2 = Create.builder().name("a").version("").build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too short version should not be created")
.isThrownBy(() -> distributionSetManagement.create(distributionSetCreate2));
final DistributionSetManagement.Update distributionSetUpdate = DistributionSetManagement.Update.builder().id(set.getId())
final Update distributionSetUpdate = Update.builder().id(set.getId())
.version(randomString(NamedVersionedEntity.VERSION_MAX_SIZE + 1)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too long version should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate));
final DistributionSetManagement.Update distributionSetUpdate2 = DistributionSetManagement.Update.builder().id(set.getId()).version("").build();
final Update distributionSetUpdate2 = Update.builder().id(set.getId()).version("").build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("entity with too short version should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate2));
}
private void validateFindAll(final List<? extends DistributionSet> expectedDistributionSets) {
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder(), expectedDistributionSets);
}
private void validateDeleted(final DistributionSet deletedDistributionSet, final int notDeletedSize) {
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().isDeleted(Boolean.TRUE),
singletonList(deletedDistributionSet));
assertThatFilterHasSizeAndDoesNotContainDistributionSet(
DistributionSetFilter.builder().isDeleted(Boolean.FALSE), notDeletedSize, deletedDistributionSet);
}
private void validateCompleted(final DistributionSet dsIncomplete, final int completedSize) {
assertThatFilterHasSizeAndDoesNotContainDistributionSet(
DistributionSetFilter.builder().isComplete(Boolean.TRUE), completedSize, dsIncomplete);
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().isComplete(Boolean.FALSE), singletonList(dsIncomplete));
}
private void validateType(final DistributionSetType newType, final DistributionSet dsNewType, final int standardDsTypeSize) {
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().typeId(newType.getId()),
singletonList(dsNewType));
assertThatFilterHasSizeAndDoesNotContainDistributionSet(
DistributionSetFilter.builder().typeId(standardDsType.getId()), standardDsTypeSize, dsNewType);
}
private void validateSearchText(final List<? extends DistributionSet> allDistributionSets, final String dsNamePrefix) {
final List<? extends DistributionSet> withTestNamePrefix = allDistributionSets.stream()
.filter(ds -> ds.getName().startsWith(dsNamePrefix)).toList();
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().searchText(dsNamePrefix),
withTestNamePrefix);
final List<? extends DistributionSet> withTestNameExact = withTestNamePrefix.stream()
.filter(ds -> ds.getName().equals(dsNamePrefix)).toList();
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().searchText(dsNamePrefix + ":"), withTestNameExact);
final List<? extends DistributionSet> withTestNameExactAndVersionPrefix = withTestNameExact.stream()
.filter(ds -> ds.getVersion().startsWith("1")).toList();
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().searchText(dsNamePrefix + ":1"),
withTestNameExactAndVersionPrefix);
final List<? extends DistributionSet> dsWithExactNameAndVersion = withTestNameExactAndVersionPrefix.stream()
.filter(ds -> ds.getVersion().equals("1.0.0")).toList();
assertThat(dsWithExactNameAndVersion).hasSize(1);
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().searchText(dsNamePrefix + ":1.0.0"), dsWithExactNameAndVersion);
final List<? extends DistributionSet> withVersionPrefix = allDistributionSets.stream()
.filter(ds -> ds.getVersion().startsWith("1.0.")).toList();
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().searchText(":1.0."),
withVersionPrefix);
final List<? extends DistributionSet> withVersionExact = withVersionPrefix.stream()
.filter(ds -> ds.getVersion().equals("1.0.0")).toList();
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().searchText(":1.0.0"), withVersionExact);
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().searchText(":"), allDistributionSets);
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().searchText(" : "), allDistributionSets);
}
private void validateTags(final DistributionSetTag dsTagA, final DistributionSetTag dsTagB,
final DistributionSetTag dsTagC, final List<? extends DistributionSet> dsWithTagA,
final List<? extends DistributionSet> dsWithTagB) {
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().tagNames(singletonList(dsTagA.getName())), dsWithTagA);
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().tagNames(singletonList(dsTagB.getName())), dsWithTagB);
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().tagNames(Arrays.asList(dsTagA.getName(), dsTagB.getName())),
dsWithTagA);
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().tagNames(Arrays.asList(dsTagC.getName(), dsTagB.getName())),
dsWithTagB);
assertThatFilterDoesNotContainAnyDistributionSet(
DistributionSetFilter.builder().tagNames(singletonList(dsTagC.getName())));
}
private void validateDeletedAndCompleted(final List<? extends DistributionSet> completedStandardType,
final DistributionSet dsNewType, final DistributionSet dsDeleted) {
final List<DistributionSet> completedNotDeleted = new ArrayList<>(completedStandardType);
completedNotDeleted.add(dsNewType);
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().isComplete(Boolean.TRUE).isDeleted(Boolean.FALSE),
completedNotDeleted);
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().isComplete(Boolean.TRUE).isDeleted(Boolean.TRUE),
singletonList(dsDeleted));
assertThatFilterDoesNotContainAnyDistributionSet(
DistributionSetFilter.builder().isComplete(Boolean.FALSE).isDeleted(Boolean.TRUE));
}
private void validateDeletedAndCompletedAndType(final List<? extends DistributionSet> deletedAndCompletedAndStandardType,
final DistributionSet dsDeleted, final DistributionSetType newType, final DistributionSet dsNewType) {
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().isDeleted(Boolean.FALSE)
.isComplete(Boolean.TRUE).typeId(standardDsType.getId()), deletedAndCompletedAndStandardType);
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().isComplete(Boolean.TRUE)
.typeId(standardDsType.getId()).isDeleted(Boolean.TRUE), singletonList(dsDeleted));
assertThatFilterDoesNotContainAnyDistributionSet(DistributionSetFilter.builder().isDeleted(Boolean.TRUE)
.isComplete(Boolean.FALSE).typeId(standardDsType.getId()));
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().isComplete(Boolean.TRUE).typeId(newType.getId()),
singletonList(dsNewType));
}
private void validateDeletedAndCompletedAndTypeAndSearchText(
final List<? extends DistributionSet> completedAndStandardTypeAndSearchText, final DistributionSetType newType, final String text) {
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().isDeleted(Boolean.FALSE)
.isComplete(Boolean.TRUE).typeId(standardDsType.getId()).searchText(text),
completedAndStandardTypeAndSearchText);
assertThatFilterDoesNotContainAnyDistributionSet(DistributionSetFilter.builder().isComplete(Boolean.TRUE)
.isDeleted(Boolean.TRUE).typeId(standardDsType.getId()).searchText(text + ":"));
assertThatFilterDoesNotContainAnyDistributionSet(
DistributionSetFilter.builder().typeId(standardDsType.getId()).searchText(text)
.isComplete(Boolean.FALSE).isDeleted(Boolean.FALSE));
assertThatFilterDoesNotContainAnyDistributionSet(DistributionSetFilter.builder().typeId(newType.getId())
.searchText(text).isComplete(Boolean.TRUE).isDeleted(Boolean.FALSE));
}
private void validateDeletedAndCompletedAndTypeAndSearchText(
final List<? extends DistributionSet> completedAndNotDeletedStandardTypeAndFilterString,
final DistributionSet dsDeleted, final DistributionSet dsInComplete, final DistributionSet dsNewType,
final DistributionSetType newType, final String filterString) {
final List<DistributionSet> completedAndStandardTypeAndFilterString = new ArrayList<>(
completedAndNotDeletedStandardTypeAndFilterString);
completedAndStandardTypeAndFilterString.add(dsDeleted);
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().isComplete(Boolean.TRUE)
.typeId(standardDsType.getId()).searchText(filterString),
completedAndStandardTypeAndFilterString);
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().isComplete(Boolean.TRUE).isDeleted(Boolean.FALSE)
.typeId(standardDsType.getId()).searchText(filterString),
completedAndNotDeletedStandardTypeAndFilterString);
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().isComplete(Boolean.TRUE)
.isDeleted(Boolean.TRUE).typeId(standardDsType.getId()).searchText(filterString),
singletonList(dsDeleted));
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().typeId(standardDsType.getId()).searchText(filterString)
.isComplete(Boolean.FALSE).isDeleted(Boolean.FALSE),
singletonList(dsInComplete));
assertThatFilterContainsOnlyGivenDistributionSets(DistributionSetFilter.builder().typeId(newType.getId())
.searchText(filterString).isComplete(Boolean.TRUE).isDeleted(Boolean.FALSE),
singletonList(dsNewType));
}
private void validateDeletedAndCompletedAndTypeAndSearchTextAndTag(
final List<? extends DistributionSet> completedAndStandartTypeAndSearchTextAndTagA, final DistributionSetTag dsTagA, final String text) {
assertThatFilterContainsOnlyGivenDistributionSets(
DistributionSetFilter.builder().isComplete(Boolean.TRUE).typeId(standardDsType.getId())
.searchText(text).tagNames(singletonList(dsTagA.getName())),
completedAndStandartTypeAndSearchTextAndTagA);
assertThatFilterDoesNotContainAnyDistributionSet(DistributionSetFilter.builder()
.typeId(standardDsType.getId()).searchText(text).tagNames(singletonList(dsTagA.getName()))
.isComplete(Boolean.FALSE).isDeleted(Boolean.FALSE));
}
private void insertMetadata(final String knownKey, final String knownValue, final DistributionSet distributionSet) {
distributionSetManagement.createMetadata(distributionSet.getId(), Map.of(knownKey, knownValue));
assertThat(distributionSetManagement.getMetadata(distributionSet.getId()).get(knownKey)).isEqualTo(knownValue);
}
private void assertThatFilterContainsOnlyGivenDistributionSets(final DistributionSetFilterBuilder filterBuilder,
final List<? extends DistributionSet> distributionSets) {
final int expectedDsSize = distributionSets.size();
assertThat(findDsByDistributionSetFilter(filterBuilder.build(), PAGE).getContent())
.hasSize(expectedDsSize).containsOnly(distributionSets.toArray(new JpaDistributionSet[expectedDsSize]));
}
private void assertThatFilterDoesNotContainAnyDistributionSet(final DistributionSetFilterBuilder filterBuilder) {
assertThat(findDsByDistributionSetFilter(filterBuilder.build(), PAGE).getContent()).isEmpty();
}
private void assertThatFilterHasSizeAndDoesNotContainDistributionSet(
final DistributionSetFilterBuilder filterBuilder, final int size, final DistributionSet ds) {
assertThat((List)findDsByDistributionSetFilter(filterBuilder.build(), PAGE).getContent()).hasSize(size).doesNotContain(ds);
}
// can be removed with java-11
private <T> T getOrThrow(final Optional<T> opt) {
return opt.orElseThrow(NoSuchElementException::new);
}
}

View File

@@ -14,27 +14,20 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import org.assertj.core.api.Assertions;
import org.eclipse.hawkbit.repository.DistributionSetTagManagement;
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTagUpdatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetTagUpdatedEvent;
import org.eclipse.hawkbit.repository.DistributionSetTagManagement.Create;
import org.eclipse.hawkbit.repository.DistributionSetTagManagement.Update;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetFilter;
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
import org.eclipse.hawkbit.repository.model.Tag;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Pageable;
/**
* {@link DistributionSetTagManagement} tests.
@@ -42,78 +35,40 @@ import org.springframework.data.domain.Pageable;
* Feature: Component Tests - Repository<br/>
* Story: DistributionSet Tag Management
*/
class DistributionSetTagManagementTest extends AbstractJpaIntegrationTest {
class DistributionSetTagManagementTest extends AbstractRepositoryManagementTest<DistributionSetTag, Create, Update> {
private static final String TAG_1 = "tag1";
private static final String TAG_DESCRIPTION_1 = "tag_description_1";
/**
* Verifies that management get access reacts as specified on calls for non existing entities by means of Optional not present.
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) })
void nonExistingEntityAccessReturnsNotPresent() {
assertThat(distributionSetTagManagement.find(NOT_EXIST_IDL)).isNotPresent();
}
void createAssignAndDeleteTags() {
final Collection<DistributionSet> dsAs = testdataFactory.createDistributionSets("DS-A", 1);
final Collection<DistributionSet> dsBs = testdataFactory.createDistributionSets("DS-B", 2);
final Collection<DistributionSet> dsCs = testdataFactory.createDistributionSets("DS-C", 4);
final Collection<DistributionSet> dsABs = testdataFactory.createDistributionSets("DS-AB", 8);
final Collection<DistributionSet> dsACs = testdataFactory.createDistributionSets("DS-AC", 16);
final Collection<DistributionSet> dsBCs = testdataFactory.createDistributionSets("DS-BC", 32);
final Collection<DistributionSet> dsABCs = testdataFactory.createDistributionSets("DS-ABC", 64);
/**
* Verifies that management queries react as specified on calls for non existing entities by means of throwing
* EntityNotFoundException.
*/
@Test
@ExpectEvents({
@Expect(type = DistributionSetTagUpdatedEvent.class, count = 0),
@Expect(type = TargetTagUpdatedEvent.class, count = 0) })
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
verifyThrownExceptionBy(() -> distributionSetTagManagement.delete(NOT_EXIST_IDL), "DistributionSetTag");
verifyThrownExceptionBy(() -> distributionSetTagManagement.update(
DistributionSetTagManagement.Update.builder().id(NOT_EXIST_IDL).build()), "DistributionSetTag");
}
/**
* Full DS tag lifecycle tested. Create tags, assign them to sets and delete the tags.
*/
@Test
void createAndAssignAndDeleteDistributionSetTags() {
final Collection<DistributionSet> dsAs = testdataFactory.createDistributionSets("DS-A", 20);
final Collection<DistributionSet> dsBs = testdataFactory.createDistributionSets("DS-B", 10);
final Collection<DistributionSet> dsCs = testdataFactory.createDistributionSets("DS-C", 25);
final Collection<DistributionSet> dsABs = testdataFactory.createDistributionSets("DS-AB", 5);
final Collection<DistributionSet> dsACs = testdataFactory.createDistributionSets("DS-AC", 11);
final Collection<DistributionSet> dsBCs = testdataFactory.createDistributionSets("DS-BC", 13);
final Collection<DistributionSet> dsABCs = testdataFactory.createDistributionSets("DS-ABC", 9);
final DistributionSetTag tagA = distributionSetTagManagement.create(DistributionSetTagManagement.Create.builder().name("A").build());
final DistributionSetTag tagB = distributionSetTagManagement.create(DistributionSetTagManagement.Create.builder().name("B").build());
final DistributionSetTag tagC = distributionSetTagManagement.create(DistributionSetTagManagement.Create.builder().name("C").build());
final DistributionSetTag tagX = distributionSetTagManagement.create(DistributionSetTagManagement.Create.builder().name("X").build());
final DistributionSetTag tagY = distributionSetTagManagement.create(DistributionSetTagManagement.Create.builder().name("Y").build());
final DistributionSetTag tagA = distributionSetTagManagement.create(Create.builder().name("A").build());
final DistributionSetTag tagB = distributionSetTagManagement.create(Create.builder().name("B").build());
final DistributionSetTag tagC = distributionSetTagManagement.create(Create.builder().name("C").build());
final DistributionSetTag tagX = distributionSetTagManagement.create(Create.builder().name("X").build());
final DistributionSetTag tagY = distributionSetTagManagement.create(Create.builder().name("Y").build());
assignTag(dsAs, tagA);
assignTag(dsBs, tagB);
assignTag(dsCs, tagC);
assignTag(dsABs, distributionSetTagManagement.find(tagA.getId()).orElseThrow());
assignTag(dsABs, distributionSetTagManagement.find(tagB.getId()).orElseThrow());
assignTag(dsACs, distributionSetTagManagement.find(tagA.getId()).orElseThrow());
assignTag(dsACs, distributionSetTagManagement.find(tagC.getId()).orElseThrow());
assignTag(dsBCs, distributionSetTagManagement.find(tagB.getId()).orElseThrow());
assignTag(dsBCs, distributionSetTagManagement.find(tagC.getId()).orElseThrow());
assignTag(dsABCs, distributionSetTagManagement.find(tagA.getId()).orElseThrow());
assignTag(dsABCs, distributionSetTagManagement.find(tagB.getId()).orElseThrow());
assignTag(dsABCs, distributionSetTagManagement.find(tagC.getId()).orElseThrow());
assignTags(dsABs, tagA, tagB);
assignTags(dsACs, tagA, tagC);
assignTags(dsBCs, tagB, tagC);
assignTags(dsABCs, tagA, tagB, tagC);
// search for not deleted
final DistributionSetFilter.DistributionSetFilterBuilder distributionSetFilterBuilder = getDistributionSetFilterBuilder()
.isComplete(true);
verifyExpectedFilteredDistributionSets(distributionSetFilterBuilder.tagNames(Arrays.asList(tagA.getName())),
Stream.of(dsAs, dsABs, dsACs, dsABCs));
verifyExpectedFilteredDistributionSets(distributionSetFilterBuilder.tagNames(Arrays.asList(tagB.getName())),
Stream.of(dsBs, dsABs, dsBCs, dsABCs));
verifyExpectedFilteredDistributionSets(distributionSetFilterBuilder.tagNames(Arrays.asList(tagC.getName())),
Stream.of(dsCs, dsACs, dsBCs, dsABCs));
verifyExpectedFilteredDistributionSets(distributionSetFilterBuilder.tagNames(Arrays.asList(tagX.getName())),
Stream.empty());
assertTaggedWithATagAreAsExpected(tagA, dsAs, dsABs, dsACs, dsABCs);
assertTaggedWithATagAreAsExpected(tagB, dsBs, dsABs, dsBCs, dsABCs);
assertTaggedWithATagAreAsExpected(tagC, dsCs, dsACs, dsBCs, dsABCs);
assertTaggedWithATagAreAsExpected(tagX);
assertThat(distributionSetTagRepository.findAll()).hasSize(5);
distributionSetTagManagement.delete(tagY.getId());
@@ -123,64 +78,58 @@ class DistributionSetTagManagementTest extends AbstractJpaIntegrationTest {
distributionSetTagManagement.delete(tagB.getId());
assertThat(distributionSetTagRepository.findAll()).hasSize(2);
verifyExpectedFilteredDistributionSets(distributionSetFilterBuilder.tagNames(Arrays.asList(tagA.getName())),
Stream.of(dsAs, dsABs, dsACs, dsABCs));
verifyExpectedFilteredDistributionSets(distributionSetFilterBuilder.tagNames(Arrays.asList(tagB.getName())),
Stream.empty());
verifyExpectedFilteredDistributionSets(distributionSetFilterBuilder.tagNames(Arrays.asList(tagC.getName())),
Stream.of(dsCs, dsACs, dsBCs, dsABCs));
assertTaggedWithATagAreAsExpected(tagA, dsAs, dsABs, dsACs, dsABCs);
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> assertTaggedWithATagAreAsExpected(tagB));
assertTaggedWithATagAreAsExpected(tagC, dsCs, dsACs, dsBCs, dsABCs);
}
/**
* Verifies assign/unassign.
*/
@Test
void assignAndUnassignDistributionSetTags() {
final Collection<DistributionSet> groupA = testdataFactory.createDistributionSets(20);
final Collection<DistributionSet> groupB = testdataFactory.createDistributionSets("unassigned", 20);
void assignAndUnassign() {
final DistributionSetTag tag = distributionSetTagManagement.create(Create.builder().name(TAG_1).description(TAG_DESCRIPTION_1).build());
final DistributionSetTag tag = distributionSetTagManagement
.create(DistributionSetTagManagement.Create.builder().name("tag1").description("tagdesc1").build());
final Collection<DistributionSet> groupA = testdataFactory.createDistributionSets("A_", 5);
final Collection<DistributionSet> groupB = testdataFactory.createDistributionSets("B_", 5);
final Collection<DistributionSet> groupAB = concat(groupA, groupB);
// toggle A only -> A is now assigned
// set A only -> A is now assigned
List<? extends DistributionSet> result = assignTag(groupA, tag);
assertThat(result)
.hasSize(20)
.containsAll((Collection) distributionSetManagement.get(groupA.stream().map(DistributionSet::getId).toList()));
Assertions.<DistributionSet> assertThat(result)
.hasSameSizeAs(groupA)
.containsAll(distributionSetManagement.get(groupA.stream().map(DistributionSet::getId).toList()));
assertThat(
distributionSetManagement.findByTag(tag.getId(), Pageable.unpaged()).getContent().stream()
distributionSetManagement.findByTag(tag.getId(), UNPAGED).getContent().stream()
.map(DistributionSet::getId)
.sorted()
.toList())
.isEqualTo(groupA.stream().map(DistributionSet::getId).sorted().toList());
final Collection<DistributionSet> groupAB = concat(groupA, groupB);
// toggle A+B -> A is still assigned and B is assigned as well
// set to A+B -> A is still assigned and B is assigned as well
result = assignTag(groupAB, tag);
assertThat((List) result)
.hasSize(40)
Assertions.<DistributionSet> assertThat(result)
.hasSameSizeAs(groupAB)
.containsAll(distributionSetManagement.get(groupAB.stream().map(DistributionSet::getId).toList()));
assertThat(
distributionSetManagement.findByTag(
tag.getId(), Pageable.unpaged()).getContent().stream().map(DistributionSet::getId).sorted().toList())
distributionSetManagement.findByTag(tag.getId(), UNPAGED).getContent().stream()
.map(DistributionSet::getId)
.sorted()
.toList())
.isEqualTo(groupAB.stream().map(DistributionSet::getId).sorted().toList());
// toggle A+B -> both unassigned
result = unassignTag(concat(groupA, groupB), tag);
assertThat(result)
.hasSize(40)
.containsAll((List) distributionSetManagement.get(concat(groupB, groupA).stream().map(DistributionSet::getId).toList()));
assertThat(distributionSetManagement.findByTag(tag.getId(), Pageable.unpaged()).getContent()).isEmpty();
result = unassignTag(groupAB, tag);
Assertions.<DistributionSet> assertThat(result)
.hasSameSizeAs(groupAB)
.containsAll(distributionSetManagement.get(groupAB.stream().map(DistributionSet::getId).toList()));
assertThat(distributionSetManagement.findByTag(tag.getId(), UNPAGED).getContent()).isEmpty();
}
/**
* Verifies that tagging of set containing missing DS throws meaningful and correct exception.
*/
@Test
void failOnMissingDs() {
void failToAssignTagIfMissingDs() {
final Collection<Long> group = testdataFactory.createDistributionSets(5).stream().map(DistributionSet::getId).toList();
final DistributionSetTag tag = distributionSetTagManagement.create(
DistributionSetTagManagement.Create.builder().name("tag1").description("tagdesc1").build());
final DistributionSetTag tag = distributionSetTagManagement.create(Create.builder().name(TAG_1).description(TAG_DESCRIPTION_1).build());
final List<Long> missing = new ArrayList<>();
for (int i = 0; i < 5; i++) {
while (true) {
@@ -204,142 +153,33 @@ class DistributionSetTagManagementTest extends AbstractJpaIntegrationTest {
});
}
/**
* Ensures that a created tag is persisted in the repository as defined.
*/
@Test
void createDistributionSetTag() {
final Tag tag = distributionSetTagManagement
.create(DistributionSetTagManagement.Create.builder().name("kai1").description("kai2").colour("colour").build());
assertThat(distributionSetTagRepository.findById(tag.getId()).orElseThrow().getDescription()).as("wrong tag found")
.isEqualTo("kai2");
assertThat(distributionSetTagManagement.find(tag.getId()).orElseThrow().getColour()).as("wrong tag found")
.isEqualTo("colour");
assertThat(distributionSetTagManagement.find(tag.getId()).orElseThrow().getColour()).as("wrong tag found")
.isEqualTo("colour");
}
/**
* Ensures that a deleted tag is removed from the repository as defined.
*/
@Test
void deleteDistributionSetTag() {
// create test data
final Iterable<DistributionSetTag> tags = createDsSetsWithTags();
final DistributionSetTag toDelete = tags.iterator().next();
for (final DistributionSet set : distributionSetRepository.findAll()) {
assertThat(distributionSetRepository.findById(set.getId()).get().getTags()).as("Wrong tag found")
.contains(toDelete);
}
// delete
distributionSetTagManagement.delete(tags.iterator().next().getId());
// check
assertThat(distributionSetTagRepository.findById(toDelete.getId())).as("Deleted tag should be null")
.isNotPresent();
assertThat(distributionSetTagManagement.findAll(PAGE).getContent()).as("Wrong size of tags after deletion")
.hasSize(19);
for (final DistributionSet set : distributionSetRepository.findAll()) {
assertThat(distributionSetRepository.findById(set.getId()).get().getTags()).as("Wrong found tags")
.doesNotContain(toDelete);
}
}
/**
* Ensures that a tag cannot be created if one exists already with that name (ecpects EntityAlreadyExistsException).
*/
@Test
void failedDuplicateDsTagNameException() {
final DistributionSetTagManagement.Create tag = DistributionSetTagManagement.Create.builder().name("A").build();
void failedToCreateIfNameAlreadyExists() {
final Create tag = Create.builder().name("A").build();
distributionSetTagManagement.create(tag);
assertThatExceptionOfType(EntityAlreadyExistsException.class).as("should not have worked as tag already exists")
assertThatExceptionOfType(EntityAlreadyExistsException.class)
.as("should not have worked as tag already exists")
.isThrownBy(() -> distributionSetTagManagement.create(tag));
}
/**
* Ensures that a tag cannot be updated to a name that already exists on another tag (ecpects EntityAlreadyExistsException).
*/
@Test
void failedDuplicateDsTagNameExceptionAfterUpdate() {
distributionSetTagManagement.create(DistributionSetTagManagement.Create.builder().name("A").build());
final DistributionSetTag tag = distributionSetTagManagement.create(DistributionSetTagManagement.Create.builder().name("B").build());
final DistributionSetTagManagement.Update tagUpdate = DistributionSetTagManagement.Update.builder().id(tag.getId()).name("A").build();
assertThatExceptionOfType(EntityAlreadyExistsException.class).as("should not have worked as tag already exists")
void failedToUpdateIfNameAlreadyExists() {
distributionSetTagManagement.create(Create.builder().name("A").build());
final DistributionSetTag tag = distributionSetTagManagement.create(Create.builder().name("B").build());
final Update tagUpdate = Update.builder().id(tag.getId()).name("A").build();
assertThatExceptionOfType(EntityAlreadyExistsException.class)
.as("Constraint not applied - tag with same name already exists")
.isThrownBy(() -> distributionSetTagManagement.update(tagUpdate));
}
/**
* Tests the name update of a target tag.
*/
@Test
void updateDistributionSetTag() {
// create test data
final List<DistributionSetTag> tags = createDsSetsWithTags();
// change data
final DistributionSetTag savedAssigned = tags.iterator().next();
// persist
distributionSetTagManagement.update(DistributionSetTagManagement.Update.builder().id(savedAssigned.getId()).name("test123").build());
// check data
assertThat(distributionSetTagManagement.findAll(PAGE).getContent()).as("Wrong size of ds tags")
.hasSize(tags.size());
assertThat(distributionSetTagRepository.findById(savedAssigned.getId()).get().getName())
.as("Wrong ds tag found").isEqualTo("test123");
}
/**
* Ensures that all tags are retrieved through repository.
*/
@Test
void findDistributionSetTagsAll() {
final List<DistributionSetTag> tags = createDsSetsWithTags();
// test
assertThat(distributionSetTagManagement.findAll(PAGE).getContent()).as("Wrong size of tags")
.hasSize(tags.size());
assertThat(distributionSetTagRepository.findAll()).as("Wrong size of tags").hasSize(20);
}
/**
* Ensures that a created tags are persisted in the repository as defined.
*/
@Test
void createDistributionSetTags() {
final List<DistributionSetTag> tags = createDsSetsWithTags();
assertThat(distributionSetTagRepository.findAll()).as("Wrong size of tags created").hasSize(tags.size());
}
private void verifyExpectedFilteredDistributionSets(final DistributionSetFilter.DistributionSetFilterBuilder distributionSetFilterBuilder,
final Stream<Collection<DistributionSet>> expectedFilteredDistributionSets) {
final Collection<Long> retrievedFilteredDsIds = findDsByDistributionSetFilter(distributionSetFilterBuilder.build(), PAGE).stream()
.map(DistributionSet::getId).toList();
final Collection<Long> expectedFilteredDsIds = expectedFilteredDistributionSets.flatMap(Collection::stream)
.map(DistributionSet::getId).toList();
@SafeVarargs
private void assertTaggedWithATagAreAsExpected(final DistributionSetTag tag, final Collection<DistributionSet>... expectedDistributionSets) {
final Collection<Long> retrievedFilteredDsIds = distributionSetManagement.findByTag(tag.getId(), UNPAGED).getContent().stream()
.map(DistributionSet::getId)
.toList();
final Collection<Long> expectedFilteredDsIds = Stream.of(expectedDistributionSets).flatMap(Collection::stream)
.map(DistributionSet::getId)
.toList();
assertThat(retrievedFilteredDsIds).hasSameElementsAs(expectedFilteredDsIds);
}
private List<DistributionSetTag> createDsSetsWithTags() {
final Collection<DistributionSet> sets = testdataFactory.createDistributionSets(20);
final Iterable<DistributionSetTag> tags = testdataFactory.createDistributionSetTags(20);
tags.forEach(tag -> assignTag(sets, tag));
return distributionSetTagManagement.findAll(PAGE).getContent().stream().map(DistributionSetTag.class::cast).toList();
}
private DistributionSetFilter.DistributionSetFilterBuilder getDistributionSetFilterBuilder() {
return DistributionSetFilter.builder();
}
@SafeVarargs
private <T> Collection<T> concat(final Collection<T>... targets) {
final List<T> result = new ArrayList<>();
Arrays.asList(targets).forEach(result::addAll);
return result;
}
}

View File

@@ -22,7 +22,8 @@ import java.util.Set;
import jakarta.validation.ConstraintViolationException;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.DistributionSetTypeManagement;
import org.eclipse.hawkbit.repository.DistributionSetTypeManagement.Create;
import org.eclipse.hawkbit.repository.DistributionSetTypeManagement.Update;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTypeCreatedEvent;
@@ -30,7 +31,6 @@ import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdated
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
@@ -47,67 +47,9 @@ import org.junit.jupiter.api.Test;
* Feature: Component Tests - Repository<br/>
* Story: DistributionSet Management
*/
class DistributionSetTypeManagementTest extends AbstractJpaIntegrationTest {
class DistributionSetTypeManagementTest extends AbstractRepositoryManagementTest<DistributionSetType, Create, Update> {
/**
* Verifies that management get access react as specified on calls for non existing entities by means
* of Optional not present.
*/
@Test
@ExpectEvents({ @Expect(type = DistributionSetCreatedEvent.class, count = 0) })
void nonExistingEntityAccessReturnsNotPresent() {
assertThat(distributionSetTypeManagement.find(NOT_EXIST_IDL)).isNotPresent();
assertThat(distributionSetTypeManagement.findByKey(NOT_EXIST_ID)).isNotPresent();
assertThat(distributionSetTypeManagement.findByName(NOT_EXIST_ID)).isNotPresent();
}
/**
* Verifies that management queries react as specified on calls for non existing entities
* by means of throwing EntityNotFoundException.
*/
@Test
@ExpectEvents({
@Expect(type = DistributionSetCreatedEvent.class, count = 0),
@Expect(type = DistributionSetTypeCreatedEvent.class, count = 1) })
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
final List<Long> softwareModuleTypes = Collections.singletonList(osType.getId());
verifyThrownExceptionBy(() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(NOT_EXIST_IDL,
softwareModuleTypes), "DistributionSetType");
final List<Long> notExistingSwModuleTypeIds = Collections.singletonList(NOT_EXIST_IDL);
verifyThrownExceptionBy(() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(
testdataFactory.findOrCreateDistributionSetType("xxx", "xxx").getId(), notExistingSwModuleTypeIds),
"SoftwareModuleType");
verifyThrownExceptionBy(() -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(NOT_EXIST_IDL,
softwareModuleTypes), "DistributionSetType");
verifyThrownExceptionBy(() -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(
testdataFactory.findOrCreateDistributionSetType("xxx", "xxx").getId(), notExistingSwModuleTypeIds),
"SoftwareModuleType");
verifyThrownExceptionBy(() -> distributionSetTypeManagement.delete(NOT_EXIST_IDL), "DistributionSetType");
verifyThrownExceptionBy(
() -> distributionSetTypeManagement.update(DistributionSetTypeManagement.Update.builder().id(NOT_EXIST_IDL).build()),
"DistributionSetType");
}
/**
* Verify that a DistributionSet with invalid properties cannot be created or updated
*/
@Test
@ExpectEvents({
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
@Expect(type = DistributionSetUpdatedEvent.class, count = 0) })
void createAndUpdateDistributionSetWithInvalidFields() {
final DistributionSet set = testdataFactory.createDistributionSet();
createAndUpdateDistributionSetWithInvalidDescription(set);
createAndUpdateDistributionSetWithInvalidName(set);
createAndUpdateDistributionSetWithInvalidVersion(set);
}
public static final String USED_BY_DS_TYPE_KEY = "updatableType";
/**
* Tests the successful module update of unused distribution set type which is in fact allowed.
@@ -115,150 +57,50 @@ class DistributionSetTypeManagementTest extends AbstractJpaIntegrationTest {
@Test
void updateUnassignedDistributionSetTypeModules() {
final DistributionSetType updatableType = distributionSetTypeManagement
.create(DistributionSetTypeManagement.Create.builder().key("updatableType").name("to be deleted").build());
assertThat(distributionSetTypeManagement.findByKey("updatableType").get().getMandatoryModuleTypes()).isEmpty();
.create(Create.builder().key(USED_BY_DS_TYPE_KEY).name("to be deleted").build());
assertThat(distributionSetTypeManagement.findByKey(USED_BY_DS_TYPE_KEY).orElseThrow().getMandatoryModuleTypes()).isEmpty();
// add OS
distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(updatableType.getId(),
Set.of(osType.getId()));
assertThat(distributionSetTypeManagement.findByKey("updatableType").get().getMandatoryModuleTypes())
distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(updatableType.getId(), Set.of(osType.getId()));
assertThat(distributionSetTypeManagement.findByKey(USED_BY_DS_TYPE_KEY).orElseThrow().getMandatoryModuleTypes())
.containsOnly(osType);
// add JVM
distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(updatableType.getId(),
Set.of(runtimeType.getId()));
assertThat(distributionSetTypeManagement.findByKey("updatableType").get().getMandatoryModuleTypes())
distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(updatableType.getId(), Set.of(runtimeType.getId()));
assertThat(distributionSetTypeManagement.findByKey(USED_BY_DS_TYPE_KEY).orElseThrow().getMandatoryModuleTypes())
.containsOnly(osType, runtimeType);
// remove OS
distributionSetTypeManagement.unassignSoftwareModuleType(updatableType.getId(), osType.getId());
assertThat(distributionSetTypeManagement.findByKey("updatableType").get().getMandatoryModuleTypes())
.containsOnly(runtimeType);
assertThat(distributionSetTypeManagement.findByKey(USED_BY_DS_TYPE_KEY).orElseThrow().getMandatoryModuleTypes()).containsOnly(runtimeType);
}
/**
* Verifies that the quota for software module types per distribution set type is enforced as expected.
* Tests the successful update of used distribution set type properties that are allowed.
*/
@Test
void quotaMaxSoftwareModuleTypes() {
final int quota = quotaManagement.getMaxSoftwareModuleTypesPerDistributionSetType();
// create software module types
final List<Long> moduleTypeIds = new ArrayList<>();
for (int i = 0; i < quota + 1; ++i) {
final SoftwareModuleTypeManagement.Create smCreate = SoftwareModuleTypeManagement.Create.builder().name("smType_" + i)
.description("smType_" + i).maxAssignments(1).colour("blue").key("smType_" + i).build();
moduleTypeIds.add(softwareModuleTypeManagement.create(smCreate).getId());
}
// assign all types at once
final DistributionSetType dsType1 = distributionSetTypeManagement
.create(DistributionSetTypeManagement.Create.builder().key("dst1").name("dst1").build());
final Long dsType1Id = dsType1.getId();
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType1Id, moduleTypeIds));
assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(
() -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(dsType1Id, moduleTypeIds));
// assign as many mandatory modules as possible
final DistributionSetType dsType2 = distributionSetTypeManagement
.create(DistributionSetTypeManagement.Create.builder().key("dst2").name("dst2").build());
final Long dsType2Id = dsType2.getId();
distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType2Id,
moduleTypeIds.subList(0, quota));
assertThat(distributionSetTypeManagement.find(dsType2Id)).isNotEmpty();
assertThat(distributionSetTypeManagement.find(dsType2Id).get().getMandatoryModuleTypes()).hasSize(quota);
// assign one more to trigger the quota exceeded error
final List<Long> softwareModuleTypeIds = Collections.singletonList(moduleTypeIds.get(quota));
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType2Id, softwareModuleTypeIds));
// assign as many optional modules as possible
final DistributionSetType dsType3 = distributionSetTypeManagement
.create(DistributionSetTypeManagement.Create.builder().key("dst3").name("dst3").build());
final Long dsType3Id = dsType3.getId();
distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(dsType3Id, moduleTypeIds.subList(0, quota));
assertThat(distributionSetTypeManagement.find(dsType3Id)).isNotEmpty();
assertThat(distributionSetTypeManagement.find(dsType3Id).get().getOptionalModuleTypes()).hasSize(quota);
// assign one more to trigger the quota exceeded error
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(dsType3Id, softwareModuleTypeIds));
void updateAssignedDistributionSetTypeProperties() {
final DistributionSetType dsType = createDistributionSetTypeUsedByDs();
distributionSetTypeManagement.update(Update.builder().id(dsType.getId()).description("a new description").build());
assertThat(distributionSetTypeManagement.findByKey(USED_BY_DS_TYPE_KEY).orElseThrow().getDescription()).isEqualTo("a new description");
assertThat(distributionSetTypeManagement.findByKey(USED_BY_DS_TYPE_KEY).orElseThrow().getColour()).isEqualTo("test123");
}
/**
* Tests the successfull update of used distribution set type meta data which is in fact allowed.
* Tests the successful deletion of used (soft delete) distribution set types.
*/
@Test
void updateAssignedDistributionSetTypeMetaData() {
final DistributionSetType nonUpdatableType = createDistributionSetTypeUsedByDs();
distributionSetTypeManagement.update(
DistributionSetTypeManagement.Update.builder().id(nonUpdatableType.getId()).description("a new description").build());
assertThat(distributionSetTypeManagement.findByKey("updatableType").orElseThrow().getDescription()).isEqualTo("a new description");
assertThat(distributionSetTypeManagement.findByKey("updatableType").orElseThrow().getColour()).isEqualTo("test123");
}
/**
* Tests the unsuccessful update of used distribution set type (module addition).
*/
@Test
void addModuleToAssignedDistributionSetTypeFails() {
final Long nonUpdatableTypeId = createDistributionSetTypeUsedByDs().getId();
final Set<Long> osTypeId = Set.of(osType.getId());
assertThatThrownBy(() -> distributionSetTypeManagement
.assignMandatorySoftwareModuleTypes(nonUpdatableTypeId, osTypeId))
.isInstanceOf(EntityReadOnlyException.class);
}
/**
* Tests the unsuccessful update of used distribution set type (module removal).
*/
@Test
void removeModuleToAssignedDistributionSetTypeFails() {
DistributionSetType nonUpdatableType = distributionSetTypeManagement
.create(DistributionSetTypeManagement.Create.builder().key("updatableType").name("to be deleted").build());
assertThat(distributionSetTypeManagement.findByKey("updatableType").get().getMandatoryModuleTypes()).isEmpty();
final Long osTypeId = osType.getId();
nonUpdatableType = distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(nonUpdatableType.getId(), Set.of(osTypeId));
distributionSetManagement.create(
DistributionSetManagement.Create.builder().type(nonUpdatableType).name("newtypesoft").version("1").build());
final Long typeId = nonUpdatableType.getId();
assertThatThrownBy(() -> distributionSetTypeManagement.unassignSoftwareModuleType(typeId, osTypeId))
.isInstanceOf(EntityReadOnlyException.class);
}
/**
* Tests the successfull deletion of unused (hard delete) distribution set types.
*/
@Test
void deleteUnassignedDistributionSetType() {
final JpaDistributionSetType hardDelete = (JpaDistributionSetType) distributionSetTypeManagement
.create(DistributionSetTypeManagement.Create.builder().key("delete").name("to be deleted").build());
assertThat(distributionSetTypeRepository.findAll()).contains(hardDelete);
distributionSetTypeManagement.delete(hardDelete.getId());
assertThat(distributionSetTypeRepository.findAll()).doesNotContain(hardDelete);
}
/**
* Tests the successfull deletion of used (soft delete) distribution set types.
*/
@Test
void deleteAssignedDistributionSetType() {
void softDeleteAssignedDistributionSetType() {
final int existing = (int) distributionSetTypeManagement.count();
final JpaDistributionSetType toBeDeleted = (JpaDistributionSetType) distributionSetTypeManagement
.create(DistributionSetTypeManagement.Create.builder().key("softdeleted").name("to be deleted").build());
.create(Create.builder().key("softDeleted").name("to be deleted").build());
assertThat(distributionSetTypeRepository.findAll()).contains(toBeDeleted);
distributionSetManagement.create(
DistributionSetManagement.Create.builder().type(toBeDeleted).name("softdeleted").version("1").build());
DistributionSetManagement.Create.builder().type(toBeDeleted).name("softDeleted").version("1").build());
distributionSetTypeManagement.delete(toBeDeleted.getId());
final Optional<? extends DistributionSetType> softDeleted = distributionSetTypeManagement.findByKey("softdeleted");
final Optional<? extends DistributionSetType> softDeleted = distributionSetTypeManagement.findByKey("softDeleted");
assertThat(softDeleted).isPresent();
assertThat(softDeleted.get().isDeleted()).isTrue();
assertThat(distributionSetTypeManagement.findAll(PAGE)).hasSize(existing);
@@ -270,18 +112,139 @@ class DistributionSetTypeManagementTest extends AbstractJpaIntegrationTest {
* Verifies that when no SoftwareModules are assigned to a Distribution then the DistributionSet is not complete.
*/
@Test
void shouldFailWhenDistributionSetHasNoSoftwareModulesAssigned() {
void incompleteIfDistributionSetHasNoSoftwareModulesAssigned() {
final JpaDistributionSetType jpaDistributionSetType = (JpaDistributionSetType) distributionSetTypeManagement
.create(DistributionSetTypeManagement.Create.builder().key("newType").name("new Type").build());
final List<SoftwareModule> softwareModules = new ArrayList<>();
final DistributionSet distributionSet = testdataFactory.createDistributionSet("DistributionOne", "3.1.2",
jpaDistributionSetType, softwareModules);
.create(Create.builder().key("newType").name("new Type").build());
final DistributionSet distributionSet = testdataFactory.createDistributionSet(
"DistributionOne", "3.1.2", jpaDistributionSetType, new ArrayList<>());
assertThat(jpaDistributionSetType.checkComplete(distributionSet)).isFalse();
}
/**
* Verifies that the quota for software module types per distribution set type is enforced as expected.
*/
@Test
void failIfQuotaExceeded() {
final int quota = quotaManagement.getMaxSoftwareModuleTypesPerDistributionSetType();
// create software module types
final List<Long> moduleTypeIds = new ArrayList<>();
for (int i = 0; i < quota + 1; ++i) {
final SoftwareModuleTypeManagement.Create smCreate = SoftwareModuleTypeManagement.Create.builder().name("smType_" + i)
.description("smType_" + i).maxAssignments(1).colour("blue").key("smType_" + i).build();
moduleTypeIds.add(softwareModuleTypeManagement.create(smCreate).getId());
}
// assign all types at once
final DistributionSetType dsType1 = distributionSetTypeManagement
.create(Create.builder().key("dst1").name("dst1").build());
final Long dsType1Id = dsType1.getId();
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType1Id, moduleTypeIds));
assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(
() -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(dsType1Id, moduleTypeIds));
// assign as many mandatory modules as possible
final DistributionSetType dsType2 = distributionSetTypeManagement
.create(Create.builder().key("dst2").name("dst2").build());
final Long dsType2Id = dsType2.getId();
distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType2Id,
moduleTypeIds.subList(0, quota));
assertThat(distributionSetTypeManagement.find(dsType2Id)).isNotEmpty();
assertThat(distributionSetTypeManagement.find(dsType2Id).get().getMandatoryModuleTypes()).hasSize(quota);
// assign one more to trigger the quota exceeded error
final List<Long> softwareModuleTypeIds = Collections.singletonList(moduleTypeIds.get(quota));
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType2Id, softwareModuleTypeIds));
// assign as many optional modules as possible
final DistributionSetType dsType3 = distributionSetTypeManagement.create(Create.builder().key("dst3").name("dst3").build());
final Long dsType3Id = dsType3.getId();
distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(dsType3Id, moduleTypeIds.subList(0, quota));
assertThat(distributionSetTypeManagement.find(dsType3Id)).isNotEmpty();
assertThat(distributionSetTypeManagement.find(dsType3Id).get().getOptionalModuleTypes()).hasSize(quota);
// assign one more to trigger the quota exceeded error
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(dsType3Id, softwareModuleTypeIds));
}
@Test
@ExpectEvents({
@Expect(type = DistributionSetCreatedEvent.class, count = 0),
@Expect(type = DistributionSetTypeCreatedEvent.class, count = 1) })
void failIfReferNotExistingEntity() {
final List<Long> softwareModuleTypes = Collections.singletonList(osType.getId());
verifyThrownExceptionBy(
() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(NOT_EXIST_IDL, softwareModuleTypes),
"DistributionSetType");
final List<Long> notExistingSwModuleTypeIds = Collections.singletonList(NOT_EXIST_IDL);
verifyThrownExceptionBy(
() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(
testdataFactory.findOrCreateDistributionSetType("xxx", "xxx").getId(), notExistingSwModuleTypeIds),
"SoftwareModuleType");
verifyThrownExceptionBy(
() -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(NOT_EXIST_IDL, softwareModuleTypes),
"DistributionSetType");
verifyThrownExceptionBy(
() -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(
testdataFactory.findOrCreateDistributionSetType("xxx", "xxx").getId(), notExistingSwModuleTypeIds),
"SoftwareModuleType");
verifyThrownExceptionBy(() -> distributionSetTypeManagement.delete(NOT_EXIST_IDL), "DistributionSetType");
verifyThrownExceptionBy(
() -> distributionSetTypeManagement.update(Update.builder().id(NOT_EXIST_IDL).build()),
"DistributionSetType");
}
/**
* Verify that a DistributionSet with invalid properties cannot be created or updated
*/
@Test
@ExpectEvents({
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
@Expect(type = DistributionSetUpdatedEvent.class, count = 0) })
void failToCreateAndUpdateDistributionSetWithInvalidFields() {
final DistributionSet set = testdataFactory.createDistributionSet();
createAndUpdateDistributionSetWithInvalidDescription(set);
createAndUpdateDistributionSetWithInvalidName(set);
createAndUpdateDistributionSetWithInvalidVersion(set);
}
/**
* Tests the unsuccessful update of used distribution set type (module addition).
*/
@Test
void failAddModuleIfAssignedDistributionSetType() {
final Long nonUpdatableTypeId = createDistributionSetTypeUsedByDs().getId();
final Set<Long> osTypeId = Set.of(osType.getId());
assertThatThrownBy(() -> distributionSetTypeManagement
.assignMandatorySoftwareModuleTypes(nonUpdatableTypeId, osTypeId))
.isInstanceOf(EntityReadOnlyException.class);
}
/**
* Tests the unsuccessful update of used distribution set type (module removal).
*/
@Test
void failToRemoveModuleIfAssignedDistributionSetType() {
DistributionSetType nonUpdatableType = distributionSetTypeManagement
.create(Create.builder().key(USED_BY_DS_TYPE_KEY).name("to be deleted").build());
assertThat(distributionSetTypeManagement.findByKey(USED_BY_DS_TYPE_KEY).get().getMandatoryModuleTypes()).isEmpty();
final Long osTypeId = osType.getId();
nonUpdatableType = distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(nonUpdatableType.getId(), Set.of(osTypeId));
distributionSetManagement.create(
DistributionSetManagement.Create.builder().type(nonUpdatableType).name("newtypesoft").version("1").build());
final Long typeId = nonUpdatableType.getId();
assertThatThrownBy(() -> distributionSetTypeManagement.unassignSoftwareModuleType(typeId, osTypeId))
.isInstanceOf(EntityReadOnlyException.class);
}
private void createAndUpdateDistributionSetWithInvalidDescription(final DistributionSet set) {
final DistributionSetManagement.Create distributionSetCreate = DistributionSetManagement.Create.builder()
.name("a").version("a").description(randomString(513)).build();
@@ -301,7 +264,8 @@ class DistributionSetTypeManagementTest extends AbstractJpaIntegrationTest {
.as("set with too long description should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate));
final DistributionSetManagement.Update distributionSetUpdate2 = DistributionSetManagement.Update.builder().id(set.getId()).description(INVALID_TEXT_HTML).build();
final DistributionSetManagement.Update distributionSetUpdate2 = DistributionSetManagement.Update.builder().id(set.getId())
.description(INVALID_TEXT_HTML).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("set with invalid description should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate2));
@@ -396,13 +360,13 @@ class DistributionSetTypeManagementTest extends AbstractJpaIntegrationTest {
}
private DistributionSetType createDistributionSetTypeUsedByDs() {
final DistributionSetType nonUpdatableType = distributionSetTypeManagement.create(
DistributionSetTypeManagement.Create.builder().key("updatableType").name("to be deleted").colour("test123").build());
assertThat(distributionSetTypeManagement.findByKey("updatableType").orElseThrow().getMandatoryModuleTypes()).isEmpty();
final DistributionSetType dsType = distributionSetTypeManagement.create(
Create.builder().key(USED_BY_DS_TYPE_KEY).name("to be deleted").colour("test123").build());
assertThat(distributionSetTypeManagement.findByKey(USED_BY_DS_TYPE_KEY).orElseThrow().getMandatoryModuleTypes()).isEmpty();
distributionSetManagement.create(DistributionSetManagement.Create.builder()
.type(nonUpdatableType)
.name("newtypesoft").version("1")
.type(dsType)
.name("newTypeSoft").version("1")
.build());
return nonUpdatableType;
return dsType;
}
}

View File

@@ -352,9 +352,6 @@ class ManagementSecurityTest extends AbstractJpaIntegrationTest {
} else {
try {
final Constructor[] constructors = clazz.getDeclaredConstructors();
if (ObjectUtils.isEmpty(constructors)) {
throw new IllegalStateException("No public constructor found for " + clazz);
}
// prefer empty constructor
for (final Constructor constructor : constructors) {
if (constructor.getParameterCount() == 0) {

View File

@@ -64,7 +64,6 @@ class RolloutGroupManagementTest extends AbstractJpaIntegrationTest {
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
testdataFactory.createRollout();
verifyThrownExceptionBy(() -> rolloutGroupManagement.countByRollout(NOT_EXIST_IDL), "Rollout");
verifyThrownExceptionBy(() -> rolloutGroupManagement.countTargetsOfRolloutsGroup(NOT_EXIST_IDL), "RolloutGroup");
verifyThrownExceptionBy(() -> rolloutGroupManagement.findByRolloutWithDetailedStatus(NOT_EXIST_IDL, PAGE), "Rollout");
verifyThrownExceptionBy(() -> rolloutGroupManagement.findByRolloutAndRsql(NOT_EXIST_IDL, "name==*", PAGE), "Rollout");

View File

@@ -14,32 +14,24 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import jakarta.validation.ConstraintViolationException;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Create;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Update;
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactBinaryNotFoundException;
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.LockedException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.RandomGeneratedInputStream;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction_;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet_;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
import org.eclipse.hawkbit.repository.model.DistributionSet;
@@ -48,162 +40,34 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule.MetadataValue;
import org.eclipse.hawkbit.repository.model.SoftwareModule.MetadataValueCreate;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.junit.jupiter.api.Test;
/**
* Feature: Component Tests - Repository<br/>
* Story: Software Module Management
*/
class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
class SoftwareModuleManagementTest
extends AbstractRepositoryManagementWithMetadataTest<SoftwareModule, Create, Update, MetadataValue, MetadataValueCreate> {
/**
* Verifies that management get access reacts as specified on calls for non existing entities by means
* of Optional not present.
*/
@Test
@ExpectEvents({ @Expect(type = SoftwareModuleCreatedEvent.class, count = 1) })
void nonExistingEntityAccessReturnsNotPresent() {
final SoftwareModule module = testdataFactory.createSoftwareModuleApp();
assertThat(softwareModuleManagement.find(1234L)).isNotPresent();
final Long moduleId = module.getId();
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> softwareModuleManagement.getMetadata(moduleId, NOT_EXIST_ID));
@SuppressWarnings("unchecked")
@Override
protected <O> O forType(final Class<O> type) {
if (type == MetadataValueCreate.class) {
return (O) new MetadataValueCreate(forType(String.class), forType(Boolean.class));
}
return super.forType(type);
}
/**
* Verifies that management queries react as specfied on calls for non existing entities
* by means of throwing EntityNotFoundException.
*/
@Test
@ExpectEvents({ @Expect(type = SoftwareModuleCreatedEvent.class, count = 1) })
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
testdataFactory.createSoftwareModuleApp();
@Override
protected Object builderParameterValue(final Method builderSetter) {
// encrypted true is not supported
if (builderSetter.getDeclaringClass() == Create.CreateBuilder.class && "encrypted".equals(builderSetter.getName())) {
return Boolean.FALSE;
}
final SoftwareModuleManagement.Create noType = SoftwareModuleManagement.Create.builder().name("xxx").type(null).build();
final List<SoftwareModuleManagement.Create> noTypeList = List.of(noType);
assertThatExceptionOfType(ConstraintViolationException.class)
.isThrownBy(() -> softwareModuleManagement.create(noTypeList));
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> softwareModuleManagement.create(noType));
verifyThrownExceptionBy(
() -> softwareModuleManagement.createMetadata(
NOT_EXIST_IDL, "xxx", new MetadataValueCreate("xxx")), "SoftwareModule");
verifyThrownExceptionBy(
() -> softwareModuleManagement.createMetadata(
NOT_EXIST_IDL, Map.of("xxx", new MetadataValueCreate("xxx"))), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.delete(NOT_EXIST_IDL), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.delete(Collections.singletonList(NOT_EXIST_IDL)), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.deleteMetadata(NOT_EXIST_IDL, "xxx"), "SoftwareModule");
verifyThrownExceptionBy(
() -> softwareModuleManagement.createMetadata(NOT_EXIST_IDL, "xxx", new MetadataValueCreate("xxx")), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.findByAssignedTo(NOT_EXIST_IDL, PAGE), "DistributionSet");
verifyThrownExceptionBy(() -> softwareModuleManagement.getMetadata(NOT_EXIST_IDL, NOT_EXIST_ID), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.getMetadata(NOT_EXIST_IDL), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.update(SoftwareModuleManagement.Update.builder().id(NOT_EXIST_IDL).build()),
"SoftwareModule");
}
/**
* Calling update without changing fields results in no recorded change in the repository including unchanged audit fields.
*/
@Test
void updateNothingResultsInUnchangedRepository() {
final SoftwareModule ah = testdataFactory.createSoftwareModuleOs();
final SoftwareModule updated = softwareModuleManagement
.update(SoftwareModuleManagement.Update.builder().id(ah.getId()).build());
assertThat(updated.getOptLockRevision())
.as("Expected version number of updated entity to be equal to created version")
.isEqualTo(ah.getOptLockRevision());
}
/**
* Calling update for changed fields results in change in the repository.
*/
@Test
void updateSoftwareModuleFieldsToNewValue() {
final SoftwareModule ah = testdataFactory.createSoftwareModuleOs();
final SoftwareModule updated = softwareModuleManagement
.update(SoftwareModuleManagement.Update.builder().id(ah.getId()).description("changed").vendor("changed").build());
assertThat(updated.getOptLockRevision())
.as("Expected version number of updated entitity is")
.isEqualTo(ah.getOptLockRevision() + 1);
assertThat(updated.getDescription()).as("Updated description is").isEqualTo("changed");
assertThat(updated.getVendor()).as("Updated vendor is").isEqualTo("changed");
}
/**
* Create Software Module call fails when called for existing entity.
*/
@Test
void createModuleCallFailsForExistingModule() {
testdataFactory.createSoftwareModuleOs();
assertThatExceptionOfType(EntityAlreadyExistsException.class)
.as("Should not have worked as module already exists.")
.isThrownBy(() -> testdataFactory.createSoftwareModuleOs());
}
/**
* searched for software modules based on the various filter options, e.g. name,desc,type, version.
*/
@Test
void findSoftwareModuleByFilters() {
softwareModuleManagement
.create(SoftwareModuleManagement.Create.builder().type(appType).name("agent-hub").version("1.0.1").build());
final SoftwareModule jvm = softwareModuleManagement
.create(SoftwareModuleManagement.Create.builder().type(runtimeType).name("oracle-jre").version("1.7.2").build());
final SoftwareModule os = softwareModuleManagement
.create(SoftwareModuleManagement.Create.builder().type(osType).name("poky").version("3.0.2").build());
final SoftwareModule ah2 = softwareModuleManagement
.create(SoftwareModuleManagement.Create.builder().type(appType).name("agent-hub").version("1.0.2").build());
JpaDistributionSet ds = (JpaDistributionSet) distributionSetManagement
.create(DistributionSetManagement.Create.builder()
.type(standardDsType)
.name("ds-1").version("1.0.1")
.modules(Set.of(os, jvm, ah2))
.build());
final JpaTarget target = (JpaTarget) testdataFactory.createTarget();
ds = (JpaDistributionSet) assignSet(target, ds).getDistributionSet();
implicitLock(os);
implicitLock(jvm);
distributionSetManagement.unlock(ds); // otherwise delete will be rejected as a part of a locked DS
softwareModuleManagement.delete(ah2.getId());
}
/**
* Searches for software modules based on a list of IDs.
*/
@Test
void findSoftwareModulesById() {
final List<Long> modules = List.of(testdataFactory.createSoftwareModuleOs().getId(), testdataFactory.createSoftwareModuleApp().getId());
assertThat(softwareModuleManagement.get(modules)).hasSize(2);
}
/**
* Counts all software modules in the repsitory that are not marked as deleted.
*/
@Test
void countSoftwareModulesAll() {
// found in test
testdataFactory.createSoftwareModuleOs("one");
testdataFactory.createSoftwareModuleOs("two");
final SoftwareModule deleted = testdataFactory.createSoftwareModuleOs("deleted");
// ignored
softwareModuleManagement.delete(deleted.getId());
assertThat(softwareModuleManagement.count()).as("Expected to find the following number of modules:").isEqualTo(2);
return super.builderParameterValue(builderSetter);
}
/**
@@ -211,7 +75,6 @@ class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
*/
@Test
void hardDeleteOfNotAssignedArtifact() {
// [STEP1]: Create SoftwareModuleX with Artifacts
final SoftwareModule unassignedModule = createSoftwareModuleWithArtifacts(osType, "moduleX", "3.0.2", 2);
final Iterator<Artifact> artifactsIt = unassignedModule.getArtifacts().iterator();
@@ -273,7 +136,6 @@ class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
*/
@Test
void softDeleteOfHistoricalAssignedArtifact() {
// Init target
final Target target = testdataFactory.createTarget();
@@ -311,11 +173,10 @@ class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Delete an software module with an artifact, which is also used by another software module.
* Delete a software module with an artifact, which is also used by another software module.
*/
@Test
void deleteSoftwareModulesWithSharedArtifact() {
// Init artifact binary data, target and DistributionSets
final int artifactSize = 1024;
final byte[] source = randomBytes(artifactSize);
@@ -355,11 +216,10 @@ class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
// verify: meta data of artifactY is not deleted
assertThat(artifactRepository.findById(artifactY.getId())).isPresent();
}
/**
* Delete two assigned softwaremodules which share an artifact.
* Delete two assigned software modules which share an artifact.
*/
@Test
void deleteMultipleSoftwareModulesWhichShareAnArtifact() {
@@ -419,10 +279,10 @@ class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Verifies that all undeleted software modules are found in the repository.
* Verifies that all soft deleted software modules are found in the repository.
*/
@Test
void countSoftwareModuleTypesAll() {
void countSoftwareModules() {
testdataFactory.createSoftwareModuleOs();
// one soft deleted
@@ -454,187 +314,6 @@ class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
.as("Found this number of modules").hasSize(2);
}
/**
* Checks that metadata for a software module can be created.
*/
@Test
void createSoftwareModuleMetadata() {
final String knownKey1 = "myKnownKey1";
final String knownValue1 = "myKnownValue1";
final String knownKey2 = "myKnownKey2";
final String knownValue2 = "myKnownValue2";
final SoftwareModule softwareModule = testdataFactory.createSoftwareModuleApp();
assertThat(softwareModule.getOptLockRevision()).isEqualTo(1);
softwareModuleManagement.createMetadata(
softwareModule.getId(),
Map.of(
knownKey1, new MetadataValueCreate(knownValue1, true),
knownKey2, new MetadataValueCreate(knownValue2)));
final SoftwareModule changedLockRevisionModule = softwareModuleManagement.find(softwareModule.getId()).get();
assertThat(changedLockRevisionModule.getOptLockRevision()).isEqualTo(2);
assertThat(softwareModuleManagement.getMetadata(softwareModule.getId(), knownKey1)).satisfies(metadata -> {
assertThat(metadata.getValue()).isEqualTo(knownValue1);
assertThat(metadata.isTargetVisible()).isTrue();
});
assertThat(softwareModuleManagement.getMetadata(softwareModule.getId(), knownKey2)).satisfies(metadata -> {
assertThat(metadata.getValue()).isEqualTo(knownValue2);
assertThat(metadata.isTargetVisible()).isFalse();
});
}
/**
* Verifies the enforcement of the metadata quota per software module.
*/
@Test
void createSoftwareModuleMetadataUntilQuotaIsExceeded() {
// add meta-data one by one
final SoftwareModule module = testdataFactory.createSoftwareModuleApp("m1");
final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerSoftwareModule();
for (int i = 0; i < maxMetaData; ++i) {
softwareModuleManagement.createMetadata(module.getId(), "k" + i, new MetadataValueCreate("v" + i));
}
// quota exceeded
final Long moduleId = module.getId();
final MetadataValueCreate metadata = new MetadataValueCreate("v" + maxMetaData);
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> softwareModuleManagement.createMetadata(moduleId, "k" + maxMetaData, metadata));
// add multiple meta data entries at once
final SoftwareModule module2 = testdataFactory.createSoftwareModuleApp("m2");
final Long moduleId2 = module2.getId();
final Map<String, MetadataValueCreate> create = new HashMap<>();
for (int i = 0; i < maxMetaData + 1; ++i) {
create.put("k" + i, new MetadataValueCreate("v" + i));
}
// quota exceeded
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> softwareModuleManagement.createMetadata(moduleId2, create));
// add some meta data entries
final SoftwareModule module3 = testdataFactory.createSoftwareModuleApp("m3");
final int firstHalf = Math.round((maxMetaData) / 2.f);
for (int i = 0; i < firstHalf; ++i) {
softwareModuleManagement.createMetadata(module3.getId(), "k" + i, new MetadataValueCreate("v" + i));
}
// add too many data entries
final Long moduleId3 = module3.getId();
final int secondHalf = maxMetaData - firstHalf;
final Map<String, MetadataValueCreate> create2 = new HashMap<>();
for (int i = 0; i < secondHalf + 1; ++i) {
create2.put("kk" + i, new MetadataValueCreate("vv" + i));
}
// quota exceeded
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> softwareModuleManagement.createMetadata(moduleId3, create2));
}
/**
* Checks that metadata for a software module can be updated.
*/
@Test
@WithUser(allSpPermissions = true)
void updateSoftwareModuleMetadata() {
final String knownKey = "myKnownKey";
final String knownValue = "myKnownValue";
final String knownUpdateValue = "myNewUpdatedValue";
// create a base software module
final SoftwareModule ah = testdataFactory.createSoftwareModuleApp();
// initial opt lock revision must be 1
assertThat(ah.getOptLockRevision()).isEqualTo(1);
// create an software module meta data entry
softwareModuleManagement.createMetadata(ah.getId(), knownKey, new MetadataValueCreate(knownValue));
final MetadataValue softwareModuleMetadata = softwareModuleManagement.getMetadata(ah.getId(), knownKey);
assertThat(softwareModuleMetadata.isTargetVisible()).isFalse();
assertThat(softwareModuleMetadata.getValue()).isEqualTo(knownValue);
// base software module should have now the opt lock revision one
// because we are modifying the base software module
SoftwareModule changedLockRevisionModule = softwareModuleManagement.find(ah.getId()).get();
assertThat(changedLockRevisionModule.getOptLockRevision()).isEqualTo(2);
// update the software module metadata
softwareModuleManagement.createMetadata(ah.getId(), knownKey, new MetadataValueCreate(knownUpdateValue, true));
final MetadataValue updated = softwareModuleManagement.getMetadata(ah.getId(), knownKey);
// we are updating the sw metadata so also modifying the base software
// module so opt lock revision must be two
changedLockRevisionModule = softwareModuleManagement.find(ah.getId()).get();
assertThat(changedLockRevisionModule.getOptLockRevision()).isEqualTo(3);
// verify updated meta data contains the updated value
assertThat(updated).isNotNull();
assertThat(updated.getValue()).isEqualTo(knownUpdateValue);
assertThat(updated.isTargetVisible()).isTrue();
}
/**
* Verifies that existing metadata can be deleted.
*/
@Test
void deleteSoftwareModuleMetadata() {
final String knownKey1 = "myKnownKey1";
final String knownValue1 = "myKnownValue1";
final SoftwareModule swModule = testdataFactory.createSoftwareModuleApp();
softwareModuleManagement.createMetadata(swModule.getId(), knownKey1, new MetadataValueCreate(knownValue1));
assertThat(softwareModuleManagement.getMetadata(swModule.getId()).entrySet())
.as("Contains the created metadata element").allSatisfy(metadata -> {
assertThat(metadata.getKey()).isEqualTo(knownKey1);
assertThat(metadata.getValue().getValue()).isEqualTo(knownValue1);
});
softwareModuleManagement.deleteMetadata(swModule.getId(), knownKey1);
assertThat(softwareModuleManagement.getMetadata(swModule.getId())).as("Metadata elements are").isEmpty();
}
/**
* Queries and loads the metadata related to a given software module.
*/
@Test
void findAllSoftwareModuleMetadataBySwId() {
final SoftwareModule sw1 = testdataFactory.createSoftwareModuleApp();
final int metadataCountSw1 = 8;
final SoftwareModule sw2 = testdataFactory.createSoftwareModuleOs();
final int metadataCountSw2 = 10;
for (int index = 0; index < metadataCountSw1; index++) {
softwareModuleManagement.createMetadata(sw1.getId(), "key" + index, new MetadataValueCreate("value" + index, true));
}
for (int index = 0; index < metadataCountSw2; index++) {
softwareModuleManagement.createMetadata(sw2.getId(), "key" + index, new MetadataValueCreate("value" + index, false));
}
final Map<String, MetadataValue> metadataSw1 = softwareModuleManagement.getMetadata(sw1.getId());
final Map<String, MetadataValue> metadataSw2 = softwareModuleManagement.getMetadata(sw2.getId());
assertThat(metadataSw1).hasSize(metadataCountSw1);
assertThat(metadataSw2).hasSize(metadataCountSw2);
final Map<String, String> metadataSw1V = softwareModuleManagement
.findMetaDataBySoftwareModuleIdsAndTargetVisible(List.of(sw1.getId())).get(sw1.getId());
final Map<String, String> metadataSw2V = softwareModuleManagement
.findMetaDataBySoftwareModuleIdsAndTargetVisible(List.of(sw2.getId())).get(sw2.getId());
assertThat(metadataSw1V).hasSize(metadataCountSw1);
assertThat(metadataSw2V).isNull();
}
/**
* Locks a SM.
*/
@Test
void lockSoftwareModule() {
final SoftwareModule softwareModule = testdataFactory.createSoftwareModule("sm-1");
@@ -643,9 +322,6 @@ class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
assertThat(softwareModuleManagement.find(softwareModule.getId()).map(SoftwareModule::isLocked).orElse(false)).isTrue();
}
/**
* Unlocks a SM.
*/
@Test
void unlockSoftwareModule() {
SoftwareModule softwareModule = testdataFactory.createSoftwareModule("sm-1");
@@ -711,32 +387,40 @@ class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
.isThrownBy(() -> softwareModuleManagement.delete(moduleId));
}
private Action assignSet(final JpaTarget target, final JpaDistributionSet ds) {
assignDistributionSet(ds.getId(), target.getControllerId());
implicitLock(ds);
assertThat(targetManagement.getByControllerId(target.getControllerId()).orElseThrow().getUpdateStatus())
.isEqualTo(TargetUpdateStatus.PENDING);
final Optional<DistributionSet> assignedDistributionSet = deploymentManagement
.getAssignedDistributionSet(target.getControllerId());
assertThat(assignedDistributionSet).contains(ds);
final Action action = actionRepository
.findAll(
(root, query, cb) ->
cb.and(
cb.equal(root.get(JpaAction_.target).get(JpaTarget_.id), target.getId()),
cb.equal(root.get(JpaAction_.distributionSet).get(JpaDistributionSet_.id), ds.getId())),
PAGE).getContent().get(0);
assertThat(action).isNotNull();
return action;
@Test
@ExpectEvents({ @Expect(type = SoftwareModuleCreatedEvent.class, count = 1) })
void failIfReferNotExistingEntity() {
testdataFactory.createSoftwareModuleApp();
final Create noType = Create.builder().name("xxx").type(null).build();
final List<Create> noTypeList = List.of(noType);
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> softwareModuleManagement.create(noTypeList));
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> softwareModuleManagement.create(noType));
verifyThrownExceptionBy(
() -> softwareModuleManagement.createMetadata(NOT_EXIST_IDL, "xxx", new MetadataValueCreate("xxx")), "SoftwareModule");
verifyThrownExceptionBy(
() -> softwareModuleManagement.createMetadata(NOT_EXIST_IDL, Map.of("xxx", new MetadataValueCreate("xxx"))), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.delete(NOT_EXIST_IDL), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.delete(Collections.singletonList(NOT_EXIST_IDL)), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.deleteMetadata(NOT_EXIST_IDL, "xxx"), "SoftwareModule");
verifyThrownExceptionBy(
() -> softwareModuleManagement.createMetadata(NOT_EXIST_IDL, "xxx", new MetadataValueCreate("xxx")), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.findByAssignedTo(NOT_EXIST_IDL, PAGE), "DistributionSet");
verifyThrownExceptionBy(() -> softwareModuleManagement.getMetadata(NOT_EXIST_IDL, NOT_EXIST_ID), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.getMetadata(NOT_EXIST_IDL), "SoftwareModule");
verifyThrownExceptionBy(() -> softwareModuleManagement.update(Update.builder().id(NOT_EXIST_IDL).build()), "SoftwareModule");
}
private SoftwareModule createSoftwareModuleWithArtifacts(final SoftwareModuleType type, final String name,
final String version, final int numberArtifacts) {
private SoftwareModule createSoftwareModuleWithArtifacts(
final SoftwareModuleType type, final String name, final String version, final int numberArtifacts) {
final long countSoftwareModule = softwareModuleRepository.count();
// create SoftwareModule
SoftwareModule softwareModule = softwareModuleManagement.create(SoftwareModuleManagement.Create.builder()
SoftwareModule softwareModule = softwareModuleManagement.create(Create.builder()
.type(type).name(name).version(version).description("description of artifact " + name).build());
final int artifactSize = 5 * 1024;
@@ -764,8 +448,7 @@ class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
assertThat(artifactRepository.findAll()).hasSize(results.length);
for (final Artifact result : results) {
assertThat(result.getId()).isNotNull();
assertThat(binaryArtifactRepository.getBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash()))
.isNotNull();
assertThat(binaryArtifactRepository.getBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())).isNotNull();
}
}

View File

@@ -12,7 +12,6 @@ package org.eclipse.hawkbit.repository.jpa.management;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.Arrays;
import java.util.List;
import jakarta.validation.ConstraintViolationException;
@@ -21,133 +20,75 @@ import org.assertj.core.api.Assertions;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement.Create;
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement.Update;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.junit.jupiter.api.Test;
/**
* Feature: Component Tests - Repository<br/>
* Story: Software Module Management
*/
class SoftwareModuleTypeManagementTest extends AbstractJpaIntegrationTest {
class SoftwareModuleTypeManagementTest extends AbstractRepositoryManagementTest<SoftwareModuleType, Create, Update> {
/**
* Verifies that management get access reacts as specfied on calls for non existing entities by means
* of Optional not present.
*/
@Test
@ExpectEvents({ @Expect(type = SoftwareModuleCreatedEvent.class) })
void nonExistingEntityAccessReturnsNotPresent() {
assertThat(softwareModuleTypeManagement.find(NOT_EXIST_IDL)).isNotPresent();
void failIfReferNotExistingEntity() {
assertThat(softwareModuleTypeManagement.findByKey(NOT_EXIST_ID)).isNotPresent();
assertThat(softwareModuleTypeManagement.findByName(NOT_EXIST_ID)).isNotPresent();
}
/**
* Verifies that management queries react as specfied on calls for non existing entities
* by means of throwing EntityNotFoundException.
*/
@Test
@ExpectEvents({ @Expect(type = SoftwareModuleCreatedEvent.class) })
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
verifyThrownExceptionBy(() -> softwareModuleTypeManagement.delete(NOT_EXIST_IDL), "SoftwareModuleType");
verifyThrownExceptionBy(
() -> softwareModuleTypeManagement.update(SoftwareModuleTypeManagement.Update.builder().id(NOT_EXIST_IDL).build()),
"SoftwareModuleType");
verifyThrownExceptionBy(() -> softwareModuleTypeManagement.update(Update.builder().id(NOT_EXIST_IDL).build()), "SoftwareModuleType");
}
/**
* Calling update without changing fields results in no recorded change in the repository including unchanged audit fields.
*/
@Test
void updateNothingResultsInUnchangedRepositoryForType() {
final SoftwareModuleType created = softwareModuleTypeManagement.create(Create.builder().key("test-key").name("test-name").build());
final SoftwareModuleType updated = softwareModuleTypeManagement.update(SoftwareModuleTypeManagement.Update.builder().id(created.getId())
.build());
assertThat(updated.getOptLockRevision())
.as("Expected version number of updated entitity to be equal to created version")
.isEqualTo(created.getOptLockRevision());
}
/**
* Calling update for changed fields results in change in the repository.
*/
@Test
void updateSoftwareModuleTypeFieldsToNewValue() {
final SoftwareModuleType created = softwareModuleTypeManagement
.create(Create.builder().key("test-key").name("test-name").build());
final SoftwareModuleType updated = softwareModuleTypeManagement.update(
SoftwareModuleTypeManagement.Update.builder().id(created.getId()).description("changed").colour("changed").build());
assertThat(updated.getOptLockRevision()).as("Expected version number of updated entities is")
.isEqualTo(created.getOptLockRevision() + 1);
assertThat(updated.getDescription()).as("Updated description is").isEqualTo("changed");
assertThat(updated.getColour()).as("Updated vendor is").isEqualTo("changed");
}
/**
* Create Software Module Types call fails when called for existing entities.
*/
@Test
void createModuleTypesCallFailsForExistingTypes() {
final List<Create> created = Arrays.asList(
Create.builder().key("test-key").name("test-name").build(),
Create.builder().key("test-key2").name("test-name2").build());
softwareModuleTypeManagement.create(created);
assertThatExceptionOfType(EntityAlreadyExistsException.class)
.as("should not have worked as module type already exists")
.isThrownBy(() -> softwareModuleTypeManagement.create(created));
}
/**
* Tests the successfull deletion of software module types. Both unused (hard delete) and used ones (soft delete).
* Tests the successful deletion of software module types. Both unused (hard delete) and used ones (soft delete).
*/
@Test
void deleteAssignedAndUnassignedSoftwareModuleTypes() {
Assertions.<SoftwareModuleType>assertThat(softwareModuleTypeManagement.findAll(PAGE)).hasSize(3).contains(osType, runtimeType, appType);
Assertions.<SoftwareModuleType> assertThat(softwareModuleTypeManagement.findAll(PAGE)).hasSize(3)
.contains(osType, runtimeType, appType);
SoftwareModuleType type = softwareModuleTypeManagement
.create(Create.builder().key("bundle").name("OSGi Bundle").build());
SoftwareModuleType type = softwareModuleTypeManagement.create(Create.builder().key("bundle").name("OSGi Bundle").build());
Assertions.<SoftwareModuleType>assertThat(softwareModuleTypeManagement.findAll(PAGE)).hasSize(4).contains(osType, runtimeType, appType, type);
Assertions.<SoftwareModuleType> assertThat(softwareModuleTypeManagement.findAll(PAGE))
.hasSize(4)
.contains(osType, runtimeType, appType, type);
// delete unassigned
softwareModuleTypeManagement.delete(type.getId());
Assertions.<SoftwareModuleType>assertThat(softwareModuleTypeManagement.findAll(PAGE)).hasSize(3).contains(osType, runtimeType, appType);
Assertions.<SoftwareModuleType>assertThat(softwareModuleTypeRepository.findAll()).hasSize(3).contains(osType, runtimeType, appType);
Assertions.<SoftwareModuleType> assertThat(softwareModuleTypeManagement.findAll(PAGE))
.hasSize(3)
.contains(osType, runtimeType, appType);
Assertions.<SoftwareModuleType> assertThat(softwareModuleTypeRepository.findAll()).hasSize(3).contains(osType, runtimeType, appType);
type = softwareModuleTypeManagement
.create(Create.builder().key("bundle2").name("OSGi Bundle2").build());
type = softwareModuleTypeManagement.create(Create.builder().key("bundle2").name("OSGi Bundle2").build());
Assertions.<SoftwareModuleType>assertThat(softwareModuleTypeManagement.findAll(PAGE)).hasSize(4).contains(osType, runtimeType, appType, type);
Assertions.<SoftwareModuleType> assertThat(softwareModuleTypeManagement.findAll(PAGE))
.hasSize(4)
.contains(osType, runtimeType, appType, type);
softwareModuleManagement
.create(SoftwareModuleManagement.Create.builder().type(type).name("Test SM").version("1.0").build());
softwareModuleManagement.create(SoftwareModuleManagement.Create.builder().type(type).name("Test SM").version("1.0").build());
// delete assigned
softwareModuleTypeManagement.delete(type.getId());
Assertions.<SoftwareModuleType>assertThat(softwareModuleTypeManagement.findAll(PAGE)).hasSize(3).contains(osType, runtimeType, appType);
Assertions.<SoftwareModuleType>assertThat(softwareModuleTypeManagement.findByRsql("name==*", PAGE)).hasSize(3).contains(osType, runtimeType, appType);
Assertions.<SoftwareModuleType> assertThat(softwareModuleTypeManagement.findAll(PAGE))
.hasSize(3)
.contains(osType, runtimeType, appType);
Assertions.<SoftwareModuleType> assertThat(softwareModuleTypeManagement.findByRsql("name==*", PAGE))
.hasSize(3)
.contains(osType, runtimeType, appType);
assertThat(softwareModuleTypeManagement.count()).isEqualTo(3);
assertThat(softwareModuleTypeRepository.findAll()).hasSize(4).contains((JpaSoftwareModuleType) osType,
(JpaSoftwareModuleType) runtimeType, (JpaSoftwareModuleType) appType,
softwareModuleTypeRepository.findById(type.getId()).orElseThrow());
assertThat(softwareModuleTypeRepository.findAll())
.hasSize(4)
.contains(
(JpaSoftwareModuleType) osType,
(JpaSoftwareModuleType) runtimeType,
(JpaSoftwareModuleType) appType,
softwareModuleTypeRepository.findById(type.getId()).orElseThrow());
}
/**
* Checks that software module typeis found based on given name.
*/
@SuppressWarnings("unchecked")
@Test
void findSoftwareModuleTypeByName() {
testdataFactory.createSoftwareModuleOs();
@@ -156,59 +97,18 @@ class SoftwareModuleTypeManagementTest extends AbstractJpaIntegrationTest {
softwareModuleTypeManagement
.create(Create.builder().key("thetype2").name("anothername").build());
Assertions.<SoftwareModuleType>assertThat(((SoftwareModuleTypeManagement<SoftwareModuleType>) softwareModuleTypeManagement).findByName("thename"))
Assertions.assertThat(((SoftwareModuleTypeManagement<SoftwareModuleType>) softwareModuleTypeManagement).findByName("thename"))
.as("Type with given name").contains(found);
}
/**
* Verifies that it is not possible to create a type that alrady exists.
*/
@Test
void createSoftwareModuleTypeFailsWithExistingEntity() {
final Create create = Create.builder().key("thetype").name("thename").build();
softwareModuleTypeManagement.create(create);
assertThatExceptionOfType(EntityAlreadyExistsException.class)
.as("should not have worked as module type already exists")
.isThrownBy(() -> softwareModuleTypeManagement.create(create));
}
/**
* Verifies that it is not possible to create a list of types where one already exists.
*/
@Test
void createSoftwareModuleTypesFailsWithExistingEntity() {
softwareModuleTypeManagement.create(Create.builder().key("thetype").name("thename").build());
final List<Create> creates = List.of(
Create.builder().key("thetype").name("thename").build(),
Create.builder().key("anothertype").name("anothername").build());
assertThatExceptionOfType(EntityAlreadyExistsException.class)
.as("should not have worked as module type already exists")
.isThrownBy(() -> softwareModuleTypeManagement.create(creates));
}
/**
* Verifies that the creation of a softwareModuleType is failing because of invalid max assignment
*/
@Test
void createSoftwareModuleTypesFailsWithInvalidMaxAssignment() {
final Create create =
Create.builder().key("type").name("name").maxAssignments(0).build();
final Create create = Create.builder().key("type").name("name").maxAssignments(0).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("should not have worked as max assignment is invalid. Should be greater than 0")
.isThrownBy(() -> softwareModuleTypeManagement.create(create));
}
/**
* Verifies that multiple types are created as requested.
*/
@Test
void createMultipleSoftwareModuleTypes() {
final List<? extends SoftwareModuleType> created = softwareModuleTypeManagement
.create(List.of(
Create.builder().key("thetype").name("thename").build(),
Create.builder().key("thetype2").name("thename2").build()));
assertThat(created).as("Number of created types").hasSize(2);
assertThat(softwareModuleTypeManagement.count()).as("Number of types in repository").isEqualTo(5);
}
}

View File

@@ -11,14 +11,16 @@ package org.eclipse.hawkbit.repository.jpa.management;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.eclipse.hawkbit.repository.model.TargetFilterQuery.ALLOWED_AUTO_ASSIGN_ACTION_TYPES;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import jakarta.validation.ConstraintViolationException;
@@ -30,22 +32,21 @@ import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement.AutoAssignDistributionSetUpdate;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement.Create;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement.Update;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement.UpdateCreate;
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetFilterQueryCreatedEvent;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.DeletedException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException;
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetFilterQuery;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
@@ -61,64 +62,31 @@ import org.springframework.data.domain.Slice;
* Feature: Component Tests - Repository<br/>
* Story: Target Filter Query Management
*/
class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
class TargetFilterQueryManagementTest extends AbstractRepositoryManagementTest<TargetFilterQuery, Create, Update> {
/**
* Verifies that management get access reacts as specfied on calls for non existing entities by means of Optional not present.
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class) })
void nonExistingEntityAccessReturnsNotPresent() {
assertThat(targetFilterQueryManagement.find(NOT_EXIST_IDL)).isNotPresent();
@SuppressWarnings("unchecked")
@Override
protected <O> O forType(final Class<O> type) {
if (DistributionSet.class.isAssignableFrom(type)) {
// need to be completed in order to be assigned
incrementEvents(DistributionSet.class, EventType.CREATED);
incrementEvents(SoftwareModule.class, EventType.CREATED, 3);
return (O) testdataFactory.createDistributionSet();
} else if (type == Action.ActionType.class) {
return (O) ALLOWED_AUTO_ASSIGN_ACTION_TYPES.toArray()[RND.nextInt(ALLOWED_AUTO_ASSIGN_ACTION_TYPES.size())];
}
return super.forType(type);
}
/**
* Verifies that management queries react as specfied on calls for non existing entities by means of throwing EntityNotFoundException.
*/
@Test
@ExpectEvents({
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
@Expect(type = TargetFilterQueryCreatedEvent.class, count = 1) })
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
final DistributionSet set = testdataFactory.createDistributionSet();
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(
Create.builder().name("test filter").query("name==PendingTargets001").build());
@Override
protected Object builderParameterValue(final Method builderSetter) {
// encrypted true is not supported
if (builderSetter.getDeclaringClass() == UpdateCreate.UpdateCreateBuilder.class && "query".equals(builderSetter.getName())) {
return "controllerId==PendingTargets001";
}
verifyThrownExceptionBy(() -> targetFilterQueryManagement.delete(NOT_EXIST_IDL), "TargetFilterQuery");
verifyThrownExceptionBy(
() -> targetFilterQueryManagement.findByAutoAssignDSAndRsql(NOT_EXIST_IDL, "name==*", PAGE),
"DistributionSet");
verifyThrownExceptionBy(
() -> targetFilterQueryManagement.update(Update.builder().id(NOT_EXIST_IDL).build()),
"TargetFilterQuery");
verifyThrownExceptionBy(() -> targetFilterQueryManagement.updateAutoAssignDS(
new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(NOT_EXIST_IDL)),
"DistributionSet");
verifyThrownExceptionBy(
() -> targetFilterQueryManagement.updateAutoAssignDS(
new AutoAssignDistributionSetUpdate(NOT_EXIST_IDL).ds(set.getId())),
"TargetFilterQuery");
verifyThrownExceptionBy(() -> targetFilterQueryManagement.updateAutoAssignDS(
new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(NOT_EXIST_IDL)),
"DistributionSet");
}
/**
* Test creation of target filter query.
*/
@Test
void createTargetFilterQuery() {
final String filterName = "new target filter";
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(Create.builder().name(filterName).query("name==PendingTargets001").build());
assertEquals(targetFilterQuery, targetFilterQueryManagement.find(targetFilterQuery.getId()).get(),
"Retrieved newly created custom target filter");
return super.builderParameterValue(builderSetter);
}
/**
@@ -126,7 +94,6 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
*/
@Test
void createTargetFilterQueryThatExceedsQuota() {
// create targets
final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment();
testdataFactory.createTargets(maxTargets + 1, "target%s");
@@ -134,16 +101,13 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
// creation is supposed to work as there is no distribution set
final Create targetFilterQueryCreate = Create.builder()
.name("testfilter").autoAssignDistributionSet(set).query("name==target*").build();
.name("testFilter").autoAssignDistributionSet(set).query("name==target*").build();
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> targetFilterQueryManagement.create(targetFilterQueryCreate));
}
/**
* Test searching a target filter query.
*/
@Test
void searchTargetFilterQuery() {
void findByRsqlTargetFilterQuery() {
final String filterName = "targetFilterQueryName";
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(Create.builder().name(filterName).query("name==PendingTargets001").build());
@@ -152,33 +116,8 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
final List<? extends TargetFilterQuery> results = targetFilterQueryManagement
.findByRsql("name==" + filterName, PageRequest.of(0, 10)).getContent();
assertEquals(1, results.size(), "Search result should have 1 result");
assertEquals(targetFilterQuery, results.get(0), "Retrieved newly created custom target filter");
}
/**
* Test searching a target filter query with an invalid filter.
*/
@Test
void searchTargetFilterQueryInvalidField() {
final PageRequest pageRequest = PageRequest.of(0, 10);
Assertions.assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.isThrownBy(() -> targetFilterQueryManagement.findByRsql("unknownField==testValue", pageRequest));
}
/**
* Checks if the EntityAlreadyExistsException is thrown if a targetFilterQuery with the same name are created more than once.
*/
@Test
void createDuplicateTargetFilterQuery() {
final String filterName = "new target filter duplicate";
final Create targetFilterQueryCreate = Create.builder().name(filterName).query("name==PendingTargets001").build();
targetFilterQueryManagement.create(targetFilterQueryCreate);
assertThatExceptionOfType(EntityAlreadyExistsException.class)
.as("should not have worked as query already exists")
.isThrownBy(() -> targetFilterQueryManagement.create(targetFilterQueryCreate));
assertThat(results).hasSize(1);
assertThat(results.get(0)).isEqualTo(targetFilterQuery);
}
/**
@@ -195,29 +134,14 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
"Returns null as the target filter is deleted");
}
/**
* Test update of a target filter query.
*/
@Test
void updateTargetFilterQuery() {
final String filterName = "target_filter_01";
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(Create.builder().name(filterName).query("name==PendingTargets001").build());
final String newQuery = "name==PendingTargets002";
targetFilterQueryManagement.update(Update.builder().id(targetFilterQuery.getId()).query(newQuery).build());
assertEquals(newQuery, targetFilterQueryManagement.find(targetFilterQuery.getId()).get().getQuery(),
"Returns updated target filter query");
}
/**
* Test assigning a distribution set for auto assignment with different action types
*/
@Test
void assignDistributionSet() {
final String filterName = "target_filter_02";
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(Create.builder().name(filterName).query("name==PendingTargets001").build());
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(
Create.builder().name(filterName).query("name==PendingTargets001").build());
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
verifyAutoAssignmentWithDefaultActionType(targetFilterQuery, distributionSet);
@@ -228,48 +152,6 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
verifyAutoAssignmentWithSoftDeletedDs(targetFilterQuery);
}
/**
* Assigns a distribution set to an existing filter query and verifies that the quota 'max targets per auto assignment' is enforced.
*/
@Test
void assignDistributionSetToTargetFilterQueryThatExceedsQuota() {
// create targets
final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment();
testdataFactory.createTargets(maxTargets + 1, "target%s");
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
// creation is supposed to work as there is no distribution set
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(Create.builder().name("testfilter").query("name==target*").build());
// assigning a distribution set is supposed to fail as the query
// addresses too many targets
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate =
new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(distributionSet.getId());
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate));
}
/**
* Updates an existing filter query with a query string that addresses too many targets.
*/
@Test
void updateTargetFilterQueryWithQueryThatExceedsQuota() {
// create targets
final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment();
testdataFactory.createTargets(maxTargets + 1, "target%s");
final DistributionSet set = testdataFactory.createDistributionSet();
// creation is supposed to work as the query does not exceed the quota
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(
Create.builder().name("testfilter").autoAssignDistributionSet(set).query("name==foo").build());
// update with a query string that addresses too many targets
final Update targetFilterQueryUpdate = Update.builder().id(targetFilterQuery.getId()).query("name==target*").build();
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> targetFilterQueryManagement.update(targetFilterQueryUpdate));
}
/**
* Test removing distribution set while it has a relation to a target filter query
*/
@@ -286,14 +168,12 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
implicitLock(distributionSet);
// Check if target filter query is there
TargetFilterQuery tfq = targetFilterQueryManagement.find(targetFilterQuery.getId()).get();
assertEquals(distributionSet, tfq.getAutoAssignDistributionSet(), "Returns correct distribution set");
assertEquals(ActionType.FORCED, tfq.getAutoAssignActionType(), "Return correct action type");
verifyAutoAssignDsAndActionType(targetFilterQuery.getId(), distributionSet, ActionType.FORCED);
distributionSetManagement.delete(distributionSet.getId());
// Check if auto assign distribution set is null
tfq = targetFilterQueryManagement.find(targetFilterQuery.getId()).get();
final TargetFilterQuery tfq = targetFilterQueryManagement.get(targetFilterQuery.getId());
assertNotNull(tfq, "Returns target filter query");
assertNull(tfq.getAutoAssignDistributionSet(), "Returns distribution set as null");
assertNull(tfq.getAutoAssignActionType(), "Returns action type as null");
@@ -319,17 +199,15 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
implicitLock(distributionSet);
// Check if target filter query is there with the distribution set
TargetFilterQuery tfq = targetFilterQueryManagement.find(filterId).get();
assertEquals(distributionSet, tfq.getAutoAssignDistributionSet(), "Returns correct distribution set");
assertEquals(ActionType.FORCED, tfq.getAutoAssignActionType(), "Return correct action type");
verifyAutoAssignDsAndActionType(filterId, distributionSet, ActionType.FORCED);
distributionSetManagement.delete(distributionSet.getId());
// Check if distribution set is still in the database with deleted flag
assertTrue(distributionSetManagement.find(distributionSet.getId()).get().isDeleted(), "Distribution set should be deleted");
assertTrue(distributionSetManagement.get(distributionSet.getId()).isDeleted(), "Distribution set should be deleted");
// Check if auto assign distribution set is null
tfq = targetFilterQueryManagement.find(filterId).get();
final TargetFilterQuery tfq = targetFilterQueryManagement.get(filterId);
assertNotNull(tfq, "Returns target filter query");
assertNull(tfq.getAutoAssignDistributionSet(), "Returns distribution set as null");
assertNull(tfq.getAutoAssignActionType(), "Returns action type as null");
@@ -416,9 +294,6 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
.isNotNull();
}
/**
* Weight is validated and saved to the Filter.
*/
@Test
void weightValidatedAndSaved() {
enableMultiAssignments();
@@ -431,7 +306,7 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
final Long filterId = targetFilterQueryManagement.create(Create.builder().name("a")
.query("name==*").autoAssignDistributionSet(ds).autoAssignWeight(Action.WEIGHT_MAX).build()).getId();
assertThat(targetFilterQueryManagement.find(filterId).get().getAutoAssignWeight()).contains(Action.WEIGHT_MAX);
assertThat(targetFilterQueryManagement.get(filterId).getAutoAssignWeight()).contains(Action.WEIGHT_MAX);
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate(filterId)
.ds(ds.getId()).weight(Action.WEIGHT_MAX + 1);
@@ -445,80 +320,14 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
new AutoAssignDistributionSetUpdate(filterId).ds(ds.getId()).weight(Action.WEIGHT_MAX));
targetFilterQueryManagement.updateAutoAssignDS(
new AutoAssignDistributionSetUpdate(filterId).ds(ds.getId()).weight(Action.WEIGHT_MIN));
assertThat(targetFilterQueryManagement.find(filterId).get().getAutoAssignWeight()).contains(Action.WEIGHT_MIN);
}
/**
* Verifies that an exception is thrown when trying to create a target filter with an invalidated distribution set.
*/
@Test
void createTargetFilterWithInvalidDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet();
final Create targetFilterQueryCreate = Create.builder()
.name("createTargetFilterWithInvalidDistributionSet").query("name==*").autoAssignDistributionSet(distributionSet)
.build();
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Invalid distributionSet should throw an exception")
.isThrownBy(() -> targetFilterQueryManagement.create(targetFilterQueryCreate));
}
/**
* Verifies that an exception is thrown when trying to create a target filter with an incomplete distribution set.
*/
@Test
void createTargetFilterWithIncompleteDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet();
final Create targetFilterQueryCreate = Create.builder()
.name("createTargetFilterWithIncompleteDistributionSet").query("name==*")
.autoAssignDistributionSet(distributionSet)
.build();
assertThatExceptionOfType(IncompleteDistributionSetException.class)
.as("Incomplete distributionSet should throw an exception")
.isThrownBy(() -> targetFilterQueryManagement.create(targetFilterQueryCreate));
}
/**
* Verifies that an exception is thrown when trying to update a target filter with an invalidated distribution set.
*/
@Test
void updateAutoAssignDsWithInvalidDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(Create.builder()
.name("updateAutoAssignDsWithInvalidDistributionSet").query("name==*").autoAssignDistributionSet(distributionSet).build());
final DistributionSet invalidDistributionSet = testdataFactory.createAndInvalidateDistributionSet();
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
.ds(invalidDistributionSet.getId());
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Invalid distributionSet should throw an exception")
.isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate));
}
/**
* Verifies that an exception is thrown when trying to update a target filter with an incomplete distribution set.
*/
@Test
void updateAutoAssignDsWithIncompleteDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(
Create.builder().name("updateAutoAssignDsWithIncompleteDistributionSet")
.query("name==*").autoAssignDistributionSet(distributionSet).build());
final DistributionSet incompleteDistributionSet = testdataFactory.createIncompleteDistributionSet();
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
.ds(incompleteDistributionSet.getId());
assertThatExceptionOfType(IncompleteDistributionSetException.class)
.as("Incomplete distributionSet should throw an exception")
.isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate));
assertThat(targetFilterQueryManagement.get(filterId).getAutoAssignWeight()).contains(Action.WEIGHT_MIN);
}
/**
* Tests the auto assign action type mapping.
*/
@Test
void testAutoAssignActionTypeConvert() {
void autoAssignActionTypeConvert() {
for (final ActionType actionType : ActionType.values()) {
final Supplier<Long> create = () ->
targetFilterQueryManagement.create(
@@ -537,8 +346,147 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
final JpaTargetFilterQuery jpaTargetFilterQuery = (JpaTargetFilterQuery) targetFilterQueryManagement.create(
Create.builder().name("testAutoAssignActionTypeConvert").query("name==*").build());
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
jpaTargetFilterQuery.setAutoAssignActionType(ActionType.TIMEFORCED));
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> jpaTargetFilterQuery.setAutoAssignActionType(ActionType.TIMEFORCED));
}
@Test
@ExpectEvents({
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
@Expect(type = TargetFilterQueryCreatedEvent.class, count = 1) })
void failIfReferNotExistingEntity() {
final DistributionSet set = testdataFactory.createDistributionSet();
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(
Create.builder().name("test filter").query("name==PendingTargets001").build());
verifyThrownExceptionBy(() -> targetFilterQueryManagement.delete(NOT_EXIST_IDL), "TargetFilterQuery");
verifyThrownExceptionBy(
() -> targetFilterQueryManagement.findByAutoAssignDSAndRsql(NOT_EXIST_IDL, "name==*", PAGE), "DistributionSet");
verifyThrownExceptionBy(
() -> targetFilterQueryManagement.update(Update.builder().id(NOT_EXIST_IDL).build()), "TargetFilterQuery");
verifyThrownExceptionBy(
() -> targetFilterQueryManagement.updateAutoAssignDS(
new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(NOT_EXIST_IDL)),
"DistributionSet");
verifyThrownExceptionBy(
() -> targetFilterQueryManagement.updateAutoAssignDS(new AutoAssignDistributionSetUpdate(NOT_EXIST_IDL).ds(set.getId())),
"TargetFilterQuery");
verifyThrownExceptionBy(
() -> targetFilterQueryManagement.updateAutoAssignDS(
new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(NOT_EXIST_IDL)),
"DistributionSet");
}
/**
* Test searching a target filter query with an invalid filter.
*/
@Test
void failToFindTargetFilterQueryByInvalidField() {
final PageRequest pageRequest = PageRequest.of(0, 10);
Assertions.assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.isThrownBy(() -> targetFilterQueryManagement.findByRsql("unknownField==testValue", pageRequest));
}
@Test
void failToCreateTargetFilterWithInvalidDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createAndInvalidateDistributionSet();
final Create targetFilterQueryCreate = Create.builder()
.name("createTargetFilterWithInvalidDistributionSet").query("name==*").autoAssignDistributionSet(distributionSet)
.build();
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Invalid distributionSet should throw an exception")
.isThrownBy(() -> targetFilterQueryManagement.create(targetFilterQueryCreate));
}
@Test
void failToCreateTargetFilterWithIncompleteDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createIncompleteDistributionSet();
final Create targetFilterQueryCreate = Create.builder()
.name("createTargetFilterWithIncompleteDistributionSet").query("name==*")
.autoAssignDistributionSet(distributionSet)
.build();
assertThatExceptionOfType(IncompleteDistributionSetException.class)
.as("Incomplete distributionSet should throw an exception")
.isThrownBy(() -> targetFilterQueryManagement.create(targetFilterQueryCreate));
}
@Test
void failToUpdateAutoAssignDsWithInvalidDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(Create.builder()
.name("updateAutoAssignDsWithInvalidDistributionSet").query("name==*").autoAssignDistributionSet(distributionSet).build());
final DistributionSet invalidDistributionSet = testdataFactory.createAndInvalidateDistributionSet();
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
.ds(invalidDistributionSet.getId());
assertThatExceptionOfType(InvalidDistributionSetException.class)
.as("Invalid distributionSet should throw an exception")
.isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate));
}
@Test
void failToUpdateAutoAssignDsWithIncompleteDistributionSet() {
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(
Create.builder().name("updateAutoAssignDsWithIncompleteDistributionSet")
.query("name==*").autoAssignDistributionSet(distributionSet).build());
final DistributionSet incompleteDistributionSet = testdataFactory.createIncompleteDistributionSet();
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
.ds(incompleteDistributionSet.getId());
assertThatExceptionOfType(IncompleteDistributionSetException.class)
.as("Incomplete distributionSet should throw an exception")
.isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate));
}
/**
* Assigns a distribution set to an existing filter query and verifies that the quota 'max targets per auto assignment' is enforced.
*/
@Test
void failToAssignDistributionSetToTargetFilterQueryOnQuotaExceeded() {
// create targets
final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment();
testdataFactory.createTargets(maxTargets + 1, "target%s");
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
// creation is supposed to work as there is no distribution set
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(
Create.builder().name("testFilter").query("name==target*").build());
// assigning a distribution set is supposed to fail as the query
// addresses too many targets
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate =
new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(distributionSet.getId());
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate));
}
/**
* Updates an existing filter query with a query string that addresses too many targets.
*/
@Test
void failToUpdateTargetFilterQueryOnQuotaExceeded() {
// create targets
final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment();
testdataFactory.createTargets(maxTargets + 1, "target%s");
final DistributionSet set = testdataFactory.createDistributionSet();
// creation is supposed to work as the query does not exceed the quota
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create(
Create.builder().name("testfilter").autoAssignDistributionSet(set).query("name==foo").build());
// update with a query string that addresses too many targets
final Update targetFilterQueryUpdate = Update.builder().id(targetFilterQuery.getId()).query("name==target*").build();
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> targetFilterQueryManagement.update(targetFilterQueryUpdate));
}
private void verifyAutoAssignmentWithDefaultActionType(final TargetFilterQuery targetFilterQuery, final DistributionSet distributionSet) {
@@ -626,9 +574,8 @@ class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest {
}
private void verifyAutoAssignDsAndActionType(final Long filterId, final DistributionSet distributionSet, final ActionType actionType) {
final TargetFilterQuery tfq = targetFilterQueryManagement.find(filterId).get();
assertEquals(distributionSet, tfq.getAutoAssignDistributionSet(), "Returns correct distribution set");
assertEquals(actionType, tfq.getAutoAssignActionType(), "Return correct action type");
final TargetFilterQuery tfq = targetFilterQueryManagement.get(filterId);
assertThat(tfq.getAutoAssignDistributionSet()).isEqualTo(distributionSet);
assertThat(tfq.getAutoAssignActionType()).isEqualTo(actionType);
}
}

View File

@@ -16,8 +16,6 @@ import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility.RsqlToSpecBuil
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -34,17 +32,15 @@ import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.SetJoin;
import jakarta.validation.ConstraintViolationException;
import org.assertj.core.api.Assertions;
import org.awaitility.Awaitility;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.im.authentication.SpRole;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.MetadataSupport;
import org.eclipse.hawkbit.repository.TargetManagement.Create;
import org.eclipse.hawkbit.repository.TargetManagement.Update;
import org.eclipse.hawkbit.repository.TargetTagManagement;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
@@ -56,12 +52,10 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetTagCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaNamedEntity_;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
@@ -87,7 +81,6 @@ import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.domain.Specification;
@@ -95,78 +88,48 @@ import org.springframework.data.jpa.domain.Specification;
* Feature: Component Tests - Repository<br/>
* Story: Target Management
*/
class TargetManagementTest extends AbstractJpaIntegrationTest {
class TargetManagementTest extends AbstractRepositoryManagementWithMetadataTest<Target, Create, Update, String, String> {
private static final String WHITESPACE_ERROR = "target with whitespaces in controller id should not be created";
/**
* Verifies that management get access react as specified on calls for non existing entities by means
* of Optional not present.
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1) })
void nonExistingEntityAccessReturnsNotPresent() {
final Target target = testdataFactory.createTarget();
assertThat(targetManagement.getByControllerId(NOT_EXIST_ID)).isNotPresent();
assertThat(targetManagement.find(NOT_EXIST_IDL)).isNotPresent();
assertThat(targetManagement.getMetadata(target.getControllerId()).get(NOT_EXIST_ID)).isNull();
@Override
protected void setMetadataSupport() {
metadataSupport = new MetadataSupport<>() {
@Override
public void createMetadata(final Long id, final String key, final String value) {
targetManagement.createMetadata(toControllerId(id), key, value);
}
@SuppressWarnings("unchecked")
@Override
public void createMetadata(final Long id, final Map<String, ? extends String> metadata) {
targetManagement.createMetadata(toControllerId(id), (Map<String, String>) metadata);
}
@Override
public String getMetadata(final Long id, final String key) {
return targetManagement.getMetadata(targetManagement.get(id).getControllerId(), key);
}
@Override
public Map<String, String> getMetadata(final Long id) {
return targetManagement.getMetadata(toControllerId(id));
}
@Override
public void deleteMetadata(final Long id, final String key) {
targetManagement.deleteMetadata(toControllerId(id), key);
}
private String toControllerId(final Long id) {
return targetManagement.get(id).getControllerId();
}
};
}
/**
* Verifies that management queries react as specified on calls for non existing entities
* by means of throwing EntityNotFoundException.
*/
@Test
@ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetTagCreatedEvent.class, count = 1) })
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
final TargetTag tag = targetTagManagement.create(TargetTagManagement.Create.builder().name("A").build());
final Target target = testdataFactory.createTarget();
verifyThrownExceptionBy(
() -> targetManagement.assignTag(Collections.singletonList(target.getControllerId()), NOT_EXIST_IDL), "TargetTag");
verifyThrownExceptionBy(() -> targetManagement.assignTag(Collections.singletonList(NOT_EXIST_ID), tag.getId()), "Target");
verifyThrownExceptionBy(() -> targetManagement.findByTag(NOT_EXIST_IDL, PAGE), "TargetTag");
verifyThrownExceptionBy(() -> targetManagement.findByRsqlAndTag("name==*", NOT_EXIST_IDL, PAGE), "TargetTag");
verifyThrownExceptionBy(() -> targetManagement.countByRsqlAndNonDsAndCompatibleAndUpdatable(NOT_EXIST_IDL, "name==*"),
"DistributionSet");
verifyThrownExceptionBy(() -> targetManagement.deleteByControllerId(NOT_EXIST_ID), "Target");
verifyThrownExceptionBy(() -> targetManagement.delete(Collections.singletonList(NOT_EXIST_IDL)), "Target");
verifyThrownExceptionBy(
() -> targetManagement.findByTargetFilterQueryAndNonDSAndCompatibleAndUpdatable(NOT_EXIST_IDL, "name==*", PAGE),
"DistributionSet");
verifyThrownExceptionBy(() -> targetManagement.findByInRolloutGroupWithoutAction(NOT_EXIST_IDL, PAGE), "RolloutGroup");
verifyThrownExceptionBy(() -> targetManagement.findByAssignedDistributionSet(NOT_EXIST_IDL, PAGE), "DistributionSet");
verifyThrownExceptionBy(
() -> targetManagement.findByAssignedDistributionSetAndRsql(NOT_EXIST_IDL, "name==*", PAGE), "DistributionSet");
verifyThrownExceptionBy(() -> targetManagement.findByInstalledDistributionSet(NOT_EXIST_IDL, PAGE), "DistributionSet");
verifyThrownExceptionBy(
() -> targetManagement.findByInstalledDistributionSetAndRsql(NOT_EXIST_IDL, "name==*", PAGE), "DistributionSet");
verifyThrownExceptionBy(() -> targetManagement
.assignTag(Collections.singletonList(target.getControllerId()), Long.parseLong(NOT_EXIST_ID)), "TargetTag");
verifyThrownExceptionBy(
() -> targetManagement.assignTag(Collections.singletonList(NOT_EXIST_ID), tag.getId()), "Target");
verifyThrownExceptionBy(() -> targetManagement.unassignTag(List.of(NOT_EXIST_ID), tag.getId()), "Target");
verifyThrownExceptionBy(() -> targetManagement.unassignTag(List.of(target.getControllerId()), NOT_EXIST_IDL), "TargetTag");
verifyThrownExceptionBy(() -> targetManagement.update(Update.builder().id(NOT_EXIST_IDL).build()), "Target");
verifyThrownExceptionBy(() -> targetManagement.createMetadata(NOT_EXIST_ID, Map.of("123", "123")), "Target");
verifyThrownExceptionBy(() -> targetManagement.deleteMetadata(NOT_EXIST_ID, "xxx"), "Target");
verifyThrownExceptionBy(() -> targetManagement.deleteMetadata(target.getControllerId(), NOT_EXIST_ID), "Target");
verifyThrownExceptionBy(() -> targetManagement.getMetadata(NOT_EXIST_ID).get("xxx"), "Target");
verifyThrownExceptionBy(() -> targetManagement.createMetadata(NOT_EXIST_ID, "xxx", "xxx"), "Target");
}
/**
* Ensures that retrieving the target security is only permitted with the necessary permissions.
* Ensures that retrieving the target security token is only permitted with the necessary permissions.
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1) })
@@ -199,41 +162,9 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
assertThat(securityTokenWithTargetAdminPermission).isNotNull();
assertThat(securityTokenWithTenantAdminPermission).isNotNull();
assertThat(securityTokenAsSystemCode).isNotNull();
assertThat(securityTokenWithoutPermission).isNull();
}
/**
* Verify that a target with same controller ID than another device cannot be created.
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1) })
void createTargetThatViolatesUniqueConstraintFails() {
final Create targetCreate = Create.builder().controllerId("123").build();
targetManagement.create(targetCreate);
assertThatExceptionOfType(EntityAlreadyExistsException.class).isThrownBy(() -> targetManagement.create(targetCreate));
}
/**
* Verify that a target with with invalid properties cannot be created or updated
*/
@Test
@ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetUpdatedEvent.class) })
void createAndUpdateTargetWithInvalidFields() {
final Target target = testdataFactory.createTarget();
createTargetWithInvalidControllerId();
createAndUpdateTargetWithInvalidDescription(target);
createAndUpdateTargetWithInvalidName(target);
createAndUpdateTargetWithInvalidSecurityToken(target);
createAndUpdateTargetWithInvalidAddress(target);
}
/**
* Ensures that targets can assigned and unassigned to a target tag. Not exists target will be ignored for the assignment.
*/
@Test
@ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 4),
@@ -253,7 +184,8 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
assignedTargets.forEach(target -> assertThat(getTargetTags(target.getControllerId())).hasSize(1));
final TargetTag findTargetTag = targetTagManagement.find(targetTag.getId()).orElseThrow(IllegalStateException::new);
assertThat(assignedTargets).as("Assigned targets are wrong")
assertThat(assignedTargets)
.as("Assigned targets are wrong")
.hasSize(targetManagement.findByTag(targetTag.getId(), PAGE).getNumberOfElements());
final Target unAssignTarget = targetManagement.unassignTag(List.of("targetId123"), findTargetTag.getId()).get(0);
@@ -268,36 +200,6 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Ensures that targets can deleted e.g. test all cascades
*/
@Test
@ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 12),
@Expect(type = TargetDeletedEvent.class, count = 12),
@Expect(type = TargetUpdatedEvent.class, count = 6) })
void deleteAndCreateTargets() {
Target target = targetManagement.create(Create.builder().controllerId("targetId123").build());
assertThat(targetManagement.count()).as("target count is wrong").isEqualTo(1);
targetManagement.delete(Collections.singletonList(target.getId()));
assertThat(targetManagement.count()).as("target count is wrong").isZero();
target = createTargetWithAttributes("4711");
assertThat(targetManagement.count()).as("target count is wrong").isEqualTo(1);
targetManagement.delete(Collections.singletonList(target.getId()));
assertThat(targetManagement.count()).as("target count is wrong").isZero();
final List<Long> targets = new ArrayList<>();
for (int i = 0; i < 5; i++) {
target = targetManagement.create(Create.builder().controllerId("" + i).build());
targets.add(target.getId());
targets.add(createTargetWithAttributes("" + (i * i + 1000)).getId());
}
assertThat(targetManagement.count()).as("target count is wrong").isEqualTo(10);
targetManagement.delete(targets);
assertThat(targetManagement.count()).as("target count is wrong").isZero();
}
/**
* Finds a target by given ID and checks if all data is in the response (including the data defined as lazy).
*/
@@ -344,135 +246,6 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
assertThat(installedDs).as("Installed ds is wrong").isEqualTo(testDs1);
}
/**
* Checks if the EntityAlreadyExistsException is thrown if the targets with the same controller ID are created twice.
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 5) })
void createMultipleTargetsDuplicate() {
testdataFactory.createTargets(5, "mySimpleTargs", "my simple targets");
assertThatExceptionOfType(EntityAlreadyExistsException.class)
.as("Targets already exists")
.isThrownBy(() -> testdataFactory.createTargets(5, "mySimpleTargs", "my simple targets"));
}
/**
* Checks if the EntityAlreadyExistsException is thrown if a single target with the same controller ID are created twice.
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1) })
void createTargetDuplicate() {
final Create targetCreate = Create.builder().controllerId("4711").build();
targetManagement.create(targetCreate);
assertThatExceptionOfType(EntityAlreadyExistsException.class)
.as("Target already exists")
.isThrownBy(() -> targetManagement.create(targetCreate));
}
/**
* Creates and updates a target and verifies the changes in the repository.
*/
@Test
@WithUser(allSpPermissions = true)
@ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetUpdatedEvent.class, count = 1) })
void singleTargetIsInsertedIntoRepo() {
final String myCtrlID = "myCtrlID";
Target savedTarget = testdataFactory.createTarget(myCtrlID);
assertThat(savedTarget).as("The target should not be null").isNotNull();
final long createdAt = savedTarget.getCreatedAt();
long modifiedAt = savedTarget.getLastModifiedAt();
assertThat(createdAt).as("CreatedAt compared with modifiedAt").isEqualTo(modifiedAt);
Awaitility.await().until(() -> System.currentTimeMillis() > createdAt + 1);
savedTarget = targetManagement.update(Update.builder().id(savedTarget.getId()).description("changed description").build());
assertThat(createdAt).as("CreatedAt compared with saved modifiedAt").isNotEqualTo(savedTarget.getLastModifiedAt());
assertThat(modifiedAt).as("ModifiedAt compared with saved modifiedAt").isNotEqualTo(savedTarget.getLastModifiedAt());
modifiedAt = savedTarget.getLastModifiedAt();
final Target foundTarget = targetManagement.getByControllerId(savedTarget.getControllerId())
.orElseThrow(IllegalStateException::new);
assertThat(foundTarget).as("The target should not be null").isNotNull();
assertThat(myCtrlID).as("ControllerId compared with saved controllerId").isEqualTo(foundTarget.getControllerId());
assertThat(savedTarget).as("Target compared with saved target").isEqualTo(foundTarget);
assertThat(createdAt).as("CreatedAt compared with saved createdAt").isEqualTo(foundTarget.getCreatedAt());
assertThat(modifiedAt).as("LastModifiedAt compared with saved lastModifiedAt").isEqualTo(foundTarget.getLastModifiedAt());
}
/**
* Create multiple targets as bulk operation and delete them in bulk.
*/
@Test
@WithUser(allSpPermissions = true)
@ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 101),
@Expect(type = TargetUpdatedEvent.class, count = 100),
@Expect(type = TargetDeletedEvent.class, count = 51) })
void bulkTargetCreationAndDelete() {
final String myCtrlID = "myCtrlID";
List<? extends Target> firstList = testdataFactory.createTargets(100, myCtrlID, "first description");
final Target extra = testdataFactory.createTarget("myCtrlID-00081XX");
final Iterable<JpaTarget> allFound = targetRepository.findAll();
assertThat(Long.valueOf(firstList.size())).as("List size of targets")
.isEqualTo(firstList.spliterator().getExactSizeIfKnown());
assertThat(Long.valueOf(firstList.size() + 1)).as("LastModifiedAt compared with saved lastModifiedAt")
.isEqualTo(allFound.spliterator().getExactSizeIfKnown());
// change the objects and save to again to trigger a change on
// lastModifiedAt
firstList = firstList.stream()
.map(t -> targetManagement.update(
Update.builder().id(t.getId()).name(t.getName().concat("\tchanged")).build()))
.toList();
// verify that all entries are found
_founds:
for (final Target foundTarget : allFound) {
for (final Target changedTarget : firstList) {
if (changedTarget.getControllerId().equals(foundTarget.getControllerId())) {
assertThat(changedTarget.getDescription())
.as("Description of changed target compared with description saved target")
.isEqualTo(foundTarget.getDescription());
assertThat(changedTarget.getName()).as("Name of changed target starts with name of saved target")
.startsWith(foundTarget.getName());
assertThat(changedTarget.getName()).as("Name of changed target ends with 'changed'")
.endsWith("changed");
assertThat(changedTarget.getCreatedAt()).as("CreatedAt compared with saved createdAt")
.isEqualTo(foundTarget.getCreatedAt());
assertThat(changedTarget.getLastModifiedAt()).as("LastModifiedAt compared with saved createdAt")
.isNotEqualTo(changedTarget.getCreatedAt());
continue _founds;
}
}
if (!foundTarget.getControllerId().equals(extra.getControllerId())) {
fail("The controllerId of the found target is not equal to the controllerId of the saved target");
}
}
targetManagement.deleteByControllerId(extra.getControllerId());
final int numberToDelete = 50;
final Collection<? extends Target> targetsToDelete = firstList.subList(0, numberToDelete);
final Target[] deletedTargets = toArray(targetsToDelete, Target.class);
final List<Long> targetsIdsToDelete = targetsToDelete.stream().map(Target::getId).toList();
targetManagement.delete(targetsIdsToDelete);
final List<? extends Target> targetsLeft = targetManagement.findAll(PageRequest.of(0, 200)).getContent();
assertThat(firstList.spliterator().getExactSizeIfKnown() - numberToDelete).as("Size of split list")
.isEqualTo(targetsLeft.spliterator().getExactSizeIfKnown());
Assertions.<Target> assertThat(targetsLeft).as("Not all undeleted found").doesNotContain(deletedTargets);
}
/**
* Tests the assignment of tags to the a single target.
*/
@@ -581,14 +354,14 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Tests the unassigment of tags to multiple targets.
* Tests the un-assigment of tags to multiple targets.
*/
@Test
@ExpectEvents({
@Expect(type = TargetTagCreatedEvent.class, count = 3),
@Expect(type = TargetCreatedEvent.class, count = 109),
@Expect(type = TargetUpdatedEvent.class, count = 227) })
void targetTagBulkUnassignments() {
void targetTagBulkUnAssignments() {
final TargetTag targTagA = targetTagManagement.create(TargetTagManagement.Create.builder().name("Targ-A-Tag").build());
final TargetTag targTagB = targetTagManagement.create(TargetTagManagement.Create.builder().name("Targ-B-Tag").build());
final TargetTag targTagC = targetTagManagement.create(TargetTagManagement.Create.builder().name("Targ-C-Tag").build());
@@ -641,9 +414,6 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
checkTargetHasNotTags(targABCs, targTagC);
}
/**
* Test that NO TAG functionality which gives all targets with no tag assigned.
*/
@Test
@ExpectEvents({
@Expect(type = TargetTagCreatedEvent.class, count = 1),
@@ -705,30 +475,11 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
.isEqualTo(27L);
}
/**
* Verify that the find all targets by ids method contains the entities that we are looking for
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 12) })
void verifyFindTargetAllById() {
final List<Long> searchIds = Arrays.asList(testdataFactory.createTarget("target-4").getId(),
testdataFactory.createTarget("target-5").getId(), testdataFactory.createTarget("target-6").getId());
for (int i = 0; i < 9; i++) {
testdataFactory.createTarget("test" + i);
}
final List<? extends Target> foundDs = targetManagement.get(searchIds);
assertThat(foundDs).hasSize(3);
final List<Long> collect = foundDs.stream().map(Target::getId).toList();
assertThat(collect).containsAll(searchIds);
}
/**
* Verify that the flag for requesting controller attributes is set correctly.
*/
@Test
void verifyRequestControllerAttributes() {
void requestControllerAttributes() {
final String knownControllerId = "KnownControllerId";
final Target target = createTargetWithAttributes(knownControllerId);
assertThat(target.isRequestControllerAttributes()).isFalse();
@@ -738,112 +489,6 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
assertThat(updated.isRequestControllerAttributes()).isTrue();
}
/**
* Verifies the enforcement of the metadata quota per target.
*/
@Test
void createMetadataUntilQuotaIsExceeded() {
// add meta-data one by one
final Target target1 = testdataFactory.createTarget("target1");
final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerTarget();
for (int i = 0; i < maxMetaData; ++i) {
insertMetadata("k" + i, "v" + i, target1);
}
// quota exceeded
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> insertMetadata("k" + maxMetaData, "v" + maxMetaData, target1));
// add multiple meta data entries at once
final Target target2 = testdataFactory.createTarget("target2");
final Map<String, String> metaData2 = new HashMap<>();
for (int i = 0; i < maxMetaData + 1; i++) {
metaData2.put("k" + i, "v" + i);
}
// verify quota is exceeded
final String target2ControllerId = target2.getControllerId();
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> targetManagement.createMetadata(target2ControllerId, metaData2));
// add some meta data entries
final Target target3 = testdataFactory.createTarget("target3");
final int firstHalf = maxMetaData / 2;
for (int i = 0; i < firstHalf; i++) {
insertMetadata("k" + i, "v" + i, target3);
}
// add too many data entries
final int secondHalf = maxMetaData - firstHalf;
final Map<String, String> metaData3 = new HashMap<>();
for (int i = 0; i < secondHalf + 1; i++) {
metaData3.put("kk" + i, "vv" + i);
}
// verify quota is exceeded
final String target3ControllerId = target3.getControllerId();
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> targetManagement.createMetadata(target3ControllerId, metaData3));
}
/**
* Queries and loads the metadata related to a given target.
*/
@Test
void getMetadata() {
// create targets
assertThat(targetManagement.getMetadata(testdataFactory.createTarget("target0").getControllerId())).isEmpty();
assertThat(targetManagement.getMetadata(createTargetWithMetadata("target1", 10).getControllerId())).hasSize(10);
assertThat(targetManagement.getMetadata(createTargetWithMetadata("target2", 8).getControllerId())).hasSize(8);
}
/**
* Checks that metadata for a target can be updated.
*/
@Test
@WithUser(allSpPermissions = true)
void createMetadata() {
final String knownKey = "myKnownKey";
final String knownValue = "myKnownValue";
final String knownUpdateValue = "myNewUpdatedValue";
// create a target
final Target target = testdataFactory.createTarget("target1");
// initial opt lock revision must be zero
assertThat(target.getOptLockRevision()).isEqualTo(1);
// create target meta data entry
insertMetadata(knownKey, knownValue, target);
final Target changedLockRevisionTarget = targetManagement.find(target.getId()).orElseThrow(NoSuchElementException::new);
assertThat(changedLockRevisionTarget.getOptLockRevision()).isEqualTo(2);
// update the target metadata
targetManagement.createMetadata(target.getControllerId(), knownKey, knownUpdateValue);
// we are updating the target meta-data so also modifying the target so opt lock revision must be three
final Target changedLockRevisionTarget2 = targetManagement.find(target.getId()).orElseThrow(NoSuchElementException::new);
assertThat(changedLockRevisionTarget2.getOptLockRevision()).isEqualTo(3);
assertThat(changedLockRevisionTarget2.getLastModifiedAt()).isPositive();
// verify updated meta data contains the updated value
assertThat(targetManagement.getMetadata(target.getControllerId())).containsEntry(knownKey, knownUpdateValue);
}
/**
* Queries and loads the metadata related to a given target.
*/
@Test
void deleteMetadata() {
final String knownKey = "myKnownKey";
final String knownValue = "myKnownValue";
// create target
final String controllerId = createTargetWithMetadata("target1", 10).getControllerId();
targetManagement.createMetadata(controllerId, Map.of(knownKey, knownValue));
targetManagement.deleteMetadata(controllerId, knownKey);
// already removed
assertThatExceptionOfType(EntityNotFoundException.class)
.isThrownBy(() -> targetManagement.deleteMetadata(controllerId, knownKey));
}
/**
* Checks that target type for a target can be created, updated and unassigned.
*/
@@ -939,36 +584,6 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
checkTargetsHaveType(typeBTargets, typeB);
}
/**
* Checks that target type is not assigned to target if invalid.
*/
@Test
@WithUser(allSpPermissions = true)
void assignInvalidTargetTypeToTarget() {
// create a target
final Target target = testdataFactory.createTarget("target1", "testtarget");
// initial opt lock revision must be one
final Optional<JpaTarget> targetFound = targetRepository.findById(target.getId());
assertThat(targetFound).isPresent();
assertThat(targetFound.get().getOptLockRevision()).isEqualTo(1);
assertThat(targetFound.get().getTargetType()).isNull();
// assign target type to target
final String controllerId = targetFound.get().getControllerId();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("target type with id=null cannot be assigned")
.isThrownBy(() -> targetManagement.assignType(controllerId, null));
assertThatExceptionOfType(EntityNotFoundException.class)
.as("target type with id that does not exists cannot be assigned")
.isThrownBy(() -> targetManagement.assignType(controllerId, 114L));
// opt lock revision is not changed
final Optional<JpaTarget> targetFound1 = targetRepository.findById(target.getId());
assertThat(targetFound1).isPresent();
assertThat(targetFound1.get().getOptLockRevision()).isEqualTo(1);
}
/**
* Checks that target type can be unassigned from target.
*/
@@ -1055,8 +670,8 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
final DistributionSet ds = testdataFactory.createDistributionSet();
final String filter = "metadata.key1==target1-value1";
assertThat(targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(target.getControllerId(),
ds.getId(), filter)).isTrue();
assertThat(targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(
target.getControllerId(), ds.getId(), filter)).isTrue();
}
/**
@@ -1084,8 +699,8 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
assignDistributionSet(ds2, target);
final String filter = "name==*";
assertThat(targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(target.getControllerId(),
ds1.getId(), filter)).isFalse();
assertThat(targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(
target.getControllerId(), ds1.getId(), filter)).isFalse();
}
/**
@@ -1097,8 +712,8 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
final Target target = testdataFactory.createTarget("target", "target", type);
final DistributionSet ds = testdataFactory.createDistributionSet();
assertThat(targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(target.getControllerId(),
ds.getId(), "name==*")).isFalse();
assertThat(targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(
target.getControllerId(), ds.getId(), "name==*")).isFalse();
}
/**
@@ -1195,7 +810,7 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
* Test update status convert
*/
@Test
void testUpdateStatusConvert() {
void updateStatusConvert() {
final long id = testdataFactory.createTarget().getId();
for (final TargetUpdateStatus status : TargetUpdateStatus.values()) {
final JpaTarget target = targetRepository.findById(id).orElseThrow(() -> new IllegalStateException("Target not found"));
@@ -1206,6 +821,108 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
}
}
@Test
@ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetTagCreatedEvent.class, count = 1) })
void failIfReferNotExistingEntity() {
final TargetTag tag = targetTagManagement.create(TargetTagManagement.Create.builder().name("A").build());
final Target target = testdataFactory.createTarget();
verifyThrownExceptionBy(
() -> targetManagement.assignTag(Collections.singletonList(target.getControllerId()), NOT_EXIST_IDL), "TargetTag");
verifyThrownExceptionBy(() -> targetManagement.assignTag(Collections.singletonList(NOT_EXIST_ID), tag.getId()), "Target");
verifyThrownExceptionBy(() -> targetManagement.findByTag(NOT_EXIST_IDL, PAGE), "TargetTag");
verifyThrownExceptionBy(() -> targetManagement.findByRsqlAndTag("name==*", NOT_EXIST_IDL, PAGE), "TargetTag");
verifyThrownExceptionBy(
() -> targetManagement.countByRsqlAndNonDsAndCompatibleAndUpdatable(NOT_EXIST_IDL, "name==*"),
"DistributionSet");
verifyThrownExceptionBy(() -> targetManagement.deleteByControllerId(NOT_EXIST_ID), "Target");
verifyThrownExceptionBy(() -> targetManagement.delete(Collections.singletonList(NOT_EXIST_IDL)), "Target");
verifyThrownExceptionBy(
() -> targetManagement.findByTargetFilterQueryAndNonDSAndCompatibleAndUpdatable(NOT_EXIST_IDL, "name==*", PAGE),
"DistributionSet");
verifyThrownExceptionBy(() -> targetManagement.findByInRolloutGroupWithoutAction(NOT_EXIST_IDL, PAGE), "RolloutGroup");
verifyThrownExceptionBy(() -> targetManagement.findByAssignedDistributionSet(NOT_EXIST_IDL, PAGE), "DistributionSet");
verifyThrownExceptionBy(
() -> targetManagement.findByAssignedDistributionSetAndRsql(NOT_EXIST_IDL, "name==*", PAGE), "DistributionSet");
verifyThrownExceptionBy(() -> targetManagement.findByInstalledDistributionSet(NOT_EXIST_IDL, PAGE), "DistributionSet");
verifyThrownExceptionBy(
() -> targetManagement.findByInstalledDistributionSetAndRsql(NOT_EXIST_IDL, "name==*", PAGE), "DistributionSet");
verifyThrownExceptionBy(() -> targetManagement
.assignTag(Collections.singletonList(target.getControllerId()), Long.parseLong(NOT_EXIST_ID)), "TargetTag");
verifyThrownExceptionBy(
() -> targetManagement.assignTag(Collections.singletonList(NOT_EXIST_ID), tag.getId()), "Target");
verifyThrownExceptionBy(() -> targetManagement.unassignTag(List.of(NOT_EXIST_ID), tag.getId()), "Target");
verifyThrownExceptionBy(() -> targetManagement.unassignTag(List.of(target.getControllerId()), NOT_EXIST_IDL), "TargetTag");
verifyThrownExceptionBy(() -> targetManagement.update(Update.builder().id(NOT_EXIST_IDL).build()), "Target");
verifyThrownExceptionBy(() -> targetManagement.createMetadata(NOT_EXIST_ID, Map.of("123", "123")), "Target");
verifyThrownExceptionBy(() -> targetManagement.deleteMetadata(NOT_EXIST_ID, "xxx"), "Target");
verifyThrownExceptionBy(() -> targetManagement.deleteMetadata(target.getControllerId(), NOT_EXIST_ID), "Target");
verifyThrownExceptionBy(() -> targetManagement.getMetadata(NOT_EXIST_ID).get("xxx"), "Target");
verifyThrownExceptionBy(() -> targetManagement.createMetadata(NOT_EXIST_ID, "xxx", "xxx"), "Target");
}
/**
* Verify that a target with same controller ID than another device cannot be created.
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1) })
void failToCreateTargetWithSameControllerId() {
final Create targetCreate = Create.builder().controllerId("123").build();
targetManagement.create(targetCreate);
assertThatExceptionOfType(EntityAlreadyExistsException.class).isThrownBy(() -> targetManagement.create(targetCreate));
}
@Test
@ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetUpdatedEvent.class) })
void failToCreateAndUpdateTargetWithInvalidFields() {
final Target target = testdataFactory.createTarget();
createTargetWithInvalidControllerId();
createAndUpdateTargetWithInvalidDescription(target);
createAndUpdateTargetWithInvalidName(target);
createAndUpdateTargetWithInvalidSecurityToken(target);
createAndUpdateTargetWithInvalidAddress(target);
}
@Test
@WithUser(allSpPermissions = true)
void failToAssignInvalidTargetTypeToTarget() {
// create a target
final Target target = testdataFactory.createTarget("target1", "testtarget");
// initial opt lock revision must be one
final Optional<JpaTarget> targetFound = targetRepository.findById(target.getId());
assertThat(targetFound).isPresent();
assertThat(targetFound.get().getOptLockRevision()).isEqualTo(1);
assertThat(targetFound.get().getTargetType()).isNull();
// assign target type to target
final String controllerId = targetFound.get().getControllerId();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("target type with id=null cannot be assigned")
.isThrownBy(() -> targetManagement.assignType(controllerId, null));
assertThatExceptionOfType(EntityNotFoundException.class)
.as("target type with id that does not exists cannot be assigned")
.isThrownBy(() -> targetManagement.assignType(controllerId, 114L));
// opt lock revision is not changed
final Optional<JpaTarget> targetFound1 = targetRepository.findById(target.getId());
assertThat(targetFound1).isPresent();
assertThat(targetFound1.get().getOptLockRevision()).isEqualTo(1);
}
private void createAndUpdateTargetWithInvalidDescription(final Target target) {
final Create targetCreateTooLong = Create.builder().controllerId("a").description(randomString(513)).build();
assertThatExceptionOfType(ConstraintViolationException.class)

View File

@@ -14,11 +14,9 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import jakarta.validation.ConstraintViolationException;
@@ -26,12 +24,9 @@ import org.eclipse.hawkbit.repository.TargetTagManagement;
import org.eclipse.hawkbit.repository.TargetTagManagement.Create;
import org.eclipse.hawkbit.repository.TargetTagManagement.Update;
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTagUpdatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetTagUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag;
import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.repository.model.Tag;
import org.eclipse.hawkbit.repository.model.Target;
@@ -47,15 +42,44 @@ import org.springframework.data.domain.Pageable;
* Feature: Component Tests - Repository<br/>
* Story: Target Tag Management
*/
class TargetTagManagementTest extends AbstractJpaIntegrationTest {
class TargetTagManagementTest extends AbstractRepositoryManagementTest<TargetTag, Create, Update> {
private static final Random RND = new Random();
/**
* Verifies that tagging of set containing missing DS throws meaningful and correct exception.
*/
@Test
void failOnMissingDs() {
void assignAndUnassignTargetTags() {
final List<Target> groupA = testdataFactory.createTargets(20);
final List<Target> groupB = testdataFactory.createTargets(20, "groupb", "groupb");
final TargetTag tag = targetTagManagement.create(Create.builder().name("tag1").description("tagdesc1").build());
// toggle A only -> A is now assigned
List<Target> result = assignTag(groupA, tag);
assertThat(result)
.containsAll(targetManagement.getByControllerId(groupA.stream().map(Target::getControllerId).toList()))
.size().isEqualTo(20);
assertThat(targetManagement.findByTag(tag.getId(), Pageable.unpaged()).getContent().stream().map(Target::getControllerId).sorted()
.toList())
.isEqualTo(groupA.stream().map(Target::getControllerId).sorted().toList());
// toggle A+B -> A is still assigned and B is assigned as well
final Collection<Target> groupAB = concat(groupA, groupB);
result = assignTag(groupAB, tag);
assertThat(result)
.containsAll(targetManagement.getByControllerId(groupAB.stream().map(Target::getControllerId).toList()))
.size().isEqualTo(40);
assertThat(targetManagement.findByTag(tag.getId(), Pageable.unpaged()).getContent().stream().map(Target::getControllerId).sorted()
.toList())
.isEqualTo(groupAB.stream().map(Target::getControllerId).sorted().toList());
// toggle A+B -> both unassigned
result = unassignTag(groupAB, tag);
assertThat(result)
.containsAll(targetManagement.getByControllerId(groupAB.stream().map(Target::getControllerId).toList()))
.size().isEqualTo(40);
assertThat(targetManagement.findByTag(tag.getId(), Pageable.unpaged()).getContent()).isEmpty();
}
@Test
void failToAssignOnMissingTargetTag() {
final Collection<String> group = testdataFactory.createTargets(5).stream()
.map(Target::getControllerId)
.toList();
@@ -84,162 +108,29 @@ class TargetTagManagementTest extends AbstractJpaIntegrationTest {
});
}
/**
* Verifies that management get access reacts as specfied on calls for non existing entities by means
* of Optional not present.
*/
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class) })
void nonExistingEntityAccessReturnsNotPresent() {
assertThat(targetTagManagement.find(NOT_EXIST_IDL)).isNotPresent();
}
/**
* Verifies that management queries react as specfied on calls for non existing entities
* by means of throwing EntityNotFoundException.
*/
@Test
@ExpectEvents({
@Expect(type = DistributionSetTagUpdatedEvent.class),
@Expect(type = TargetTagUpdatedEvent.class) })
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
void failIfReferNotExistingEntity() {
verifyThrownExceptionBy(() -> targetTagManagement.delete(NOT_EXIST_IDL), "TargetTag");
verifyThrownExceptionBy(() -> targetTagManagement.update(Update.builder().id(NOT_EXIST_IDL).build()), "TargetTag");
verifyThrownExceptionBy(() -> getTargetTags(NOT_EXIST_ID), "Target");
}
/**
* Verify that a tag with with invalid properties cannot be created or updated
*/
@Test
void createAndUpdateTagWithInvalidFields() {
void failToCreateAndUpdateTagWithInvalidFields() {
final TargetTag tag = targetTagManagement.create(Create.builder().name("tag1").description("tagdesc1").build());
createAndUpdateTagWithInvalidDescription(tag);
createAndUpdateTagWithInvalidColour(tag);
createAndUpdateTagWithInvalidName(tag);
}
/**
* Verifies assign/unassign.
*/
@Test
void assignAndUnassignTargetTags() {
final List<Target> groupA = testdataFactory.createTargets(20);
final List<Target> groupB = testdataFactory.createTargets(20, "groupb", "groupb");
final TargetTag tag = targetTagManagement.create(Create.builder().name("tag1").description("tagdesc1").build());
// toggle A only -> A is now assigned
List<Target> result = assignTag(groupA, tag);
assertThat(result)
.containsAll(
targetManagement.getByControllerId(groupA.stream().map(Target::getControllerId).toList()))
.size().isEqualTo(20);
assertThat(targetManagement.findByTag(tag.getId(), Pageable.unpaged()).getContent().stream().map(Target::getControllerId).sorted()
.toList())
.isEqualTo(groupA.stream().map(Target::getControllerId).sorted().toList());
// toggle A+B -> A is still assigned and B is assigned as well
final Collection<Target> groupAB = concat(groupA, groupB);
result = assignTag(groupAB, tag);
assertThat(result)
.containsAll(
targetManagement.getByControllerId(groupAB.stream().map(Target::getControllerId).toList()))
.size().isEqualTo(40);
assertThat(targetManagement.findByTag(tag.getId(), Pageable.unpaged()).getContent().stream().map(Target::getControllerId).sorted()
.toList())
.isEqualTo(groupAB.stream().map(Target::getControllerId).sorted().toList());
// toggle A+B -> both unassigned
result = unassignTag(groupAB, tag);
assertThat(result)
.containsAll(targetManagement.getByControllerId(groupAB.stream().map(Target::getControllerId).toList()))
.size().isEqualTo(40);
assertThat(targetManagement.findByTag(tag.getId(), Pageable.unpaged()).getContent()).isEmpty();
}
/**
* Ensures that all tags are retrieved through repository.
*/
@Test
void findAllTargetTags() {
final List<JpaTargetTag> tags = createTargetsWithTags();
assertThat(targetTagRepository.findAll()).isEqualTo(targetTagRepository.findAll()).isEqualTo(tags)
.as("Wrong tag size").hasSize(20);
}
/**
* Ensures that a created tag is persisted in the repository as defined.
*/
@Test
void createTargetTag() {
final Tag tag = targetTagManagement.create(Create.builder().name("k1").description("k2").colour("colour").build());
assertThat(targetTagManagement.find(tag.getId()).orElseThrow().getColour()).as("wrong tag found").isEqualTo("colour");
}
/**
* Ensures that a deleted tag is removed from the repository as defined.
*/
@Test
void deleteTargetTags() {
// create test data
final Iterable<JpaTargetTag> tags = createTargetsWithTags();
final TargetTag toDelete = tags.iterator().next();
for (final Target target : targetRepository.findAll()) {
assertThat(getTargetTags(target.getControllerId())).contains(toDelete);
}
// delete
targetTagManagement.delete(toDelete.getId());
// check
for (final Target target : targetRepository.findAll()) {
assertThat(getTargetTags(target.getControllerId()))
.doesNotContain(toDelete);
}
assertThat(targetTagRepository.findById(toDelete.getId())).as("No tag should be found").isNotPresent();
assertThat(targetTagRepository.findAll()).as("Wrong target tag size").hasSize(19);
}
/**
* Tests the name update of a target tag.
*/
@Test
void updateTargetTag() {
final List<JpaTargetTag> tags = createTargetsWithTags();
// change data
final TargetTag savedAssigned = tags.iterator().next();
// persist
targetTagManagement.update(Update.builder().id(savedAssigned.getId()).name("test123").build());
// check data
assertThat(targetTagRepository.findAll()).as("Wrong target tag size").hasSize(tags.size());
assertThat(targetTagRepository.findById(savedAssigned.getId()).orElseThrow().getName()).as("wrong target tag is saved")
.isEqualTo("test123");
assertThat(targetTagRepository.findById(savedAssigned.getId()).orElseThrow().getOptLockRevision())
.as("wrong target tag is saved")
.isEqualTo(2);
}
/**
* Ensures that a tag cannot be created if one exists already with that name (expects EntityAlreadyExistsException).
*/
@Test
void failedDuplicateTargetTagNameException() {
final Create tagCreate = Create.builder().name("A").build();
targetTagManagement.create(tagCreate);
assertThatExceptionOfType(EntityAlreadyExistsException.class).isThrownBy(() -> targetTagManagement.create(tagCreate));
}
/**
* Ensures that a tag cannot be updated to a name that already exists on another tag (expects EntityAlreadyExistsException).
*/
@Test
void failedDuplicateTargetTagNameExceptionAfterUpdate() {
void failToDuplicateTargetTagNameOnUpdate() {
targetTagManagement.create(Create.builder().name("A").build());
final TargetTag tag = targetTagManagement.create(Create.builder().name("B").build());
@@ -306,20 +197,4 @@ class TargetTagManagementTest extends AbstractJpaIntegrationTest {
.as("tag with too short name should not be updated")
.isThrownBy(() -> targetTagManagement.update(tagUpdateEmpty));
}
@SafeVarargs
private <T> Collection<T> concat(final Collection<T>... targets) {
final List<T> result = new ArrayList<>();
Arrays.asList(targets).forEach(result::addAll);
return result;
}
private List<JpaTargetTag> createTargetsWithTags() {
final List<Target> targets = testdataFactory.createTargets(20);
final Iterable<? extends TargetTag> tags = testdataFactory.createTargetTags(20, "");
tags.forEach(tag -> assignTag(targets, tag));
return targetTagRepository.findAll();
}
}

View File

@@ -20,10 +20,8 @@ import jakarta.validation.ConstraintViolationException;
import org.eclipse.hawkbit.repository.TargetTypeManagement.Create;
import org.eclipse.hawkbit.repository.TargetTypeManagement.Update;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.NamedEntity;
@@ -37,77 +35,16 @@ import org.junit.jupiter.api.Test;
* Feature: Component Tests - Repository<br/>
* Story: Target Type Management
*/
class TargetTypeManagementTest extends AbstractJpaIntegrationTest {
/**
* Verifies that management get access react as specified on calls for non existing entities by means
* of Optional not present.
*/
@Test
@ExpectEvents({ @Expect(type = TargetTypeCreatedEvent.class) })
void nonExistingEntityAccessReturnsNotPresent() {
assertThat(targetTypeManagement.find(NOT_EXIST_IDL)).isNotPresent();
assertThat(targetTypeManagement.getByName(NOT_EXIST_ID)).isNotPresent();
}
/**
* Verifies that management queries react as specified on calls for non existing entities
* by means of throwing EntityNotFoundException.
*/
@Test
@ExpectEvents({ @Expect(type = TargetTypeUpdatedEvent.class) })
void entityQueriesReferringToNotExistingEntitiesThrowsException() {
verifyThrownExceptionBy(() -> targetTypeManagement.delete(NOT_EXIST_IDL), "TargetType");
verifyThrownExceptionBy(() -> targetTypeManagement.update(Update.builder().id(NOT_EXIST_IDL).build()),"TargetType");
}
/**
* Verify that a target type with invalid properties cannot be created or updated
*/
@Test
void createAndUpdateTargetTypeWithInvalidFields() {
final TargetType targetType = targetTypeManagement
.create(Create.builder().name("targettype1").description("targettypedes1").key("targettype1.key").build());
createAndUpdateTargetTypeWithInvalidDescription(targetType);
createAndUpdateTargetTypeWithInvalidColour(targetType);
createTargetTypeWithInvalidKey();
createAndUpdateTargetTypeWithInvalidName(targetType);
}
void createAndUpdateTargetTypeWithInvalidDescription(final TargetType targetType) {
final Create targetTypeCreateTooLong = Create.builder()
.name("a").description(randomString(TargetType.DESCRIPTION_MAX_SIZE + 1)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("targetType with too long description should not be created")
.isThrownBy(() -> targetTypeManagement.create(targetTypeCreateTooLong));
final Create targetTypeCreateInvalidHtml = Create.builder().name("a").description(INVALID_TEXT_HTML).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("targetType with invalid description should not be created")
.isThrownBy(() -> targetTypeManagement.create(targetTypeCreateInvalidHtml));
final Update targetTypeUpdateTooLong = Update.builder().id(targetType.getId())
.description(randomString(TargetType.DESCRIPTION_MAX_SIZE + 1)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("targetType with too long description should not be updated")
.isThrownBy(() -> targetTypeManagement.update(targetTypeUpdateTooLong));
final Update targetTypeUpdateInvalidHtml = Update.builder().id(targetType.getId()).description(INVALID_TEXT_HTML).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("targetType with invalid description should not be updated")
.isThrownBy(() -> targetTypeManagement.update(targetTypeUpdateInvalidHtml));
}
class TargetTypeManagementTest extends AbstractRepositoryManagementTest<TargetType, Create, Update> {
/**
* Tests the successful assignment of compatible distribution set types to a target type
*/
@Test
void assignCompatibleDistributionSetTypesToTargetType() {
final TargetType targetType = targetTypeManagement
.create(Create.builder()
.name("targettype1").description("targettypedes1").key("targettyp1.key").build());
DistributionSetType distributionSetType = testdataFactory.findOrCreateDistributionSetType("testDst", "dst1");
final TargetType targetType = targetTypeManagement.create(
Create.builder().name("targettype1").description("targettypedes1").key("targettyp1.key").build());
final DistributionSetType distributionSetType = testdataFactory.findOrCreateDistributionSetType("testDst", "dst1");
targetTypeManagement.assignCompatibleDistributionSetTypes(targetType.getId(), Collections.singletonList(distributionSetType.getId()));
Optional<JpaTargetType> targetTypeWithDsTypes = targetTypeRepository.findById(targetType.getId());
@@ -133,86 +70,60 @@ class TargetTypeManagementTest extends AbstractJpaIntegrationTest {
assertThat(targetTypeWithDsTypes1.get().getDistributionSetTypes()).isEmpty();
}
/**
* Ensures that all types are retrieved through repository.
*/
@Test
void findAllTargetTypes() {
testdataFactory.createTargetTypes("targettype", 10);
assertThat(targetTypeRepository.findAll()).as("Target type size").hasSize(10);
@ExpectEvents({ @Expect(type = TargetTypeUpdatedEvent.class) })
void failIfReferNotExistingEntity() {
verifyThrownExceptionBy(() -> targetTypeManagement.delete(NOT_EXIST_IDL), "TargetType");
verifyThrownExceptionBy(() -> targetTypeManagement.update(Update.builder().id(NOT_EXIST_IDL).build()),"TargetType");
}
/**
* Ensures that a created target type is persisted in the repository as defined.
* Verify that a target type with invalid properties cannot be created or updated
*/
@Test
void createTargetType() {
final String name = "targettype1";
final String key = "targettype1.key";
targetTypeManagement.create(Create.builder().name(name).description("targettypedes1").key(key).colour("colour1").build());
void failToCreateAndUpdateTargetTypeWithInvalidFields() {
final TargetType targetType = targetTypeManagement
.create(Create.builder().name("targettype1").description("targettypedes1").key("targettype1.key").build());
assertThat(findByName(name).map(JpaTargetType::getName).orElse(null)).as("type found (name)")
.isEqualTo(name);
assertThat(findByName(name).map(JpaTargetType::getDescription).orElse(null))
.as("type found (des)").isEqualTo("targettypedes1");
assertThat(findByName(name).map(JpaTargetType::getKey).orElse(null)).as("type found (key)")
.isEqualTo(key);
assertThat(findByName(name).map(JpaTargetType::getColour).orElse(null))
.as("type found (colour)").isEqualTo("colour1");
assertThat(findByKey(key).map(JpaTargetType::getName).orElse(null)).as("type found (name)")
.isEqualTo(name);
assertThat(findByKey(key).map(JpaTargetType::getDescription).orElse(null))
.as("type found (des)").isEqualTo("targettypedes1");
assertThat(findByKey(key).map(JpaTargetType::getKey).orElse(null)).as("type found (key)")
.isEqualTo(key);
assertThat(findByKey(key).map(JpaTargetType::getColour).orElse(null)).as("type found (colour)")
.isEqualTo("colour1");
}
/**
* Ensures that a deleted target type is removed from the repository as defined.
*/
@Test
void deleteTargetType() {
// create test data
final TargetType targetType = targetTypeManagement.create(Create.builder().name("targettype11").description("targettypedes11").build());
assertThat(findByName("targettype11").get().getDescription()).as("type found").isEqualTo("targettypedes11");
targetTypeManagement.delete(targetType.getId());
assertThat(targetTypeRepository.findById(targetType.getId())).as("No target type should be found").isNotPresent();
}
/**
* Tests the name update of a target type.
*/
@Test
void updateTargetType() {
final TargetType targetType =
targetTypeManagement.create(Create.builder().name("targettype111").description("targettypedes111").build());
assertThat(findByName("targettype111").get().getDescription()).as("type found").isEqualTo("targettypedes111");
targetTypeManagement.update(Update.builder().id(targetType.getId()).name("updatedtargettype111").build());
assertThat(findByName("updatedtargettype111")).as("Updated target type should be found").isPresent();
}
/**
* Ensures that a target type cannot be created if one exists already with that name (expects EntityAlreadyExistsException).
*/
@Test
void failedDuplicateTargetTypeNameException() {
final Create targetTypeCreate = Create.builder().name("targettype123").build();
targetTypeManagement.create(targetTypeCreate);
assertThrows(EntityAlreadyExistsException.class, () -> targetTypeManagement.create(targetTypeCreate));
createAndUpdateTargetTypeWithInvalidDescription(targetType);
createAndUpdateTargetTypeWithInvalidColour(targetType);
createTargetTypeWithInvalidKey();
createAndUpdateTargetTypeWithInvalidName(targetType);
}
/**
* Ensures that a target type cannot be updated to a name that already exists (expects EntityAlreadyExistsException).
*/
@Test
void failedDuplicateTargetTypeNameExceptionAfterUpdate() {
void failToDuplicateTargetTypeNameExceptionOnUpdate() {
targetTypeManagement.create(Create.builder().name("targettype1234").build());
TargetType targetType = targetTypeManagement.create(Create.builder().name("targettype12345").build());
assertThrows(EntityAlreadyExistsException.class,
() -> targetTypeManagement.update(Update.builder().id(targetType.getId()).name("targettype1234").build()));
final TargetType targetType = targetTypeManagement.create(Create.builder().name("targettype12345").build());
final Update update = Update.builder().id(targetType.getId()).name("targettype1234").build();
assertThrows(EntityAlreadyExistsException.class, () -> targetTypeManagement.update(update));
}
private void createAndUpdateTargetTypeWithInvalidDescription(final TargetType targetType) {
final Create targetTypeCreateTooLong = Create.builder()
.name("a").description(randomString(TargetType.DESCRIPTION_MAX_SIZE + 1)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("targetType with too long description should not be created")
.isThrownBy(() -> targetTypeManagement.create(targetTypeCreateTooLong));
final Create targetTypeCreateInvalidHtml = Create.builder().name("a").description(INVALID_TEXT_HTML).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("targetType with invalid description should not be created")
.isThrownBy(() -> targetTypeManagement.create(targetTypeCreateInvalidHtml));
final Update targetTypeUpdateTooLong = Update.builder().id(targetType.getId())
.description(randomString(TargetType.DESCRIPTION_MAX_SIZE + 1)).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("targetType with too long description should not be updated")
.isThrownBy(() -> targetTypeManagement.update(targetTypeUpdateTooLong));
final Update targetTypeUpdateInvalidHtml = Update.builder().id(targetType.getId()).description(INVALID_TEXT_HTML).build();
assertThatExceptionOfType(ConstraintViolationException.class)
.as("targetType with invalid description should not be updated")
.isThrownBy(() -> targetTypeManagement.update(targetTypeUpdateInvalidHtml));
}
private void createAndUpdateTargetTypeWithInvalidColour(final TargetType targetType) {
@@ -279,12 +190,4 @@ class TargetTypeManagementTest extends AbstractJpaIntegrationTest {
.isThrownBy(() -> targetTypeManagement.update(targetTypeUpdateEmpty));
}
private Optional<JpaTargetType> findByName(final String name) {
return targetTypeManagement.getByName(name).map(JpaTargetType.class::cast);
}
private Optional<JpaTargetType> findByKey(final String key) {
return targetTypeManagement.getByKey(key).map(JpaTargetType.class::cast);
}
}

View File

@@ -8,25 +8,35 @@
# SPDX-License-Identifier: EPL-2.0
#
NOISE_SUPPRESS_LEVEL=WARN
### General logging configuration - START
## Remove noisy logs from libraries, could be set to INFO (or more detailed) when needed by switching the NOISE_SUPPRES_LEVEL
logging.level.org.springframework.integration=${NOISE_SUPPRESS_LEVEL}
logging.level.org.flywaydb=${NOISE_SUPPRESS_LEVEL}
logging.level.com.zaxxer.hikari=${NOISE_SUPPRESS_LEVEL}
logging.level.org.springframework.data.jpa.repository.query.QueryEnhancerFactory=${NOISE_SUPPRESS_LEVEL}
## Remove JpaRepositoryFactory$EclipseLinkProjectionQueryCreationListener log, could be useful for EclipseLink debugging
## see https://github.com/spring-projects/spring-data-jpa/blob/main/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java
logging.level.org.springframework.data.jpa.repository.support.JpaRepositoryFactory=${NOISE_SUPPRESS_LEVEL}
logging.level.org.eclipse.hawkbit.repository.test.matcher.EventVerifier=${NOISE_SUPPRESS_LEVEL}
### General logging configuration - END
### Debug & Monitor Eclipselink - START
logging.level.org.eclipse.persistence=ERROR
logging.level.org.eclipse.persistence=${NOISE_SUPPRESS_LEVEL}
## Uncomment to see the debug of persistence, e.g. to see the generated SQLs
#logging.level.org.eclipse.persistence=DEBUG
#spring.jpa.properties.eclipselink.logging.level=FINE
#spring.jpa.properties.eclipselink.logging.level.sql=FINE
#spring.jpa.properties.eclipselink.logging.parameters=true
## Enable EclipseLink performance monitor (monitoring and profile)
#spring.jpa.properties.eclipselink.profiler=PerformanceMonitor
### Debug & Monitor Eclipselink - END
### Debug & Monitor Hibernate - START
## Enable the generated SQLs logging
#logging.level.org.hibernate.SQL=TRACE
#logging.level.org.hibernate.stat=TRACE
## Enable Hibernate statistics
#spring.jpa.properties.hibernate.generate_statistics=true
## Disables info log messages from Hibernate statistics
@@ -37,8 +47,6 @@ logging.level.org.eclipse.persistence=ERROR
#logging.level.org.springframework.aop=TRACE
#spring.aop.proxy-target-class=true
### Debug utility functions - END
### Switch to MySQL or MariaDB - START
#spring.jpa.database=MYSQL
#spring.datasource.url=jdbc:mariadb://localhost:3306/hawkbit_test
@@ -47,13 +55,13 @@ logging.level.org.eclipse.persistence=ERROR
#spring.datasource.password=
### Switch to MySQL or MariaDB - END
# enable / disable case sensitiveness of the DB when playing around
## enable / disable case sensitiveness of the DB when playing around
#hawkbit.rsql.caseInsensitiveDB=true
hawkbit.repository.cluster.lock.ttl=1000
hawkbit.repository.cluster.lock.refreshOnRemainMS=200
hawkbit.repository.cluster.lock.refreshOnRemainPercent=10
# reduce scheduler tic period to speed up tests
## reduce scheduler tic period to speed up tests
hawkbit.repository.cluster.lock.ticPeriodMS=10
org.eclipse.hawkbit.events.remote-enabled=false

View File

@@ -25,6 +25,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -35,8 +36,14 @@ import org.eclipse.hawkbit.repository.event.remote.AbstractRemoteEvent;
import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent;
import org.eclipse.hawkbit.repository.event.remote.MultiActionAssignEvent;
import org.eclipse.hawkbit.repository.event.remote.MultiActionCancelEvent;
import org.eclipse.hawkbit.repository.event.remote.RemoteIdEvent;
import org.eclipse.hawkbit.repository.event.remote.RemoteTenantAwareEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.event.remote.service.ActionCreatedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.ActionUpdatedServiceEvent;
@@ -47,12 +54,6 @@ import org.eclipse.hawkbit.repository.event.remote.service.TargetAssignDistribut
import org.eclipse.hawkbit.repository.event.remote.service.TargetAttributesRequestedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetCreatedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetDeletedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.RemoteIdEvent;
import org.eclipse.hawkbit.repository.event.remote.RemoteTenantAwareEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetUpdatedServiceEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
@@ -70,15 +71,12 @@ public class EventVerifier extends AbstractTestExecutionListener {
private EventCaptor eventCaptor;
/**
* Publishes a reset counter marker event on the context to reset the
* current counted events. This allows test to prepare a setup such in
* {@code @Before} annotations which are actually counted to the executed
* test-method and maybe fire events which are not covered / recognized by
* the test-method itself and reset the counter again.
* Publishes a reset counter marker event on the context to reset the current counted events. This allows test to prepare a setup such in
* {@code @Before} annotations which are actually counted to the executed test-method and maybe fire events which are not
* covered / recognized by the test-method itself and reset the counter again.
* <p/>
* Note that this approach is only working when using a single-thread
* executor in the ApplicationEventMultiCaster, so the order of the events
* keep the same.
* Note that this approach is only working when using a single-thread executor in the ApplicationEventMultiCaster, so the order of the
* events keep the same.
*
* @param publisher the {@link ApplicationEventPublisher} to publish the marker event to
*/
@@ -88,8 +86,12 @@ public class EventVerifier extends AbstractTestExecutionListener {
@Override
public void beforeTestMethod(final TestContext testContext) {
final Optional<Expect[]> expectedEvents = getExpectationsFrom(testContext.getTestMethod());
expectedEvents.ifPresent(events -> beforeTest(testContext));
Optional.ofNullable(testContext.getTestMethod().getAnnotation(ExpectEvents.class))
.map(ExpectEvents::value)
.ifPresent(events -> {
eventCaptor = new EventCaptor();
((ConfigurableApplicationContext) testContext.getApplicationContext()).addApplicationListener(eventCaptor);
});
}
@Override
@@ -100,75 +102,16 @@ public class EventVerifier extends AbstractTestExecutionListener {
return;
}
final Optional<Expect[]> expectedEvents = getExpectationsFrom(testContext.getTestMethod());
try {
expectedEvents.ifPresent(this::afterTest);
} finally {
expectedEvents.ifPresent(listener -> removeEventListener(testContext));
}
}
private Optional<Expect[]> getExpectationsFrom(final Method testMethod) {
final Optional<Expect[]> expectedEvents = Optional.ofNullable(testMethod.getAnnotation(ExpectEvents.class)).map(ExpectEvents::value);
if (expectedEvents.isPresent()) {
List<Expect> modifiedEvents = new ArrayList<>(Arrays.asList(expectedEvents.get()));
for (Expect event : expectedEvents.get()) {
final Class<?> type = event.type();
if (type.isAssignableFrom(TargetCreatedEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(TargetCreatedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetUpdatedEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(TargetUpdatedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetDeletedEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(TargetDeletedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetAssignDistributionSetEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(TargetAssignDistributionSetServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(MultiActionAssignEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(MultiActionAssignServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(MultiActionCancelEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(MultiActionCancelServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetAttributesRequestedEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(TargetAttributesRequestedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(CancelTargetAssignmentEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(CancelTargetAssignmentServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(ActionCreatedEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(ActionCreatedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(ActionUpdatedEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(ActionUpdatedServiceEvent.class, event.count()));
}
getExpectationsFrom(testContext.getTestMethod()).ifPresent(expectedEvents -> {
try {
verifyRightCountOfEvents(expectedEvents);
verifyAllEventsCounted(expectedEvents);
log.info("Expected events received:\n\t{}", Arrays.stream(expectedEvents).map(Object::toString).collect(Collectors.joining("\n\t")));
} finally {
testContext.getApplicationContext().getBean(ApplicationEventMulticaster.class).removeApplicationListener(eventCaptor);
DYNAMIC_EXPECTATIONS.remove();
}
return Optional.of(modifiedEvents.toArray(new Expect[0]));
}
return Optional.empty();
}
private static Expect toExpectServiceEvent(final Class<?> clazz, final int count) {
return new Expect() {
@Override
public Class<? extends Annotation> annotationType() {
return Expect.class;
}
@Override
public Class<?> type() {
return clazz;
}
@Override
public int count() {
return count;
}
};
}
private void beforeTest(final TestContext testContext) {
eventCaptor = new EventCaptor();
((ConfigurableApplicationContext) testContext.getApplicationContext()).addApplicationListener(eventCaptor);
}
private void afterTest(final Expect[] expectedEvents) {
verifyRightCountOfEvents(expectedEvents);
verifyAllEventsCounted(expectedEvents);
});
}
private void verifyRightCountOfEvents(final Expect[] expectedEvents) {
@@ -188,7 +131,7 @@ public class EventVerifier extends AbstractTestExecutionListener {
private void verifyAllEventsCounted(final Expect[] expectedEvents) {
final Set<Class<?>> diffSet = eventCaptor.diff(expectedEvents);
if (!diffSet.isEmpty()) {
final StringBuilder failMessage = new StringBuilder("Missing event verification for ");
final StringBuilder failMessage = new StringBuilder("Missing event expectation for ");
for (final Class<?> element : diffSet) {
final int count = eventCaptor.getCountFor(element);
failMessage.append(element).append(" with count: ").append(count).append(" ");
@@ -197,11 +140,67 @@ public class EventVerifier extends AbstractTestExecutionListener {
}
}
private void removeEventListener(final TestContext testContext) {
testContext.getApplicationContext().getBean(ApplicationEventMulticaster.class).removeApplicationListener(eventCaptor);
public static final ThreadLocal<Supplier<Expect[]>> DYNAMIC_EXPECTATIONS = new ThreadLocal<>();
private Optional<Expect[]> getExpectationsFrom(final Method testMethod) {
return Optional.ofNullable(testMethod.getAnnotation(ExpectEvents.class))
.map(ExpectEvents::value)
.map(expectedEvents -> {
final Supplier<Expect[]> supplier = DYNAMIC_EXPECTATIONS.get();
if (expectedEvents.length == 0) {
if (supplier != null) {
return supplier.get();
}
} else {
if (supplier != null && supplier.get().length > 0) {
fail("Expectations defined - both static and dynamic - only one definition is allowed");
}
}
return expectedEvents;
})
.map(expectedEvents -> {
final List<Expect> modifiedEvents = new ArrayList<>(Arrays.asList(expectedEvents));
for (final Expect event : expectedEvents) {
addServiceEventIfNeeded(event, modifiedEvents);
}
return modifiedEvents.toArray(new Expect[0]);
});
}
private static class EventCaptor implements ApplicationListener<AbstractRemoteEvent> {
private static void addServiceEventIfNeeded(final Expect event, final List<Expect> modifiedEvents) {
final Class<?> type = event.type();
if (type.isAssignableFrom(TargetCreatedEvent.class)) {
modifiedEvents.add(new DynamicExpect(TargetCreatedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetUpdatedEvent.class)) {
modifiedEvents.add(new DynamicExpect(TargetUpdatedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetDeletedEvent.class)) {
modifiedEvents.add(new DynamicExpect(TargetDeletedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetAssignDistributionSetEvent.class)) {
modifiedEvents.add(new DynamicExpect(TargetAssignDistributionSetServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(MultiActionAssignEvent.class)) {
modifiedEvents.add(new DynamicExpect(MultiActionAssignServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(MultiActionCancelEvent.class)) {
modifiedEvents.add(new DynamicExpect(MultiActionCancelServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetAttributesRequestedEvent.class)) {
modifiedEvents.add(new DynamicExpect(TargetAttributesRequestedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(CancelTargetAssignmentEvent.class)) {
modifiedEvents.add(new DynamicExpect(CancelTargetAssignmentServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(ActionCreatedEvent.class)) {
modifiedEvents.add(new DynamicExpect(ActionCreatedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(ActionUpdatedEvent.class)) {
modifiedEvents.add(new DynamicExpect(ActionUpdatedServiceEvent.class, event.count()));
}
}
public record DynamicExpect(Class<?> type, int count) implements Expect {
@Override
public Class<? extends Annotation> annotationType() {
return Expect.class;
}
}
private static final class EventCaptor implements ApplicationListener<AbstractRemoteEvent> {
private final ConcurrentHashMap<Class<?>, Integer> capturedEvents = new ConcurrentHashMap<>();
@@ -225,7 +224,6 @@ public class EventVerifier extends AbstractTestExecutionListener {
if (event instanceof TargetAssignDistributionSetEvent targetAssignDistributionSetEvent) {
assertThat(targetAssignDistributionSetEvent.getActions()).isNotEmpty();
assertThat(targetAssignDistributionSetEvent.getDistributionSetId()).isNotNull();
}
capturedEvents.compute(event.getClass(), (k, v) -> v == null ? 1 : v + 1);

View File

@@ -20,6 +20,8 @@ import java.net.URI;
import java.nio.file.Files;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -117,6 +119,8 @@ import org.springframework.test.context.TestPropertySource;
public abstract class AbstractIntegrationTest {
protected static final Pageable PAGE = PageRequest.of(0, 500, Sort.by(Direction.ASC, "id"));
protected static final Pageable UNPAGED = Pageable.unpaged();
protected static final URI LOCALHOST = URI.create("http://127.0.0.1");
protected static final int DEFAULT_TEST_WEIGHT = 500;
protected static final Random RND = new Random();
@@ -516,4 +520,11 @@ public abstract class AbstractIntegrationTest {
protected List<? extends Target> findByUpdateStatus(final TargetUpdateStatus status, final Pageable pageable) {
return targetManagement.findAll(pageable).stream().filter(target -> status.equals(target.getUpdateStatus())).toList();
}
@SafeVarargs
protected static <T> Collection<T> concat(final Collection<T>... targets) {
final List<T> result = new ArrayList<>();
List.of(targets).forEach(result::addAll);
return result;
}
}

View File

@@ -29,6 +29,6 @@ public class TestLoggerExtension implements BeforeTestExecutionCallback, TestWat
@Override
public void beforeTestExecution(final ExtensionContext context) {
log.info("Starting Test {}...", context.getTestMethod());
log.debug("Starting Test {}...", context.getTestMethod());
}
}

View File

@@ -430,16 +430,16 @@ public class TestdataFactory {
/**
* Creates {@link DistributionSet}s in repository including three {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} ,
* {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} followed by an iterative number and {@link DistributionSet#isRequiredMigrationStep()}
* {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} followed by an iterative count and {@link DistributionSet#isRequiredMigrationStep()}
* <code>false</code>.
*
* @param prefix for {@link SoftwareModule}s and {@link DistributionSet}s name, vendor and description.
* @param number of {@link DistributionSet}s to create
* @param count of {@link DistributionSet}s to create
* @return {@link List} of {@link DistributionSet} entities
*/
public List<DistributionSet> createDistributionSets(final String prefix, final int number) {
public List<DistributionSet> createDistributionSets(final String prefix, final int count) {
final List<DistributionSet> sets = new ArrayList<>();
for (int i = 0; i < number; i++) {
for (int i = 0; i < count; i++) {
sets.add(createDistributionSet(prefix, DEFAULT_VERSION + "." + i, false));
}
return sets;