Add scraper indexes and switch to validation collection names
All checks were successful
CI/CD Pipeline - Apartment API / Scan Dependencies (pull_request) Successful in 13s
CI/CD Pipeline - Apartment API / Lint & Test (pull_request) Successful in 43s
CI/CD Pipeline - Apartment API / Send Webhook Notification (pull_request) Successful in 2s
CI/CD Pipeline - Apartment API / Build & Push Image (pull_request) Has been skipped
CI/CD Pipeline - Apartment API / Deploy to Production (pull_request) Has been skipped

Add createScraperIndexes() to create indexes on the scraper_runs
collection: a compound index on status+startedAt for active job queries
and a descending index on startedAt for recent run lookups.

Change default collection names from production tables
(units_migration_test, unit_prices_migration_test) to dedicated
validation collections (units_scraper, unit_prices_scraper). This
enables the Node.js scraper to run in parallel with the existing Python
scraper during validation without interfering with production data.

Update all scraper test files to reference the new default collection
names.
This commit is contained in:
2026-02-07 12:04:37 -07:00
parent 80d44bca04
commit 2e3ef0580c
8 changed files with 101 additions and 20 deletions

View File

@ -22,7 +22,7 @@ let mongoServer;
let client;
let db;
const PRICES_COLLECTION = 'unit_prices_migration_test';
const PRICES_COLLECTION = 'unit_prices_scraper';
// Mock logger for capturing log calls
function createMockLogger() {

View File

@ -21,7 +21,7 @@ let mongoServer;
let client;
let db;
const UNITS_COLLECTION = 'units_migration_test';
const UNITS_COLLECTION = 'units_scraper';
// Mock logger for capturing log calls
function createMockLogger() {

View File

@ -142,14 +142,14 @@ describe('config/scraper', () => {
describe('COLLECTIONS', () => {
describe('default values', () => {
test('UNITS should default to "units_migration_test"', () => {
test('UNITS should default to "units_scraper"', () => {
const config = require('../../config/scraper');
expect(config.COLLECTIONS.UNITS).toBe('units_migration_test');
expect(config.COLLECTIONS.UNITS).toBe('units_scraper');
});
test('PRICES should default to "unit_prices_migration_test"', () => {
test('PRICES should default to "unit_prices_scraper"', () => {
const config = require('../../config/scraper');
expect(config.COLLECTIONS.PRICES).toBe('unit_prices_migration_test');
expect(config.COLLECTIONS.PRICES).toBe('unit_prices_scraper');
});
test('DAILY_SUMMARIES should default to "daily_summaries"', () => {

View File

@ -118,8 +118,8 @@ jest.mock('../../config/scraper', () => ({
RETRY_CONFIG: { maxRetries: 3, baseDelay: 1000, timeout: 30000 },
SHUTDOWN_TIMEOUT: 30000,
COLLECTIONS: {
UNITS: 'units_migration_test',
PRICES: 'unit_prices_migration_test',
UNITS: 'units_scraper',
PRICES: 'unit_prices_scraper',
DAILY_SUMMARIES: 'daily_summaries',
SCRAPER_RUNS: 'scraper_runs'
}

View File

@ -20,9 +20,10 @@ let mongoServer;
let client;
let db;
// We will require runScrape and recordScraperRun after implementation
// We will require runScrape, recordScraperRun, and createScraperIndexes after implementation
let runScrape;
let recordScraperRun;
let createScraperIndexes;
// Store original module references so we can mock individual functions
let scraperService;
@ -36,7 +37,7 @@ beforeAll(async () => {
// Dynamically require to pick up implementation
scraperService = require('../../services/scraperService');
({ runScrape, recordScraperRun } = scraperService);
({ runScrape, recordScraperRun, createScraperIndexes } = scraperService);
});
afterAll(async () => {
@ -219,11 +220,11 @@ describe('runScrape', () => {
expect(result.staleUnitsCount).toBe(0);
// Verify no units were written to the units collection
const unitsCount = await db.collection('units_migration_test').countDocuments();
const unitsCount = await db.collection('units_scraper').countDocuments();
expect(unitsCount).toBe(0);
// Verify no prices were written
const pricesCount = await db.collection('unit_prices_migration_test').countDocuments();
const pricesCount = await db.collection('unit_prices_scraper').countDocuments();
expect(pricesCount).toBe(0);
});
@ -277,7 +278,7 @@ describe('runScrape', () => {
const faultyDb = {
collection: (name) => {
const realCollection = db.collection(name);
if (name === 'units_migration_test') {
if (name === 'units_scraper') {
return new Proxy(realCollection, {
get(target, prop) {
if (prop === 'bulkWrite') {
@ -343,7 +344,7 @@ describe('runScrape', () => {
const faultyDb = {
collection: (name) => {
const realCollection = db.collection(name);
if (name === 'unit_prices_migration_test') {
if (name === 'unit_prices_scraper') {
return new Proxy(realCollection, {
get(target, prop) {
if (prop === 'bulkWrite') {
@ -388,7 +389,7 @@ describe('runScrape', () => {
const yesterdayStr = yesterday.toISOString().split('T')[0];
// Yesterday had units: OLD-A, OLD-B, OLD-C
await db.collection('unit_prices_migration_test').insertMany([
await db.collection('unit_prices_scraper').insertMany([
{ unit_code: 'OLD-A', date_checked: yesterdayStr, price: 1000 },
{ unit_code: 'OLD-B', date_checked: yesterdayStr, price: 1100 },
{ unit_code: 'OLD-C', date_checked: yesterdayStr, price: 1200 }
@ -631,7 +632,7 @@ describe('sanitizeError', () => {
const faultyDb = {
collection: (name) => {
const realCollection = db.collection(name);
if (name === 'units_migration_test') {
if (name === 'units_scraper') {
return new Proxy(realCollection, {
get(target, prop) {
if (prop === 'bulkWrite') {
@ -674,3 +675,51 @@ describe('sanitizeError', () => {
});
});
});
// ============================================================
// Test: createScraperIndexes
// ============================================================
describe('createScraperIndexes', () => {
it('should create the status_startedAt compound index', async () => {
await createScraperIndexes(db);
const indexes = await db.collection('scraper_runs').indexes();
const statusIndex = indexes.find(idx => idx.name === 'status_startedAt');
expect(statusIndex).toBeTruthy();
expect(statusIndex.key).toEqual({ status: 1, startedAt: -1 });
});
it('should create the startedAt_desc index', async () => {
await createScraperIndexes(db);
const indexes = await db.collection('scraper_runs').indexes();
const startedAtIndex = indexes.find(idx => idx.name === 'startedAt_desc');
expect(startedAtIndex).toBeTruthy();
expect(startedAtIndex.key).toEqual({ startedAt: -1 });
});
it('should be idempotent (no error on second call)', async () => {
await createScraperIndexes(db);
// Calling again should not throw
await expect(createScraperIndexes(db)).resolves.not.toThrow();
// Indexes should still exist
const indexes = await db.collection('scraper_runs').indexes();
const statusIndex = indexes.find(idx => idx.name === 'status_startedAt');
const startedAtIndex = indexes.find(idx => idx.name === 'startedAt_desc');
expect(statusIndex).toBeTruthy();
expect(startedAtIndex).toBeTruthy();
});
it('should handle database errors gracefully', async () => {
// Pass an object that will throw when collection() is called
const faultyDb = {
collection: () => {
throw new Error('Connection lost');
}
};
await expect(createScraperIndexes(faultyDb)).rejects.toThrow('Connection lost');
});
});

View File

@ -56,7 +56,7 @@ beforeEach(async () => {
});
describe('upsertUnits', () => {
const UNITS_COLLECTION = 'units_migration_test';
const UNITS_COLLECTION = 'units_scraper';
// ---------------------------------------------------------------
// 1. bulkWrite is called with correct updateOne operations