Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/app/core/provider/sentry.provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { provideOSFCore } from '@testing/osf.testing.provider';

import { SENTRY_PROVIDER, SENTRY_TOKEN } from './sentry.provider';

import * as Sentry from '@sentry/angular';

describe('Provider: Sentry', () => {
beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -15,7 +13,7 @@ describe('Provider: Sentry', () => {

it('should provide the Sentry module via the injection token', () => {
const provided = TestBed.inject(SENTRY_TOKEN);
expect(provided).toBe(Sentry);
expect(provided).toBe(SENTRY_PROVIDER.useValue);
expect(typeof provided.captureException).toBe('function');
});
});
Comment thread
nsemets marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,37 +1,26 @@
import { Store } from '@ngxs/store';

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

import { Mock } from 'vitest';

import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ReadonlyInputComponent } from '@osf/shared/components/readonly-input/readonly-input.component';
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
import { LoaderService } from '@osf/shared/services/loader.service';
import { ToastService } from '@osf/shared/services/toast.service';

import { provideOSFCore } from '@testing/osf.testing.provider';
import {
CustomConfirmationServiceMock,
CustomConfirmationServiceMockType,
} from '@testing/providers/custom-confirmation-provider.mock';
import { LoaderServiceMock } from '@testing/providers/loader-service.mock';
ExternalIdentityDeletionServiceMock,
ExternalIdentityDeletionServiceMockType,
} from '@testing/providers/external-identity-deletion-service.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';
import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock';

import { ExternalIdentity } from '../../models';
import { AccountSettingsSelectors, DeleteExternalIdentity } from '../../store';
import { ExternalIdentityDeletionService } from '../../services';
import { AccountSettingsSelectors } from '../../store';

import { ConnectedIdentitiesComponent } from './connected-identities.component';

