Skip to content

Commit

Permalink
feat(console) resume endpoint for failed customer status
Browse files Browse the repository at this point in the history
(cherry picked from commit c382745)

# Conflicts:
#	gravitee-apim-console-webui/src/management/api/subscriptions/edit/api-subscription-edit.component.html
  • Loading branch information
skorda42 authored and mergify[bot] committed Dec 18, 2024
1 parent 507d173 commit 57474e2
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,15 @@
<mat-icon svgIcon="gio:calendar"></mat-icon>
Change end date
</button>
<<<<<<< HEAD
<button mat-raised-button color="warn" (click)="closeSubscription()" [disabled]="subscription.origin === 'KUBERNETES'">
=======
<button mat-stroked-button (click)="resumeFailureSubscription()" *ngIf="subscription.consumerStatus === 'FAILURE'">
<mat-icon svgIcon="gio:play-circle"></mat-icon>
Resume from failure
</button>
<button mat-raised-button color="warn" (click)="closeSubscription()">
>>>>>>> c382745a10 (feat(console) resume endpoint for failed customer status)
<mat-icon svgIcon="gio:x-circle"></mat-icon>
Close
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,36 @@ export class ApiSubscriptionEditComponent implements OnInit {
);
}

resumeFailureSubscription() {
this.matDialog
.open<GioConfirmDialogComponent, GioConfirmDialogData>(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<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,18 @@ export class ApiSubscriptionEditHarness extends ComponentHarness {
return this.btnIsVisible('Resume');
}

public async resumeFailureBtnIsVisible(): Promise<boolean> {
return this.btnIsVisible('Resume from failure');
}

public async openResumeDialog(): Promise<void> {
return this.getBtnByText('Resume').then((btn) => btn.click());
}

public async openResumeFailureDialog(): Promise<void> {
return this.getBtnByText('Resume from failure').then((btn) => btn.click());
}

public async changeEndDateBtnIsVisible(): Promise<boolean> {
return this.btnIsVisible('Change end date');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export class ApiSubscriptionV2Service {
resume(subscriptionId: string, apiId: string): Observable<Subscription> {
return this.http.post<Subscription>(`${this.constants.env.v2BaseURL}/apis/${apiId}/subscriptions/${subscriptionId}/_resume`, {});
}
resumeFailure(subscriptionId: string, apiId: string): Observable<Subscription> {
return this.http.post<Subscription>(`${this.constants.env.v2BaseURL}/apis/${apiId}/subscriptions/${subscriptionId}/_resumeFailure`, {});
}

create(apiId: string, createSubscription: CreateSubscription): Observable<Subscription> {
return this.http.post<Subscription>(`${this.constants.env.v2BaseURL}/apis/${apiId}/subscriptions`, createSubscription);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand All @@ -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 -> {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 57474e2

Please sign in to comment.