import { ComponentFixture, TestBed, fakeAsync, tick, } from '@angular/core/testing' import { ProfileEditDialogComponent } from './profile-edit-dialog.component' import { ProfileService } from 'src/app/services/profile.service' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { NgbAccordionModule, NgbActiveModal, NgbModalModule, NgbPopoverModule, } from '@ng-bootstrap/ng-bootstrap' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { TextComponent } from '../input/text/text.component' import { PasswordComponent } from '../input/password/password.component' import { of, throwError } from 'rxjs' import { ToastService } from 'src/app/services/toast.service' import { Clipboard } from '@angular/cdk/clipboard' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component' const socialAccount = { id: 1, provider: 'test_provider', name: 'Test Provider', } const profile = { email: 'foo@bar.com', password: '*********', first_name: 'foo', last_name: 'bar', auth_token: '123456789abcdef', social_accounts: [socialAccount], } const socialAccountProviders = [ { name: 'Test Provider', login_url: 'https://example.com' }, ] describe('ProfileEditDialogComponent', () => { let component: ProfileEditDialogComponent let fixture: ComponentFixture let profileService: ProfileService let toastService: ToastService let clipboard: Clipboard beforeEach(() => { TestBed.configureTestingModule({ declarations: [ ProfileEditDialogComponent, TextComponent, PasswordComponent, ConfirmButtonComponent, ], imports: [ ReactiveFormsModule, FormsModule, NgbModalModule, NgbAccordionModule, NgxBootstrapIconsModule.pick(allIcons), NgbPopoverModule, ], providers: [NgbActiveModal, provideHttpClient(withInterceptorsFromDi())], }) profileService = TestBed.inject(ProfileService) toastService = TestBed.inject(ToastService) clipboard = TestBed.inject(Clipboard) fixture = TestBed.createComponent(ProfileEditDialogComponent) component = fixture.componentInstance fixture.detectChanges() }) it('should get profile on init, display in form', () => { const getSpy = jest.spyOn(profileService, 'get') getSpy.mockReturnValue(of(profile)) const getProvidersSpy = jest.spyOn( profileService, 'getSocialAccountProviders' ) getProvidersSpy.mockReturnValue(of(socialAccountProviders)) component.ngOnInit() expect(getSpy).toHaveBeenCalled() fixture.detectChanges() expect(component.form.get('email').value).toEqual(profile.email) }) it('should update profile on save, display error if needed', () => { const newProfile = { email: 'foo@bar2.com', password: profile.password, first_name: 'foo2', last_name: profile.last_name, auth_token: profile.auth_token, } const updateSpy = jest.spyOn(profileService, 'update') const errorSpy = jest.spyOn(toastService, 'showError') updateSpy.mockReturnValueOnce(throwError(() => new Error('failed to save'))) component.save() expect(errorSpy).toHaveBeenCalled() updateSpy.mockClear() const infoSpy = jest.spyOn(toastService, 'showInfo') component.form.patchValue(newProfile) updateSpy.mockReturnValueOnce(of(newProfile)) component.save() expect(updateSpy).toHaveBeenCalledWith(newProfile) expect(infoSpy).toHaveBeenCalled() }) it('should close on cancel', () => { const closeSpy = jest.spyOn(component.activeModal, 'close') component.cancel() expect(closeSpy).toHaveBeenCalled() }) it('should show additional confirmation field when email changes, warn with error & disable save', () => { expect(component.form.get('email_confirm').enabled).toBeFalsy() const getSpy = jest.spyOn(profileService, 'get') getSpy.mockReturnValue(of(profile)) const getProvidersSpy = jest.spyOn( profileService, 'getSocialAccountProviders' ) getProvidersSpy.mockReturnValue(of(socialAccountProviders)) component.ngOnInit() component.form.get('email').patchValue('foo@bar2.com') component.onEmailKeyUp({ target: { value: 'foo@bar2.com' } } as any) fixture.detectChanges() expect(component.form.get('email_confirm').enabled).toBeTruthy() expect(fixture.debugElement.nativeElement.textContent).toContain( 'Emails must match' ) expect(component.saveDisabled).toBeTruthy() component.form.get('email_confirm').patchValue('foo@bar2.com') component.onEmailConfirmKeyUp({ target: { value: 'foo@bar2.com' } } as any) fixture.detectChanges() expect(fixture.debugElement.nativeElement.textContent).not.toContain( 'Emails must match' ) expect(component.saveDisabled).toBeFalsy() component.form.get('email').patchValue(profile.email) fixture.detectChanges() expect(component.form.get('email_confirm').enabled).toBeFalsy() expect(fixture.debugElement.nativeElement.textContent).not.toContain( 'Emails must match' ) expect(component.saveDisabled).toBeFalsy() }) it('should show additional confirmation field when password changes, warn with error & disable save', () => { expect(component.form.get('password_confirm').enabled).toBeFalsy() const getSpy = jest.spyOn(profileService, 'get') getSpy.mockReturnValue(of(profile)) const getProvidersSpy = jest.spyOn( profileService, 'getSocialAccountProviders' ) getProvidersSpy.mockReturnValue(of(socialAccountProviders)) component.hasUsablePassword = true component.ngOnInit() component.form.get('password').patchValue('new*pass') component.onPasswordKeyUp({ target: { value: 'new*pass', tagName: 'input' }, } as any) component.onPasswordKeyUp({ target: { tagName: 'button' } } as any) // coverage fixture.detectChanges() expect(component.form.get('password_confirm').enabled).toBeTruthy() expect(fixture.debugElement.nativeElement.textContent).toContain( 'Passwords must match' ) expect(component.saveDisabled).toBeTruthy() component.form.get('password_confirm').patchValue('new*pass') component.onPasswordConfirmKeyUp({ target: { value: 'new*pass' } } as any) fixture.detectChanges() expect(fixture.debugElement.nativeElement.textContent).not.toContain( 'Passwords must match' ) expect(component.saveDisabled).toBeFalsy() component.form.get('password').patchValue(profile.password) fixture.detectChanges() expect(component.form.get('password_confirm').enabled).toBeFalsy() expect(fixture.debugElement.nativeElement.textContent).not.toContain( 'Passwords must match' ) expect(component.saveDisabled).toBeFalsy() }) it('should logout on save if password changed', fakeAsync(() => { const getSpy = jest.spyOn(profileService, 'get') getSpy.mockReturnValue(of(profile)) const getProvidersSpy = jest.spyOn( profileService, 'getSocialAccountProviders' ) getProvidersSpy.mockReturnValue(of(socialAccountProviders)) component.ngOnInit() component['newPassword'] = 'new*pass' component.form.get('password').patchValue('new*pass') component.form.get('password_confirm').patchValue('new*pass') const updateSpy = jest.spyOn(profileService, 'update') updateSpy.mockReturnValue(of(null)) Object.defineProperty(window, 'location', { value: { href: 'http://localhost/', }, writable: true, // possibility to override }) component.save() expect(updateSpy).toHaveBeenCalled() tick(2600) expect(window.location.href).toContain('logout') })) it('should support auth token copy', fakeAsync(() => { const getSpy = jest.spyOn(profileService, 'get') getSpy.mockReturnValue(of(profile)) const getProvidersSpy = jest.spyOn( profileService, 'getSocialAccountProviders' ) getProvidersSpy.mockReturnValue(of(socialAccountProviders)) component.ngOnInit() const copySpy = jest.spyOn(clipboard, 'copy') component.copyAuthToken() expect(copySpy).toHaveBeenCalledWith(profile.auth_token) expect(component.copied).toBeTruthy() tick(3000) expect(component.copied).toBeFalsy() })) it('should support generate token, display error if needed', () => { const getSpy = jest.spyOn(profileService, 'get') getSpy.mockReturnValue(of(profile)) const generateSpy = jest.spyOn(profileService, 'generateAuthToken') const errorSpy = jest.spyOn(toastService, 'showError') generateSpy.mockReturnValueOnce( throwError(() => new Error('failed to generate')) ) component.generateAuthToken() expect(errorSpy).toHaveBeenCalled() generateSpy.mockClear() const newToken = '789101112hijk' generateSpy.mockReturnValueOnce(of(newToken)) component.generateAuthToken() expect(generateSpy).toHaveBeenCalled() expect(component.form.get('auth_token').value).not.toEqual( profile.auth_token ) expect(component.form.get('auth_token').value).toEqual(newToken) }) it('should get social account providers on init', () => { const getSpy = jest.spyOn(profileService, 'get') getSpy.mockReturnValue(of(profile)) const getProvidersSpy = jest.spyOn( profileService, 'getSocialAccountProviders' ) getProvidersSpy.mockReturnValue(of(socialAccountProviders)) component.ngOnInit() expect(getProvidersSpy).toHaveBeenCalled() }) it('should remove disconnected social account from component, show error if needed', () => { const disconnectSpy = jest.spyOn(profileService, 'disconnectSocialAccount') const getSpy = jest.spyOn(profileService, 'get') getSpy.mockImplementation(() => of(profile)) component.ngOnInit() const errorSpy = jest.spyOn(toastService, 'showError') expect(component.socialAccounts).toContainEqual(socialAccount) // fail first disconnectSpy.mockReturnValueOnce( throwError(() => new Error('unable to disconnect')) ) component.disconnectSocialAccount(socialAccount.id) expect(errorSpy).toHaveBeenCalled() // succeed disconnectSpy.mockReturnValue(of(socialAccount.id)) component.disconnectSocialAccount(socialAccount.id) expect(disconnectSpy).toHaveBeenCalled() expect(component.socialAccounts).not.toContainEqual(socialAccount) }) it('should get totp settings', () => { const settings = { url: 'http://localhost/', qr_svg: 'svg', secret: 'secret', } const getSpy = jest.spyOn(profileService, 'getTotpSettings') const toastSpy = jest.spyOn(toastService, 'showError') getSpy.mockReturnValueOnce( throwError(() => new Error('failed to get settings')) ) component.gettotpSettings() expect(getSpy).toHaveBeenCalled() expect(toastSpy).toHaveBeenCalled() getSpy.mockReturnValue(of(settings)) component.gettotpSettings() expect(getSpy).toHaveBeenCalled() expect(component.totpSettings).toEqual(settings) }) it('should activate totp', () => { const activateSpy = jest.spyOn(profileService, 'activateTotp') const toastErrorSpy = jest.spyOn(toastService, 'showError') const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const error = new Error('failed to activate totp') activateSpy.mockReturnValueOnce(throwError(() => error)) component.totpSettings = { url: 'http://localhost/', qr_svg: 'svg', secret: 'secret', } component.form.get('totp_code').patchValue('123456') component.activateTotp() expect(activateSpy).toHaveBeenCalledWith( component.totpSettings.secret, component.form.get('totp_code').value ) expect(toastErrorSpy).toHaveBeenCalled() activateSpy.mockReturnValueOnce(of({ success: false, recovery_codes: [] })) component.activateTotp() expect(toastErrorSpy).toHaveBeenCalledWith('Error activating TOTP', error) activateSpy.mockReturnValueOnce( of({ success: true, recovery_codes: ['1', '2', '3'] }) ) component.activateTotp() expect(toastInfoSpy).toHaveBeenCalled() expect(component.isTotpEnabled).toBeTruthy() expect(component.recoveryCodes).toEqual(['1', '2', '3']) }) it('should deactivate totp', () => { const deactivateSpy = jest.spyOn(profileService, 'deactivateTotp') const toastErrorSpy = jest.spyOn(toastService, 'showError') const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const error = new Error('failed to deactivate totp') deactivateSpy.mockReturnValueOnce(throwError(() => error)) component.deactivateTotp() expect(deactivateSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled() deactivateSpy.mockReturnValueOnce(of(false)) component.deactivateTotp() expect(toastErrorSpy).toHaveBeenCalledWith('Error deactivating TOTP', error) deactivateSpy.mockReturnValueOnce(of(true)) component.deactivateTotp() expect(toastInfoSpy).toHaveBeenCalled() expect(component.isTotpEnabled).toBeFalsy() }) it('should copy recovery codes', fakeAsync(() => { const copySpy = jest.spyOn(clipboard, 'copy') component.recoveryCodes = ['1', '2', '3'] component.copyRecoveryCodes() expect(copySpy).toHaveBeenCalledWith('1\n2\n3') tick(3000) })) })