describe('ConnectedIdentitiesComponent', () => {
let component: ConnectedIdentitiesComponent;
let fixture: ComponentFixture<ConnectedIdentitiesComponent>;
let store: Store;
let loaderService: LoaderServiceMock;
let confirmationService: CustomConfirmationServiceMockType;
let toastService: ToastServiceMockType;
let externalIdentityDeletion: ExternalIdentityDeletionServiceMockType;

const mockIdentity: ExternalIdentity = {
id: 'orcid',
Expand All @@ -40,24 +29,19 @@ describe('ConnectedIdentitiesComponent', () => {
};

beforeEach(() => {
loaderService = new LoaderServiceMock();
confirmationService = CustomConfirmationServiceMock.simple();
toastService = ToastServiceMock.simple();
externalIdentityDeletion = ExternalIdentityDeletionServiceMock.simple();

TestBed.configureTestingModule({
imports: [ConnectedIdentitiesComponent, MockComponent(ReadonlyInputComponent)],
providers: [
provideOSFCore(),
MockProvider(LoaderService, loaderService),
MockProvider(CustomConfirmationService, confirmationService),
MockProvider(ToastService, toastService),
MockProvider(ExternalIdentityDeletionService, externalIdentityDeletion),
provideMockStore({
signals: [{ selector: AccountSettingsSelectors.getExternalIdentities, value: [mockIdentity] }],
}),
],
});

store = TestBed.inject(Store);
fixture = TestBed.createComponent(ConnectedIdentitiesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
Expand All @@ -71,36 +55,9 @@ describe('ConnectedIdentitiesComponent', () => {
expect(component.externalIdentities()).toEqual([mockIdentity]);
});

it('should open delete confirmation with expected payload', () => {
component.deleteExternalIdentity(mockIdentity);

expect(confirmationService.confirmDelete).toHaveBeenCalledWith({
headerKey: 'settings.accountSettings.connectedIdentities.deleteDialog.header',
messageParams: { name: mockIdentity.id },
messageKey: 'settings.accountSettings.connectedIdentities.deleteDialog.message',
onConfirm: expect.any(Function),
});
});

it('should dispatch delete and show success when confirmation is accepted', () => {
(store.dispatch as Mock).mockClear();
component.deleteExternalIdentity(mockIdentity);

const { onConfirm } = confirmationService.confirmDelete.mock.calls[0][0];
onConfirm();

expect(loaderService.show).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(new DeleteExternalIdentity(mockIdentity.id));
expect(toastService.showSuccess).toHaveBeenCalledWith('settings.accountSettings.connectedIdentities.successDelete');
expect(loaderService.hide).toHaveBeenCalled();
});

it('should not dispatch delete when confirmation is not accepted', () => {
(store.dispatch as Mock).mockClear();
it('should delegate deletion of the identity to the deletion service', () => {
component.deleteExternalIdentity(mockIdentity);

expect(loaderService.show).not.toHaveBeenCalled();
expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(DeleteExternalIdentity));
expect(toastService.showSuccess).not.toHaveBeenCalled();
expect(externalIdentityDeletion.confirmAndDelete).toHaveBeenCalledWith(mockIdentity.id);
});
});
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { createDispatchMap, select } from '@ngxs/store';
import { select } from '@ngxs/store';

import { TranslatePipe } from '@ngx-translate/core';

import { Card } from 'primeng/card';

import { finalize } from 'rxjs';

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { ReadonlyInputComponent } from '@osf/shared/components/readonly-input/readonly-input.component';
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
import { LoaderService } from '@osf/shared/services/loader.service';
import { ToastService } from '@osf/shared/services/toast.service';

import { ExternalIdentity } from '../../models';
import { AccountSettingsSelectors, DeleteExternalIdentity } from '../../store';
import { ExternalIdentityDeletionService } from '../../services';
import { AccountSettingsSelectors } from '../../store';

@Component({
selector: 'osf-connected-identities',
Expand All @@ -24,25 +20,11 @@ import { AccountSettingsSelectors, DeleteExternalIdentity } from '../../store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConnectedIdentitiesComponent {
private readonly customConfirmationService = inject(CustomConfirmationService);
private readonly loaderService = inject(LoaderService);
private readonly toastService = inject(ToastService);
private readonly externalIdentityDeletion = inject(ExternalIdentityDeletionService);

readonly actions = createDispatchMap({ deleteExternalIdentity: DeleteExternalIdentity });
readonly externalIdentities = select(AccountSettingsSelectors.getExternalIdentities);

deleteExternalIdentity(identity: ExternalIdentity): void {
this.customConfirmationService.confirmDelete({
headerKey: 'settings.accountSettings.connectedIdentities.deleteDialog.header',
messageParams: { name: identity.id },
messageKey: 'settings.accountSettings.connectedIdentities.deleteDialog.message',
onConfirm: () => {
this.loaderService.show();
this.actions
.deleteExternalIdentity(identity.id)
.pipe(finalize(() => this.loaderService.hide()))
.subscribe(() => this.toastService.showSuccess('settings.accountSettings.connectedIdentities.successDelete'));
},
});
this.externalIdentityDeletion.confirmAndDelete(identity.id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TestBed } from '@angular/core/testing';

import { BYPASS_ERROR_INTERCEPTOR } from '@core/interceptors/error-interceptor.tokens';
import { JsonApiService } from '@osf/shared/services/json-api.service';

import { MOCK_API_URL, provideMockEnvironment } from '@testing/providers/environment.provider.mock';
import { JsonApiServiceMock, JsonApiServiceMockType } from '@testing/providers/json-api.service.mock';

import { AccountSettingsService } from './account-settings.service';

describe('AccountSettingsService', () => {
let service: AccountSettingsService;
let jsonApiService: JsonApiServiceMockType;

beforeEach(() => {
jsonApiService = JsonApiServiceMock.simple();

TestBed.configureTestingModule({
providers: [
AccountSettingsService,
provideMockEnvironment(),
{ provide: JsonApiService, useValue: jsonApiService },
],
});

service = TestBed.inject(AccountSettingsService);
});

it('should delete an external identity with the error interceptor bypassed', () => {
service.deleteExternalIdentity('orcid').subscribe();

expect(jsonApiService.delete).toHaveBeenCalledTimes(1);
const [url, , , context] = jsonApiService.delete.mock.calls[0];
expect(url).toBe(`${MOCK_API_URL}/users/me/settings/identities/orcid/`);
expect(context?.get(BYPASS_ERROR_INTERCEPTOR)).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { map, Observable } from 'rxjs';

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

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

deleteExternalIdentity(id: string): Observable<void> {
return this.jsonApiService.delete(`${this.apiUrl}/users/me/settings/identities/${id}/`);
const context = new HttpContext().set(BYPASS_ERROR_INTERCEPTOR, true);

return this.jsonApiService.delete(
`${this.apiUrl}/users/me/settings/identities/${id}/`,
undefined,
undefined,
context
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Store } from '@ngxs/store';

import { MockProvider } from 'ng-mocks';

import { throwError } from 'rxjs';

import { Mock } from 'vitest';

import { HttpErrorResponse } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';

import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
import { LoaderService } from '@osf/shared/services/loader.service';
import { ToastService } from '@osf/shared/services/toast.service';

import { provideOSFCore } from '@testing/osf.testing.provider';
import {
CustomConfirmationServiceMock,
CustomConfirmationServiceMockType,
} from '@testing/providers/custom-confirmation-provider.mock';
import { LoaderServiceMock } from '@testing/providers/loader-service.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';
import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock';

import { DeleteExternalIdentity } from '../store';

import { ExternalIdentityDeletionService } from './external-identity-deletion.service';

describe('ExternalIdentityDeletionService', () => {
let service: ExternalIdentityDeletionService;
let store: Store;
let loaderService: LoaderServiceMock;
let confirmationService: CustomConfirmationServiceMockType;
let toastService: ToastServiceMockType;

const identityId = 'ORCID';

const confirm = () => {
const { onConfirm } = confirmationService.confirmDelete.mock.calls[0][0];
onConfirm();
};

beforeEach(() => {
loaderService = new LoaderServiceMock();
confirmationService = CustomConfirmationServiceMock.simple();
toastService = ToastServiceMock.simple();

TestBed.configureTestingModule({
providers: [
provideOSFCore(),
MockProvider(LoaderService, loaderService),
MockProvider(CustomConfirmationService, confirmationService),
MockProvider(ToastService, toastService),
provideMockStore(),
],
});

store = TestBed.inject(Store);
service = TestBed.inject(ExternalIdentityDeletionService);
});

it('should open delete confirmation with expected payload', () => {
service.confirmAndDelete(identityId);

expect(confirmationService.confirmDelete).toHaveBeenCalledWith({
headerKey: 'settings.accountSettings.connectedIdentities.deleteDialog.header',
messageParams: { name: identityId },
messageKey: 'settings.accountSettings.connectedIdentities.deleteDialog.message',
onConfirm: expect.any(Function),
});
});

it('should not dispatch delete until confirmation is accepted', () => {
(store.dispatch as Mock).mockClear();
service.confirmAndDelete(identityId);

expect(loaderService.show).not.toHaveBeenCalled();
expect(store.dispatch).not.toHaveBeenCalled();
expect(toastService.showSuccess).not.toHaveBeenCalled();
});

it('should dispatch delete and show success when confirmation is accepted', () => {
(store.dispatch as Mock).mockClear();
service.confirmAndDelete(identityId);
confirm();

expect(loaderService.show).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(new DeleteExternalIdentity(identityId));
expect(toastService.showSuccess).toHaveBeenCalledWith('settings.accountSettings.connectedIdentities.successDelete');
expect(toastService.showError).not.toHaveBeenCalled();
expect(loaderService.hide).toHaveBeenCalled();
});

it.each([
[503, 'settings.accountSettings.connectedIdentities.errors.retryLater'],
[409, 'settings.accountSettings.connectedIdentities.errors.signInViaOrcid'],
[500, 'settings.accountSettings.connectedIdentities.errors.contactSupport'],
[404, 'settings.accountSettings.connectedIdentities.errors.contactSupport'],
])('should show the matching error toast for status %i', (status, messageKey) => {
(store.dispatch as Mock).mockReturnValue(throwError(() => new HttpErrorResponse({ status })));
service.confirmAndDelete(identityId);
confirm();

expect(toastService.showError).toHaveBeenCalledWith(messageKey);
expect(toastService.showSuccess).not.toHaveBeenCalled();
expect(loaderService.hide).toHaveBeenCalled();
});
});
Loading
Loading