Skip to content

Commit 8063362

Browse files
feat: wire constraint generation on form save and validate on subscription
1 parent d0af8a6 commit 8063362

File tree

10 files changed

+298
-8
lines changed

10 files changed

+298
-8
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
import io.gravitee.apim.core.subscription.use_case.ImportSubscriptionSpecUseCase;
161161
import io.gravitee.apim.core.subscription.use_case.RejectSubscriptionUseCase;
162162
import io.gravitee.apim.core.subscription.use_case.UpdateSubscriptionUseCase;
163+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator;
163164
import io.gravitee.apim.core.user.domain_service.UserContextLoader;
164165
import io.gravitee.apim.infra.adapter.SubscriptionAdapter;
165166
import io.gravitee.apim.infra.adapter.SubscriptionAdapterImpl;
@@ -1084,4 +1085,9 @@ public JsonSchemaChecker jsonSchemaChecker() {
10841085
public ClusterConfigurationSchemaService clusterConfigurationSchemaService() {
10851086
return mock(ClusterConfigurationSchemaService.class);
10861087
}
1088+
1089+
@Bean
1090+
public SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator() {
1091+
return mock(SubscriptionFormSchemaGenerator.class);
1092+
}
10871093
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
import io.gravitee.apim.core.subscription.use_case.ImportSubscriptionSpecUseCase;
190190
import io.gravitee.apim.core.subscription.use_case.RejectSubscriptionUseCase;
191191
import io.gravitee.apim.core.subscription.use_case.UpdateSubscriptionUseCase;
192+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator;
192193
import io.gravitee.apim.core.user.domain_service.UserContextLoader;
193194
import io.gravitee.apim.core.user.domain_service.UserDomainService;
194195
import io.gravitee.apim.core.user.use_case.GetUserApisUseCase;
@@ -1267,4 +1268,9 @@ public GetUserApplicationsUseCase getUserApplicationsUseCase() {
12671268
public GetUserGroupsUseCase getUserGroupsUseCase() {
12681269
return mock(GetUserGroupsUseCase.class);
12691270
}
1271+
1272+
@Bean
1273+
public SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator() {
1274+
return mock(SubscriptionFormSchemaGenerator.class);
1275+
}
12701276
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import inmemory.PortalNavigationItemsQueryServiceInMemory;
3131
import inmemory.PortalPageContentQueryServiceInMemory;
3232
import inmemory.SharedPolicyGroupCrudServiceInMemory;
33+
import inmemory.SubscriptionFormQueryServiceInMemory;
3334
import inmemory.SubscriptionSearchQueryServiceInMemory;
3435
import inmemory.spring.InMemoryConfiguration;
3536
import io.gravitee.apim.core.access_point.query_service.AccessPointQueryService;
@@ -148,6 +149,8 @@
148149
import io.gravitee.apim.core.subscription.use_case.GetSubscriptionsUseCase;
149150
import io.gravitee.apim.core.subscription.use_case.ImportSubscriptionSpecUseCase;
150151
import io.gravitee.apim.core.subscription.use_case.UpdateSubscriptionUseCase;
152+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator;
153+
import io.gravitee.apim.core.subscription_form.query_service.SubscriptionFormQueryService;
151154
import io.gravitee.apim.core.user.domain_service.UserContextLoader;
152155
import io.gravitee.apim.infra.adapter.SubscriptionAdapter;
153156
import io.gravitee.apim.infra.adapter.SubscriptionAdapterImpl;
@@ -1260,6 +1263,16 @@ public ApplicationCertificatesUpdateDomainService applicationCertificatesUpdateD
12601263
return mock(ApplicationCertificatesUpdateDomainService.class);
12611264
}
12621265

1266+
@Bean
1267+
public SubscriptionFormQueryService subscriptionFormQueryService() {
1268+
return new SubscriptionFormQueryServiceInMemory();
1269+
}
1270+
1271+
@Bean
1272+
public SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator() {
1273+
return mock(SubscriptionFormSchemaGenerator.class);
1274+
}
1275+
12631276
@Bean
12641277
public ClientCertificateValidationDomainService clientCertificateValidationDomainService() {
12651278
return mock(ClientCertificateValidationDomainService.class);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.mockito.Mockito.reset;
1919

2020
import io.gravitee.apim.core.subscription.use_case.CreateSubscriptionUseCase;
21+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator;
2122
import io.gravitee.rest.api.portal.rest.JerseySpringTest;
2223
import io.gravitee.rest.api.portal.rest.mapper.AnalyticsMapper;
2324
import io.gravitee.rest.api.portal.rest.mapper.ApiMapper;
@@ -319,6 +320,9 @@ public abstract class AbstractResourceTest extends JerseySpringTest {
319320
@Autowired
320321
protected EndpointConnectorPluginService endpointConnectorPluginService;
321322

323+
@Autowired
324+
protected SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator;
325+
322326
public AbstractResourceTest() {
323327
super(
324328
new AuthenticationProviderManager() {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import io.gravitee.apim.core.subscription.model.SubscriptionEntity;
3737
import io.gravitee.apim.core.subscription.use_case.CreateSubscriptionUseCase;
38+
import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormValidationException;
3839
import io.gravitee.common.data.domain.Page;
3940
import io.gravitee.common.http.HttpStatusCode;
4041
import io.gravitee.rest.api.model.ApiKeyEntity;
@@ -58,6 +59,7 @@
5859
import jakarta.ws.rs.client.Entity;
5960
import jakarta.ws.rs.core.Response;
6061
import java.util.Collections;
62+
import java.util.List;
6163
import java.util.Map;
6264
import lombok.AllArgsConstructor;
6365
import lombok.Getter;
@@ -235,6 +237,19 @@ void shouldReturnBadRequestWhenMetadataKeyIsInvalid() {
235237
verify(createSubscriptionUseCase, times(1)).execute(any());
236238
}
237239

240+
@Test
241+
void shouldReturnBadRequestWhenSubscriptionFormMetadataIsInvalid() {
242+
doThrow(new SubscriptionFormValidationException(List.of("Field 'email' is required")))
243+
.when(createSubscriptionUseCase)
244+
.execute(any());
245+
246+
SubscriptionInput subscriptionInput = new SubscriptionInput().application(APPLICATION).plan(PLAN).metadata(Map.of());
247+
248+
final Response response = target().request().post(Entity.json(subscriptionInput));
249+
Assertions.assertEquals(HttpStatusCode.BAD_REQUEST_400, response.getStatus());
250+
verify(createSubscriptionUseCase, times(1)).execute(any());
251+
}
252+
238253
@Test
239254
public void shouldHaveBadRequestWhileCreatingSubscription() {
240255
final Response response = target().request().post(Entity.json(null));

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
import io.gravitee.apim.core.subscription.use_case.GetSubscriptionsUseCase;
146146
import io.gravitee.apim.core.subscription.use_case.ImportSubscriptionSpecUseCase;
147147
import io.gravitee.apim.core.subscription.use_case.UpdateSubscriptionUseCase;
148+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator;
148149
import io.gravitee.apim.core.user.domain_service.UserContextLoader;
149150
import io.gravitee.apim.infra.adapter.SubscriptionAdapter;
150151
import io.gravitee.apim.infra.adapter.SubscriptionAdapterImpl;
@@ -1227,4 +1228,9 @@ public JsonSchemaChecker jsonSchemaChecker() {
12271228
public ClusterConfigurationSchemaService clusterConfigurationSchemaService() {
12281229
return mock(ClusterConfigurationSchemaService.class);
12291230
}
1231+
1232+
@Bean
1233+
SubscriptionFormSchemaGenerator subscriptionFormSchemaGenerator() {
1234+
return mock(SubscriptionFormSchemaGenerator.class);
1235+
}
12301236
}

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCase.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@
1919
import io.gravitee.apim.core.gravitee_markdown.GraviteeMarkdown;
2020
import io.gravitee.apim.core.gravitee_markdown.GraviteeMarkdownValidator;
2121
import io.gravitee.apim.core.subscription_form.crud_service.SubscriptionFormCrudService;
22+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormConstraintsFactory;
23+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator;
24+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSubmissionValidator;
2225
import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormNotFoundException;
26+
import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormValidationException;
2327
import io.gravitee.apim.core.subscription_form.model.SubscriptionForm;
24-
import io.gravitee.apim.core.subscription_form.model.SubscriptionFormFieldConstraints;
2528
import io.gravitee.apim.core.subscription_form.model.SubscriptionFormId;
29+
import io.gravitee.apim.core.subscription_form.model.SubscriptionFormSchema;
2630
import io.gravitee.apim.core.subscription_form.query_service.SubscriptionFormQueryService;
31+
import java.util.List;
2732
import lombok.CustomLog;
2833
import lombok.RequiredArgsConstructor;
2934

@@ -42,6 +47,7 @@ public class UpdateSubscriptionFormUseCase {
4247
private final SubscriptionFormCrudService subscriptionFormCrudService;
4348
private final SubscriptionFormQueryService subscriptionFormQueryService;
4449
private final GraviteeMarkdownValidator graviteeMarkdownValidator;
50+
private final SubscriptionFormSchemaGenerator schemaGenerator;
4551

4652
public Output execute(Input input) {
4753
graviteeMarkdownValidator.validateNotEmpty(GraviteeMarkdown.of(input.gmdContent()));
@@ -55,14 +61,26 @@ public Output execute(Input input) {
5561
)
5662
);
5763

58-
existingForm.update(GraviteeMarkdown.of(input.gmdContent()), SubscriptionFormFieldConstraints.empty());
64+
var gmd = GraviteeMarkdown.of(input.gmdContent());
65+
var schema = schemaGenerator.generate(gmd);
66+
validateFieldCount(schema);
67+
var constraints = SubscriptionFormConstraintsFactory.fromSchema(schema);
68+
existingForm.update(gmd, constraints);
5969
var savedForm = subscriptionFormCrudService.update(existingForm);
6070

6171
log.info("Updated subscription form [{}] for environment [{}]", input.subscriptionFormId(), input.environmentId());
6272

6373
return new Output(savedForm);
6474
}
6575

76+
private void validateFieldCount(SubscriptionFormSchema schema) {
77+
if (schema != null && schema.fields().size() > SubscriptionFormSubmissionValidator.MAX_METADATA_COUNT) {
78+
throw new SubscriptionFormValidationException(
79+
List.of("Subscription form must not exceed " + SubscriptionFormSubmissionValidator.MAX_METADATA_COUNT + " fields")
80+
);
81+
}
82+
}
83+
6684
public record Input(String environmentId, SubscriptionFormId subscriptionFormId, String gmdContent) {}
6785

6886
public record Output(SubscriptionForm subscriptionForm) {}

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/SubscriptionValidationServiceImpl.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
import io.gravitee.apim.core.application_certificate.crud_service.ClientCertificateCrudService;
1919
import io.gravitee.apim.core.application_certificate.model.ClientCertificate;
2020
import io.gravitee.apim.core.application_certificate.model.ClientCertificateStatus;
21+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSubmissionValidator;
22+
import io.gravitee.apim.core.subscription_form.model.SubscriptionForm;
23+
import io.gravitee.apim.core.subscription_form.query_service.SubscriptionFormQueryService;
2124
import io.gravitee.definition.model.v4.plan.PlanMode;
2225
import io.gravitee.rest.api.model.NewSubscriptionEntity;
2326
import io.gravitee.rest.api.model.PlanSecurityType;
@@ -32,6 +35,7 @@
3235
import io.gravitee.rest.api.service.v4.validation.SubscriptionMetadataSanitizer;
3336
import io.gravitee.rest.api.service.v4.validation.SubscriptionValidationService;
3437
import java.util.List;
38+
import java.util.Map;
3539
import java.util.Objects;
3640
import lombok.CustomLog;
3741
import lombok.RequiredArgsConstructor;
@@ -48,6 +52,7 @@ public class SubscriptionValidationServiceImpl extends TransactionalService impl
4852

4953
private final EntrypointConnectorPluginService entrypointService;
5054
private final SubscriptionMetadataSanitizer subscriptionMetadataSanitizer;
55+
private final SubscriptionFormQueryService subscriptionFormQueryService;
5156

5257
private final ClientCertificateCrudService clientCertificateCrudService;
5358

@@ -57,6 +62,22 @@ public void validateAndSanitize(final GenericPlanEntity genericPlanEntity, final
5762
if (subscription.getMetadata() != null) {
5863
subscription.setMetadata(subscriptionMetadataSanitizer.sanitizeAndValidate(subscription.getMetadata()));
5964
}
65+
validateSubscriptionFormMetadataIfApplicable(genericPlanEntity, subscription);
66+
}
67+
68+
private void validateSubscriptionFormMetadataIfApplicable(
69+
final GenericPlanEntity genericPlanEntity,
70+
final NewSubscriptionEntity subscription
71+
) {
72+
subscriptionFormQueryService
73+
.findDefaultForEnvironmentId(genericPlanEntity.getEnvironmentId())
74+
.filter(SubscriptionForm::isEnabled)
75+
.map(SubscriptionForm::getValidationConstraints)
76+
.filter(constraints -> !constraints.isEmpty())
77+
.ifPresent(constraints -> {
78+
var submitted = subscription.getMetadata() != null ? subscription.getMetadata() : Map.<String, String>of();
79+
new SubscriptionFormSubmissionValidator(constraints).validate(submitted);
80+
});
6081
}
6182

6283
@Override

gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/subscription_form/use_case/UpdateSubscriptionFormUseCaseTest.java

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@
2424
import io.gravitee.apim.core.gravitee_markdown.GraviteeMarkdown;
2525
import io.gravitee.apim.core.gravitee_markdown.GraviteeMarkdownValidator;
2626
import io.gravitee.apim.core.gravitee_markdown.exception.GraviteeMarkdownContentEmptyException;
27+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator;
28+
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSubmissionValidator;
2729
import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormNotFoundException;
30+
import io.gravitee.apim.core.subscription_form.exception.SubscriptionFormValidationException;
2831
import io.gravitee.apim.core.subscription_form.model.SubscriptionForm;
29-
import io.gravitee.apim.core.subscription_form.model.SubscriptionFormFieldConstraints;
3032
import io.gravitee.apim.core.subscription_form.model.SubscriptionFormId;
33+
import io.gravitee.apim.infra.domain_service.subscription_form.SubscriptionFormSchemaGeneratorImpl;
3134
import java.util.List;
3235
import org.junit.jupiter.api.BeforeEach;
3336
import org.junit.jupiter.api.Test;
@@ -37,13 +40,15 @@ class UpdateSubscriptionFormUseCaseTest {
3740
private final SubscriptionFormCrudServiceInMemory crudService = new SubscriptionFormCrudServiceInMemory();
3841
private final SubscriptionFormQueryServiceInMemory queryService = new SubscriptionFormQueryServiceInMemory();
3942
private final GraviteeMarkdownValidator gmdValidator = new GraviteeMarkdownValidator();
43+
private final SubscriptionFormSchemaGenerator schemaGenerator = new SubscriptionFormSchemaGeneratorImpl();
44+
4045
private UpdateSubscriptionFormUseCase useCase;
4146

4247
@BeforeEach
4348
void setUp() {
4449
crudService.reset();
4550
queryService.reset();
46-
useCase = new UpdateSubscriptionFormUseCase(crudService, queryService, gmdValidator);
51+
useCase = new UpdateSubscriptionFormUseCase(crudService, queryService, gmdValidator, schemaGenerator);
4752
}
4853

4954
@Test
@@ -67,7 +72,22 @@ void should_update_existing_form() {
6772
GraviteeMarkdown.of("<gmd-input name=\"updated\" fieldKey=\"updated\"/>")
6873
);
6974
assertThat(result.subscriptionForm().getId()).isEqualTo(existingForm.getId());
70-
assertThat(result.subscriptionForm().getValidationConstraints()).isEqualTo(SubscriptionFormFieldConstraints.empty());
75+
assertThat(result.subscriptionForm().getValidationConstraints()).isNotNull();
76+
assertThat(result.subscriptionForm().getValidationConstraints().byFieldKey()).containsKey("updated");
77+
}
78+
79+
@Test
80+
void should_persist_empty_constraints_when_gmd_has_no_form_fields() {
81+
SubscriptionForm existingForm = SubscriptionFormFixtures.aSubscriptionForm();
82+
crudService.initWith(List.of(existingForm));
83+
queryService.initWith(List.of(existingForm));
84+
85+
var result = useCase.execute(
86+
new UpdateSubscriptionFormUseCase.Input(existingForm.getEnvironmentId(), existingForm.getId(), "<p>Only static content</p>")
87+
);
88+
89+
assertThat(result.subscriptionForm().getValidationConstraints()).isNotNull();
90+
assertThat(result.subscriptionForm().getValidationConstraints().isEmpty()).isTrue();
7191
}
7292

7393
@Test
@@ -80,6 +100,30 @@ void should_throw_exception_when_form_not_exists() {
80100
assertThatThrownBy(() -> useCase.execute(input)).isInstanceOf(SubscriptionFormNotFoundException.class);
81101
}
82102

103+
@Test
104+
void should_throw_when_form_exceeds_max_field_count() {
105+
SubscriptionForm existingForm = SubscriptionFormFixtures.aSubscriptionForm();
106+
crudService.initWith(List.of(existingForm));
107+
queryService.initWith(List.of(existingForm));
108+
109+
int tooMany = SubscriptionFormSubmissionValidator.MAX_METADATA_COUNT + 1;
110+
StringBuilder gmd = new StringBuilder();
111+
for (int i = 0; i < tooMany; i++) {
112+
gmd.append("<gmd-input fieldKey=\"field").append(i).append("\"/>");
113+
}
114+
115+
var input = new UpdateSubscriptionFormUseCase.Input(existingForm.getEnvironmentId(), existingForm.getId(), gmd.toString());
116+
117+
assertThatThrownBy(() -> useCase.execute(input))
118+
.isInstanceOf(SubscriptionFormValidationException.class)
119+
.extracting(e -> ((SubscriptionFormValidationException) e).getErrors())
120+
.satisfies(errors ->
121+
assertThat(errors).containsExactly(
122+
"Subscription form must not exceed " + SubscriptionFormSubmissionValidator.MAX_METADATA_COUNT + " fields"
123+
)
124+
);
125+
}
126+
83127
@Test
84128
void should_throw_when_content_is_empty() {
85129
// Given

0 commit comments

Comments
 (0)