Skip to content

Commit 0332963

Browse files
committed
fix(ENG-12403): show specific error toasts when disconnecting an orcid identity
1 parent 1b295d1 commit 0332963

7 files changed

Lines changed: 97 additions & 5 deletions

File tree

‎src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.spec.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { Store } from '@ngxs/store';
22

33
import { MockComponent, MockProvider } from 'ng-mocks';
44

5+
import { throwError } from 'rxjs';
6+
57
import { Mock } from 'vitest';
68

9+
import { HttpErrorResponse } from '@angular/common/http';
710
import { ComponentFixture, TestBed } from '@angular/core/testing';
811

912
import { ReadonlyInputComponent } from '@osf/shared/components/readonly-input/readonly-input.component';
@@ -95,6 +98,22 @@ describe('ConnectedIdentitiesComponent', () => {
9598
expect(loaderService.hide).toHaveBeenCalled();
9699
});
97100

101+
it.each([
102+
[503, 'settings.accountSettings.connectedIdentities.errors.retryLater'],
103+
[409, 'settings.accountSettings.connectedIdentities.errors.signInViaOrcid'],
104+
[500, 'settings.accountSettings.connectedIdentities.errors.contactSupport'],
105+
])('should show the matching error toast for status %i', (status, messageKey) => {
106+
(store.dispatch as Mock).mockReturnValue(throwError(() => new HttpErrorResponse({ status })));
107+
component.deleteExternalIdentity(mockIdentity);
108+
109+
const { onConfirm } = confirmationService.confirmDelete.mock.calls[0][0];
110+
onConfirm();
111+
112+
expect(toastService.showError).toHaveBeenCalledWith(messageKey);
113+
expect(toastService.showSuccess).not.toHaveBeenCalled();
114+
expect(loaderService.hide).toHaveBeenCalled();
115+
});
116+
98117
it('should not dispatch delete when confirmation is not accepted', () => {
99118
(store.dispatch as Mock).mockClear();
100119
component.deleteExternalIdentity(mockIdentity);

‎src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Card } from 'primeng/card';
66

77
import { finalize } from 'rxjs';
88

9+
import { HttpErrorResponse } from '@angular/common/http';
910
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
1011

1112
import { ReadonlyInputComponent } from '@osf/shared/components/readonly-input/readonly-input.component';
@@ -41,8 +42,24 @@ export class ConnectedIdentitiesComponent {
4142
this.actions
4243
.deleteExternalIdentity(identity.id)
4344
.pipe(finalize(() => this.loaderService.hide()))
44-
.subscribe(() => this.toastService.showSuccess('settings.accountSettings.connectedIdentities.successDelete'));
45+
.subscribe({
46+
next: () => this.toastService.showSuccess('settings.accountSettings.connectedIdentities.successDelete'),
47+
error: (error: HttpErrorResponse) => this.toastService.showError(this.getDeleteErrorKey(error)),
48+
});
4549
},
4650
});
4751
}
52+
53+
private getDeleteErrorKey(error: HttpErrorResponse): string {
54+
const prefix = 'settings.accountSettings.connectedIdentities.errors';
55+
56+
switch (error.status) {
57+
case 503:
58+
return `${prefix}.retryLater`;
59+
case 409:
60+
return `${prefix}.signInViaOrcid`;
61+
default:
62+
return `${prefix}.contactSupport`;
63+
}
64+
}
4865
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { BYPASS_ERROR_INTERCEPTOR } from '@core/interceptors/error-interceptor.tokens';
4+
import { JsonApiService } from '@osf/shared/services/json-api.service';
5+
6+
import { MOCK_API_URL, provideMockEnvironment } from '@testing/providers/environment.provider.mock';
7+
import { JsonApiServiceMock, JsonApiServiceMockType } from '@testing/providers/json-api.service.mock';
8+
9+
import { AccountSettingsService } from './account-settings.service';
10+
11+
describe('AccountSettingsService', () => {
12+
let service: AccountSettingsService;
13+
let jsonApiService: JsonApiServiceMockType;
14+
15+
beforeEach(() => {
16+
jsonApiService = JsonApiServiceMock.simple();
17+
18+
TestBed.configureTestingModule({
19+
providers: [
20+
AccountSettingsService,
21+
provideMockEnvironment(),
22+
{ provide: JsonApiService, useValue: jsonApiService },
23+
],
24+
});
25+
26+
service = TestBed.inject(AccountSettingsService);
27+
});
28+
29+
it('should delete an external identity with the error interceptor bypassed', () => {
30+
service.deleteExternalIdentity('orcid').subscribe();
31+
32+
expect(jsonApiService.delete).toHaveBeenCalledTimes(1);
33+
const [url, , , context] = jsonApiService.delete.mock.calls[0];
34+
expect(url).toBe(`${MOCK_API_URL}/users/me/settings/identities/orcid/`);
35+
expect(context?.get(BYPASS_ERROR_INTERCEPTOR)).toBe(true);
36+
});
37+
});

