test: facade coverage for orders, moderation, transactions, monitoring, products (F63 partial)
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>
This commit is contained in:
sdarbinyan
2026-08-18 21:38:42 +04:00
parent ec949b5a19
commit f8063b320e
5 changed files with 761 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
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']);
});
});
});