Skip to content

Commit a374ede

Browse files
committed
feat: create a promoted API V4
1 parent b2427cc commit a374ede

File tree

12 files changed

+888
-60
lines changed

12 files changed

+888
-60
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
@@ -109,6 +109,7 @@
109109
import io.gravitee.apim.core.portal_page.query_service.PortalPageQueryService;
110110
import io.gravitee.apim.core.promotion.service_provider.CockpitPromotionServiceProvider;
111111
import io.gravitee.apim.core.promotion.use_case.CreatePromotionUseCase;
112+
import io.gravitee.apim.core.promotion.use_case.ProcessPromotionUseCase;
112113
import io.gravitee.apim.core.sanitizer.HtmlSanitizer;
113114
import io.gravitee.apim.core.shared_policy_group.crud_service.SharedPolicyGroupCrudService;
114115
import io.gravitee.apim.core.shared_policy_group.crud_service.SharedPolicyGroupHistoryCrudService;
@@ -905,4 +906,9 @@ public GetMetricFiltersUseCase getMetricFiltersUseCase(AnalyticsDefinition analy
905906
public GetMetricFacetsUseCase getMetricFacetsUseCase(AnalyticsDefinition analyticsDefinition) {
906907
return new GetMetricFacetsUseCase(analyticsDefinition);
907908
}
909+
910+
@Bean
911+
public ProcessPromotionUseCase promotionUseCase() {
912+
return mock(ProcessPromotionUseCase.class);
913+
}
908914
}

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/mapper/ImportExportApiMapper.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static io.gravitee.apim.core.utils.CollectionUtils.stream;
1919

20+
import com.fasterxml.jackson.core.JsonProcessingException;
2021
import io.gravitee.apim.core.api.model.import_definition.ApiDescriptor;
2122
import io.gravitee.apim.core.api.model.import_definition.ApiExport;
2223
import io.gravitee.apim.core.api.model.import_definition.GraviteeDefinition;
@@ -30,7 +31,6 @@
3031
import io.gravitee.definition.model.v4.listener.tcp.TcpListener;
3132
import io.gravitee.definition.model.v4.nativeapi.NativeListener;
3233
import io.gravitee.definition.model.v4.nativeapi.kafka.KafkaListener;
33-
import io.gravitee.rest.api.management.v2.rest.mapper.CorsMapper;
3434
import io.gravitee.rest.api.management.v2.rest.model.ApiV4;
3535
import io.gravitee.rest.api.management.v2.rest.model.BaseOriginContext;
3636
import io.gravitee.rest.api.management.v2.rest.model.EndpointV4;
@@ -41,6 +41,7 @@
4141
import io.gravitee.rest.api.model.ApiMetadataEntity;
4242
import io.gravitee.rest.api.model.MemberEntity;
4343
import io.gravitee.rest.api.model.context.OriginContext;
44+
import io.gravitee.rest.api.service.exceptions.TechnicalManagementException;
4445
import jakarta.annotation.Nullable;
4546
import java.util.Map;
4647
import java.util.Set;
@@ -50,6 +51,7 @@
5051
import org.mapstruct.Mapper;
5152
import org.mapstruct.Mapping;
5253
import org.mapstruct.MappingTarget;
54+
import org.mapstruct.Named;
5355
import org.mapstruct.factory.Mappers;
5456

5557
@Mapper(
@@ -194,6 +196,15 @@ default Set<PlanWithFlows> buildPlans(ExportApiV4 exportApiV4) {
194196
return PlanMapper.INSTANCE.map(exportApiV4.getPlans(), exportApiV4.getApi().getType());
195197
}
196198

199+
@Named("definitionToExportApiV4")
200+
default ExportApiV4 definitionToExportApiV4(String exportApiV4) {
201+
try {
202+
return JSON_MAPPER.readValue(exportApiV4, ExportApiV4.class);
203+
} catch (JsonProcessingException e) {
204+
throw new TechnicalManagementException("An error occurred while trying to parse exported api during promotion" + e);
205+
}
206+
}
207+
197208
@Mapping(target = "apiId", expression = "java(apiId)")
198209
ApiMetadataEntity map(Metadata metadata, String apiId);
199210
}

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/promotions/PromotionsResource.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@
1515
*/
1616
package io.gravitee.rest.api.management.v2.rest.resource.promotions;
1717

