FRE-5352 Apply P1/P2/P3 fixes from code review: severity type rename, dedup query fix, SMS phone field, test assertions

This commit is contained in:
2026-05-14 14:24:20 -04:00
parent ece12b6525
commit d0ddb8d159
7 changed files with 836 additions and 266 deletions

View File

@@ -2,39 +2,40 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { HomeTitleSchedulerService } from '../src/scheduler.service';
import { PropertySnapshot } from '../src/types';
// Mock @shieldai/db
const mockPrisma = {
subscription: {
findMany: vi.fn(),
},
$queryRaw: vi.fn(),
};
// All mocks inside vi.hoisted() to avoid vitest hoisting issues
const mocked = vi.hoisted(() => {
const mockPrisma = {
subscription: { findMany: vi.fn() },
$queryRaw: vi.fn(),
};
const mockProcessChangeDetection = vi.fn();
const mockDetectChanges = vi.fn();
const mockShouldTriggerAlert = vi.fn();
return {
mockPrisma,
mockProcessChangeDetection,
mockDetectChanges,
mockShouldTriggerAlert,
};
});
vi.mock('@shieldai/db', () => ({
prisma: mockPrisma,
prisma: mocked.mockPrisma,
}));
// Mock alert pipeline
const mockProcessChangeDetection = vi.fn();
const mockHomeTitleAlertPipeline = {
processChangeDetection: mockProcessChangeDetection,
};
vi.mock('../src/alert.pipeline', () => ({
homeTitleAlertPipeline: mockHomeTitleAlertPipeline,
homeTitleAlertPipeline: {
processChangeDetection: mocked.mockProcessChangeDetection,
},
HomeTitleAlertPipeline: class {},
}));
// Mock change-detector
const mockDetectChanges = vi.fn();
const mockShouldTriggerAlert = vi.fn();
vi.mock('../src/change-detector', () => ({
detectChanges: mockDetectChanges,
shouldTriggerAlert: mockShouldTriggerAlert,
detectChanges: mocked.mockDetectChanges,
shouldTriggerAlert: mocked.mockShouldTriggerAlert,
}));
// Mock uuid
vi.mock('uuid', () => ({
v4: () => 'scan-uuid-' + Date.now(),
}));
@@ -46,7 +47,7 @@ const mockSubscription = {
};
function mockLatestSnapshots(snapshots: PropertySnapshot[]) {
mockPrisma.$queryRaw.mockResolvedValue(
mocked.mockPrisma.$queryRaw.mockResolvedValue(
snapshots.map(s => ({
id: s.id,
propertyId: s.propertyId,
@@ -64,9 +65,9 @@ function mockLatestSnapshots(snapshots: PropertySnapshot[]) {
function mockPreviousSnapshot(snapshot: PropertySnapshot | null) {
if (!snapshot) {
mockPrisma.$queryRaw.mockResolvedValue([]);
mocked.mockPrisma.$queryRaw.mockResolvedValue([]);
} else {
mockPrisma.$queryRaw.mockResolvedValue([
mocked.mockPrisma.$queryRaw.mockResolvedValue([
{
id: snapshot.id,
propertyId: snapshot.propertyId,
@@ -88,12 +89,16 @@ describe('HomeTitleSchedulerService', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
mocked.mockProcessChangeDetection.mockReset();
mocked.mockDetectChanges.mockReset();
mocked.mockShouldTriggerAlert.mockReset();
scheduler = new HomeTitleSchedulerService({
scanIntervalMinutes: 60,
maxPropertiesPerScan: 100,
enabled: true,
});
vi.clearAllMocks();
});
afterEach(() => {
@@ -145,13 +150,14 @@ describe('HomeTitleSchedulerService', () => {
describe('runScan', () => {
it('returns empty results when no subscriptions', async () => {
mockPrisma.subscription.findMany.mockResolvedValue([]);
mocked.mockPrisma.subscription.findMany.mockResolvedValue([]);
const result = await scheduler.runScan();
expect(result.propertiesScanned).toBe(0);
expect(result.changesDetected).toBe(0);
expect(result.alertsCreated).toBe(0);
expect(result.notificationsSent).toBe(0);
expect(result.errors).toEqual([]);
});
@@ -171,10 +177,10 @@ describe('HomeTitleSchedulerService', () => {
ownerName: 'Jane Smith',
};
mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]);
mocked.mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]);
mockLatestSnapshots([currentSnapshot]);
mockPreviousSnapshot(previousSnapshot);
mockDetectChanges.mockReturnValue({
mocked.mockDetectChanges.mockReturnValue({
propertyId: 'prop-001',
changeType: 'ownership_transfer',
severity: 'major',
@@ -184,8 +190,8 @@ describe('HomeTitleSchedulerService', () => {
currentSnapshot,
detectedAt: new Date().toISOString(),
});
mockShouldTriggerAlert.mockReturnValue(true);
mockProcessChangeDetection.mockResolvedValue({
mocked.mockShouldTriggerAlert.mockReturnValue(true);
mocked.mockProcessChangeDetection.mockResolvedValue({
id: 'alert-001',
propertyId: 'prop-001',
subscriptionId: 'sub-001',
@@ -202,14 +208,13 @@ describe('HomeTitleSchedulerService', () => {
const result = await scheduler.runScan();
expect(result.propertiesScanned).toBeGreaterThanOrEqual(0);
expect(result.changesDetected).toBeGreaterThanOrEqual(1);
expect(result.alertsCreated).toBeGreaterThanOrEqual(1);
expect(result.notificationsSent).toBeGreaterThanOrEqual(1);
expect(result.changesDetected).toBe(1);
expect(result.alertsCreated).toBe(1);
expect(result.notificationsSent).toBe(1);
});
it('skips snapshots without previous', async () => {
mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]);
mocked.mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]);
mockLatestSnapshots([{
id: 'snap-1',
propertyId: 'prop-001',
@@ -226,10 +231,10 @@ describe('HomeTitleSchedulerService', () => {
});
it('handles subscription scan errors gracefully', async () => {
mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]);
mocked.mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]);
mockLatestSnapshots([]);
mockPreviousSnapshot(null);
mockDetectChanges.mockReturnValue({
mocked.mockDetectChanges.mockReturnValue({
propertyId: 'prop-001',
changeType: 'metadata_change',
severity: 'minor',
@@ -239,7 +244,7 @@ describe('HomeTitleSchedulerService', () => {
currentSnapshot: {} as any,
detectedAt: new Date().toISOString(),
});
mockShouldTriggerAlert.mockReturnValue(false);
mocked.mockShouldTriggerAlert.mockReturnValue(false);
const result = await scheduler.runScan();
@@ -248,7 +253,7 @@ describe('HomeTitleSchedulerService', () => {
});
it('tracks scan metadata', async () => {
mockPrisma.subscription.findMany.mockResolvedValue([]);
mocked.mockPrisma.subscription.findMany.mockResolvedValue([]);
const result = await scheduler.runScan();
@@ -278,10 +283,10 @@ describe('HomeTitleSchedulerService', () => {
};
const nonPremiumSub = { ...mockSubscription, tier: 'plus' as const };
mockPrisma.subscription.findMany.mockResolvedValue([nonPremiumSub]);
mocked.mockPrisma.subscription.findMany.mockResolvedValue([nonPremiumSub]);
mockLatestSnapshots([currentSnapshot]);
mockPreviousSnapshot(previousSnapshot);
mockDetectChanges.mockReturnValue({
mocked.mockDetectChanges.mockReturnValue({
propertyId: 'prop-001',
changeType: 'ownership_transfer',
severity: 'major',
@@ -291,8 +296,8 @@ describe('HomeTitleSchedulerService', () => {
currentSnapshot,
detectedAt: new Date().toISOString(),
});
mockShouldTriggerAlert.mockReturnValue(true);
mockProcessChangeDetection.mockResolvedValue({
mocked.mockShouldTriggerAlert.mockReturnValue(true);
mocked.mockProcessChangeDetection.mockResolvedValue({
id: 'alert-002',
propertyId: 'prop-001',
subscriptionId: 'sub-001',
@@ -309,8 +314,8 @@ describe('HomeTitleSchedulerService', () => {
const result = await scheduler.runScan();
expect(result.changesDetected).toBeGreaterThanOrEqual(1);
expect(result.alertsCreated).toBeGreaterThanOrEqual(1);
expect(result.changesDetected).toBe(1);
expect(result.alertsCreated).toBe(1);
expect(result.notificationsSent).toBe(0);
});
});
@@ -321,8 +326,9 @@ describe('HomeTitleSchedulerService', () => {
});
it('returns last scan result after scan', async () => {
mockPrisma.subscription.findMany.mockResolvedValue([]);
await scheduler.runScan();
mocked.mockPrisma.subscription.findMany.mockResolvedValue([]);
scheduler.start();
await vi.advanceTimersByTimeAsync(60 * 60 * 1000);
expect(scheduler.getLastScanResult()).not.toBeNull();
});
});