test(frontend): enhance AdminMembershipsComponent tests and fix deleteUserTariff compilation error

- Expand admin-memberships spec from 19 to 42 tests covering:
  approve/reject API calls, isProcessing guards, error handling,
  loading states, tab switching, card/table views, empty states
- Fix deleteUserTariff missing userId argument in user-tariff.ts
  (required after backend API change)
This commit is contained in:
Bernhard Müller 2026-07-24 08:25:00 +02:00
parent 71ecf47948
commit 43f3bb6d8a
2 changed files with 397 additions and 68 deletions

View File

@ -57,27 +57,44 @@ describe('AdminMembershipsComponent', () => {
httpMock.verify(); httpMock.verify();
}); });
it('should create', () => { function flushPendingRequest(): void {
fixture.detectChanges();
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
req.flush([]); req.flush([]);
}
it('should create', () => {
fixture.detectChanges();
flushPendingRequest();
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
describe('initial state', () => {
it('should default activeTab to pending', () => { it('should default activeTab to pending', () => {
fixture.detectChanges(); fixture.detectChanges();
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); flushPendingRequest();
req.flush([]);
expect(component.activeTab()).toBe('pending'); expect(component.activeTab()).toBe('pending');
}); });
it('should initialize allMemberships as empty array', () => { it('should initialize allMemberships as empty array', () => {
fixture.detectChanges(); fixture.detectChanges();
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); flushPendingRequest();
req.flush([]);
expect(component.allMemberships()).toEqual([]); 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);
});
});
describe('loadPendingMemberships', () => {
it('should load pending memberships on init', () => { it('should load pending memberships on init', () => {
fixture.detectChanges(); fixture.detectChanges();
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
@ -87,10 +104,26 @@ describe('AdminMembershipsComponent', () => {
expect(component.pendingMemberships()[0].firstName).toBe('Max'); 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);
});
});
describe('loadAllMemberships', () => {
it('should load all memberships when switching to active tab', () => { it('should load all memberships when switching to active tab', () => {
fixture.detectChanges(); fixture.detectChanges();
const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); flushPendingRequest();
pendingReq.flush([]);
component.switchTab('active'); component.switchTab('active');
@ -105,24 +138,56 @@ describe('AdminMembershipsComponent', () => {
it('should load all memberships when switching to all tab', () => { it('should load all memberships when switching to all tab', () => {
fixture.detectChanges(); fixture.detectChanges();
const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); flushPendingRequest();
pendingReq.flush([]);
component.switchTab('all'); component.switchTab('all');
const allReq = httpMock.expectOne( const allReq = httpMock.expectOne(
(r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'), (r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'),
); );
expect(allReq.request.method).toBe('GET');
allReq.flush([mockPendingMembership, mockActiveMembership]); allReq.flush([mockPendingMembership, mockActiveMembership]);
expect(component.allMemberships().length).toBe(2); 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);
});
});
describe('switchTab', () => {
it('should switch activeTab signal', () => { it('should switch activeTab signal', () => {
fixture.detectChanges(); fixture.detectChanges();
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); flushPendingRequest();
req.flush([]);
component.switchTab('active'); component.switchTab('active');
const activeReq = httpMock.expectOne( const activeReq = httpMock.expectOne(
@ -142,6 +207,167 @@ describe('AdminMembershipsComponent', () => {
expect(component.activeTab()).toBe('all'); 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);
});
});
describe('approveMembership', () => {
it('should call approve API and reload pending memberships', () => {
fixture.detectChanges();
flushPendingRequest();
component.approveMembership('1');
const approveReq = httpMock.expectOne(
(r) => r.url.includes('/admin/memberships/1/approve'),
);
expect(approveReq.request.method).toBe('PUT');
approveReq.flush({});
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();
});
});
describe('rejectMembership', () => {
it('should call reject API and reload pending memberships', () => {
fixture.detectChanges();
flushPendingRequest();
component.rejectMembership('1');
const rejectReq = httpMock.expectOne(
(r) => r.url.includes('/admin/memberships/1/reject'),
);
expect(rejectReq.request.method).toBe('PUT');
rejectReq.flush({});
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', () => { describe('getStatusLabel', () => {
it('should return "Ausstehend" for PENDING', () => { it('should return "Ausstehend" for PENDING', () => {
expect(component.getStatusLabel('PENDING')).toBe('Ausstehend'); expect(component.getStatusLabel('PENDING')).toBe('Ausstehend');
@ -177,8 +403,7 @@ describe('AdminMembershipsComponent', () => {
describe('template', () => { describe('template', () => {
it('should render three tabs', () => { it('should render three tabs', () => {
fixture.detectChanges(); fixture.detectChanges();
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); flushPendingRequest();
req.flush([]);
fixture.detectChanges(); fixture.detectChanges();
const tabs = fixture.nativeElement.querySelectorAll('[role="tab"]'); const tabs = fixture.nativeElement.querySelectorAll('[role="tab"]');
@ -190,8 +415,7 @@ describe('AdminMembershipsComponent', () => {
it('should show pending tab as active by default', () => { it('should show pending tab as active by default', () => {
fixture.detectChanges(); fixture.detectChanges();
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); flushPendingRequest();
req.flush([]);
fixture.detectChanges(); fixture.detectChanges();
const activeTab = fixture.nativeElement.querySelector('[role="tab"][aria-selected="true"]'); const activeTab = fixture.nativeElement.querySelector('[role="tab"][aria-selected="true"]');
@ -208,10 +432,30 @@ describe('AdminMembershipsComponent', () => {
expect(cards.length).toBe(1); 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<HTMLButtonElement> = 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', () => { it('should show table view for active tab', () => {
fixture.detectChanges(); fixture.detectChanges();
const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); flushPendingRequest();
pendingReq.flush([]);
component.switchTab('active'); component.switchTab('active');
const allReq = httpMock.expectOne( const allReq = httpMock.expectOne(
@ -228,8 +472,7 @@ describe('AdminMembershipsComponent', () => {
it('should show table view for all tab', () => { it('should show table view for all tab', () => {
fixture.detectChanges(); fixture.detectChanges();
const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending')); flushPendingRequest();
pendingReq.flush([]);
component.switchTab('all'); component.switchTab('all');
const allReq = httpMock.expectOne( const allReq = httpMock.expectOne(
@ -243,5 +486,89 @@ describe('AdminMembershipsComponent', () => {
const rows = fixture.nativeElement.querySelectorAll('tbody tr'); const rows = fixture.nativeElement.querySelectorAll('tbody tr');
expect(rows.length).toBe(2); 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');
});
}); });
}); });

View File

@ -189,7 +189,9 @@ export class UserTariffComponent implements OnInit, OnDestroy {
onDelete(tariff: UserTariffResponse) { onDelete(tariff: UserTariffResponse) {
if (!confirm('Möchtest du diesen Tarif wirklich löschen?')) return; 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: () => { next: () => {
this.toastService.success('Tarif erfolgreich gelöscht.'); this.toastService.success('Tarif erfolgreich gelöscht.');
this.loadUserTariffs(this.selectedCommunityId()); this.loadUserTariffs(this.selectedCommunityId());