import { Test, TestingModule } from '@nestjs/testing'; import { addDays, berlinToday, daysBetween, dayToDate, } from '../src/common/dates'; import { PrismaModule } from '../src/prisma/prisma.module'; import { PrismaService } from '../src/prisma/prisma.service'; import { RECENT_WINDOW_DAYS } from '../src/weather/history/weather-history.config'; import { WeatherHistoryModule } from '../src/weather/history/weather-history.module'; import { WeatherHistoryService } from '../src/weather/history/weather-history.service'; // Two stands on one rounded point (61.23 / 5.31) and one on a point that fails. // Other e2e files share the database, so nothing here counts requests or rows // of other stands. const GOOD = { lat: '61.23', lon: '5.31' }; const BAD = { lat: '62.11', lon: '6.12' }; function json(body: unknown): Response { return new Response(JSON.stringify(body), { status: 200 }); } function days(from: string, to: string): string[] { const out: string[] = []; for (let day = from; day <= to; day = addDays(day, 1)) out.push(day); return out; } describe('Weather history (e2e)', () => { const today = berlinToday(); const yesterday = addDays(today, -1); const januaryFirst = `${today.slice(0, 4)}-01-01`; const expectedDays = Math.max(0, daysBetween(januaryFirst, yesterday) + 1); const suffix = `${Date.now()}`; let moduleRef: TestingModule; let prisma: PrismaService; let service: WeatherHistoryService; let fetchMock: jest.SpiedFunction; let apiaryIds: string[]; let badApiaryId: string; const ownCalls = (point: { lat: string; lon: string }): URL[] => fetchMock.mock.calls .map(([url]) => url as URL) .filter( (url) => url.searchParams.get('latitude') === point.lat && url.searchParams.get('longitude') === point.lon, ); beforeAll(async () => { moduleRef = await Test.createTestingModule({ imports: [PrismaModule, WeatherHistoryModule], }).compile(); await moduleRef.init(); prisma = moduleRef.get(PrismaService); service = moduleRef.get(WeatherHistoryService); const a = await prisma.apiary.create({ data: { name: `Weather history A ${suffix}`, latitude: 61.2345, longitude: 5.3109, }, }); const b = await prisma.apiary.create({ data: { name: `Weather history B ${suffix}`, latitude: 61.2349, longitude: 5.3101, }, }); const bad = await prisma.apiary.create({ data: { name: `Weather history bad ${suffix}`, latitude: 62.1149, longitude: 6.1201, }, }); apiaryIds = [a.id, b.id]; badApiaryId = bad.id; }); afterAll(async () => { await prisma.apiary.deleteMany({ where: { id: { in: [...apiaryIds, badApiaryId] } }, }); await moduleRef.close(); }); beforeEach(() => { fetchMock = jest.spyOn(global, 'fetch').mockImplementation((input) => { const url = input as URL; const lat = url.searchParams.get('latitude'); if (lat === BAD.lat) { return Promise.resolve(new Response('nope', { status: 500 })); } if (lat !== GOOD.lat) { // Stands of other e2e files: no data. return Promise.resolve( json({ daily: { time: [], temperature_2m_mean: [] } }), ); } const archive = url.searchParams.has('start_date'); const time = archive ? days( url.searchParams.get('start_date') as string, url.searchParams.get('end_date') as string, ) : days(addDays(today, -92), today); return Promise.resolve( json({ daily: { time, temperature_2m_mean: time.map(() => 10.5) }, }), ); }); }); afterEach(() => { fetchMock.mockRestore(); }); it('fetches missing days once, with rounded coordinates, and writes nothing on an error', async () => { const controller = new AbortController(); const result = await service.syncOnce(today, controller.signal); // The failing point wrote nothing and counted as failed. expect(result.failedPoints).toBeGreaterThanOrEqual(1); expect( await prisma.apiaryWeatherDay.count({ where: { apiaryId: badApiaryId } }), ).toBe(0); // One archive and one forecast request for the shared rounded point. const calls = ownCalls(GOOD); expect(calls.length).toBe(expectedDays > 0 ? 2 : 0); for (const url of calls) { expect(url.searchParams.get('latitude')).toBe('61.23'); expect(url.searchParams.get('longitude')).toBe('5.31'); expect(url.searchParams.get('daily')).toBe('temperature_2m_mean'); expect(url.searchParams.get('timezone')).toBe('Europe/Berlin'); } // Both stands of the point carry every day from January 1st to yesterday. const recentStart = addDays(today, -RECENT_WINDOW_DAYS); for (const apiaryId of apiaryIds) { const rows = await prisma.apiaryWeatherDay.findMany({ where: { apiaryId }, orderBy: { day: 'asc' }, }); expect(rows).toHaveLength(expectedDays); for (const row of rows) { const day = row.day.toISOString().slice(0, 10); expect(row.meanTempC).toBe(10.5); expect(row.source).toBe(day >= recentStart ? 'forecast' : 'archive'); expect(day >= januaryFirst && day <= yesterday).toBe(true); } } if (expectedDays > 0) { expect( await prisma.apiaryWeatherDay.count({ where: { apiaryId: apiaryIds[0], day: dayToDate(yesterday) }, }), ).toBe(1); } }); it('asks for nothing when every day is already stored', async () => { await service.syncOnce(today, new AbortController().signal); expect(ownCalls(GOOD)).toHaveLength(0); }); });