diff --git a/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.html b/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.html
index 1875d12c41d..35da64264a1 100644
--- a/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.html
+++ b/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.html
@@ -139,7 +139,15 @@
Change end date
+<<<<<<< HEAD
+=======
+
+
+ Resume from failure
+
+
+>>>>>>> c382745a10 (feat(console) resume endpoint for failed customer status)
Close
diff --git a/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.spec.ts b/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.spec.ts
index df43ef6011f..26641a97da1 100644
--- a/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.spec.ts
+++ b/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.spec.ts
@@ -408,6 +408,35 @@ describe('ApiSubscriptionEditComponent', () => {
});
});
+ describe('resume failed subscription', () => {
+ const failedSubscription = BASIC_SUBSCRIPTION();
+ failedSubscription.consumerStatus = 'FAILURE';
+
+ it('should resume failed subscription', async () => {
+ await initComponent({ subscription: failedSubscription });
+ expectApiKeyListGet();
+
+ const harness = await loader.getHarness(ApiSubscriptionEditHarness);
+ expect(await harness.resumeFailureBtnIsVisible()).toEqual(true);
+
+ await harness.openResumeFailureDialog();
+
+ const resumeDialog = await TestbedHarnessEnvironment.documentRootLoader(fixture).getHarness(
+ MatDialogHarness.with({ selector: '#confirmResumeFailureSubscriptionDialog' }),
+ );
+ expect(await resumeDialog.getTitleText()).toEqual('Resume your failure subscription');
+
+ const resumeBtn = await resumeDialog.getHarness(MatButtonHarness.with({ text: 'Resume' }));
+ expect(await resumeBtn.isDisabled()).toEqual(false);
+ await resumeBtn.click();
+
+ expectApiSubscriptionFailureResume(SUBSCRIPTION_ID, BASIC_SUBSCRIPTION());
+ expectApiSubscriptionGet(BASIC_SUBSCRIPTION());
+ expectApiKeyListGet();
+ expectApiGet();
+ });
+ });
+
describe('resume subscription', () => {
const pausedSubscription = BASIC_SUBSCRIPTION();
pausedSubscription.status = 'PAUSED';
@@ -1277,6 +1306,15 @@ describe('ApiSubscriptionEditComponent', () => {
req.flush(subscription);
}
+ function expectApiSubscriptionFailureResume(subscriptionId: string, subscription: Subscription): void {
+ const req = httpTestingController.expectOne({
+ url: `${CONSTANTS_TESTING.env.v2BaseURL}/apis/${API_ID}/subscriptions/${subscriptionId}/_resumeFailure`,
+ method: 'POST',
+ });
+ expect(req.request.body).toEqual({});
+ req.flush(subscription);
+ }
+
function expectApiSubscriptionClose(subscriptionId: string, subscription: Subscription): void {
const req = httpTestingController.expectOne({
url: `${CONSTANTS_TESTING.env.v2BaseURL}/apis/${API_ID}/subscriptions/${subscriptionId}/_close`,
diff --git a/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.ts b/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.ts
index d95d4730a5d..dbe03d3b3da 100644
--- a/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.ts
+++ b/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.ts
@@ -372,6 +372,36 @@ export class ApiSubscriptionEditComponent implements OnInit {
);
}
+ resumeFailureSubscription() {
+ this.matDialog
+ .open(GioConfirmDialogComponent, {
+ data: {
+ title: `Resume your failure subscription`,
+ content: 'The application will be able to consume your API.',
+ confirmButton: 'Resume',
+ },
+ role: 'alertdialog',
+ id: 'confirmResumeFailureSubscriptionDialog',
+ })
+ .afterClosed()
+ .pipe(
+ switchMap((confirm) => {
+ if (confirm) {
+ return this.apiSubscriptionService.resumeFailure(this.subscription.id, this.apiId);
+ }
+ return EMPTY;
+ }),
+ takeUntil(this.unsubscribe$),
+ )
+ .subscribe(
+ (_) => {
+ this.snackBarService.success(`Subscription resumed`);
+ this.ngOnInit();
+ },
+ (err) => this.snackBarService.error(err.message),
+ );
+ }
+
changeEndDate() {
this.matDialog
.open<
diff --git a/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.harness.ts b/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.harness.ts
index 53dde648451..8be636a2a26 100644
--- a/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.harness.ts
+++ b/gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.harness.ts
@@ -120,10 +120,18 @@ export class ApiSubscriptionEditHarness extends ComponentHarness {
return this.btnIsVisible('Resume');
}
+ public async resumeFailureBtnIsVisible(): Promise {
+ return this.btnIsVisible('Resume from failure');
+ }
+
public async openResumeDialog(): Promise {
return this.getBtnByText('Resume').then((btn) => btn.click());
}
+ public async openResumeFailureDialog(): Promise {
+ return this.getBtnByText('Resume from failure').then((btn) => btn.click());
+ }
+
public async changeEndDateBtnIsVisible(): Promise {
return this.btnIsVisible('Change end date');
}
diff --git a/gravitee-apim-console-webui/src/services-ngx/api-subscription-v2.service.ts b/gravitee-apim-console-webui/src/services-ngx/api-subscription-v2.service.ts
index c2f970597ac..bced4ee3584 100644
--- a/gravitee-apim-console-webui/src/services-ngx/api-subscription-v2.service.ts
+++ b/gravitee-apim-console-webui/src/services-ngx/api-subscription-v2.service.ts
@@ -104,6 +104,9 @@ export class ApiSubscriptionV2Service {
resume(subscriptionId: string, apiId: string): Observable {
return this.http.post(`${this.constants.env.v2BaseURL}/apis/${apiId}/subscriptions/${subscriptionId}/_resume`, {});
}
+ resumeFailure(subscriptionId: string, apiId: string): Observable {
+ return this.http.post(`${this.constants.env.v2BaseURL}/apis/${apiId}/subscriptions/${subscriptionId}/_resumeFailure`, {});
+ }
create(apiId: string, createSubscription: CreateSubscription): Observable {
return this.http.post(`${this.constants.env.v2BaseURL}/apis/${apiId}/subscriptions`, createSubscription);
diff --git a/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/api/ApiSubscriptionsResource.java b/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/api/ApiSubscriptionsResource.java
index bec560c4846..eaaf7d95e89 100644
--- a/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/api/ApiSubscriptionsResource.java
+++ b/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/api/ApiSubscriptionsResource.java
@@ -57,6 +57,10 @@
import io.gravitee.rest.api.service.common.GraviteeContext;
import io.gravitee.rest.api.service.exceptions.InvalidApplicationApiKeyModeException;
import io.gravitee.rest.api.service.v4.PlanSearchService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
@@ -221,6 +225,28 @@ public Response exportApiSubscriptions(
.build();
}
+ @POST
+ @Path("/{subscriptionId}/_resumeFailure")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(
+ summary = "Resume the failed subscription",
+ description = "User must have the APPLICATION_SUBSCRIPTION[UPDATE] permission to use this service"
+ )
+ @ApiResponse(
+ responseCode = "200",
+ description = "Subscription successfully resumed",
+ content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Subscription.class))
+ )
+ @ApiResponse(responseCode = "400", description = "Status changes not authorized")
+ @ApiResponse(responseCode = "404", description = "API subscription does not exist")
+ @ApiResponse(responseCode = "500", description = "Internal server error")
+ @Permissions({ @Permission(value = RolePermission.API_SUBSCRIPTION, acls = { RolePermissionAction.UPDATE }) })
+ public Response resumeFailedSubscription(@PathParam("subscriptionId") String subscriptionId) {
+ final ExecutionContext executionContext = GraviteeContext.getExecutionContext();
+ SubscriptionEntity updatedSubscriptionEntity = subscriptionService.resumeFailed(executionContext, subscriptionId);
+ return Response.ok(subscriptionMapper.map(updatedSubscriptionEntity)).build();
+ }
+
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/SubscriptionService.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/SubscriptionService.java
index f6e0d439a94..796446bc7a8 100644
--- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/SubscriptionService.java
+++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/SubscriptionService.java
@@ -81,6 +81,7 @@ SubscriptionEntity update(
SubscriptionEntity pause(ExecutionContext executionContext, String subscription);
SubscriptionEntity resumeConsumer(ExecutionContext executionContext, String subscriptionId);
+ SubscriptionEntity resumeFailed(ExecutionContext executionContext, String subscriptionId);
SubscriptionEntity resume(ExecutionContext executionContext, String subscription);
diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/exceptions/SubscriptionFailureCustomerStatusRequiredException.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/exceptions/SubscriptionFailureCustomerStatusRequiredException.java
new file mode 100644
index 00000000000..e79cf7d661b
--- /dev/null
+++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/exceptions/SubscriptionFailureCustomerStatusRequiredException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.rest.api.service.exceptions;
+
+public class SubscriptionFailureCustomerStatusRequiredException extends RuntimeException {
+
+ public SubscriptionFailureCustomerStatusRequiredException() {
+ super("Subscription FAILURE customer status required to resume subscription");
+ }
+}
diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/SubscriptionServiceImpl.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/SubscriptionServiceImpl.java
index a0cca71e8e1..82f34a315a3 100644
--- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/SubscriptionServiceImpl.java
+++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/SubscriptionServiceImpl.java
@@ -110,6 +110,7 @@
import io.gravitee.rest.api.service.exceptions.PlanOAuth2OrJWTAlreadySubscribedException;
import io.gravitee.rest.api.service.exceptions.PlanRestrictedException;
import io.gravitee.rest.api.service.exceptions.SubscriptionConsumerStatusNotUpdatableException;
+import io.gravitee.rest.api.service.exceptions.SubscriptionFailureCustomerStatusRequiredException;
import io.gravitee.rest.api.service.exceptions.SubscriptionFailureException;
import io.gravitee.rest.api.service.exceptions.SubscriptionMismatchEnvironmentException;
import io.gravitee.rest.api.service.exceptions.SubscriptionNotClosedException;
@@ -1033,31 +1034,38 @@ public SubscriptionEntity resumeConsumer(final ExecutionContext executionContext
validateConsumerStatus(subscription, genericApiModel);
if (subscription.canBeStartedByConsumer()) {
- Subscription previousSubscription = new Subscription(subscription);
- final Date now = new Date();
- subscription.setUpdatedAt(now);
- subscription.setConsumerPausedAt(null);
- subscription.setConsumerStatus(Subscription.ConsumerStatus.STARTED);
+ return resumeSubscription(executionContext, subscription, apiId);
+ }
- subscription = subscriptionRepository.update(subscription);
+ throw new SubscriptionNotPausedException(subscription);
+ } catch (TechnicalException ex) {
+ throw new TechnicalManagementException(
+ String.format("An error occurs while trying to resume subscription %s", subscriptionId),
+ ex
+ );
+ }
+ }
- createAudit(
- executionContext,
- apiId,
- subscription.getApplication(),
- SUBSCRIPTION_RESUMED_BY_CONSUMER,
- subscription.getUpdatedAt(),
- previousSubscription,
- subscription
- );
+ @Override
+ public SubscriptionEntity resumeFailed(final ExecutionContext executionContext, String subscriptionId) {
+ try {
+ logger.debug("Resume failed subscription by {} by consumer", subscriptionId);
- // active API Keys are automatically unpause
- resumeApiKeys(executionContext, subscription);
+ Subscription subscription = subscriptionRepository
+ .findById(subscriptionId)
+ .orElseThrow(() -> new SubscriptionNotFoundException(subscriptionId));
- return convert(subscription);
+ Subscription.ConsumerStatus consumerStatus = subscription.getConsumerStatus();
+
+ if (!Subscription.ConsumerStatus.FAILURE.equals(consumerStatus)) {
+ throw new SubscriptionFailureCustomerStatusRequiredException();
}
- throw new SubscriptionNotPausedException(subscription);
+ String apiId = subscription.getApi();
+ final GenericApiModel genericApiModel = apiTemplateService.findByIdForTemplates(executionContext, apiId);
+ checkApiDefinitionVersion(subscription, genericApiModel);
+
+ return resumeSubscription(executionContext, subscription, apiId);
} catch (TechnicalException ex) {
throw new TechnicalManagementException(
String.format("An error occurs while trying to resume subscription %s", subscriptionId),
@@ -1066,6 +1074,32 @@ public SubscriptionEntity resumeConsumer(final ExecutionContext executionContext
}
}
+ private SubscriptionEntity resumeSubscription(ExecutionContext executionContext, Subscription subscription, String apiId)
+ throws TechnicalException {
+ Subscription previousSubscription = new Subscription(subscription);
+ final Date now = new Date();
+ subscription.setUpdatedAt(now);
+ subscription.setConsumerPausedAt(null);
+ subscription.setConsumerStatus(Subscription.ConsumerStatus.STARTED);
+
+ subscription = subscriptionRepository.update(subscription);
+
+ createAudit(
+ executionContext,
+ apiId,
+ subscription.getApplication(),
+ SUBSCRIPTION_RESUMED_BY_CONSUMER,
+ subscription.getUpdatedAt(),
+ previousSubscription,
+ subscription
+ );
+
+ // active API Keys are automatically unpause
+ resumeApiKeys(executionContext, subscription);
+
+ return convert(subscription);
+ }
+
private void resumeApiKeys(ExecutionContext executionContext, Subscription subscription) {
streamActiveApiKeys(executionContext, subscription.getId())
.forEach(apiKey -> {
@@ -1078,6 +1112,10 @@ private static void validateConsumerStatus(Subscription subscription, GenericApi
if (subscription.getConsumerStatus() == Subscription.ConsumerStatus.FAILURE) {
throw new SubscriptionFailureException(subscription);
}
+ checkApiDefinitionVersion(subscription, genericApiModel);
+ }
+
+ private static void checkApiDefinitionVersion(Subscription subscription, GenericApiModel genericApiModel) {
if (!DefinitionVersion.V4.equals(genericApiModel.getDefinitionVersion())) {
throw new SubscriptionConsumerStatusNotUpdatableException(
subscription,
diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/impl/SubscriptionServiceTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/impl/SubscriptionServiceTest.java
index f05ce7db66f..2716cbf950a 100644
--- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/impl/SubscriptionServiceTest.java
+++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/impl/SubscriptionServiceTest.java
@@ -109,6 +109,7 @@
import io.gravitee.rest.api.service.exceptions.PlanNotYetPublishedException;
import io.gravitee.rest.api.service.exceptions.PlanRestrictedException;
import io.gravitee.rest.api.service.exceptions.SubscriptionConsumerStatusNotUpdatableException;
+import io.gravitee.rest.api.service.exceptions.SubscriptionFailureCustomerStatusRequiredException;
import io.gravitee.rest.api.service.exceptions.SubscriptionFailureException;
import io.gravitee.rest.api.service.exceptions.SubscriptionNotFoundException;
import io.gravitee.rest.api.service.exceptions.SubscriptionNotPausableException;
@@ -2207,6 +2208,64 @@ public void shouldResumeByConsumer() throws Exception {
);
}
+ @Test
+ public void shouldResumeFailureByConsumer() throws Exception {
+ Subscription subscription = buildTestSubscription(ACCEPTED);
+ subscription.setConsumerStatus(Subscription.ConsumerStatus.FAILURE);
+ subscription.setApi(API_ID);
+
+ when(subscriptionRepository.findById(SUBSCRIPTION_ID)).thenReturn(Optional.of(subscription));
+ io.gravitee.rest.api.model.v4.api.ApiModel apiModel = mock(io.gravitee.rest.api.model.v4.api.ApiModel.class);
+ when(apiTemplateService.findByIdForTemplates(GraviteeContext.getExecutionContext(), API_ID)).thenReturn(apiModel);
+ when(apiModel.getDefinitionVersion()).thenReturn(DefinitionVersion.V4);
+ when(apiModel.getListeners()).thenReturn(List.of(new SubscriptionListener()));
+ when(subscriptionRepository.update(subscription)).thenReturn(subscription);
+ final ApiKeyEntity apiKey = buildTestApiKey(subscription.getId(), false, false);
+ when(apiKeyService.findBySubscription(any(), any())).thenReturn(List.of(apiKey));
+
+ subscriptionService.resumeFailed(GraviteeContext.getExecutionContext(), SUBSCRIPTION_ID);
+
+ assertThat(subscription.getConsumerPausedAt()).isNull();
+ assertThat(subscription.getConsumerStatus()).isEqualTo(Subscription.ConsumerStatus.STARTED);
+ verify(apiKeyService).update(GraviteeContext.getExecutionContext(), apiKey);
+ verify(auditService)
+ .createApiAuditLog(
+ eq(GraviteeContext.getExecutionContext()),
+ eq(API_ID),
+ anyMap(),
+ eq(Subscription.AuditEvent.SUBSCRIPTION_RESUMED_BY_CONSUMER),
+ any(),
+ any(),
+ any()
+ );
+ verify(auditService)
+ .createApplicationAuditLog(
+ eq(GraviteeContext.getExecutionContext()),
+ eq(APPLICATION_ID),
+ anyMap(),
+ eq(Subscription.AuditEvent.SUBSCRIPTION_RESUMED_BY_CONSUMER),
+ any(),
+ any(),
+ any()
+ );
+ }
+
+ @Test(expected = SubscriptionFailureCustomerStatusRequiredException.class)
+ public void shouldNotResumeFailureByConsumerBecauseApiDefinitionNotV4() throws Exception {
+ Subscription subscription = buildTestSubscription(ACCEPTED);
+ when(subscriptionRepository.findById(SUBSCRIPTION_ID)).thenReturn(Optional.of(subscription));
+
+ subscriptionService.resumeFailed(GraviteeContext.getExecutionContext(), SUBSCRIPTION_ID);
+ }
+
+ @Test(expected = SubscriptionNotFoundException.class)
+ public void shouldNotResumeFailureByConsumerBecauseDoesNoExist() throws Exception {
+ // Stub
+ when(subscriptionRepository.findById(SUBSCRIPTION_ID)).thenReturn(Optional.empty());
+
+ subscriptionService.resumeFailed(GraviteeContext.getExecutionContext(), SUBSCRIPTION_ID);
+ }
+
@Test
public void update_should_not_override_clientId_if_not_present() throws Exception {
testUpdateSubscriptionDependingOnClientIdSituation(null, "client-id", null);