fix(logout): enhance logout handling with success and error toasts
This commit is contained in:
@@ -3,11 +3,21 @@ import { provideRouter, Router } from '@angular/router';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||
import { ToastService } from '../../../../core/services/toast.service';
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
|
||||
describe('LoginPageComponent', () => {
|
||||
let toastService: {
|
||||
danger: ReturnType<typeof vi.fn>;
|
||||
showAfterReload: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
toastService = {
|
||||
danger: vi.fn(),
|
||||
showAfterReload: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('submits credentials and redirects to home with a full page reload on success', async () => {
|
||||
@@ -16,14 +26,18 @@ describe('LoginPageComponent', () => {
|
||||
of({
|
||||
id: 1,
|
||||
nombre_apellido: 'Ada Lovelace',
|
||||
email: 'ada@example.com'
|
||||
})
|
||||
)
|
||||
email: 'ada@example.com',
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(LoginPageComponent);
|
||||
@@ -34,17 +48,56 @@ describe('LoginPageComponent', () => {
|
||||
|
||||
component.form.setValue({
|
||||
email: 'ada@example.com',
|
||||
password: 'secret123'
|
||||
password: 'secret123',
|
||||
});
|
||||
|
||||
component.onSubmit();
|
||||
|
||||
expect(authService.login).toHaveBeenCalledWith({
|
||||
email: 'ada@example.com',
|
||||
password: 'secret123'
|
||||
password: 'secret123',
|
||||
});
|
||||
expect(redirectSpy).toHaveBeenCalled();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
expect(toastService.showAfterReload).toHaveBeenCalledWith(
|
||||
'Sesión iniciada correctamente.',
|
||||
'success',
|
||||
);
|
||||
});
|
||||
|
||||
it('serializes a stored return URL before reloading so tenant base paths are restored', async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginPageComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: AuthService, useValue: {} },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(LoginPageComponent);
|
||||
const component = fixture.componentInstance as any;
|
||||
const router = TestBed.inject(Router);
|
||||
const assign = vi.fn();
|
||||
const parsedUrl = router.parseUrl('/');
|
||||
|
||||
component.document = {
|
||||
defaultView: {
|
||||
sessionStorage: {
|
||||
getItem: vi.fn().mockReturnValue('/'),
|
||||
removeItem: vi.fn(),
|
||||
},
|
||||
},
|
||||
location: { assign },
|
||||
};
|
||||
const parseUrlSpy = vi.spyOn(router, 'parseUrl').mockReturnValue(parsedUrl);
|
||||
const serializeUrlSpy = vi.spyOn(router, 'serializeUrl').mockReturnValue('/sonder');
|
||||
|
||||
component.redirectToHome();
|
||||
|
||||
expect(parseUrlSpy).toHaveBeenCalledWith('/');
|
||||
expect(serializeUrlSpy).toHaveBeenCalledWith(parsedUrl);
|
||||
expect(assign).toHaveBeenCalledWith('/sonder');
|
||||
});
|
||||
|
||||
it('surfaces backend login errors', async () => {
|
||||
@@ -53,16 +106,20 @@ describe('LoginPageComponent', () => {
|
||||
throwError(() => ({
|
||||
error: {
|
||||
errors: {
|
||||
email: ['Las credenciales son invalidas.']
|
||||
}
|
||||
}
|
||||
}))
|
||||
)
|
||||
email: ['Las credenciales son invalidas.'],
|
||||
},
|
||||
},
|
||||
})),
|
||||
),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(LoginPageComponent);
|
||||
@@ -70,22 +127,27 @@ describe('LoginPageComponent', () => {
|
||||
|
||||
component.form.setValue({
|
||||
email: 'ada@example.com',
|
||||
password: 'wrong-password'
|
||||
password: 'wrong-password',
|
||||
});
|
||||
|
||||
component.onSubmit();
|
||||
|
||||
expect(component.serverError()).toBe('Las credenciales son invalidas.');
|
||||
expect(toastService.danger).toHaveBeenCalledWith('Las credenciales son invalidas.');
|
||||
});
|
||||
|
||||
it('validates email length and password minimum length before submit', async () => {
|
||||
const authService = {
|
||||
login: vi.fn()
|
||||
login: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(LoginPageComponent);
|
||||
@@ -93,12 +155,15 @@ describe('LoginPageComponent', () => {
|
||||
|
||||
component.form.setValue({
|
||||
email: `${'a'.repeat(250)}@example.com`,
|
||||
password: '1234567'
|
||||
password: '1234567',
|
||||
});
|
||||
component.onSubmit();
|
||||
|
||||
expect(authService.login).not.toHaveBeenCalled();
|
||||
expect(component.getControlError('email')).toBe('No puede superar los 255 caracteres.');
|
||||
expect(component.getControlError('password')).toBe('Debe tener al menos 8 caracteres.');
|
||||
expect(toastService.danger).toHaveBeenCalledWith(
|
||||
'Revisá los datos ingresados para iniciar sesión.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||
import { ToastService } from '../../../../core/services/toast.service';
|
||||
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
||||
import { InputComponent } from '../../../../shared/components/input/input.component';
|
||||
|
||||
@@ -16,13 +17,14 @@ const POST_LOGIN_RETURN_URL_KEY = 'shopit.auth.return-url';
|
||||
imports: [ReactiveFormsModule, InputComponent, ButtonComponent],
|
||||
templateUrl: './login-page.component.html',
|
||||
styleUrl: './login-page.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LoginPageComponent {
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
private readonly router = inject(Router);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly authService = inject(AuthService);
|
||||
private readonly toastService = inject(ToastService);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
|
||||
@@ -32,7 +34,7 @@ export class LoginPageComponent {
|
||||
|
||||
protected readonly form = this.formBuilder.nonNullable.group({
|
||||
email: ['', [Validators.required, Validators.email, Validators.maxLength(EMAIL_MAX_LENGTH)]],
|
||||
password: ['', [Validators.required, Validators.minLength(PASSWORD_MIN_LENGTH)]]
|
||||
password: ['', [Validators.required, Validators.minLength(PASSWORD_MIN_LENGTH)]],
|
||||
});
|
||||
protected readonly submitted = this.submittedState.asReadonly();
|
||||
protected readonly serverError = this.serverErrorState.asReadonly();
|
||||
@@ -64,6 +66,7 @@ export class LoginPageComponent {
|
||||
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
this.toastService.danger('Revisá los datos ingresados para iniciar sesión.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,12 +75,13 @@ export class LoginPageComponent {
|
||||
this.authService.login(this.form.getRawValue()).subscribe({
|
||||
next: () => {
|
||||
this.isSubmittingState.set(false);
|
||||
this.toastService.showAfterReload('Sesión iniciada correctamente.', 'success');
|
||||
this.redirectToHome();
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.isSubmittingState.set(false);
|
||||
this.serverErrorState.set(this.resolveErrorMessage(error));
|
||||
}
|
||||
this.showLoginError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,7 +95,7 @@ export class LoginPageComponent {
|
||||
}
|
||||
this.authService.loginWithGoogle();
|
||||
} catch (error: unknown) {
|
||||
this.serverErrorState.set(this.resolveErrorMessage(error));
|
||||
this.showLoginError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,10 +143,9 @@ export class LoginPageComponent {
|
||||
const requestedUrl =
|
||||
this.route.snapshot.queryParamMap.get('returnUrl') ??
|
||||
this.document.defaultView?.sessionStorage.getItem(POST_LOGIN_RETURN_URL_KEY);
|
||||
const destination =
|
||||
requestedUrl?.startsWith('/') && !requestedUrl.startsWith('//')
|
||||
? requestedUrl
|
||||
: this.router.serializeUrl(this.router.createUrlTree(['/']));
|
||||
const internalDestination =
|
||||
requestedUrl?.startsWith('/') && !requestedUrl.startsWith('//') ? requestedUrl : '/';
|
||||
const destination = this.router.serializeUrl(this.router.parseUrl(internalDestination));
|
||||
|
||||
this.document.defaultView?.sessionStorage.removeItem(POST_LOGIN_RETURN_URL_KEY);
|
||||
this.document.location.assign(destination);
|
||||
@@ -155,15 +158,22 @@ export class LoginPageComponent {
|
||||
this.authService.completeGoogleLogin(oauthCode).subscribe({
|
||||
next: () => {
|
||||
this.isSubmittingState.set(false);
|
||||
this.toastService.showAfterReload('Sesión iniciada correctamente.', 'success');
|
||||
this.redirectToHome();
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.isSubmittingState.set(false);
|
||||
this.serverErrorState.set(this.resolveErrorMessage(error));
|
||||
}
|
||||
this.showLoginError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private showLoginError(error: unknown): void {
|
||||
const message = this.resolveErrorMessage(error);
|
||||
this.serverErrorState.set(message);
|
||||
this.toastService.danger(message);
|
||||
}
|
||||
|
||||
private resolveErrorMessage(error: unknown): string {
|
||||
const errorPayload =
|
||||
typeof error === 'object' && error !== null && 'error' in error
|
||||
|
||||
Reference in New Issue
Block a user