diff --git a/APP/src/app/features/account/components/change-password-form/change-password-form.component.spec.ts b/APP/src/app/features/account/components/change-password-form/change-password-form.component.spec.ts new file mode 100644 index 0000000..0c55619 --- /dev/null +++ b/APP/src/app/features/account/components/change-password-form/change-password-form.component.spec.ts @@ -0,0 +1,85 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of, throwError } from 'rxjs'; +import { ChangePasswordFormComponent } from './change-password-form.component'; +import { AuthService } from '../../../../shared/services/auth.service'; +import { ToastService } from '../../../../core/services/toast.service'; + +describe('ChangePasswordFormComponent', () => { + let fixture: ComponentFixture; + let component: ChangePasswordFormComponent; + let authSpy: jasmine.SpyObj; + let toastSpy: jasmine.SpyObj; + + beforeEach(async () => { + authSpy = jasmine.createSpyObj('AuthService', ['changePassword']); + toastSpy = jasmine.createSpyObj('ToastService', ['success', 'error']); + + await TestBed.configureTestingModule({ + imports: [ChangePasswordFormComponent], + providers: [ + { provide: AuthService, useValue: authSpy }, + { provide: ToastService, useValue: toastSpy }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ChangePasswordFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + const fill = (current: string, next: string, confirm: string) => { + component.form.setValue({ + currentPassword: current, + newPassword: next, + confirmPassword: confirm, + }); + }; + + it('is invalid when the new password is weak', () => { + fill('Old1234!', 'weak', 'weak'); + expect(component.form.invalid).toBeTrue(); + }); + + it('is invalid when confirm does not match', () => { + fill('Old1234!', 'New1234!', 'Other1234!'); + expect(component.form.invalid).toBeTrue(); + }); + + it('is invalid when the new password equals the current password', () => { + fill('Same1234!', 'Same1234!', 'Same1234!'); + expect(component.form.invalid).toBeTrue(); + }); + + it('is valid for a strong, matching, different new password', () => { + fill('Old1234!', 'New1234!', 'New1234!'); + expect(component.form.valid).toBeTrue(); + }); + + it('does not call the service when submitting an invalid form', () => { + fill('Old1234!', 'weak', 'weak'); + component.onSubmit(); + expect(authSpy.changePassword).not.toHaveBeenCalled(); + }); + + it('calls the service with current+new and shows success + resets on 204', () => { + authSpy.changePassword.and.returnValue(of(void 0)); + fill('Old1234!', 'New1234!', 'New1234!'); + + component.onSubmit(); + + expect(authSpy.changePassword).toHaveBeenCalledWith('Old1234!', 'New1234!'); + expect(toastSpy.success).toHaveBeenCalled(); + expect(component.form.get('newPassword')?.value).toBeNull(); + }); + + it('shows the server error message on failure', () => { + authSpy.changePassword.and.returnValue( + throwError(() => ({ error: { message: 'Incorrect password.' } })) + ); + fill('Wrong1234!', 'New1234!', 'New1234!'); + + component.onSubmit(); + + expect(toastSpy.error).toHaveBeenCalledWith('Incorrect password.'); + }); +}); diff --git a/APP/src/app/features/account/components/change-password-form/change-password-form.component.ts b/APP/src/app/features/account/components/change-password-form/change-password-form.component.ts new file mode 100644 index 0000000..c110797 --- /dev/null +++ b/APP/src/app/features/account/components/change-password-form/change-password-form.component.ts @@ -0,0 +1,113 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { InputsModule } from '@progress/kendo-angular-inputs'; +import { LabelModule } from '@progress/kendo-angular-label'; +import { ButtonsModule } from '@progress/kendo-angular-buttons'; +import { AuthService } from '../../../../shared/services/auth.service'; +import { ToastService } from '../../../../core/services/toast.service'; +import { + passwordStrengthValidator, + passwordMatchValidator, +} from '../../validators/password.validators'; + +@Component({ + selector: 'app-change-password-form', + standalone: true, + imports: [ + CommonModule, ReactiveFormsModule, + InputsModule, LabelModule, ButtonsModule, + ], + template: ` +
+
+ + + + + + Required. + + + + + + + + Required. + + + Must be at least 8 characters with an uppercase letter, a lowercase letter, + a digit, and a special character. + + + + + + + + Required. + + + Passwords do not match. + + + New password must be different from the current password. + + + +
+ +
+ +
+
+ `, +}) +export class ChangePasswordFormComponent { + form: FormGroup; + submitting = false; + + constructor( + private fb: FormBuilder, + private authService: AuthService, + private toast: ToastService, + ) { + this.form = this.fb.group( + { + currentPassword: ['', [Validators.required]], + newPassword: ['', [Validators.required, passwordStrengthValidator()]], + confirmPassword: ['', [Validators.required]], + }, + { validators: passwordMatchValidator() }, + ); + } + + onSubmit(): void { + if (this.form.invalid) { + this.form.markAllAsTouched(); + return; + } + + this.submitting = true; + const { currentPassword, newPassword } = this.form.value; + + this.authService.changePassword(currentPassword, newPassword).subscribe({ + next: () => { + this.toast.success('Password changed successfully.'); + this.form.reset(); + this.submitting = false; + }, + error: (err) => { + this.toast.error(err?.error?.message || 'Failed to change password.'); + this.submitting = false; + }, + }); + } +}