diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-automation/gravitee-apim-rest-api-automation-rest/src/test/java/io/gravitee/apim/rest/api/automation/spring/ResourceContextConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-automation/gravitee-apim-rest-api-automation-rest/src/test/java/io/gravitee/apim/rest/api/automation/spring/ResourceContextConfiguration.java index d08797a916c..726942ccd62 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-automation/gravitee-apim-rest-api-automation-rest/src/test/java/io/gravitee/apim/rest/api/automation/spring/ResourceContextConfiguration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-automation/gravitee-apim-rest-api-automation-rest/src/test/java/io/gravitee/apim/rest/api/automation/spring/ResourceContextConfiguration.java @@ -160,6 +160,7 @@ import io.gravitee.apim.core.subscription.use_case.ImportSubscriptionSpecUseCase; import io.gravitee.apim.core.subscription.use_case.RejectSubscriptionUseCase; import io.gravitee.apim.core.subscription.use_case.UpdateSubscriptionUseCase; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator; import io.gravitee.apim.core.user.domain_service.UserContextLoader; import io.gravitee.apim.infra.adapter.SubscriptionAdapter; import io.gravitee.apim.infra.adapter.SubscriptionAdapterImpl; @@ -1084,4 +1085,9 @@ public JsonSchemaChecker jsonSchemaChecker() { public ClusterConfigurationSchemaService clusterConfigurationSchemaService() { return mock(ClusterConfigurationSchemaService.class); } + + @Bean + public SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator() { + return mock(SubscriptionFormSchemaGenerator.class); + } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/spring/ResourceContextConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/spring/ResourceContextConfiguration.java index 59227832e4c..7793b0f4dd9 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/spring/ResourceContextConfiguration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/spring/ResourceContextConfiguration.java @@ -189,6 +189,7 @@ import io.gravitee.apim.core.subscription.use_case.ImportSubscriptionSpecUseCase; import io.gravitee.apim.core.subscription.use_case.RejectSubscriptionUseCase; import io.gravitee.apim.core.subscription.use_case.UpdateSubscriptionUseCase; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator; import io.gravitee.apim.core.user.domain_service.UserContextLoader; import io.gravitee.apim.core.user.domain_service.UserDomainService; import io.gravitee.apim.core.user.use_case.GetUserApisUseCase; @@ -1267,4 +1268,9 @@ public GetUserApplicationsUseCase getUserApplicationsUseCase() { public GetUserGroupsUseCase getUserGroupsUseCase() { return mock(GetUserGroupsUseCase.class); } + + @Bean + public SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator() { + return mock(SubscriptionFormSchemaGenerator.class); + } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-management/gravitee-apim-rest-api-management-rest/src/test/java/io/gravitee/rest/api/management/rest/spring/ResourceContextConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-management/gravitee-apim-rest-api-management-rest/src/test/java/io/gravitee/rest/api/management/rest/spring/ResourceContextConfiguration.java index f6de05e75d3..a7f0baf7f60 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-management/gravitee-apim-rest-api-management-rest/src/test/java/io/gravitee/rest/api/management/rest/spring/ResourceContextConfiguration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-management/gravitee-apim-rest-api-management-rest/src/test/java/io/gravitee/rest/api/management/rest/spring/ResourceContextConfiguration.java @@ -30,6 +30,7 @@ import inmemory.PortalNavigationItemsQueryServiceInMemory; import inmemory.PortalPageContentQueryServiceInMemory; import inmemory.SharedPolicyGroupCrudServiceInMemory; +import inmemory.SubscriptionFormQueryServiceInMemory; import inmemory.SubscriptionSearchQueryServiceInMemory; import inmemory.spring.InMemoryConfiguration; import io.gravitee.apim.core.access_point.query_service.AccessPointQueryService; @@ -148,6 +149,8 @@ import io.gravitee.apim.core.subscription.use_case.GetSubscriptionsUseCase; import io.gravitee.apim.core.subscription.use_case.ImportSubscriptionSpecUseCase; import io.gravitee.apim.core.subscription.use_case.UpdateSubscriptionUseCase; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator; +import io.gravitee.apim.core.subscription_form.query_service.SubscriptionFormQueryService; import io.gravitee.apim.core.user.domain_service.UserContextLoader; import io.gravitee.apim.infra.adapter.SubscriptionAdapter; import io.gravitee.apim.infra.adapter.SubscriptionAdapterImpl; @@ -1260,6 +1263,16 @@ public ApplicationCertificatesUpdateDomainService applicationCertificatesUpdateD return mock(ApplicationCertificatesUpdateDomainService.class); } + @Bean + public SubscriptionFormQueryService subscriptionFormQueryService() { + return new SubscriptionFormQueryServiceInMemory(); + } + + @Bean + public SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator() { + return mock(SubscriptionFormSchemaGenerator.class); + } + @Bean public ClientCertificateValidationDomainService clientCertificateValidationDomainService() { return mock(ClientCertificateValidationDomainService.class); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/AbstractResourceTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/AbstractResourceTest.java index 46fc9477fd2..11189585863 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/AbstractResourceTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/AbstractResourceTest.java @@ -18,6 +18,7 @@ import static org.mockito.Mockito.reset; import io.gravitee.apim.core.subscription.use_case.CreateSubscriptionUseCase; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator; import io.gravitee.rest.api.portal.rest.JerseySpringTest; import io.gravitee.rest.api.portal.rest.mapper.AnalyticsMapper; import io.gravitee.rest.api.portal.rest.mapper.ApiMapper; @@ -319,6 +320,9 @@ public abstract class AbstractResourceTest extends JerseySpringTest { @Autowired protected EndpointConnectorPluginService endpointConnectorPluginService; + @Autowired + protected SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator; + public AbstractResourceTest() { super( new AuthenticationProviderManager() { diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/SubscriptionsResourceTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/SubscriptionsResourceTest.java index 3c89ee383d9..a626caab97a 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/SubscriptionsResourceTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/SubscriptionsResourceTest.java @@ -35,6 +35,7 @@ import io.gravitee.apim.core.subscription.model.SubscriptionEntity; import io.gravitee.apim.core.subscription.use_case.CreateSubscriptionUseCase; +import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormValidationException; import io.gravitee.common.data.domain.Page; import io.gravitee.common.http.HttpStatusCode; import io.gravitee.rest.api.model.ApiKeyEntity; @@ -58,6 +59,7 @@ import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.Response; import java.util.Collections; +import java.util.List; import java.util.Map; import lombok.AllArgsConstructor; import lombok.Getter; @@ -235,6 +237,19 @@ void shouldReturnBadRequestWhenMetadataKeyIsInvalid() { verify(createSubscriptionUseCase, times(1)).execute(any()); } + @Test + void shouldReturnBadRequestWhenSubscriptionFormMetadataIsInvalid() { + doThrow(new SubscriptionFormValidationException(List.of("Field 'email' is required"))) + .when(createSubscriptionUseCase) + .execute(any()); + + SubscriptionInput subscriptionInput = new SubscriptionInput().application(APPLICATION).plan(PLAN).metadata(Map.of()); + + final Response response = target().request().post(Entity.json(subscriptionInput)); + Assertions.assertEquals(HttpStatusCode.BAD_REQUEST_400, response.getStatus()); + verify(createSubscriptionUseCase, times(1)).execute(any()); + } + @Test public void shouldHaveBadRequestWhileCreatingSubscription() { final Response response = target().request().post(Entity.json(null)); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/spring/ResourceContextConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/spring/ResourceContextConfiguration.java index 3c0a781b30e..c8d7f1b22d8 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/spring/ResourceContextConfiguration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/spring/ResourceContextConfiguration.java @@ -145,6 +145,7 @@ import io.gravitee.apim.core.subscription.use_case.GetSubscriptionsUseCase; import io.gravitee.apim.core.subscription.use_case.ImportSubscriptionSpecUseCase; import io.gravitee.apim.core.subscription.use_case.UpdateSubscriptionUseCase; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator; import io.gravitee.apim.core.user.domain_service.UserContextLoader; import io.gravitee.apim.infra.adapter.SubscriptionAdapter; import io.gravitee.apim.infra.adapter.SubscriptionAdapterImpl; @@ -1227,4 +1228,9 @@ public JsonSchemaChecker jsonSchemaChecker() { public ClusterConfigurationSchemaService clusterConfigurationSchemaService() { return mock(ClusterConfigurationSchemaService.class); } + + @Bean + SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator() { + return mock(SubscriptionFormSchemaGenerator.class); + } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCase.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCase.java index 7bf2b60a649..9264e3c59bd 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCase.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCase.java @@ -19,11 +19,16 @@ import io.gravitee.apim.core.gravitee_markdown.GraviteeMarkdown; import io.gravitee.apim.core.gravitee_markdown.GraviteeMarkdownValidator; import io.gravitee.apim.core.subscription_form.crud_service.SubscriptionFormCrudService; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormConstraintsFactory; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSubmissionValidator; import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormNotFoundException; +import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormValidationException; import io.gravitee.apim.core.subscription_form.model.SubscriptionForm; -import io.gravitee.apim.core.subscription_form.model.SubscriptionFormFieldConstraints; import io.gravitee.apim.core.subscription_form.model.SubscriptionFormId; +import io.gravitee.apim.core.subscription_form.model.SubscriptionFormSchema; import io.gravitee.apim.core.subscription_form.query_service.SubscriptionFormQueryService; +import java.util.List; import lombok.CustomLog; import lombok.RequiredArgsConstructor; @@ -42,6 +47,7 @@ public class UpdateSubscriptionFormUseCase { private final SubscriptionFormCrudService subscriptionFormCrudService; private final SubscriptionFormQueryService subscriptionFormQueryService; private final GraviteeMarkdownValidator graviteeMarkdownValidator; + private final SubscriptionFormSchemaGenerator schemaGenerator; public Output execute(Input input) { graviteeMarkdownValidator.validateNotEmpty(GraviteeMarkdown.of(input.gmdContent())); @@ -55,7 +61,11 @@ public Output execute(Input input) { ) ); - existingForm.update(GraviteeMarkdown.of(input.gmdContent()), SubscriptionFormFieldConstraints.empty()); + var gmd = GraviteeMarkdown.of(input.gmdContent()); + var schema = schemaGenerator.generate(gmd); + validateFieldCount(schema); + var constraints = SubscriptionFormConstraintsFactory.fromSchema(schema); + existingForm.update(gmd, constraints); var savedForm = subscriptionFormCrudService.update(existingForm); log.info("Updated subscription form [{}] for environment [{}]", input.subscriptionFormId(), input.environmentId()); @@ -63,6 +73,14 @@ public Output execute(Input input) { return new Output(savedForm); } + private void validateFieldCount(SubscriptionFormSchema schema) { + if (schema != null && schema.fields().size() > SubscriptionFormSubmissionValidator.MAX_METADATA_COUNT) { + throw new SubscriptionFormValidationException( + List.of("Subscription form must not exceed " + SubscriptionFormSubmissionValidator.MAX_METADATA_COUNT + " fields") + ); + } + } + public record Input(String environmentId, SubscriptionFormId subscriptionFormId, String gmdContent) {} public record Output(SubscriptionForm subscriptionForm) {} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/SubscriptionValidationServiceImpl.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/SubscriptionValidationServiceImpl.java index fdc0aedaac9..bf7a14418a5 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/SubscriptionValidationServiceImpl.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/SubscriptionValidationServiceImpl.java @@ -18,6 +18,9 @@ import io.gravitee.apim.core.application_certificate.crud_service.ClientCertificateCrudService; import io.gravitee.apim.core.application_certificate.model.ClientCertificate; import io.gravitee.apim.core.application_certificate.model.ClientCertificateStatus; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSubmissionValidator; +import io.gravitee.apim.core.subscription_form.model.SubscriptionForm; +import io.gravitee.apim.core.subscription_form.query_service.SubscriptionFormQueryService; import io.gravitee.definition.model.v4.plan.PlanMode; import io.gravitee.rest.api.model.NewSubscriptionEntity; import io.gravitee.rest.api.model.PlanSecurityType; @@ -32,6 +35,7 @@ import io.gravitee.rest.api.service.v4.validation.SubscriptionMetadataSanitizer; import io.gravitee.rest.api.service.v4.validation.SubscriptionValidationService; import java.util.List; +import java.util.Map; import java.util.Objects; import lombok.CustomLog; import lombok.RequiredArgsConstructor; @@ -48,6 +52,7 @@ public class SubscriptionValidationServiceImpl extends TransactionalService impl private final EntrypointConnectorPluginService entrypointService; private final SubscriptionMetadataSanitizer subscriptionMetadataSanitizer; + private final SubscriptionFormQueryService subscriptionFormQueryService; private final ClientCertificateCrudService clientCertificateCrudService; @@ -57,6 +62,22 @@ public void validateAndSanitize(final GenericPlanEntity genericPlanEntity, final if (subscription.getMetadata() != null) { subscription.setMetadata(subscriptionMetadataSanitizer.sanitizeAndValidate(subscription.getMetadata())); } + validateSubscriptionFormMetadataIfApplicable(genericPlanEntity, subscription.getMetadata()); + } + + private void validateSubscriptionFormMetadataIfApplicable( + final GenericPlanEntity genericPlanEntity, + final Map metadata + ) { + subscriptionFormQueryService + .findDefaultForEnvironmentId(genericPlanEntity.getEnvironmentId()) + .filter(SubscriptionForm::isEnabled) + .map(SubscriptionForm::getValidationConstraints) + .filter(constraints -> !constraints.isEmpty()) + .ifPresent(constraints -> { + var submitted = metadata != null ? metadata : Map.of(); + new SubscriptionFormSubmissionValidator(constraints).validate(submitted); + }); } @Override @@ -70,6 +91,7 @@ public void validateAndSanitize( if (subscription.getMetadata() != null) { subscription.setMetadata(subscriptionMetadataSanitizer.sanitizeAndValidate(subscription.getMetadata())); } + validateSubscriptionFormMetadataIfApplicable(genericPlanEntity, subscription.getMetadata()); } private void validateTls(final GenericPlanEntity genericPlanEntity, final UpdateSubscriptionEntity subscription, String applicationId) { @@ -109,6 +131,7 @@ public void validateAndSanitize( subscriptionMetadataSanitizer.sanitizeAndValidate(subscriptionConfiguration.getMetadata()) ); } + validateSubscriptionFormMetadataIfApplicable(genericPlanEntity, subscriptionConfiguration.getMetadata()); } private SubscriptionConfigurationEntity validateAndSanitizeSubscriptionConfiguration( diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCaseTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCaseTest.java index 5b472af2076..df09aa00b4d 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCaseTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCaseTest.java @@ -24,10 +24,13 @@ import io.gravitee.apim.core.gravitee_markdown.GraviteeMarkdown; import io.gravitee.apim.core.gravitee_markdown.GraviteeMarkdownValidator; import io.gravitee.apim.core.gravitee_markdown.exception.GraviteeMarkdownContentEmptyException; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSubmissionValidator; import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormNotFoundException; +import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormValidationException; import io.gravitee.apim.core.subscription_form.model.SubscriptionForm; -import io.gravitee.apim.core.subscription_form.model.SubscriptionFormFieldConstraints; import io.gravitee.apim.core.subscription_form.model.SubscriptionFormId; +import io.gravitee.apim.infra.domain_service.subscription_form.SubscriptionFormSchemaGeneratorImpl; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,13 +40,15 @@ class UpdateSubscriptionFormUseCaseTest { private final SubscriptionFormCrudServiceInMemory crudService = new SubscriptionFormCrudServiceInMemory(); private final SubscriptionFormQueryServiceInMemory queryService = new SubscriptionFormQueryServiceInMemory(); private final GraviteeMarkdownValidator gmdValidator = new GraviteeMarkdownValidator(); + private final SubscriptionFormSchemaGenerator schemaGenerator = new SubscriptionFormSchemaGeneratorImpl(); + private UpdateSubscriptionFormUseCase useCase; @BeforeEach void setUp() { crudService.reset(); queryService.reset(); - useCase = new UpdateSubscriptionFormUseCase(crudService, queryService, gmdValidator); + useCase = new UpdateSubscriptionFormUseCase(crudService, queryService, gmdValidator, schemaGenerator); } @Test @@ -67,7 +72,22 @@ void should_update_existing_form() { GraviteeMarkdown.of("") ); assertThat(result.subscriptionForm().getId()).isEqualTo(existingForm.getId()); - assertThat(result.subscriptionForm().getValidationConstraints()).isEqualTo(SubscriptionFormFieldConstraints.empty()); + assertThat(result.subscriptionForm().getValidationConstraints()).isNotNull(); + assertThat(result.subscriptionForm().getValidationConstraints().byFieldKey()).containsKey("updated"); + } + + @Test + void should_persist_empty_constraints_when_gmd_has_no_form_fields() { + SubscriptionForm existingForm = SubscriptionFormFixtures.aSubscriptionForm(); + crudService.initWith(List.of(existingForm)); + queryService.initWith(List.of(existingForm)); + + var result = useCase.execute( + new UpdateSubscriptionFormUseCase.Input(existingForm.getEnvironmentId(), existingForm.getId(), "

Only static content

") + ); + + assertThat(result.subscriptionForm().getValidationConstraints()).isNotNull(); + assertThat(result.subscriptionForm().getValidationConstraints().isEmpty()).isTrue(); } @Test @@ -80,6 +100,30 @@ void should_throw_exception_when_form_not_exists() { assertThatThrownBy(() -> useCase.execute(input)).isInstanceOf(SubscriptionFormNotFoundException.class); } + @Test + void should_throw_when_form_exceeds_max_field_count() { + SubscriptionForm existingForm = SubscriptionFormFixtures.aSubscriptionForm(); + crudService.initWith(List.of(existingForm)); + queryService.initWith(List.of(existingForm)); + + int tooMany = SubscriptionFormSubmissionValidator.MAX_METADATA_COUNT + 1; + StringBuilder gmd = new StringBuilder(); + for (int i = 0; i < tooMany; i++) { + gmd.append(""); + } + + var input = new UpdateSubscriptionFormUseCase.Input(existingForm.getEnvironmentId(), existingForm.getId(), gmd.toString()); + + assertThatThrownBy(() -> useCase.execute(input)) + .isInstanceOf(SubscriptionFormValidationException.class) + .extracting(e -> ((SubscriptionFormValidationException) e).getErrors()) + .satisfies(errors -> + assertThat(errors).containsExactly( + "Subscription form must not exceed " + SubscriptionFormSubmissionValidator.MAX_METADATA_COUNT + " fields" + ) + ); + } + @Test void should_throw_when_content_is_empty() { // Given diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/SubscriptionValidationServiceImplTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/SubscriptionValidationServiceImplTest.java index ec5951f56c9..373ae9109ad 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/SubscriptionValidationServiceImplTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/SubscriptionValidationServiceImplTest.java @@ -16,14 +16,22 @@ package io.gravitee.rest.api.service.v4.impl.validation; import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; +import fixtures.core.model.SubscriptionFormFixtures; +import inmemory.SubscriptionFormQueryServiceInMemory; import io.gravitee.apim.core.application_certificate.crud_service.ClientCertificateCrudService; import io.gravitee.apim.core.application_certificate.model.ClientCertificateStatus; +import io.gravitee.apim.core.gravitee_markdown.GraviteeMarkdown; +import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormConstraintsFactory; +import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormValidationException; +import io.gravitee.apim.core.subscription_form.model.SubscriptionForm; +import io.gravitee.apim.core.subscription_form.model.SubscriptionFormFieldConstraints; +import io.gravitee.apim.core.subscription_form.model.SubscriptionFormId; +import io.gravitee.apim.core.subscription_form.model.SubscriptionFormSchema; import io.gravitee.definition.model.v4.plan.PlanMode; import io.gravitee.definition.model.v4.plan.PlanSecurity; import io.gravitee.rest.api.model.NewSubscriptionEntity; @@ -41,6 +49,8 @@ import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -70,13 +80,17 @@ public class SubscriptionValidationServiceImplTest { @Mock private SubscriptionMetadataSanitizer subscriptionMetadataSanitizer; + private SubscriptionFormQueryServiceInMemory subscriptionFormQueryService; + private PlanEntity planEntity; @BeforeEach void setUp() { + subscriptionFormQueryService = new SubscriptionFormQueryServiceInMemory(); cut = new SubscriptionValidationServiceImpl( entrypointConnectorPluginService, subscriptionMetadataSanitizer, + subscriptionFormQueryService, clientCertificateCrudService ); lenient() @@ -84,8 +98,12 @@ void setUp() { .thenAnswer(invocation -> invocation.getArgument(0)); planEntity = new PlanEntity(); - PlanSecurity security = new PlanSecurity(); - planEntity.setSecurity(security); + planEntity.setSecurity(new PlanSecurity()); + } + + @AfterEach + void tearDown() { + subscriptionFormQueryService.reset(); } @Nested @@ -355,4 +373,341 @@ void should_do_nothing() { } } } + + @Nested + class Subscription_form_metadata { + + private static SubscriptionFormFieldConstraints required_email_constraints() { + return SubscriptionFormConstraintsFactory.fromSchema( + new SubscriptionFormSchema(List.of(new SubscriptionFormSchema.InputField("email", true, null, null, null, null))) + ); + } + + @BeforeEach + void beforeEach() { + planEntity.setEnvironmentId(SubscriptionFormFixtures.ENVIRONMENT_ID); + } + + @Test + void should_throw_when_form_enabled_and_metadata_invalid() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var subscription = new NewSubscriptionEntity(); + subscription.setMetadata(Map.of()); + + assertThatThrownBy(() -> cut.validateAndSanitize(planEntity, subscription)).isInstanceOf( + SubscriptionFormValidationException.class + ); + } + + @Test + void should_not_throw_when_form_enabled_and_metadata_valid() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var subscription = new NewSubscriptionEntity(); + subscription.setMetadata(Map.of("email", "user@example.com")); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, subscription)).doesNotThrowAnyException(); + } + + @Test + void should_not_validate_when_validation_constraints_null() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionForm.builder() + .id(SubscriptionFormId.of(SubscriptionFormFixtures.FORM_ID)) + .environmentId(SubscriptionFormFixtures.ENVIRONMENT_ID) + .gmdContent(GraviteeMarkdown.of("

")) + .enabled(true) + .validationConstraints(null) + .build() + ) + ); + + var subscription = new NewSubscriptionEntity(); + subscription.setMetadata(Map.of()); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, subscription)).doesNotThrowAnyException(); + } + + @Test + void should_not_validate_when_validation_constraints_empty() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(SubscriptionFormFieldConstraints.empty()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var subscription = new NewSubscriptionEntity(); + subscription.setMetadata(Map.of()); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, subscription)).doesNotThrowAnyException(); + } + + @Test + void should_not_validate_when_form_disabled_even_if_constraints_present() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(false) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var subscription = new NewSubscriptionEntity(); + subscription.setMetadata(Map.of()); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, subscription)).doesNotThrowAnyException(); + } + + @Test + void should_not_validate_when_no_form_for_environment() { + // storage is empty — no form registered for any environment + + var subscription = new NewSubscriptionEntity(); + subscription.setMetadata(Map.of()); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, subscription)).doesNotThrowAnyException(); + } + + @Test + void should_treat_null_metadata_as_empty_map_when_validating() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var subscription = new NewSubscriptionEntity(); + subscription.setMetadata(null); + + assertThatThrownBy(() -> cut.validateAndSanitize(planEntity, subscription)).isInstanceOf( + SubscriptionFormValidationException.class + ); + } + } + + @Nested + class Subscription_form_metadata_on_update_configuration { + + private static SubscriptionFormFieldConstraints required_email_constraints() { + return SubscriptionFormConstraintsFactory.fromSchema( + new SubscriptionFormSchema(List.of(new SubscriptionFormSchema.InputField("email", true, null, null, null, null))) + ); + } + + @BeforeEach + void beforeEach() { + planEntity.setEnvironmentId(SubscriptionFormFixtures.ENVIRONMENT_ID); + } + + @Test + void should_throw_when_form_enabled_and_metadata_invalid() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var subscriptionConfig = new UpdateSubscriptionConfigurationEntity(); + subscriptionConfig.setMetadata(Map.of()); + + assertThatThrownBy(() -> cut.validateAndSanitize(planEntity, subscriptionConfig)).isInstanceOf( + SubscriptionFormValidationException.class + ); + } + + @Test + void should_not_throw_when_form_enabled_and_metadata_valid() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var subscriptionConfig = new UpdateSubscriptionConfigurationEntity(); + subscriptionConfig.setMetadata(Map.of("email", "user@example.com")); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, subscriptionConfig)).doesNotThrowAnyException(); + } + + @Test + void should_not_validate_when_form_disabled() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(false) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var subscriptionConfig = new UpdateSubscriptionConfigurationEntity(); + subscriptionConfig.setMetadata(Map.of()); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, subscriptionConfig)).doesNotThrowAnyException(); + } + + @Test + void should_not_validate_when_no_form_for_environment() { + var subscriptionConfig = new UpdateSubscriptionConfigurationEntity(); + subscriptionConfig.setMetadata(Map.of()); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, subscriptionConfig)).doesNotThrowAnyException(); + } + + @Test + void should_treat_null_metadata_as_empty_map_when_validating() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var subscriptionConfig = new UpdateSubscriptionConfigurationEntity(); + subscriptionConfig.setMetadata(null); + + assertThatThrownBy(() -> cut.validateAndSanitize(planEntity, subscriptionConfig)).isInstanceOf( + SubscriptionFormValidationException.class + ); + } + } + + @Nested + class Subscription_form_metadata_on_update_subscription { + + private static SubscriptionFormFieldConstraints required_email_constraints() { + return SubscriptionFormConstraintsFactory.fromSchema( + new SubscriptionFormSchema(List.of(new SubscriptionFormSchema.InputField("email", true, null, null, null, null))) + ); + } + + @BeforeEach + void beforeEach() { + planEntity.setEnvironmentId(SubscriptionFormFixtures.ENVIRONMENT_ID); + } + + @Test + void should_throw_when_form_enabled_and_metadata_invalid() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var updateSubscription = new UpdateSubscriptionEntity(); + updateSubscription.setMetadata(Map.of()); + + assertThatThrownBy(() -> cut.validateAndSanitize(planEntity, updateSubscription, APP_ID)).isInstanceOf( + SubscriptionFormValidationException.class + ); + } + + @Test + void should_not_throw_when_form_enabled_and_metadata_valid() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var updateSubscription = new UpdateSubscriptionEntity(); + updateSubscription.setMetadata(Map.of("email", "user@example.com")); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, updateSubscription, APP_ID)).doesNotThrowAnyException(); + } + + @Test + void should_not_validate_when_form_disabled() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(false) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var updateSubscription = new UpdateSubscriptionEntity(); + updateSubscription.setMetadata(Map.of()); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, updateSubscription, APP_ID)).doesNotThrowAnyException(); + } + + @Test + void should_not_validate_when_no_form_for_environment() { + var updateSubscription = new UpdateSubscriptionEntity(); + updateSubscription.setMetadata(Map.of()); + + assertThatCode(() -> cut.validateAndSanitize(planEntity, updateSubscription, APP_ID)).doesNotThrowAnyException(); + } + + @Test + void should_treat_null_metadata_as_empty_map_when_validating() { + subscriptionFormQueryService.initWith( + List.of( + SubscriptionFormFixtures.aSubscriptionFormBuilder() + .enabled(true) + .validationConstraints(required_email_constraints()) + .gmdContent(GraviteeMarkdown.of("

")) + .build() + ) + ); + + var updateSubscription = new UpdateSubscriptionEntity(); + updateSubscription.setMetadata(null); + + assertThatThrownBy(() -> cut.validateAndSanitize(planEntity, updateSubscription, APP_ID)).isInstanceOf( + SubscriptionFormValidationException.class + ); + } + } }