diff --git a/APP/src/app/shared/services/auth.service.spec.ts b/APP/src/app/shared/services/auth.service.spec.ts new file mode 100644 index 0000000..2d1ceec --- /dev/null +++ b/APP/src/app/shared/services/auth.service.spec.ts @@ -0,0 +1,206 @@ +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController +} from '@angular/common/http/testing'; +import { + AuthService, + LoginResultType, + UserInfo +} from './auth.service'; +import { ApiConfigService } from '../../core/services/api-config.service'; + +const MOCK_USER: UserInfo = { + id: 'user-123', + email: 'test@example.com', + roles: ['Admin'], + languagePreference: 'en' +}; + +const MOCK_API_RESPONSE = { + accessToken: 'mock-access-token', + expiresIn: 900, + user: MOCK_USER +}; + +describe('AuthService', () => { + let service: AuthService; + let httpMock: HttpTestingController; + let apiConfig: ApiConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [AuthService, ApiConfigService] + }); + service = TestBed.inject(AuthService); + httpMock = TestBed.inject(HttpTestingController); + apiConfig = TestBed.inject(ApiConfigService); + }); + + afterEach(() => { + httpMock.verify(); + }); + + // ── login() ──────────────────────────────────────────────────────────────── + + describe('login()', () => { + it('should POST to /api/auth/login with email and password', () => { + service.login({ email: 'test@example.com', password: 'secret' }).subscribe(); + const req = httpMock.expectOne(`${apiConfig.authUrl}/login`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ email: 'test@example.com', password: 'secret' }); + expect(req.request.withCredentials).toBeTrue(); + req.flush(MOCK_API_RESPONSE); + }); + + it('should return LoginResultType.Success and store token + user on 200', () => { + let result: any; + service.login({ email: 'test@example.com', password: 'secret' }).subscribe(r => result = r); + httpMock.expectOne(`${apiConfig.authUrl}/login`).flush(MOCK_API_RESPONSE); + + expect(result.result).toBe(LoginResultType.Success); + expect(result.responseData).toEqual(MOCK_USER); + expect(service.getToken()).toBe('mock-access-token'); + expect(service.getCurrentUser()).toEqual(MOCK_USER); + expect(service.isAuthenticated()).toBeTrue(); + }); + + it('should return LoginResultType.InvalidCredentials on 401', () => { + let result: any; + service.login({ email: 'bad@example.com', password: 'wrong' }).subscribe(r => result = r); + httpMock.expectOne(`${apiConfig.authUrl}/login`).flush( + { message: 'Invalid credentials' }, + { status: 401, statusText: 'Unauthorized' } + ); + + expect(result.result).toBe(LoginResultType.InvalidCredentials); + expect(service.getToken()).toBeNull(); + expect(service.isAuthenticated()).toBeFalse(); + }); + + it('should return LoginResultType.Error on non-401 HTTP error', () => { + let result: any; + service.login({ email: 'test@example.com', password: 'secret' }).subscribe(r => result = r); + httpMock.expectOne(`${apiConfig.authUrl}/login`).flush( + { message: 'Server error' }, + { status: 500, statusText: 'Internal Server Error' } + ); + + expect(result.result).toBe(LoginResultType.Error); + }); + }); + + // ── refresh() ────────────────────────────────────────────────────────────── + + describe('refresh()', () => { + it('should POST to /api/auth/refresh with withCredentials', () => { + service.refresh().subscribe(); + const req = httpMock.expectOne(`${apiConfig.authUrl}/refresh`); + expect(req.request.method).toBe('POST'); + expect(req.request.withCredentials).toBeTrue(); + req.flush(MOCK_API_RESPONSE); + }); + + it('should return true and update token + user on 200', () => { + let result: boolean | undefined; + service.refresh().subscribe(r => result = r); + httpMock.expectOne(`${apiConfig.authUrl}/refresh`).flush(MOCK_API_RESPONSE); + + expect(result).toBeTrue(); + expect(service.getToken()).toBe('mock-access-token'); + expect(service.getCurrentUser()).toEqual(MOCK_USER); + }); + + it('should return false and leave state unchanged on 401', () => { + let result: boolean | undefined; + service.refresh().subscribe(r => result = r); + httpMock.expectOne(`${apiConfig.authUrl}/refresh`).flush( + { message: 'Refresh token expired' }, + { status: 401, statusText: 'Unauthorized' } + ); + + expect(result).toBeFalse(); + expect(service.getToken()).toBeNull(); + expect(service.isAuthenticated()).toBeFalse(); + }); + }); + + // ── logout() ─────────────────────────────────────────────────────────────── + + describe('logout()', () => { + it('should clear token and user from memory immediately', () => { + // Seed state + service['accessToken$'].next('some-token'); + service['currentUser$'].next(MOCK_USER); + + service.logout(); + httpMock.expectOne(`${apiConfig.authUrl}/logout`).flush(null, { status: 204, statusText: 'No Content' }); + + expect(service.getToken()).toBeNull(); + expect(service.getCurrentUser()).toBeNull(); + expect(service.isAuthenticated()).toBeFalse(); + }); + + it('should POST to /api/auth/logout with withCredentials', () => { + service.logout(); + const req = httpMock.expectOne(`${apiConfig.authUrl}/logout`); + expect(req.request.method).toBe('POST'); + expect(req.request.withCredentials).toBeTrue(); + req.flush(null, { status: 204, statusText: 'No Content' }); + }); + + it('should not throw if the logout API call fails', () => { + expect(() => { + service.logout(); + httpMock.expectOne(`${apiConfig.authUrl}/logout`).flush( + { message: 'Server error' }, + { status: 500, statusText: 'Internal Server Error' } + ); + }).not.toThrow(); + }); + }); + + // ── initializeFromRefreshToken() ─────────────────────────────────────────── + + describe('initializeFromRefreshToken()', () => { + it('should resolve even when refresh returns 401 (does not block bootstrap)', async () => { + const promise = service.initializeFromRefreshToken(); + httpMock.expectOne(`${apiConfig.authUrl}/refresh`).flush( + { message: 'No cookie' }, + { status: 401, statusText: 'Unauthorized' } + ); + await expectAsync(promise).toBeResolved(); + }); + + it('should resolve and authenticate user when refresh succeeds', async () => { + const promise = service.initializeFromRefreshToken(); + httpMock.expectOne(`${apiConfig.authUrl}/refresh`).flush(MOCK_API_RESPONSE); + await expectAsync(promise).toBeResolved(); + expect(service.isAuthenticated()).toBeTrue(); + }); + }); + + // ── setCurrentUser() / getCurrentUser() ──────────────────────────────────── + + describe('setCurrentUser()', () => { + it('should update currentUser$ and mark authenticated', () => { + service.setCurrentUser(MOCK_USER); + expect(service.getCurrentUser()).toEqual(MOCK_USER); + expect(service.isAuthenticated()).toBeTrue(); + }); + }); + + // ── redirect URL helpers ──────────────────────────────────────────────────── + + describe('redirect URL helpers', () => { + it('should default redirect to /dashboard', () => { + expect(service.getRedirectUrl()).toBe('/dashboard'); + }); + + it('should store and return a custom redirect URL', () => { + service.setRedirectUrl('/members'); + expect(service.getRedirectUrl()).toBe('/members'); + }); + }); +});