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,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<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', () => {
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');
});
});
});

View File

@ -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());