‎src/app/features/settings/account-settings/services/account-settings.service.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { map, Observable } from 'rxjs';
22

3+
import { HttpContext } from '@angular/common/http';
34
import { inject, Injectable } from '@angular/core';
45

6+
import { BYPASS_ERROR_INTERCEPTOR } from '@core/interceptors/error-interceptor.tokens';
57
import { ENVIRONMENT } from '@core/provider/environment.provider';
68
import { UserMapper } from '@osf/shared/mappers/user';
79
import { UserModel } from '@osf/shared/models/user/user.model';
@@ -113,6 +115,13 @@ export class AccountSettingsService {
113115
}
114116

115117
deleteExternalIdentity(id: string): Observable<void> {
116-
return this.jsonApiService.delete(`${this.apiUrl}/users/me/settings/identities/${id}/`);
118+
const context = new HttpContext().set(BYPASS_ERROR_INTERCEPTOR, true);
119+
120+
return this.jsonApiService.delete(
121+
`${this.apiUrl}/users/me/settings/identities/${id}/`,
122+
undefined,
123+
undefined,
124+
context
125+
);
117126
}
118127
}

‎src/app/shared/services/json-api.service.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class JsonApiService {
8080
});
8181
}
8282

83-
delete(url: string, body?: unknown, headers?: Record<string, string>): Observable<void> {
84-
return this.http.delete<void>(url, { body, headers });
83+
delete(url: string, body?: unknown, headers?: Record<string, string>, context?: HttpContext): Observable<void> {
84+
return this.http.delete<void>(url, { body, headers, context });
8585
}
8686
}

‎src/assets/i18n/en.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2552,6 +2552,11 @@
25522552
"message": "Are you sure you want to delete <b>{{name}}</b> identity?"
25532553
},
25542554
"description": "Connected identities allow you to log in to the OSF via a third-party service. You can revoke these identifies.",
2555+
"errors": {
2556+
"contactSupport": "Unable to disconnect this identity. Please contact support if the problem persists.",
2557+
"retryLater": "Unable to revoke ORCiD access at this time. Please try again later.",
2558+
"signInViaOrcid": "We couldn't find a stored ORCiD authorization for this identity. Please sign in via ORCiD and try again."
2559+
},
25552560
"noIdentities": "You have not authorized any external services to log in to the OSF.",
25562561
"successDelete": "Successfully deleted connected identities.",
25572562
"title": "Connected identities"

‎src/testing/providers/json-api.service.mock.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ type PatchFn = (
2727
) => Observable<unknown>;
2828
type PutFn = (url: string, body: unknown, params?: Record<string, unknown>) => Observable<unknown>;
2929
type PutFileFn = (url: string, file: File, params?: Record<string, string>) => Observable<HttpEvent<unknown>>;
30-
type DeleteFn = (url: string, body?: unknown, headers?: Record<string, string>) => Observable<void>;
30+
type DeleteFn = (
31+
url: string,
32+
body?: unknown,
33+
headers?: Record<string, string>,
34+
context?: HttpContext
35+
) => Observable<void>;
3136

3237
export type JsonApiServiceMockType = Pick<JsonApiService, 'get' | 'post' | 'patch' | 'put' | 'putFile' | 'delete'> & {
3338
get: Mock<GetFn>;

0 commit comments

Comments
 (0)