18+
import io.gravitee.apim.core.api.model.import_definition.ImportDefinition;
19+
import io.gravitee.apim.core.audit.model.AuditActor;
20+
import io.gravitee.apim.core.audit.model.AuditInfo;
21+
import io.gravitee.apim.core.promotion.domain_service.PromotionContextDomainService;
1822
import io.gravitee.apim.core.promotion.use_case.ProcessPromotionUseCase;
1923
import io.gravitee.common.http.MediaType;
24+
import io.gravitee.definition.model.DefinitionVersion;
25+
import io.gravitee.rest.api.management.v2.rest.mapper.ImportExportApiMapper;
2026
import io.gravitee.rest.api.management.v2.rest.mapper.PromotionMapper;
27+
import io.gravitee.rest.api.management.v2.rest.model.ExportApiV4;
28+
import io.gravitee.rest.api.management.v2.rest.resource.AbstractResource;
2129
import io.gravitee.rest.api.service.common.GraviteeContext;
2230
import jakarta.inject.Inject;
2331
import jakarta.ws.rs.POST;
@@ -26,17 +34,50 @@
2634
import jakarta.ws.rs.Produces;
2735
import jakarta.ws.rs.core.Response;
2836

29-
public class PromotionsResource {
37+
public class PromotionsResource extends AbstractResource {
3038

3139
@Inject
3240
private ProcessPromotionUseCase promotionUseCase;
3341

42+
@Inject
43+
private PromotionContextDomainService promotionContextDomainService;
44+
3445
@POST
3546
@Path("{promotionId}/_process")
3647
@Produces(MediaType.APPLICATION_JSON)
3748
public Response processPromotion(@PathParam("promotionId") String promotionId, boolean isAccepted) {
38-
var input = new ProcessPromotionUseCase.Input(promotionId, isAccepted, GraviteeContext.getCurrentOrganization());
39-
var output = promotionUseCase.execute(input);
40-
return Response.ok(PromotionMapper.INSTANCE.map(output.promotion())).build();
49+
var promotionContext = promotionContextDomainService.getPromotionContext(promotionId);
50+
var expectedDefinitionVersion = promotionContext.expectedDefinitionVersion();
51+
var promotion = promotionContext.promotion();
52+
var existingPromotedApi = promotionContext.existingPromotedApi();
53+
54+
ProcessPromotionUseCase.Input input;
55+
if (DefinitionVersion.V4.equals(expectedDefinitionVersion)) {
56+
ExportApiV4 exportApi = ImportExportApiMapper.INSTANCE.definitionToExportApiV4(promotion.getApiDefinition());
57+
ImportDefinition importApi = ImportExportApiMapper.INSTANCE.toImportDefinition(exportApi);
58+
var authenticatedUser = getAuthenticatedUserDetails();
59+
input = new ProcessPromotionUseCase.Input(
60+
promotion,
61+
expectedDefinitionVersion,
62+
isAccepted,
63+
existingPromotedApi,
64+
importApi,
65+
AuditInfo.builder()
66+
.organizationId(GraviteeContext.getCurrentOrganization())
67+
.environmentId(promotionContext.targetEnvId())
68+
.actor(
69+
AuditActor.builder()
70+
.userId(authenticatedUser.getUsername())
71+
.userSource(authenticatedUser.getSource())
72+
.userSourceId(authenticatedUser.getSourceId())
73+
.build()
74+
)
75+
.build()
76+
);
77+
} else {
78+
input = new ProcessPromotionUseCase.Input(promotion, isAccepted, expectedDefinitionVersion);
79+
}
80+
81+
return Response.ok(PromotionMapper.INSTANCE.map(promotionUseCase.execute(input).promotion())).build();
4182
}
4283
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* Copyright © 2015 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.gravitee.rest.api.management.v2.rest.resource.promotions;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.Mockito.reset;
21+
import static org.mockito.Mockito.verify;
22+
import static org.mockito.Mockito.when;
23+
24+
import fixtures.core.model.PromotionFixtures;
25+
import inmemory.ApiCrudServiceInMemory;
26+
import inmemory.EnvironmentCrudServiceInMemory;
27+
import inmemory.InMemoryAlternative;
28+
import inmemory.PromotionCrudServiceInMemory;
29+
import io.gravitee.apim.core.api.model.Api;
30+
import io.gravitee.apim.core.api.model.NewApiMetadata;
31+
import io.gravitee.apim.core.api.model.import_definition.ApiDescriptor;
32+
import io.gravitee.apim.core.api.model.import_definition.GraviteeDefinition;
33+
import io.gravitee.apim.core.api.model.import_definition.PageExport;
34+
import io.gravitee.apim.core.api.model.import_definition.PlanDescriptor;
35+
import io.gravitee.apim.core.environment.model.Environment;
36+
import io.gravitee.apim.core.json.GraviteeDefinitionSerializer;
37+
import io.gravitee.apim.core.promotion.model.PromotionStatus;
38+
import io.gravitee.apim.core.promotion.use_case.ProcessPromotionUseCase;
39+
import io.gravitee.common.http.HttpStatusCode;
40+
import io.gravitee.definition.model.DefinitionVersion;
41+
import io.gravitee.rest.api.management.v2.rest.resource.AbstractResourceTest;
42+
import jakarta.ws.rs.client.Entity;
43+
import jakarta.ws.rs.client.WebTarget;
44+
import jakarta.ws.rs.core.Response;
45+
import java.util.Collections;
46+
import java.util.List;
47+
import java.util.stream.Stream;
48+
import lombok.SneakyThrows;
49+
import org.junit.jupiter.api.AfterEach;
50+
import org.junit.jupiter.api.BeforeEach;
51+
import org.junit.jupiter.api.Test;
52+
import org.mockito.ArgumentCaptor;
53+
import org.springframework.beans.factory.annotation.Autowired;
54+
55+
class PromotionsResourceTest extends AbstractResourceTest {
56+
57+
private static final String PROMOTION_ID = "promotion-id";
58+
private static final String API_ID = "api-id";
59+
private static final String API_CROSS_ID = "api-cross-id";
60+
private static final String BASE64_PICTURE = "data:image/jpeg;base64,picture...";
61+
private static final String DEFAULT_ENV_ID = "default-env-id";
62+
private static final String TARGET_ENV_ID = "target-env-id";
63+
private static final String TARGET_ENV_COCKPIT_ID = "target-env-cockpit-id";
64+
private WebTarget target;
65+
66+
@Autowired
67+
private ProcessPromotionUseCase processPromotionUseCase;
68+
69+
@Autowired
70+
private PromotionCrudServiceInMemory promotionCrudServiceInMemory;
71+
72+
@Autowired
73+
private ApiCrudServiceInMemory apiCrudServiceInMemory;
74+
75+
@Autowired
76+
private EnvironmentCrudServiceInMemory environmentCrudServiceInMemory;
77+
78+
@Autowired
79+
private GraviteeDefinitionSerializer graviteeDefinitionSerializer;
80+
81+
@BeforeEach
82+
public void init() {
83+
environmentCrudServiceInMemory.initWith(List.of(Environment.builder().id(TARGET_ENV_ID).cockpitId(TARGET_ENV_COCKPIT_ID).build()));
84+
target = rootTarget();
85+
}
86+
87+
@AfterEach
88+
public void clean() {
89+
Stream.of(apiCrudServiceInMemory, promotionCrudServiceInMemory, environmentCrudServiceInMemory).forEach(InMemoryAlternative::reset);
90+
reset(processPromotionUseCase);
91+
}
92+
93+
@Override
94+
protected String contextPath() {
95+
return "/organizations/" + ORGANIZATION + "/promotions/" + PROMOTION_ID + "/_process";
96+
}
97+
98+
@Test
99+
@SneakyThrows
100+
void should_map_exported_definition_to_import_definition() {
101+
var export = GraviteeDefinition.from(
102+
ApiDescriptor.ApiDescriptorV4.builder().id(API_ID).crossId(API_CROSS_ID).build(),
103+
Collections.emptySet(),
104+
List.of(NewApiMetadata.builder().apiId(API_ID).name("metadata").value("metadata-value").build()),
105+
List.of(PageExport.builder().id("page-id").build()),
106+
List.of(PlanDescriptor.V4.builder().id("plan-id").build()),
107+
Collections.emptyList(),
108+
BASE64_PICTURE,
109+
BASE64_PICTURE
110+
);
111+
apiCrudServiceInMemory.initWith(List.of(Api.builder().id(API_ID).crossId(API_CROSS_ID).environmentId(DEFAULT_ENV_ID).build()));
112+
113+
var promotion = PromotionFixtures.aPromotion()
114+
.toBuilder()
115+
.id(PROMOTION_ID)
116+
.apiId(API_ID)
117+
.targetEnvCockpitId(TARGET_ENV_COCKPIT_ID)
118+
.apiDefinition(graviteeDefinitionSerializer.serialize(export))
119+
.build();
120+
promotionCrudServiceInMemory.initWith(List.of(promotion));
121+
122+
when(processPromotionUseCase.execute(any())).thenReturn(
123+
new ProcessPromotionUseCase.Output(PromotionFixtures.aPromotion().toBuilder().status(PromotionStatus.REJECTED).build())
124+
);
125+
126+
Response response = target.request().post(Entity.json(false));
127+
128+
var captor = ArgumentCaptor.forClass(ProcessPromotionUseCase.Input.class);
129+
verify(processPromotionUseCase).execute(captor.capture());
130+
131+
var inputValue = captor.getValue();
132+
assertThat(inputValue)
133+
.isNotNull()
134+
.satisfies(input -> {
135+
assertThat(input.promotion()).isEqualTo(promotion);
136+
assertThat(input.definitionVersion()).isEqualTo(DefinitionVersion.V4);
137+
assertThat(input.importDefinition()).isNotNull();
138+
assertThat(input.isAccepted()).isFalse();
139+
assertThat(input.auditInfo().environmentId()).isEqualTo(TARGET_ENV_ID);
140+
});
141+
142+
assertThat(response.getStatus()).isEqualTo(HttpStatusCode.OK_200);
143+
}
144+
145+
@Test
146+
@SneakyThrows
147+
void should_call_use_case_with_promotion_and_v2_definition_version_without_mapping_exported_definition() {
148+
var promotion = PromotionFixtures.aPromotion()
149+
.toBuilder()
150+
.id(PROMOTION_ID)
151+
.apiId(API_ID)
152+
.apiDefinition("{ \"gravitee\" : \"2.0.0\", \"id\" : \"api-id\" }")
153+
.build();
154+
promotionCrudServiceInMemory.initWith(List.of(promotion));
155+
apiCrudServiceInMemory.initWith(List.of(Api.builder().id(API_ID).crossId(API_CROSS_ID).environmentId(DEFAULT_ENV_ID).build()));
156+
157+
when(processPromotionUseCase.execute(any())).thenReturn(
158+
new ProcessPromotionUseCase.Output(PromotionFixtures.aPromotion().toBuilder().status(PromotionStatus.ACCEPTED).build())
159+
);
160+
161+
Response response = target.request().post(Entity.json(true));
162+
163+
var captor = ArgumentCaptor.forClass(ProcessPromotionUseCase.Input.class);
164+
verify(processPromotionUseCase).execute(captor.capture());
165+
166+
var inputValue = captor.getValue();
167+
assertThat(inputValue)
168+
.isNotNull()
169+
.satisfies(input -> {
170+
assertThat(input.promotion()).isEqualTo(promotion);
171+
assertThat(input.definitionVersion()).isEqualTo(DefinitionVersion.V2);
172+
assertThat(input.importDefinition()).isNull();
173+
assertThat(input.isAccepted()).isTrue();
174+
assertThat(input.auditInfo()).isNull();
175+
});
176+
177+
assertThat(response.getStatus()).isEqualTo(HttpStatusCode.OK_200);
178+
}
179+
}

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
@@ -115,6 +115,7 @@
115115
import io.gravitee.apim.core.portal_page.query_service.PortalPageQueryService;
116116
import io.gravitee.apim.core.promotion.service_provider.CockpitPromotionServiceProvider;
117117
import io.gravitee.apim.core.promotion.use_case.CreatePromotionUseCase;
118+
import io.gravitee.apim.core.promotion.use_case.ProcessPromotionUseCase;
118119
import io.gravitee.apim.core.resource.domain_service.ValidateResourceDomainService;
119120
import io.gravitee.apim.core.sanitizer.HtmlSanitizer;
120121
import io.gravitee.apim.core.shared_policy_group.crud_service.SharedPolicyGroupCrudService;
@@ -899,4 +900,9 @@ public GetMetricFiltersUseCase getMetricFiltersUseCase(AnalyticsDefinition analy
899900
public GetMetricFacetsUseCase getMetricFacetsUseCase(AnalyticsDefinition analyticsDefinition) {
900901
return new GetMetricFacetsUseCase(analyticsDefinition);
901902
}
903+
904+
@Bean
905+
public ProcessPromotionUseCase processPromotionUseCase() {
906+
return mock(ProcessPromotionUseCase.class);
907+
}
902908
}

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
import io.gravitee.apim.core.portal_page.query_service.PortalPageQueryService;
9595
import io.gravitee.apim.core.promotion.service_provider.CockpitPromotionServiceProvider;
9696
import io.gravitee.apim.core.promotion.use_case.CreatePromotionUseCase;
97+
import io.gravitee.apim.core.promotion.use_case.ProcessPromotionUseCase;
9798
import io.gravitee.apim.core.sanitizer.HtmlSanitizer;
9899
import io.gravitee.apim.core.shared_policy_group.crud_service.SharedPolicyGroupCrudService;
99100
import io.gravitee.apim.core.shared_policy_group.use_case.CreateSharedPolicyGroupUseCase;
@@ -1037,4 +1038,9 @@ public GetMetricFiltersUseCase getMetricFiltersUseCase(AnalyticsDefinition analy
10371038
public GetMetricFacetsUseCase getMetricFacetsUseCase(AnalyticsDefinition analyticsDefinition) {
10381039
return new GetMetricFacetsUseCase(analyticsDefinition);
10391040
}
1041+
1042+
@Bean
1043+
public ProcessPromotionUseCase promotionUseCase() {
1044+
return mock(ProcessPromotionUseCase.class);
1045+
}
10401046
}

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import io.gravitee.apim.core.cluster.use_case.members.TransferClusterOwnershipUseCase;
7272
import io.gravitee.apim.core.cluster.use_case.members.UpdateClusterMemberUseCase;
7373
import io.gravitee.apim.core.documentation.domain_service.ValidatePageSourceDomainService;
74+
import io.gravitee.apim.core.environment.crud_service.EnvironmentCrudService;
7475
import io.gravitee.apim.core.group.crud_service.GroupCrudService;
7576
import io.gravitee.apim.core.group.domain_service.ValidateGroupCRDDomainService;
7677
import io.gravitee.apim.core.group.query_service.GroupQueryService;
@@ -93,6 +94,7 @@
9394
import io.gravitee.apim.core.portal_page.query_service.PortalPageQueryService;
9495
import io.gravitee.apim.core.promotion.service_provider.CockpitPromotionServiceProvider;
9596
import io.gravitee.apim.core.promotion.use_case.CreatePromotionUseCase;
97+
import io.gravitee.apim.core.promotion.use_case.ProcessPromotionUseCase;
9698
import io.gravitee.apim.core.sanitizer.HtmlSanitizer;
9799
import io.gravitee.apim.core.shared_policy_group.crud_service.SharedPolicyGroupCrudService;
98100
import io.gravitee.apim.core.shared_policy_group.use_case.CreateSharedPolicyGroupUseCase;
@@ -998,4 +1000,9 @@ public GetMetricFiltersUseCase getMetricFiltersUseCase(AnalyticsDefinition analy
9981000
public GetMetricFacetsUseCase getMetricFacetsUseCase(AnalyticsDefinition analyticsDefinition) {
9991001
return new GetMetricFacetsUseCase(analyticsDefinition);
10001002
}
1003+
1004+
@Bean
1005+
public ProcessPromotionUseCase promotionUseCase() {
1006+
return mock(ProcessPromotionUseCase.class);
1007+
}
10011008
}

0 commit comments

Comments
 (0)