feat(account): add ChangePasswordFormComponent
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+85
@@ -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<ChangePasswordFormComponent>;
|
||||||
|
let component: ChangePasswordFormComponent;
|
||||||
|
let authSpy: jasmine.SpyObj<AuthService>;
|
||||||
|
let toastSpy: jasmine.SpyObj<ToastService>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
authSpy = jasmine.createSpyObj<AuthService>('AuthService', ['changePassword']);
|
||||||
|
toastSpy = jasmine.createSpyObj<ToastService>('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.');
|
||||||
|
});
|
||||||
|
});
|
||||||
+113
@@ -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: `
|
||||||
|
<form [formGroup]="form" class="k-form k-form-vertical" (ngSubmit)="onSubmit()">
|
||||||
|
<div class="grid grid-cols-1 gap-y-3 max-w-md">
|
||||||
|
|
||||||
|
<kendo-formfield>
|
||||||
|
<kendo-label text="Current Password *"></kendo-label>
|
||||||
|
<kendo-textbox formControlName="currentPassword" type="password"
|
||||||
|
[clearButton]="false"></kendo-textbox>
|
||||||
|
<kendo-formerror *ngIf="form.get('currentPassword')?.errors?.['required']">
|
||||||
|
Required.
|
||||||
|
</kendo-formerror>
|
||||||
|
</kendo-formfield>
|
||||||
|
|
||||||
|
<kendo-formfield>
|
||||||
|
<kendo-label text="New Password *"></kendo-label>
|
||||||
|
<kendo-textbox formControlName="newPassword" type="password"
|
||||||
|
[clearButton]="false"></kendo-textbox>
|
||||||
|
<kendo-formerror *ngIf="form.get('newPassword')?.errors?.['required']">
|
||||||
|
Required.
|
||||||
|
</kendo-formerror>
|
||||||
|
<kendo-formerror *ngIf="form.get('newPassword')?.errors?.['passwordStrength']">
|
||||||
|
Must be at least 8 characters with an uppercase letter, a lowercase letter,
|
||||||
|
a digit, and a special character.
|
||||||
|
</kendo-formerror>
|
||||||
|
</kendo-formfield>
|
||||||
|
|
||||||
|
<kendo-formfield>
|
||||||
|
<kendo-label text="Confirm New Password *"></kendo-label>
|
||||||
|
<kendo-textbox formControlName="confirmPassword" type="password"
|
||||||
|
[clearButton]="false"></kendo-textbox>
|
||||||
|
<kendo-formerror *ngIf="form.get('confirmPassword')?.errors?.['required']">
|
||||||
|
Required.
|
||||||
|
</kendo-formerror>
|
||||||
|
<kendo-formerror *ngIf="form.errors?.['mismatch'] && form.get('confirmPassword')?.touched">
|
||||||
|
Passwords do not match.
|
||||||
|
</kendo-formerror>
|
||||||
|
<kendo-formerror *ngIf="form.errors?.['sameAsCurrent'] && form.get('newPassword')?.touched">
|
||||||
|
New password must be different from the current password.
|
||||||
|
</kendo-formerror>
|
||||||
|
</kendo-formfield>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<button kendoButton themeColor="primary" type="submit"
|
||||||
|
[disabled]="form.invalid || submitting">
|
||||||
|
Change Password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user