diff --git a/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.spec.ts b/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.spec.ts index bd68647..9231d37 100644 --- a/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.spec.ts +++ b/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.spec.ts @@ -57,89 +57,315 @@ describe('AdminMembershipsComponent', () => { httpMock.verify(); }); - it('should create', () => { - fixture.detectChanges(); + function flushPendingRequest(): void { const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); req.flush([]); + } + + it('should create', () => { + fixture.detectChanges(); + flushPendingRequest(); expect(component).toBeTruthy(); }); - it('should default activeTab to pending', () => { - fixture.detectChanges(); - const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - req.flush([]); - expect(component.activeTab()).toBe('pending'); + describe('initial state', () => { + it('should default activeTab to pending', () => { + fixture.detectChanges(); + flushPendingRequest(); + expect(component.activeTab()).toBe('pending'); + }); + + it('should initialize allMemberships as empty array', () => { + fixture.detectChanges(); + flushPendingRequest(); + expect(component.allMemberships()).toEqual([]); + }); + + it('should initialize pendingMemberships as empty array', () => { + fixture.detectChanges(); + flushPendingRequest(); + expect(component.pendingMemberships()).toEqual([]); + }); + + it('should set isLoading to false after init completes', () => { + fixture.detectChanges(); + flushPendingRequest(); + expect(component.isLoading()).toBe(false); + }); }); - it('should initialize allMemberships as empty array', () => { - fixture.detectChanges(); - const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - req.flush([]); - expect(component.allMemberships()).toEqual([]); + describe('loadPendingMemberships', () => { + it('should load pending memberships on init', () => { + fixture.detectChanges(); + const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + expect(req.request.method).toBe('GET'); + req.flush([mockPendingMembership]); + expect(component.pendingMemberships().length).toBe(1); + expect(component.pendingMemberships()[0].firstName).toBe('Max'); + }); + + it('should set isLoading to true during loading and false after', () => { + fixture.detectChanges(); + const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + expect(component.isLoading()).toBe(true); + req.flush([]); + expect(component.isLoading()).toBe(false); + }); + + it('should set isLoading to false on failure', () => { + fixture.detectChanges(); + const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + req.flush('Error', { status: 500, statusText: 'Server Error' }); + expect(component.isLoading()).toBe(false); + }); }); - it('should load pending memberships on init', () => { - fixture.detectChanges(); - const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - expect(req.request.method).toBe('GET'); - req.flush([mockPendingMembership]); - expect(component.pendingMemberships().length).toBe(1); - expect(component.pendingMemberships()[0].firstName).toBe('Max'); + describe('loadAllMemberships', () => { + it('should load all memberships when switching to active tab', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.switchTab('active'); + + const allReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + expect(allReq.request.method).toBe('GET'); + allReq.flush([mockPendingMembership, mockActiveMembership, mockInactiveMembership]); + + expect(component.allMemberships().length).toBe(3); + }); + + it('should load all memberships when switching to all tab', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.switchTab('all'); + + const allReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + allReq.flush([mockPendingMembership, mockActiveMembership]); + + expect(component.allMemberships().length).toBe(2); + }); + + it('should not reload if allMemberships already loaded', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.switchTab('active'); + const firstReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + firstReq.flush([mockActiveMembership]); + + component.switchTab('pending'); + component.switchTab('active'); + + const additionalReqs = httpMock.match( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + expect(additionalReqs.length).toBe(0); + }); + + it('should set isLoading to false after loading completes', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.switchTab('all'); + + const allReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + expect(component.isLoading()).toBe(true); + allReq.flush([]); + expect(component.isLoading()).toBe(false); + }); }); - it('should load all memberships when switching to active tab', () => { - fixture.detectChanges(); - const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - pendingReq.flush([]); + describe('switchTab', () => { + it('should switch activeTab signal', () => { + fixture.detectChanges(); + flushPendingRequest(); - component.switchTab('active'); + component.switchTab('active'); + const activeReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + activeReq.flush([]); + expect(component.activeTab()).toBe('active'); - const allReq = httpMock.expectOne( - (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), - ); - expect(allReq.request.method).toBe('GET'); - allReq.flush([mockPendingMembership, mockActiveMembership, mockInactiveMembership]); + component.switchTab('pending'); + expect(component.activeTab()).toBe('pending'); - expect(component.allMemberships().length).toBe(3); + component.switchTab('all'); + const allReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + allReq.flush([]); + expect(component.activeTab()).toBe('all'); + }); + + it('should only call API for all memberships once per load', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.switchTab('active'); + const allReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + allReq.flush([mockActiveMembership]); + fixture.detectChanges(); + + component.switchTab('pending'); + expect(component.activeTab()).toBe('pending'); + + component.switchTab('all'); + expect(component.activeTab()).toBe('all'); + const allReqs = httpMock.match( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + expect(allReqs.length).toBe(0); + }); }); - it('should load all memberships when switching to all tab', () => { - fixture.detectChanges(); - const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - pendingReq.flush([]); + describe('approveMembership', () => { + it('should call approve API and reload pending memberships', () => { + fixture.detectChanges(); + flushPendingRequest(); - component.switchTab('all'); + component.approveMembership('1'); - const allReq = httpMock.expectOne( - (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), - ); - expect(allReq.request.method).toBe('GET'); - allReq.flush([mockPendingMembership, mockActiveMembership]); + const approveReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships/1/approve'), + ); + expect(approveReq.request.method).toBe('PUT'); + approveReq.flush({}); - expect(component.allMemberships().length).toBe(2); + const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + pendingReq.flush([]); + }); + + it('should set isProcessing while approving', () => { + fixture.detectChanges(); + flushPendingRequest(); + + expect(component.isProcessing()).toBeNull(); + component.approveMembership('1'); + expect(component.isProcessing()).toBe('1'); + + const approveReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships/1/approve'), + ); + approveReq.flush({}); + + const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + pendingReq.flush([]); + + expect(component.isProcessing()).toBeNull(); + }); + + it('should not send approve request if already processing', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.approveMembership('1'); + component.approveMembership('1'); + + const approveReqs = httpMock.match( + (r) => r.url.includes('/admin/memberships/1/approve'), + ); + expect(approveReqs.length).toBe(1); + + approveReqs[0].flush({}); + + const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + pendingReq.flush([]); + }); + + it('should reset isProcessing on error', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.approveMembership('1'); + + const approveReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships/1/approve'), + ); + approveReq.flush({ message: 'Fehler' }, { status: 500, statusText: 'Server Error' }); + + expect(component.isProcessing()).toBeNull(); + }); }); - it('should switch activeTab signal', () => { - fixture.detectChanges(); - const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - req.flush([]); + describe('rejectMembership', () => { + it('should call reject API and reload pending memberships', () => { + fixture.detectChanges(); + flushPendingRequest(); - component.switchTab('active'); - const activeReq = httpMock.expectOne( - (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), - ); - activeReq.flush([]); - expect(component.activeTab()).toBe('active'); + component.rejectMembership('1'); - component.switchTab('pending'); - expect(component.activeTab()).toBe('pending'); + const rejectReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships/1/reject'), + ); + expect(rejectReq.request.method).toBe('PUT'); + rejectReq.flush({}); - component.switchTab('all'); - const allReq = httpMock.expectOne( - (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), - ); - allReq.flush([]); - expect(component.activeTab()).toBe('all'); + const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + pendingReq.flush([]); + }); + + it('should set isProcessing while rejecting', () => { + fixture.detectChanges(); + flushPendingRequest(); + + expect(component.isProcessing()).toBeNull(); + component.rejectMembership('1'); + expect(component.isProcessing()).toBe('1'); + + const rejectReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships/1/reject'), + ); + rejectReq.flush({}); + + const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + pendingReq.flush([]); + + expect(component.isProcessing()).toBeNull(); + }); + + it('should not send reject request if already processing', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.rejectMembership('1'); + component.rejectMembership('1'); + + const rejectReqs = httpMock.match( + (r) => r.url.includes('/admin/memberships/1/reject'), + ); + expect(rejectReqs.length).toBe(1); + + rejectReqs[0].flush({}); + + const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + pendingReq.flush([]); + }); + + it('should reset isProcessing on error', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.rejectMembership('1'); + + const rejectReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships/1/reject'), + ); + rejectReq.flush({ message: 'Fehler' }, { status: 500, statusText: 'Server Error' }); + + expect(component.isProcessing()).toBeNull(); + }); }); describe('getStatusLabel', () => { @@ -177,8 +403,7 @@ describe('AdminMembershipsComponent', () => { describe('template', () => { it('should render three tabs', () => { fixture.detectChanges(); - const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - req.flush([]); + flushPendingRequest(); fixture.detectChanges(); const tabs = fixture.nativeElement.querySelectorAll('[role="tab"]'); @@ -190,8 +415,7 @@ describe('AdminMembershipsComponent', () => { it('should show pending tab as active by default', () => { fixture.detectChanges(); - const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - req.flush([]); + flushPendingRequest(); fixture.detectChanges(); const activeTab = fixture.nativeElement.querySelector('[role="tab"][aria-selected="true"]'); @@ -208,10 +432,30 @@ describe('AdminMembershipsComponent', () => { expect(cards.length).toBe(1); }); + it('should show approve and reject buttons for pending membership cards', () => { + fixture.detectChanges(); + const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + req.flush([mockPendingMembership]); + fixture.detectChanges(); + + const buttons: NodeListOf = fixture.nativeElement.querySelectorAll('button'); + const buttonTexts = Array.from(buttons).map((b) => b.textContent?.trim()); + expect(buttonTexts).toContain('Genehmigen'); + expect(buttonTexts).toContain('Ablehnen'); + }); + + it('should show empty state for pending tab when no pending memberships', () => { + fixture.detectChanges(); + flushPendingRequest(); + fixture.detectChanges(); + + const emptyMsg = fixture.nativeElement.querySelector('.bg-gray-50 p'); + expect(emptyMsg?.textContent).toContain('Keine wartenden Mitgliedschaftsanträge'); + }); + it('should show table view for active tab', () => { fixture.detectChanges(); - const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - pendingReq.flush([]); + flushPendingRequest(); component.switchTab('active'); const allReq = httpMock.expectOne( @@ -228,8 +472,7 @@ describe('AdminMembershipsComponent', () => { it('should show table view for all tab', () => { fixture.detectChanges(); - const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); - pendingReq.flush([]); + flushPendingRequest(); component.switchTab('all'); const allReq = httpMock.expectOne( @@ -243,5 +486,89 @@ describe('AdminMembershipsComponent', () => { const rows = fixture.nativeElement.querySelectorAll('tbody tr'); expect(rows.length).toBe(2); }); + + it('should show empty state for active tab when no active memberships', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.switchTab('active'); + const allReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + allReq.flush([]); + fixture.detectChanges(); + + const emptyMsg = fixture.nativeElement.querySelector('.bg-gray-50 p'); + expect(emptyMsg?.textContent).toContain('Keine aktiven Mitgliedschaften'); + }); + + it('should show empty state for all tab when no memberships exist', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.switchTab('all'); + const allReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + allReq.flush([]); + fixture.detectChanges(); + + const emptyMsg = fixture.nativeElement.querySelector('.bg-gray-50 p'); + expect(emptyMsg?.textContent).toContain('Keine Mitgliedschaften vorhanden'); + }); + + it('should show status badge in table row', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.switchTab('all'); + const allReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + allReq.flush([mockActiveMembership]); + fixture.detectChanges(); + + const badge = fixture.nativeElement.querySelector('span.rounded-full'); + expect(badge).toBeTruthy(); + expect(badge?.textContent?.trim()).toBe('Aktiv'); + }); + + it('should show status badge in all tab table', () => { + fixture.detectChanges(); + flushPendingRequest(); + + component.switchTab('all'); + const allReq = httpMock.expectOne( + (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), + ); + allReq.flush([mockActiveMembership]); + fixture.detectChanges(); + + const badge = fixture.nativeElement.querySelector('span.rounded-full'); + expect(badge?.textContent?.trim()).toBe('Aktiv'); + }); + + it('should display user name and email in pending card', () => { + fixture.detectChanges(); + const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + req.flush([mockPendingMembership]); + fixture.detectChanges(); + + const cardText = fixture.nativeElement.querySelector('.bg-white.border')?.textContent; + expect(cardText).toContain('Max Mustermann'); + expect(cardText).toContain('max@test.at'); + }); + + it('should display community, atNumber and priority in pending card', () => { + fixture.detectChanges(); + const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); + req.flush([mockPendingMembership]); + fixture.detectChanges(); + + const cardText = fixture.nativeElement.querySelector('.bg-white.border')?.textContent; + expect(cardText).toContain('Test Community'); + expect(cardText).toContain('AT0000000000000000001'); + expect(cardText).toContain('1'); + }); }); }); diff --git a/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts b/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts index a472e9d..ba001ab 100644 --- a/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts +++ b/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts @@ -189,7 +189,9 @@ export class UserTariffComponent implements OnInit, OnDestroy { onDelete(tariff: UserTariffResponse) { if (!confirm('Möchtest du diesen Tarif wirklich löschen?')) return; - this.userTariffService.deleteUserTariff(tariff.id!).subscribe({ + const userId = this.authService.getCurrentUserId(); + if (!userId) return; + this.userTariffService.deleteUserTariff(tariff.id!, userId).subscribe({ next: () => { this.toastService.success('Tarif erfolgreich gelöscht.'); this.loadUserTariffs(this.selectedCommunityId());