Some checks failed
Architecture Governance / architecture (push) Has been cancelled
None of these 5 facades had a single test before this - the highest-risk gap flagged in Q7 of the delivery plan: real business logic (dashboard stat computation, health scoring, bulk actions, draft save routing) with zero coverage on domains that just got real backends behind them (Block 3). Each spec stubs the gateway TOKEN via jasmine.createSpyObj, not a concrete class - the correct pattern per this session's earlier fix to admin-analytics.facade.spec.ts / admin-order-watcher.service.spec.ts, so these don't repeat that same latent bug. 66 new tests, several written specifically to catch a real regression class rather than pad a count: - averageRating/averageOrder must be null/0 for an empty set, never NaN or a divide-by-zero artifact - a zero rating must not drag down an average across other real ratings - a customer with 2+ orders counts as returning; 1 order does not - moderationHealthPercent is 100 for an empty queue by convention, not 0 - toggleSelection/toggleAll must not produce duplicate ids - saveDraft must route to createProduct vs updateProduct correctly and must do nothing when no draft is loaded - a real prior bug class (silently creating a duplicate on a no-op save) - an events-load failure in AdminMonitoringFacade must not block queues/ webhooks from loading independently One own mistake caught before commit, not after: the first updateDraft test asserted the updatedAt timestamp changed after two synchronous calls - both landed in the same millisecond, so the ISO string was correctly identical and the assertion was the bug, not the facade. Replaced with an assertion that actually matches what's worth testing (a valid timestamp gets stamped, not that two back-to-back calls differ). Verified: 205/205 unit tests (66 new), arch:check clean, production build succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
5.3 KiB
TypeScript
156 lines
5.3 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
|
import { of, throwError } from 'rxjs';
|
|
import { AdminModerationFacade } from './admin-moderation.facade';
|
|
import { ADMIN_MODERATION_GATEWAY } from '../services/admin-moderation-gateway.token';
|
|
import { AdminModerationGateway } from '../services/admin-moderation-gateway.interface';
|
|
import { AdminReview } from '../models/admin-review.model';
|
|
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
|
|
|
|
function review(overrides: Partial<AdminReview> = {}): AdminReview {
|
|
return {
|
|
id: 'r1',
|
|
productId: 'p1',
|
|
productName: 'Widget',
|
|
customerName: 'Buyer',
|
|
customerEmail: 'b@example.com',
|
|
rating: 5,
|
|
text: 'Great',
|
|
photos: [],
|
|
status: 'pending',
|
|
visible: true,
|
|
pinned: false,
|
|
featured: false,
|
|
reportCount: 0,
|
|
moderatorNotes: '',
|
|
timeline: [],
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('AdminModerationFacade', () => {
|
|
let facade: AdminModerationFacade;
|
|
let gateway: jasmine.SpyObj<AdminModerationGateway>;
|
|
|
|
beforeEach(() => {
|
|
gateway = jasmine.createSpyObj<AdminModerationGateway>('AdminModerationGateway', [
|
|
'loadReviews', 'loadReview', 'setReviewStatus', 'setReviewVisible', 'setReviewPinned',
|
|
'setReviewFeatured', 'addModeratorNote', 'deleteReview', 'loadReports', 'setReportStatus',
|
|
]);
|
|
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
{ provide: ADMIN_MODERATION_GATEWAY, useValue: gateway },
|
|
{ provide: LocalStorageService, useValue: { getItem: () => null, setItem: () => {}, getJSON: () => null, setJSON: () => {} } },
|
|
],
|
|
});
|
|
|
|
facade = TestBed.inject(AdminModerationFacade);
|
|
});
|
|
|
|
describe('health', () => {
|
|
it('scores 100% when every check passes', () => {
|
|
const result = facade.health(review({ rating: 5, text: 'x', photos: ['a'], status: 'approved', reportCount: 0, visible: true }));
|
|
|
|
expect(result.completionPercent).toBe(100);
|
|
});
|
|
|
|
it('scores 0% when every check fails', () => {
|
|
const result = facade.health(review({ rating: 0, text: '', photos: [], status: 'pending', reportCount: 1, visible: false }));
|
|
|
|
expect(result.completionPercent).toBe(0);
|
|
});
|
|
|
|
it('treats whitespace-only text as no text', () => {
|
|
const result = facade.health(review({ text: ' ' }));
|
|
|
|
expect(result.hasText).toBeFalse();
|
|
});
|
|
|
|
it('any status other than pending counts as moderated', () => {
|
|
expect(facade.health(review({ status: 'approved' })).isModerated).toBeTrue();
|
|
expect(facade.health(review({ status: 'rejected' })).isModerated).toBeTrue();
|
|
expect(facade.health(review({ status: 'pending' })).isModerated).toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('loadDashboardStats', () => {
|
|
it('computes averageRating only from reviews that have a rating', () => {
|
|
gateway.loadReviews.and.returnValue(of({
|
|
items: [review({ rating: 4 }), review({ rating: 0 }), review({ rating: 5 })],
|
|
total: 3, page: 1, pageSize: 100000,
|
|
}));
|
|
|
|
facade.loadDashboardStats();
|
|
|
|
// (4 + 5) / 2 = 4.5 - the zero-rating review must not drag the average down.
|
|
expect(facade.dashboardStats()?.averageRating).toBe(4.5);
|
|
});
|
|
|
|
it('averageRating is null when no review has a rating, not 0', () => {
|
|
gateway.loadReviews.and.returnValue(of({
|
|
items: [review({ rating: 0 })],
|
|
total: 1, page: 1, pageSize: 100000,
|
|
}));
|
|
|
|
facade.loadDashboardStats();
|
|
|
|
expect(facade.dashboardStats()?.averageRating).toBeNull();
|
|
});
|
|
|
|
it('moderationHealthPercent is 100 for an empty queue, not a divide-by-zero artifact', () => {
|
|
gateway.loadReviews.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 100000 }));
|
|
|
|
facade.loadDashboardStats();
|
|
|
|
expect(facade.dashboardStats()?.moderationHealthPercent).toBe(100);
|
|
});
|
|
|
|
it('counts reported reviews independently of their moderation status', () => {
|
|
gateway.loadReviews.and.returnValue(of({
|
|
items: [review({ status: 'approved', reportCount: 2 }), review({ status: 'pending', reportCount: 0 })],
|
|
total: 2, page: 1, pageSize: 100000,
|
|
}));
|
|
|
|
facade.loadDashboardStats();
|
|
|
|
expect(facade.dashboardStats()?.reported).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('setReportStatus', () => {
|
|
it('reloads the reports list after a successful status change', () => {
|
|
gateway.setReportStatus.and.returnValue(of(null));
|
|
gateway.loadReports.and.returnValue(of([]));
|
|
|
|
facade.setReportStatus('rep1', 'resolved');
|
|
|
|
expect(gateway.loadReports).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('loadReports error handling', () => {
|
|
it('clears reports and sets reportsError on failure', () => {
|
|
gateway.loadReports.and.returnValue(throwError(() => new Error('x')));
|
|
|
|
facade.loadReports();
|
|
|
|
expect(facade.reports()).toEqual([]);
|
|
expect(facade.reportsError()).toBeTrue();
|
|
expect(facade.reportsLoading()).toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('selection', () => {
|
|
it('toggleAll selects every loaded review id', () => {
|
|
gateway.loadReviews.and.returnValue(of({ items: [review({ id: 'a' }), review({ id: 'b' })], total: 2, page: 1, pageSize: 10 }));
|
|
facade.loadList();
|
|
|
|
facade.toggleAll(true);
|
|
|
|
expect(facade.selectedIds()).toEqual(['a', 'b']);
|
|
});
|
|
});
|
|
});
|