Repository: OpenAEV-Platform/openaev (java)

Issue:
Improve isReady & mandatory fields on inject form



Discussion:

Patch:
diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java
--- a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java
+++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java
@@ -91,9 +91,9 @@ private ContractExpectations expectations() {
 
   private List<Contract> abilityContracts(@NotNull final ContractConfig contractConfig) {
     // Fields
-    ContractSelect obfuscatorField = obfuscatorField();
     ContractAsset assetField = assetField(Multiple);
     ContractAssetGroup assetGroupField = assetGroupField(Multiple);
+    ContractSelect obfuscatorField = obfuscatorField();
     ContractExpectations expectationsField = expectations();
 
     List<Ability> abilities =
diff --git a/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelContract.java b/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelContract.java
--- a/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelContract.java
+++ b/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelContract.java
@@ -93,14 +93,31 @@ public List<Contract> contracts() {
             .optional(expectationsField)
             // Emailing zone
             .optional(emailingField)
-            .mandatory(
+            .mandatoryOnConditionValue(
                 textField(
                     "subject",
                     "Subject",
                     "New media pressure entries published for ${user.email}",
-                    List.of(emailingField)))
-            .mandatory(richTextareaField("body", "Body", messageBody, List.of(emailingField)))
-            .optional(checkboxField("encrypted", "Encrypted", false, List.of(emailingField)))
+                    List.of(emailingField),
+                    Map.of(emailingField.getKey(), String.valueOf(true))),
+                emailingField,
+                String.valueOf(true))
+            .mandatoryOnConditionValue(
+                richTextareaField(
+                    "body",
+                    "Body",
+                    messageBody,
+                    List.of(emailingField),
+                    Map.of(emailingField.getKey(), String.valueOf(true))),
+                emailingField,
+                String.valueOf(true))
+            .optional(
+                checkboxField(
+                    "encrypted",
+                    "Encrypted",
+                    false,
+                    List.of(emailingField),
+                    Map.of(emailingField.getKey(), String.valueOf(true))))
             .build();
     Contract publishArticle =
         executableContract(
diff --git a/openbas-api/src/main/java/io/openbas/injectors/email/EmailContract.java b/openbas-api/src/main/java/io/openbas/injectors/email/EmailContract.java
--- a/openbas-api/src/main/java/io/openbas/injectors/email/EmailContract.java
+++ b/openbas-api/src/main/java/io/openbas/injectors/email/EmailContract.java
@@ -65,7 +65,6 @@ public List<Contract> contracts() {
             .mandatory(teamField(Multiple))
             .mandatory(textField("subject", "Subject"))
             .mandatory(richTextareaField("body", "Body"))
-            // .optional(textField("inReplyTo", "InReplyTo", "HIDDEN")) - Use for direct injection
             .optional(checkboxField("encrypted", "Encrypted", false))
             .optional(attachmentField(Multiple))
             .optional(expectationsField)
@@ -85,7 +84,6 @@ public List<Contract> contracts() {
             .mandatory(teamField(Multiple))
             .mandatory(textField("subject", "Subject"))
             .mandatory(richTextareaField("body", "Body"))
-            // .mandatory(textField("inReplyTo", "InReplyTo", "HIDDEN"))  - Use for direct injection
             .optional(attachmentField(Multiple))
             .optional(expectationsField)
             .build();
diff --git a/openbas-api/src/main/java/io/openbas/migration/V4_27__Rename_asset_group_contract.java b/openbas-api/src/main/java/io/openbas/migration/V4_27__Rename_asset_group_contract.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/migration/V4_27__Rename_asset_group_contract.java
@@ -0,0 +1,72 @@
+package io.openbas.migration;
+
+import java.sql.Statement;
+import org.flywaydb.core.api.migration.BaseJavaMigration;
+import org.flywaydb.core.api.migration.Context;
+import org.springframework.stereotype.Component;
+
+@Component
+public class V4_27__Rename_asset_group_contract extends BaseJavaMigration {
+
+  @Override
+  public void migrate(Context context) throws Exception {
+    try (Statement statement = context.getConnection().createStatement()) {
+
+      String renameAssetGroupInContracts =
+          """
+          UPDATE injectors_contracts
+          SET injector_contract_content = jsonb_set(
+                  injector_contract_content::jsonb,
+                  '{fields}',
+                  (
+                      SELECT jsonb_agg(
+                                     CASE
+                                         WHEN field->>'key' = 'assetgroups'
+                                             THEN jsonb_set(
+                                                 field,
+                                                 '{key}',
+                                                 '"asset_groups"'::jsonb
+                                                  )
+                                         ELSE field
+                                         END
+                                         ||
+                                     CASE
+                                         WHEN field ? 'mandatoryGroups'
+                                             AND jsonb_typeof(field->'mandatoryGroups') = 'array'
+                                             THEN jsonb_build_object(
+                                                 'mandatoryGroups',
+                                                 (
+                                                     SELECT jsonb_agg(
+                                                                    CASE
+                                                                        WHEN mg = 'assetgroups' THEN 'asset_groups'
+                                                                        ELSE mg
+                                                                        END
+                                                            )
+                                                     FROM jsonb_array_elements_text(field->'mandatoryGroups') AS t(mg)
+                                                 )
+                                                  )
+                                         ELSE '{}'::jsonb
+                                         END
+                             )
+                      FROM jsonb_array_elements(injector_contract_content::jsonb->'fields') AS field
+                  )
+                                          )
+          WHERE EXISTS (
+              SELECT 1
+              FROM jsonb_array_elements(injector_contract_content::jsonb->'fields') AS field
+              WHERE field->>'key' = 'assetgroups'
+                 OR (field ? 'mandatoryGroups'
+                  AND jsonb_typeof(field->'mandatoryGroups') = 'array'
+                  AND EXISTS (
+                      SELECT 1
+                      FROM jsonb_array_elements_text(field->'mandatoryGroups') AS t(mg)
+                      WHERE mg = 'assetgroups'
+                  )
+                  )
+          );
+          """;
+
+      statement.executeUpdate(renameAssetGroupInContracts);
+    }
+  }
+}
diff --git a/openbas-framework/src/main/java/io/openbas/injector_contract/ContractDef.java b/openbas-framework/src/main/java/io/openbas/injector_contract/ContractDef.java
--- a/openbas-framework/src/main/java/io/openbas/injector_contract/ContractDef.java
+++ b/openbas-framework/src/main/java/io/openbas/injector_contract/ContractDef.java
@@ -4,6 +4,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 public class ContractDef {
 
@@ -17,17 +18,36 @@ public static ContractDef contractBuilder() {
     return new ContractDef();
   }
 
-  public ContractDef addFields(List<ContractElement> fields) {
-    this.fields.addAll(fields);
+  /** Adds a mandatory field to the contract */
+  public ContractDef mandatory(final ContractElement element) {
+    if (element == null) {
+      throw new IllegalArgumentException("Field cannot be null");
+    }
+    this.fields.add(element);
     return this;
   }
 
-  public ContractDef mandatory(ContractElement element) {
+  /** Adds an optional field to the contract */
+  public ContractDef optional(final ContractElement element) {
+    if (element == null) {
+      throw new IllegalArgumentException("Field cannot be null");
+    }
+    element.setMandatory(false);
     this.fields.add(element);
     return this;
   }
 
-  public ContractDef mandatoryGroup(ContractElement... elements) {
+  /**
+   * Adds a group of fields where at least one must be filled. Each field becomes part of a mutual
+   * group of optional fields that together fulfill a mandatory constraint.
+   *
+   * <p>Example: either "A" or "B" must be filled.
+   */
+  public ContractDef mandatoryGroup(final ContractElement... elements) {
+    if (elements == null || elements.length == 0) {
+      throw new IllegalArgumentException("At least one field is required in a mandatory group");
+    }
+
     List<String> keys = Arrays.stream(elements).map(ContractElement::getKey).toList();
     for (ContractElement element : elements) {
       element.setMandatory(false);
@@ -37,22 +57,39 @@ public ContractDef mandatoryGroup(ContractElement... elements) {
     return this;
   }
 
-  public ContractDef optional(ContractElement element) {
+  /**
+   * Add a field that will be mandatory if another field is set
+   *
+   * @param element element to be mandatory
+   * @param conditionalElement if this field is set the element will be mandatory
+   */
+  public ContractDef mandatoryOnCondition(
+      ContractElement element, ContractElement conditionalElement) {
+    if (element == null || conditionalElement == null) {
+      throw new IllegalArgumentException("Fields cannot be null");
+    }
+
+    element.setMandatoryConditionFields(List.of(conditionalElement.getKey()));
     element.setMandatory(false);
     this.fields.add(element);
     return this;
   }
 
   /**
-   * Add a field that will be mandatory if another field is set
+   * Add a field that will be mandatory if another field is set with specific value
    *
    * @param element element to be mandatory
-   * @param conditionalElement if this field is set the element will be mandatory
-   * @return
+   * @param conditionalElement if this field is set with a specific value the element will be
+   *     mandatory
    */
-  public ContractDef mandatoryOnCondition(
-      ContractElement element, ContractElement conditionalElement) {
-    element.setMandatoryConditionField(conditionalElement.getKey());
+  public ContractDef mandatoryOnConditionValue(
+      ContractElement element, ContractElement conditionalElement, String value) {
+    if (element == null || conditionalElement == null) {
+      throw new IllegalArgumentException("Fields cannot be null");
+    }
+
+    element.setMandatoryConditionFields(List.of(conditionalElement.getKey()));
+    element.setMandatoryConditionValues(Map.of(conditionalElement.getKey(), value));
     element.setMandatory(false);
     this.fields.add(element);
     return this;
diff --git a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractAsset.java b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractAsset.java
--- a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractAsset.java
+++ b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractAsset.java
@@ -7,7 +7,7 @@
 public class ContractAsset extends ContractCardinalityElement {
 
   public ContractAsset(ContractCardinality cardinality) {
-    super(CONTRACT_ELEMENT_CONTENT_KEY_ASSETS, "Assets", cardinality);
+    super(CONTRACT_ELEMENT_CONTENT_KEY_ASSETS, "Source assets", cardinality);
   }
 
   public static ContractAsset assetField(ContractCardinality cardinality) {
diff --git a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractAssetGroup.java b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractAssetGroup.java
--- a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractAssetGroup.java
+++ b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractAssetGroup.java
@@ -7,7 +7,7 @@
 public class ContractAssetGroup extends ContractCardinalityElement {
 
   public ContractAssetGroup(ContractCardinality cardinality) {
-    super(CONTRACT_ELEMENT_CONTENT_KEY_ASSET_GROUPS, "Asset groups", cardinality);
+    super(CONTRACT_ELEMENT_CONTENT_KEY_ASSET_GROUPS, "Source asset groups", cardinality);
   }
 
   public static ContractAssetGroup assetGroupField(ContractCardinality cardinality) {
diff --git a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractCheckbox.java b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractCheckbox.java
--- a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractCheckbox.java
+++ b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractCheckbox.java
@@ -1,6 +1,7 @@
 package io.openbas.injector_contract.fields;
 
 import java.util.List;
+import java.util.Map;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -21,10 +22,16 @@ public static ContractCheckbox checkboxField(String key, String label, boolean c
   }
 
   public static ContractCheckbox checkboxField(
-      String key, String label, boolean checked, List<ContractElement> linkedFields) {
+      String key,
+      String label,
+      boolean checked,
+      List<ContractElement> visibleConditionFields,
+      Map<String, String> visibleConditionValues) {
     ContractCheckbox contractCheckbox = new ContractCheckbox(key, label);
     contractCheckbox.setDefaultValue(checked);
-    contractCheckbox.setLinkedFields(linkedFields);
+    contractCheckbox.setVisibleConditionFields(
+        visibleConditionFields.stream().map(ContractElement::getKey).toList());
+    contractCheckbox.setVisibleConditionValues(visibleConditionValues);
     return contractCheckbox;
   }
 
diff --git a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractElement.java b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractElement.java
--- a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractElement.java
+++ b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractElement.java
@@ -3,6 +3,7 @@
 import io.openbas.model.LinkedFieldModel;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import lombok.Data;
 
 @Data
@@ -18,9 +19,13 @@ public abstract class ContractElement {
 
   private List<String> mandatoryGroups;
 
-  private String mandatoryConditionField;
+  private List<String> mandatoryConditionFields;
 
-  private String mandatoryConditionValue;
+  private Map<String, String> mandatoryConditionValues;
+
+  private List<String> visibleConditionFields;
+
+  private Map<String, String> visibleConditionValues;
 
   /**
    * List of linked fields that can be used to link this field to other fields in the contract. This
diff --git a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractNumber.java b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractNumber.java
--- a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractNumber.java
+++ b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractNumber.java
@@ -1,6 +1,5 @@
 package io.openbas.injector_contract.fields;
 
-import java.util.List;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -24,27 +23,6 @@ public static ContractNumber numberField(String key, String label, String defaul
     return contractNumber;
   }
 
-  public static ContractNumber numberField(
-      String key, String label, String defaultValue, List<ContractElement> linkedFields) {
-    ContractNumber contractNumber = new ContractNumber(key, label);
-    contractNumber.setDefaultValue(defaultValue);
-    contractNumber.setLinkedFields(linkedFields);
-    return contractNumber;
-  }
-
-  public static ContractNumber numberField(
-      String key,
-      String label,
-      String defaultValue,
-      List<ContractElement> linkedFields,
-      List<String> linkedValues) {
-    ContractNumber contractNumber = new ContractNumber(key, label);
-    contractNumber.setDefaultValue(defaultValue);
-    contractNumber.setLinkedFields(linkedFields);
-    contractNumber.setLinkedValues(linkedValues);
-    return contractNumber;
-  }
-
   @Override
   public ContractFieldType getType() {
     return ContractFieldType.Number;
diff --git a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractText.java b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractText.java
--- a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractText.java
+++ b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractText.java
@@ -1,6 +1,7 @@
 package io.openbas.injector_contract.fields;
 
 import java.util.List;
+import java.util.Map;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -25,10 +26,16 @@ public static ContractText textField(String key, String label, String defaultVal
   }
 
   public static ContractText textField(
-      String key, String label, String defaultValue, List<ContractElement> linkedFields) {
+      String key,
+      String label,
+      String defaultValue,
+      List<ContractElement> visibleConditionFields,
+      Map<String, String> values) {
     ContractText contractText = new ContractText(key, label);
     contractText.setDefaultValue(defaultValue);
-    contractText.setLinkedFields(linkedFields);
+    contractText.setVisibleConditionFields(
+        visibleConditionFields.stream().map(ContractElement::getKey).toList());
+    contractText.setVisibleConditionValues(values);
     return contractText;
   }
 
diff --git a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractTextArea.java b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractTextArea.java
--- a/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractTextArea.java
+++ b/openbas-framework/src/main/java/io/openbas/injector_contract/fields/ContractTextArea.java
@@ -1,6 +1,7 @@
 package io.openbas.injector_contract.fields;
 
 import java.util.List;
+import java.util.Map;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -30,10 +31,16 @@ public static ContractTextArea richTextareaField(String key, String label, Strin
   }
 
   public static ContractTextArea richTextareaField(
-      String key, String label, String defaultValue, List<ContractElement> linkedFields) {
+      String key,
+      String label,
+      String defaultValue,
+      List<ContractElement> visibleConditionFields,
+      Map<String, String> values) {
     ContractTextArea contractText = new ContractTextArea(key, label, true);
     contractText.setDefaultValue(defaultValue);
-    contractText.setLinkedFields(linkedFields);
+    contractText.setVisibleConditionFields(
+        visibleConditionFields.stream().map(ContractElement::getKey).toList());
+    contractText.setVisibleConditionValues(values);
     return contractText;
   }
 
diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java b/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java
--- a/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java
@@ -1,6 +1,7 @@
 package io.openbas.database.model;
 
 import static java.time.Instant.now;
+import static java.util.Optional.ofNullable;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
@@ -83,9 +84,7 @@ public class InjectorContract implements Base {
   @JsonProperty("injector_contract_arch")
   @Enumerated(EnumType.STRING)
   public Payload.PAYLOAD_EXECUTION_ARCH getArch() {
-    return Optional.ofNullable(getPayload())
-        .map(payload -> payload.getExecutionArch())
-        .orElse(null);
+    return ofNullable(getPayload()).map(payload -> payload.getExecutionArch()).orElse(null);
   }
 
   @ManyToOne(fetch = FetchType.EAGER)
@@ -214,16 +213,16 @@ public int hashCode() {
   public static final String CONTRACT_ELEMENT_CONTENT_CARDINALITY = "cardinality";
   public static final String CONTRACT_ELEMENT_CONTENT_MANDATORY = "mandatory";
   public static final String CONTRACT_ELEMENT_CONTENT_MANDATORY_GROUPS = "mandatoryGroups";
-  public static final String CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL =
-      "mandatoryConditionField";
-  public static final String CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL_VALUE =
-      "mandatoryConditionValue";
+  public static final String CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL_FIELDS =
+      "mandatoryConditionFields";
+  public static final String CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL_VALUES =
+      "mandatoryConditionValues";
   public static final String DEFAULT_VALUE_FIELD = "defaultValue";
   public static final String PREDEFINED_EXPECTATIONS = "predefinedExpectations";
 
   public static final String CONTRACT_ELEMENT_CONTENT_KEY_TEAMS = "teams";
   public static final String CONTRACT_ELEMENT_CONTENT_KEY_ASSETS = "assets";
-  public static final String CONTRACT_ELEMENT_CONTENT_KEY_ASSET_GROUPS = "assetgroups";
+  public static final String CONTRACT_ELEMENT_CONTENT_KEY_ASSET_GROUPS = "asset_groups";
   public static final String CONTRACT_ELEMENT_CONTENT_KEY_ARTICLES = "articles";
   public static final String CONTRACT_ELEMENT_CONTENT_KEY_CHALLENGES = "challenges";
   public static final String CONTRACT_ELEMENT_CONTENT_KEY_ATTACHMENTS = "attachments";
@@ -233,13 +232,12 @@ public int hashCode() {
       "targeted-asset-separator";
 
   public static final String CONTRACT_ELEMENT_CONTENT_TYPE_ASSET = "asset";
+  public static final String CONTRACT_ELEMENT_CONTENT_TYPE_ASSET_GROUP = "asset-group";
   public static final String CONTRACT_ELEMENT_CONTENT_TYPE_TEAM = "team";
 
   public static final List<String> CONTRACT_ELEMENT_CONTENT_KEY_NOT_DYNAMIC =
       List.of(
           CONTRACT_ELEMENT_CONTENT_KEY_TEAMS,
-          CONTRACT_ELEMENT_CONTENT_KEY_ASSETS,
-          CONTRACT_ELEMENT_CONTENT_KEY_ASSET_GROUPS,
           CONTRACT_ELEMENT_CONTENT_KEY_ARTICLES,
           CONTRACT_ELEMENT_CONTENT_KEY_CHALLENGES,
           CONTRACT_ELEMENT_CONTENT_KEY_ATTACHMENTS);
diff --git a/openbas-model/src/main/java/io/openbas/helper/InjectModelHelper.java b/openbas-model/src/main/java/io/openbas/helper/InjectModelHelper.java
--- a/openbas-model/src/main/java/io/openbas/helper/InjectModelHelper.java
+++ b/openbas-model/src/main/java/io/openbas/helper/InjectModelHelper.java
@@ -5,6 +5,7 @@
 import static java.time.Duration.between;
 import static java.time.Instant.now;
 import static java.util.Optional.ofNullable;
+import static java.util.stream.StreamSupport.stream;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
@@ -18,9 +19,7 @@
 import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
-import java.util.stream.StreamSupport;
 
 public class InjectModelHelper {
 
@@ -33,12 +32,7 @@ public static boolean isReady(
       @NotNull final List<String> teams,
       @NotNull final List<String> assets,
       @NotNull final List<String> assetGroups) {
-    if (injectorContract == null
-        || (content == null
-            && teams.isEmpty()
-            && !allTeams
-            && assets.isEmpty()
-            && assetGroups.isEmpty())) {
+    if (injectorContract == null) {
       return false;
     }
 
@@ -57,77 +51,114 @@ public static boolean isReady(
 
     ObjectNode contractContent = injectorContract.getConvertedContent();
     List<JsonNode> contractFields =
-        StreamSupport.stream(contractContent.get(CONTRACT_CONTENT_FIELDS).spliterator(), false)
-            .toList();
+        stream(contractContent.get(CONTRACT_CONTENT_FIELDS).spliterator(), false).toList();
 
     boolean isReady = true;
     for (JsonNode jsonField : contractFields) {
 
-      // if field is mandatory or if field is asset, check if the field is set
-      String key = jsonField.get(CONTRACT_ELEMENT_CONTENT_KEY).asText();
-      if (jsonField.get(CONTRACT_ELEMENT_CONTENT_MANDATORY).asBoolean()
-          || (jsonField.hasNonNull(CONTRACT_ELEMENT_CONTENT_MANDATORY_GROUPS)
-                  && jsonField.get(CONTRACT_ELEMENT_CONTENT_MANDATORY_GROUPS).asBoolean()
-              || CONTRACT_ELEMENT_CONTENT_KEY_ASSETS.equals(key))) {
+      // If field is mandatory
+      if (jsonField.get(CONTRACT_ELEMENT_CONTENT_MANDATORY).asBoolean()) {
         isReady =
             isFieldSet(
                 allTeams, teams, assets, assetGroups, jsonField, content, injectContractFields);
       }
 
-      // if field is mandatory conditional, if the conditional field is set check if the current
-      // field is set
-      if (jsonField.hasNonNull(CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL)) {
-        String mandatoryOnConditionFieldKey =
-            jsonField.get(CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL).asText();
-        Optional<JsonNode> mandatoryOnConditionField =
-            contractFields.stream()
-                .filter(
-                    jsonNode ->
-                        mandatoryOnConditionFieldKey.equals(
-                            jsonNode.get(CONTRACT_ELEMENT_CONTENT_KEY).asText()))
-                .findFirst();
-        // if field is mandatory conditional on a specific value and this value is equals, check the
-        // field is set
-        if (jsonField.hasNonNull(CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL_VALUE)) {
-          if (mandatoryOnConditionField.isEmpty()
-              || content == null
-              || Objects.equals(
-                  getFieldValue(mandatoryOnConditionField.get(), content),
-                  jsonField.get(CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL_VALUE).asText())) {
-            if (isFieldSet(
-                    allTeams,
-                    teams,
-                    assets,
-                    assetGroups,
-                    mandatoryOnConditionField.get(),
-                    content,
-                    injectContractFields)
-                && !isFieldSet(
+      // If field is mandatory group
+      if (jsonField.hasNonNull(CONTRACT_ELEMENT_CONTENT_MANDATORY_GROUPS)) {
+        ArrayNode mandatoryGroups =
+            (ArrayNode) jsonField.get(CONTRACT_ELEMENT_CONTENT_MANDATORY_GROUPS);
+        if (!mandatoryGroups.isEmpty()) {
+          boolean atLeastOneSet = false;
+          for (JsonNode mandatoryFieldKey : mandatoryGroups) {
+            Optional<JsonNode> groupField =
+                contractFields.stream()
+                    .filter(
+                        jsonNode ->
+                            mandatoryFieldKey
+                                .asText()
+                                .equals(jsonNode.get(CONTRACT_ELEMENT_CONTENT_KEY).asText()))
+                    .findFirst();
+            if (groupField.isPresent()
+                && isFieldSet(
                     allTeams,
                     teams,
                     assets,
                     assetGroups,
-                    jsonField,
+                    groupField.get(),
                     content,
                     injectContractFields)) {
-              isReady = false;
+              atLeastOneSet = true;
+              break;
             }
           }
-        } else if (mandatoryOnConditionField.isPresent()) {
-          if (isFieldSet(
+          if (!atLeastOneSet) {
+            isReady = false;
+          }
+        }
+      }
+
+      // If field is mandatory conditional, if the conditional field is set, check if the current
+      // field is set
+      if (jsonField.hasNonNull(CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL_FIELDS)) {
+        JsonNode fields = jsonField.get(CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL_FIELDS);
+
+        if (fields.isArray()) {
+          for (JsonNode node : fields) {
+            if (!node.isNull()) {
+              String fieldKey = node.asText();
+
+              Optional<JsonNode> conditionalFieldOpt =
+                  contractFields.stream()
+                      .filter(
+                          jsonNode ->
+                              fieldKey.equals(jsonNode.get(CONTRACT_ELEMENT_CONTENT_KEY).asText()))
+                      .findFirst();
+
+              // If field not exists -> skip
+              if (conditionalFieldOpt.isEmpty()) {
+                continue;
+              }
+              if (jsonField.hasNonNull(CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL_VALUES)) {
+                JsonNode conditionalValuesNode =
+                    jsonField.get(CONTRACT_ELEMENT_CONTENT_MANDATORY_CONDITIONAL_VALUES);
+
+                if (conditionalValuesNode.has(fieldKey)) {
+                  String specificValuesNode = conditionalValuesNode.get(fieldKey).asText();
+
+                  List<String> actualValues =
+                      getFieldValue(teams, assets, assetGroups, conditionalFieldOpt.get(), content);
+                  boolean conditionMet = actualValues.contains(specificValuesNode);
+
+                  if (!conditionMet) {
+                    continue; // condition not met → skip
+                  }
+                }
+              }
+              Optional<JsonNode> fieldOpt =
+                  contractFields.stream()
+                      .filter(
+                          jsonNode ->
+                              jsonField
+                                  .get(CONTRACT_ELEMENT_CONTENT_KEY)
+                                  .asText()
+                                  .equals(jsonNode.get(CONTRACT_ELEMENT_CONTENT_KEY).asText()))
+                      .findFirst();
+              // If field not exists -> skip
+              if (fieldOpt.isEmpty()) {
+                continue;
+              }
+              if (!isFieldSet(
                   allTeams,
                   teams,
                   assets,
                   assetGroups,
-                  mandatoryOnConditionField.get(),
+                  fieldOpt.get(),
                   content,
-                  injectContractFields)
-              && !isFieldSet(
-                  allTeams, teams, assets, assetGroups, jsonField, content, injectContractFields)) {
-            isReady = false;
+                  injectContractFields)) {
+                isReady = false;
+              }
+            }
           }
-        } else {
-          isReady = false;
         }
       }
       if (!isReady) {
@@ -251,7 +282,13 @@ public static boolean isFieldSet(
         }
       }
       case CONTRACT_ELEMENT_CONTENT_TYPE_ASSET -> {
-        if (assets.isEmpty() && assetGroups.isEmpty()) {
+        if (assets.isEmpty()) {
+          isSet = false;
+        }
+      }
+
+      case CONTRACT_ELEMENT_CONTENT_TYPE_ASSET_GROUP -> {
+        if (assetGroups.isEmpty()) {
           isSet = false;
         }
       }
@@ -273,9 +310,47 @@ public static boolean isFieldSet(
     return isSet;
   }
 
-  public static String getFieldValue(
-      @NotNull final JsonNode jsonField, @NotNull final ObjectNode content) {
+  public static List<String> getFieldValue(
+      @NotNull final List<String> teams,
+      @NotNull final List<String> assets,
+      @NotNull final List<String> assetGroups,
+      @NotNull final JsonNode jsonField,
+      @NotNull final ObjectNode content) {
+
     String key = jsonField.get(CONTRACT_ELEMENT_CONTENT_KEY).asText();
-    return content.get(key).asText();
+    String type = jsonField.get(CONTRACT_ELEMENT_CONTENT_TYPE).asText();
+
+    return switch (type) {
+      case CONTRACT_ELEMENT_CONTENT_TYPE_TEAM -> teams;
+      case CONTRACT_ELEMENT_CONTENT_TYPE_ASSET -> assets;
+      case CONTRACT_ELEMENT_CONTENT_TYPE_ASSET_GROUP -> assetGroups;
+      default -> {
+        if (content == null || content.get(key) == null || content.get(key).isNull()) {
+          yield List.of();
+        }
+
+        JsonNode valueNode = content.get(key);
+
+        if (valueNode.isArray()) {
+          List<String> values = new ArrayList<>();
+          for (JsonNode node : valueNode) {
+            if (!node.isNull()) {
+              values.add(node.asText());
+            }
+          }
+          yield values;
+        }
+
+        if (valueNode.isTextual()) {
+          String value = valueNode.asText();
+          yield value.isEmpty() ? List.of() : List.of(value);
+        } else if (valueNode.isBoolean()) {
+          String value = valueNode.asText();
+          yield value.isEmpty() ? List.of() : List.of(value);
+        }
+
+        yield List.of();
+      }
+    };
   }
 }


Test patch:
diff --git a/openbas-api/src/test/java/io/openbas/config/XTMHubConfigTest.java b/openbas-api/src/test/java/io/openbas/config/XTMHubConfigTest.java
--- a/openbas-api/src/test/java/io/openbas/config/XTMHubConfigTest.java
+++ b/openbas-api/src/test/java/io/openbas/config/XTMHubConfigTest.java
@@ -2,6 +2,7 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+import io.openbas.IntegrationTest;
 import io.openbas.injectors.xtmhub.config.XTMHubConfig;
 import io.openbas.utils.mockConfig.WithMockXTMHubConfig;
 import org.junit.jupiter.api.DisplayName;
@@ -12,7 +13,7 @@
 
 @SpringBootTest
 @DisplayName("XTMHubConfig tests")
-public class XTMHubConfigTest {
+public class XTMHubConfigTest extends IntegrationTest {
 
   @Nested
   @WithMockXTMHubConfig(enable = true, url = "https://hub.filigran.io")
diff --git a/openbas-api/src/test/java/io/openbas/helper/InjectModelHelperTest.java b/openbas-api/src/test/java/io/openbas/helper/InjectModelHelperTest.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/test/java/io/openbas/helper/InjectModelHelperTest.java
@@ -0,0 +1,400 @@
+package io.openbas.helper;
+
+import static io.openbas.helper.InjectModelHelper.isReady;
+import static io.openbas.helper.ObjectMapperHelper.openBASJsonMapper;
+import static io.openbas.utils.fixtures.InjectorContractFixture.*;
+import static io.openbas.utils.fixtures.InjectorFixture.createDefaultPayloadInjector;
+import static io.openbas.utils.fixtures.PayloadFixture.createCommand;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.openbas.database.model.Command;
+import io.openbas.database.model.Injector;
+import io.openbas.database.model.InjectorContract;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+class InjectModelHelperTest {
+
+  private final ObjectMapper mapper = openBASJsonMapper();
+
+  private InjectorContract prepareInjectorContract() throws JsonProcessingException {
+    Injector injector = createDefaultPayloadInjector();
+    Command payloadCommand = createCommand("cmd", "whoami", List.of(), "whoami");
+    return createPayloadInjectorContract(injector, payloadCommand);
+  }
+
+  @Nested
+  class MandatoryAssetTests {
+
+    @Test
+    void given_an_injector_contract_with_asset_mandatory_and_an_asset_should_be_ready()
+        throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildAssetField(true));
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = List.of("assetId");
+      List<String> assetGroups = new ArrayList<>();
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertTrue(isReady);
+    }
+
+    @Test
+    void given_an_injector_contract_with_asset_mandatory_and_no_asset_should_not_be_ready()
+        throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildAssetField(true));
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = new ArrayList<>();
+      List<String> assetGroups = new ArrayList<>();
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertFalse(isReady);
+    }
+
+    @Test
+    void given_an_injector_contract_with_asset_optional_and_an_asset_should_be_ready()
+        throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildAssetField(true));
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = List.of("assetId");
+      List<String> assetGroups = new ArrayList<>();
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertTrue(isReady);
+    }
+
+    @Test
+    void given_an_injector_contract_with_asset_optional_and_not_asset_should_be_ready()
+        throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildAssetField(false));
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = new ArrayList<>();
+      List<String> assetGroups = new ArrayList<>();
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertTrue(isReady);
+    }
+  }
+
+  @Nested
+  class MandatoryGroupTests {
+
+    @Test
+    void given_an_injector_contract_with_mandatory_groups_and_an_element_should_be_ready()
+        throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildMandatoryGroup());
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = List.of("assetId");
+      List<String> assetGroups = new ArrayList<>();
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertTrue(isReady);
+    }
+
+    @Test
+    void given_an_injector_contract_with_mandatory_groups_and_full_elements_should_be_ready()
+        throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildMandatoryGroup());
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = List.of("assetId");
+      List<String> assetGroups = List.of("assetGroupId");
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertTrue(isReady);
+    }
+
+    @Test
+    void given_an_injector_contract_with_mandatory_groups_and_no_element_should_not_be_ready()
+        throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildMandatoryGroup());
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = new ArrayList<>();
+      List<String> assetGroups = new ArrayList<>();
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertFalse(isReady);
+    }
+
+    @Nested
+    class MandatoryOnConditionTests {
+
+      @Test
+      void
+          given_an_injector_contract_with_mandatory_on_condition_and_no_element_should_not_be_ready()
+              throws JsonProcessingException {
+        // -- PREPARE --
+        InjectorContract injectorContract = prepareInjectorContract();
+        addField(injectorContract, mapper, buildMandatoryOnCondition());
+        boolean allTeams = false;
+        List<String> teams = new ArrayList<>();
+        List<String> assets = List.of();
+        List<String> assetGroups = List.of();
+
+        // -- EXECUTE --
+        boolean isReady =
+            isReady(
+                injectorContract,
+                injectorContract.getConvertedContent(),
+                allTeams,
+                teams,
+                assets,
+                assetGroups);
+
+        // -- ASSERT --
+        assertFalse(isReady);
+      }
+
+      @Test
+      void given_an_injector_contract_with_mandatory_on_condition_and_element_should_be_ready()
+          throws JsonProcessingException {
+        // -- PREPARE --
+        InjectorContract injectorContract = prepareInjectorContract();
+        addField(injectorContract, mapper, buildMandatoryOnCondition());
+        boolean allTeams = false;
+        List<String> teams = new ArrayList<>();
+        List<String> assets = List.of("assetId");
+        List<String> assetGroups = List.of();
+
+        // -- EXECUTE --
+        boolean isReady =
+            isReady(
+                injectorContract,
+                injectorContract.getConvertedContent(),
+                allTeams,
+                teams,
+                assets,
+                assetGroups);
+
+        // -- ASSERT --
+        assertTrue(isReady);
+      }
+
+      @Test
+      void
+          given_an_injector_contract_with_mandatory_on_condition_and_condition_element_should_not_be_ready()
+              throws JsonProcessingException {
+        // -- PREPARE --
+        InjectorContract injectorContract = prepareInjectorContract();
+        addField(injectorContract, mapper, buildMandatoryOnCondition());
+        boolean allTeams = false;
+        List<String> teams = new ArrayList<>();
+        List<String> assets = List.of();
+        List<String> assetGroups = List.of("assetGroupId");
+
+        // -- EXECUTE --
+        boolean isReady =
+            isReady(
+                injectorContract,
+                injectorContract.getConvertedContent(),
+                allTeams,
+                teams,
+                assets,
+                assetGroups);
+
+        // -- ASSERT --
+        assertFalse(isReady);
+      }
+
+      @Test
+      void given_an_injector_contract_with_mandatory_on_condition_and_all_elements_should_be_ready()
+          throws JsonProcessingException {
+        // -- PREPARE --
+        InjectorContract injectorContract = prepareInjectorContract();
+        addField(injectorContract, mapper, buildMandatoryOnCondition());
+        boolean allTeams = false;
+        List<String> teams = new ArrayList<>();
+        List<String> assets = List.of("assetId");
+        List<String> assetGroups = List.of("assetGroupId");
+
+        // -- EXECUTE --
+        boolean isReady =
+            isReady(
+                injectorContract,
+                injectorContract.getConvertedContent(),
+                allTeams,
+                teams,
+                assets,
+                assetGroups);
+
+        // -- ASSERT --
+        assertTrue(isReady);
+      }
+    }
+  }
+
+  @Nested
+  class MandatoryOnConditionValueTests {
+
+    @Test
+    void given_mandatory_on_condition_with_specific_value_when_condition_matches_should_be_ready()
+        throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildMandatoryOnConditionValue("assetGroupId"));
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = List.of("assetId");
+      List<String> assetGroups = List.of("assetGroupId");
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertTrue(isReady);
+    }
+
+    @Test
+    void
+        given_mandatory_on_condition_with_specific_value_when_condition_not_matches_should_not_be_ready()
+            throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildMandatoryOnConditionValue("assetGroupId"));
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = List.of();
+      List<String> assetGroups = List.of("assetGroupId");
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertFalse(isReady);
+    }
+
+    @Test
+    void
+        given_mandatory_on_condition_with_not_specific_value_when_condition_not_matches_should_be_ready()
+            throws JsonProcessingException {
+      // -- PREPARE --
+      InjectorContract injectorContract = prepareInjectorContract();
+      addField(injectorContract, mapper, buildMandatoryOnConditionValue("assetGroupId"));
+      boolean allTeams = false;
+      List<String> teams = new ArrayList<>();
+      List<String> assets = List.of();
+      List<String> assetGroups = List.of("assetGroupId2");
+
+      // -- EXECUTE --
+      boolean isReady =
+          isReady(
+              injectorContract,
+              injectorContract.getConvertedContent(),
+              allTeams,
+              teams,
+              assets,
+              assetGroups);
+
+      // -- ASSERT --
+      assertTrue(isReady);
+    }
+  }
+}
diff --git a/openbas-api/src/test/java/io/openbas/rest/finding/FindingApiTest.java b/openbas-api/src/test/java/io/openbas/rest/finding/FindingApiTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/finding/FindingApiTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/finding/FindingApiTest.java
@@ -2,6 +2,7 @@
 
 import static io.openbas.helper.StreamHelper.fromIterable;
 import static io.openbas.utils.JsonUtils.asJsonString;
+import static io.openbas.utils.fixtures.FindingFixture.createDefaultTextFindingWithRandomValue;
 import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
@@ -160,11 +161,10 @@ private Hashtable<String, InjectComposer.Composer> attachSimulationToScenario(
     private List<FindingComposer.Composer> getDefaultFindings() {
       return new ArrayList<>(
           List.of(
-              findingComposer.forFinding(FindingFixture.createDefaultTextFindingWithRandomValue()),
-              findingComposer.forFinding(FindingFixture.createDefaultTextFindingWithRandomValue()),
-              findingComposer.forFinding(FindingFixture.createDefaultTextFindingWithRandomValue()),
-              findingComposer.forFinding(
-                  FindingFixture.createDefaultTextFindingWithRandomValue())));
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue()),
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue()),
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue()),
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue())));
     }
 
     @Nested
@@ -187,8 +187,7 @@ public void ReturnsOnlyFindingsForLatestSimulationOfEachScenario() throws Except
           for (Map.Entry<String, InjectComposer.Composer> entry :
               latestSimulationInjectWrappers.entrySet()) {
             FindingComposer.Composer findingWrapper =
-                findingComposer.forFinding(
-                    FindingFixture.createDefaultTextFindingWithRandomValue());
+                findingComposer.forFinding(createDefaultTextFindingWithRandomValue());
             latestFindingWrappers.add(findingWrapper);
             entry.getValue().withFinding(findingWrapper);
           }
@@ -198,7 +197,7 @@ public void ReturnsOnlyFindingsForLatestSimulationOfEachScenario() throws Except
         // add injects (atomic testing) with findings too
         for (int i = 0; i < 2; i++) {
           FindingComposer.Composer findingWrapper =
-              findingComposer.forFinding(FindingFixture.createDefaultTextFindingWithRandomValue());
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue());
           latestFindingWrappers.add(findingWrapper);
           injectComposer
               .forInject(InjectFixture.getDefaultInject())
@@ -252,8 +251,7 @@ public void ReturnsOnlyFindingsForLatestFinishedSimulationOfEachScenario() throw
           for (Map.Entry<String, InjectComposer.Composer> entry :
               latestSimulationInjectWrappers.entrySet()) {
             FindingComposer.Composer findingWrapper =
-                findingComposer.forFinding(
-                    FindingFixture.createDefaultTextFindingWithRandomValue());
+                findingComposer.forFinding(createDefaultTextFindingWithRandomValue());
             latestFindingWrappers.add(findingWrapper);
             entry.getValue().withFinding(findingWrapper);
           }
@@ -267,7 +265,7 @@ public void ReturnsOnlyFindingsForLatestFinishedSimulationOfEachScenario() throw
         // add injects (atomic testing) with findings too
         for (int i = 0; i < 2; i++) {
           FindingComposer.Composer findingWrapper =
-              findingComposer.forFinding(FindingFixture.createDefaultTextFindingWithRandomValue());
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue());
           latestFindingWrappers.add(findingWrapper);
           injectComposer
               .forInject(InjectFixture.getDefaultInject())
@@ -322,7 +320,7 @@ public void ReturnsOnlyFindingsForLatestSimulation() throws Exception {
         for (Map.Entry<String, InjectComposer.Composer> entry :
             latestSimulationInjectWrappers.entrySet()) {
           FindingComposer.Composer findingWrapper =
-              findingComposer.forFinding(FindingFixture.createDefaultTextFindingWithRandomValue());
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue());
           latestFindingWrappers.add(findingWrapper);
           entry.getValue().withFinding(findingWrapper);
         }
@@ -370,7 +368,9 @@ public void ReturnsAllFindingsForObservedSimulation() throws Exception {
         Exercise ex = scenarioWrapper.get().getExercises().getFirst();
 
         SearchPaginationInput input = PaginationFixture.getDefault().build();
-        input.setSorts(List.of(new SortField("finding_created_at", "asc")));
+        input.setSorts(
+            List.of(
+                new SortField("finding_created_at", "asc"), new SortField("finding_value", "asc")));
 
         entityManager.flush();
         entityManager.clear();
@@ -389,7 +389,13 @@ public void ReturnsAllFindingsForObservedSimulation() throws Exception {
                             .toList()))
                 .stream()
                 .map(findingMapper::toRelatedFindingOutput)
-                .sorted(Comparator.comparing(RelatedFindingOutput::getCreationDate))
+                .sorted(
+                    (o1, o2) -> {
+                      if (o1.getCreationDate().equals(o2.getCreationDate())) {
+                        return o1.getValue().compareTo(o2.getValue());
+                      }
+                      return o1.getCreationDate().compareTo(o2.getCreationDate());
+                    })
                 .limit(input.getSize())
                 .toList();
 
@@ -466,7 +472,7 @@ public void ReturnsAllFindingsForLatestSimulationsInvolvingEndpoint() throws Exc
             latestSimulationInjectWrappers.entrySet()) {
           FindingComposer.Composer findingWrapper =
               findingComposer
-                  .forFinding(FindingFixture.createDefaultTextFindingWithRandomValue())
+                  .forFinding(createDefaultTextFindingWithRandomValue())
                   .withEndpoint(endpointWrapper);
           entry.getValue().withFinding(findingWrapper);
           latestFindingWrappers.add(findingWrapper);
@@ -476,7 +482,7 @@ public void ReturnsAllFindingsForLatestSimulationsInvolvingEndpoint() throws Exc
         // add injects (atomic testing) with findings too
         for (int i = 0; i < 2; i++) {
           FindingComposer.Composer findingWrapper =
-              findingComposer.forFinding(FindingFixture.createDefaultTextFindingWithRandomValue());
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue());
           latestFindingWrappers.add(findingWrapper);
           injectComposer
               .forInject(InjectFixture.getDefaultInject())
@@ -540,7 +546,7 @@ public void ReturnsAllUnsolvedFindingsForLatestFinishedSimulationsInvolvingEndpo
         // add injects (atomic testing) with findings too
         for (int i = 0; i < 2; i++) {
           FindingComposer.Composer findingWrapper =
-              findingComposer.forFinding(FindingFixture.createDefaultTextFindingWithRandomValue());
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue());
           latestFindingWrappers.add(findingWrapper);
           injectComposer
               .forInject(InjectFixture.getDefaultInject())
@@ -603,7 +609,7 @@ public void ReturnsAllFindingsForLatestFinishedSimulationsInvolvingEndpoint()
             latestSimulationInjectWrappers.entrySet()) {
           FindingComposer.Composer findingWrapper =
               findingComposer
-                  .forFinding(FindingFixture.createDefaultTextFindingWithRandomValue())
+                  .forFinding(createDefaultTextFindingWithRandomValue())
                   .withEndpoint(endpointWrapper);
           entry.getValue().withFinding(findingWrapper);
           latestFindingWrappers.add(findingWrapper);
@@ -616,7 +622,7 @@ public void ReturnsAllFindingsForLatestFinishedSimulationsInvolvingEndpoint()
         // add injects (atomic testing) with findings too
         for (int i = 0; i < 2; i++) {
           FindingComposer.Composer findingWrapper =
-              findingComposer.forFinding(FindingFixture.createDefaultTextFindingWithRandomValue());
+              findingComposer.forFinding(createDefaultTextFindingWithRandomValue());
           latestFindingWrappers.add(findingWrapper);
           injectComposer
               .forInject(InjectFixture.getDefaultInject())
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/AtomicTestingInputFixture.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/AtomicTestingInputFixture.java
deleted file mode 100644
--- a/openbas-api/src/test/java/io/openbas/utils/fixtures/AtomicTestingInputFixture.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package io.openbas.utils.fixtures;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import io.openbas.rest.atomic_testing.form.AtomicTestingInput;
-import java.util.Collections;
-
-public class AtomicTestingInputFixture {
-
-  public static AtomicTestingInput createDefaultAtomicTestingInput() {
-    AtomicTestingInput input = new AtomicTestingInput();
-    input.setTitle("Atomic Testing");
-    input.setDescription("Description");
-    ObjectMapper mapper = new ObjectMapper();
-    ObjectNode content = mapper.createObjectNode();
-    content.put(
-        "namedpipes_client_exe_path",
-        "./ExternalPayloads/../ExternalPayloads/build/namedpipes_client.exe");
-    content.put("path", "./ExternalPayloads/../ExternalPayloads");
-    input.setContent(content);
-    input.setTeams(Collections.emptyList());
-    input.setAssets(Collections.emptyList());
-    input.setAssetGroups(Collections.emptyList());
-    input.setDocuments(Collections.emptyList());
-    input.setTagIds(Collections.emptyList());
-    input.setAllTeams(false);
-    return input;
-  }
-}
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/InjectorContractFixture.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/InjectorContractFixture.java
--- a/openbas-api/src/test/java/io/openbas/utils/fixtures/InjectorContractFixture.java
+++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/InjectorContractFixture.java
@@ -2,12 +2,17 @@
 
 import static io.openbas.database.model.InjectorContract.CONTRACT_CONTENT_FIELDS;
 import static io.openbas.database.model.InjectorContract.CONTRACT_ELEMENT_CONTENT_KEY_TARGETED_PROPERTY;
+import static io.openbas.injector_contract.ContractCardinality.Multiple;
+import static io.openbas.injector_contract.ContractDef.contractBuilder;
+import static io.openbas.injector_contract.fields.ContractAsset.assetField;
+import static io.openbas.injector_contract.fields.ContractAssetGroup.assetGroupField;
 import static io.openbas.injector_contract.fields.ContractSelect.selectFieldWithDefault;
 import static io.openbas.injectors.email.EmailContract.EMAIL_DEFAULT;
 import static io.openbas.injectors.email.EmailContract.EMAIL_GLOBAL;
 import static io.openbas.utils.fixtures.InjectorFixture.createDefaultPayloadInjector;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -18,15 +23,19 @@
 import io.openbas.database.model.Payload;
 import io.openbas.database.repository.InjectorContractRepository;
 import io.openbas.injector_contract.ContractCardinality;
+import io.openbas.injector_contract.ContractDef;
 import io.openbas.injector_contract.ContractTargetedProperty;
+import io.openbas.injector_contract.fields.*;
 import io.openbas.injector_contract.fields.ContractElement;
 import io.openbas.injector_contract.fields.ContractSelect;
 import io.openbas.injector_contract.fields.ContractTargetedAsset;
+import jakarta.validation.constraints.NotBlank;
 import java.time.Instant;
 import java.util.*;
 import lombok.SneakyThrows;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
 
 @Component
 public class InjectorContractFixture {
@@ -48,7 +57,9 @@ private static ObjectNode createDefaultContent(ObjectMapper objectMapper) {
   }
 
   public static InjectorContract createPayloadInjectorContractWithFieldsContent(
-      Injector injector, Payload payloadCommand, List<ContractElement> customFieldsContent)
+      Injector injector,
+      Payload payloadCommand,
+      List<ContractCardinalityElement> customFieldsContent)
       throws JsonProcessingException {
     InjectorContract injectorContract = new InjectorContract();
     injectorContract.setInjector(injector);
@@ -144,6 +155,66 @@ public JsonNode toJsonNode() {
     return mapper.valueToTree(this); // Converts this object to a JsonNode
   }
 
+  // -- BUILDER --
+
+  public static void addField(
+      InjectorContract injectorContract,
+      ObjectMapper mapper,
+      List<ContractElement> contractElements)
+      throws JsonProcessingException {
+    ObjectNode content = mapper.readValue(injectorContract.getContent(), ObjectNode.class);
+    List<ContractElement> elements =
+        mapper.convertValue(content.get(CONTRACT_CONTENT_FIELDS), new TypeReference<>() {});
+    if (CollectionUtils.isEmpty(elements)) {
+      elements = new ArrayList<>();
+    }
+
+    elements.addAll(contractElements);
+
+    content.set(CONTRACT_CONTENT_FIELDS, mapper.valueToTree(elements));
+    injectorContract.setContent(mapper.writeValueAsString(content));
+    injectorContract.setConvertedContent(content);
+  }
+
+  public static List<ContractElement> buildAssetField(final boolean mandatory) {
+    ContractDef builder = contractBuilder();
+    ContractAsset assetField = assetField(Multiple);
+    if (mandatory) {
+      builder.mandatory(assetField);
+    } else {
+      builder.optional(assetField);
+    }
+    return builder.build();
+  }
+
+  public static List<ContractElement> buildMandatoryGroup() {
+    ContractAsset assetField = assetField(Multiple);
+    ContractAssetGroup assetGroupField = assetGroupField(Multiple);
+    ContractDef builder = contractBuilder();
+    builder.mandatoryGroup(assetField, assetGroupField);
+    return builder.build();
+  }
+
+  public static List<ContractElement> buildMandatoryOnCondition() {
+    ContractAsset assetField = assetField(Multiple);
+    ContractAssetGroup assetGroupField = assetGroupField(Multiple);
+    ContractDef builder = contractBuilder();
+    return builder
+        .mandatoryOnCondition(assetField, assetGroupField)
+        .optional(assetGroupField)
+        .build();
+  }
+
+  public static List<ContractElement> buildMandatoryOnConditionValue(@NotBlank final String value) {
+    ContractAsset assetField = assetField(Multiple);
+    ContractAssetGroup assetGroupField = assetGroupField(Multiple);
+    ContractDef builder = contractBuilder();
+    return builder
+        .mandatoryOnConditionValue(assetField, assetGroupField, value)
+        .optional(assetGroupField)
+        .build();
+  }
+
   public static void addTargetedAssetFields(
       InjectorContract injectorContract,
       String key,