import { TestBed } from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { IssueEntity, IssuesStore } from './issues.store'; import { API_BASE_URL } from './issues-api.service'; const makeIssue = (overrides: Partial = {}): IssueEntity => ({ id: 99, type: 'Story', assignee: '', epic: '', name: 'Test Issue', dueDate: '', description: '', estimatedTime: null, dependsOnIds: [], comments: [], priority: 'Moyenne', status: 'draft', progress: 0, ...overrides, }); describe('IssuesStore', () => { let store: IssuesStore; let httpMock: HttpTestingController; const loadWith = async (issues: IssueEntity[]) => { const p = store.load(); httpMock.expectOne(`${API_BASE_URL}/issues`).flush(issues); await p; }; beforeEach(() => { TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()], }); store = TestBed.inject(IssuesStore); httpMock = TestBed.inject(HttpTestingController); }); afterEach(() => { httpMock.verify(); }); it('should be created', () => { expect(store).toBeTruthy(); }); describe('load', () => { it('populates issues from the API', async () => { await loadWith([makeIssue({ id: 1 }), makeIssue({ id: 2 })]); expect(store.issues().length).toBe(2); }); it('sets loading to true during load and false after', async () => { const p = store.load(); expect(store.loading()).toBe(true); httpMock.expectOne(`${API_BASE_URL}/issues`).flush([]); await p; expect(store.loading()).toBe(false); expect(store.loaded()).toBe(true); }); it('does not reload if already loaded', async () => { await loadWith([]); await store.load(); httpMock.expectNone(`${API_BASE_URL}/issues`); }); }); describe('getById', () => { beforeEach(async () => { await loadWith([makeIssue({ id: 1 }), makeIssue({ id: 2 })]); }); it('returns the issue with the given id', () => { expect(store.getById(1)?.id).toBe(1); }); it('returns undefined for an unknown id', () => { expect(store.getById(9999)).toBeUndefined(); }); }); describe('getNextId', () => { it('returns max id + 1', async () => { await loadWith([makeIssue({ id: 1 }), makeIssue({ id: 5 }), makeIssue({ id: 3 })]); expect(store.getNextId()).toBe(6); }); it('returns 1 when there are no issues', async () => { await loadWith([]); expect(store.getNextId()).toBe(1); }); }); describe('upsert', () => { beforeEach(async () => { await loadWith([makeIssue({ id: 1, name: 'Existing' }), makeIssue({ id: 2 }), makeIssue({ id: 3 })]); }); it('creates a new issue via POST when id is 0', async () => { const before = store.issues().length; const p = store.upsert(makeIssue({ id: 0, name: 'New Issue' })); httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 999, name: 'New Issue' })); await p; expect(store.issues().length).toBe(before + 1); expect(store.getById(999)?.name).toBe('New Issue'); }); it('updates an existing issue via PUT', async () => { const p = store.upsert(makeIssue({ id: 1, name: 'Updated Name' })); httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, name: 'Updated Name' })); await p; expect(store.getById(1)?.name).toBe('Updated Name'); expect(store.issues().filter((i) => i.id === 1).length).toBe(1); }); it('normalizes legacy dependsOnId to dependsOnIds array', async () => { const issue = { id: 0, type: 'Story', name: 'Legacy', dependsOnId: 1 } as any; const p = store.upsert(issue); httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 998, dependsOnIds: [1] })); await p; expect(store.getById(998)?.dependsOnIds).toEqual([1]); }); it('filters non-number values from dependsOnIds', async () => { const issue = { ...makeIssue({ id: 0 }), dependsOnIds: [1, 'two', null] } as any; const p = store.upsert(issue); httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 997, dependsOnIds: [1] })); await p; expect(store.getById(997)?.dependsOnIds).toEqual([1]); }); it('ensures comments is always an array when missing', async () => { const issue = { ...makeIssue({ id: 0 }), comments: undefined } as any; const p = store.upsert(issue); httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 996, comments: [] })); await p; expect(store.getById(996)?.comments).toEqual([]); }); it('sets default type to Story when type is missing', async () => { const issue = { ...makeIssue({ id: 0 }), type: undefined } as any; const p = store.upsert(issue); httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 995, type: 'Story' })); await p; expect(store.getById(995)?.type).toBe('Story'); }); it('sets estimatedTime to null when missing in API response', async () => { const issue = { ...makeIssue({ id: 0 }), estimatedTime: undefined } as any; const p = store.upsert(issue); httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush({ ...makeIssue({ id: 994 }), estimatedTime: undefined }); await p; expect(store.getById(994)?.estimatedTime).toBeNull(); }); }); describe('deleteById', () => { beforeEach(async () => { await loadWith([ makeIssue({ id: 1 }), makeIssue({ id: 100 }), makeIssue({ id: 101, dependsOnIds: [100] }), ]); }); it('removes the issue from the store', async () => { const p = store.deleteById(1); httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/1` }).flush(null); await p; expect(store.getById(1)).toBeUndefined(); }); it('removes the deleted id from dependsOnIds of other issues', async () => { const p = store.deleteById(100); httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/100` }).flush(null); await p; expect(store.getById(101)?.dependsOnIds).toEqual([]); }); it('does not affect issues with unrelated dependsOnIds', async () => { const p = store.deleteById(1); httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/1` }).flush(null); await p; expect(store.getById(101)?.dependsOnIds).toEqual([100]); }); }); });