diff --git a/server/src/service.ts b/server/src/service.ts index b60a1d5..bb75048 100644 --- a/server/src/service.ts +++ b/server/src/service.ts @@ -259,8 +259,10 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, for const lastFetchExpired = !existingMenu?.lastUpdate || existingMenu.lastUpdate === now || // freshly initialized, never fetched (now - existingMenu.lastUpdate) > MENU_REFETCH_TTL_MS; - const shouldFetch = forceRefresh || - (!existingMenu?.food?.length && !existingMenu?.closed && lastFetchExpired); + // Data označená jako "z minulého týdne" se musí zkoušet načíst znovu, jinak by u nich + // varování zůstalo viset celý týden i poté, co podnik nabídku na svém webu aktualizuje. + const needsData = existingMenu?.isStale || (!existingMenu?.food?.length && !existingMenu?.closed); + const shouldFetch = forceRefresh || (needsData && lastFetchExpired); if (shouldFetch) { const firstDay = getFirstWorkDayOfWeek(usedDate); diff --git a/server/src/tests/service.staleMenu.test.ts b/server/src/tests/service.staleMenu.test.ts new file mode 100644 index 0000000..fbeda63 --- /dev/null +++ b/server/src/tests/service.staleMenu.test.ts @@ -0,0 +1,87 @@ +const mockStorageData = new Map(); +jest.mock('../storage', () => ({ + __esModule: true, + default: () => ({ + hasData: async (key: string) => mockStorageData.has(key), + getData: async (key: string) => mockStorageData.get(key) as T, + setData: async (key: string, val: T) => void mockStorageData.set(key, val), + }), + storageReady: Promise.resolve(), +})); + +const mockGetMenuTechTower = jest.fn(); +jest.mock('../restaurants', () => ({ + ...jest.requireActual('../restaurants'), + getMenuTechTower: (...args: any[]) => mockGetMenuTechTower(...args), +})); + +import { getMenuKey, getRestaurantMenu } from '../service'; + +// Středa 2025-01-08 (týden 2025-02) +const STREDA = new Date('2025-01-08T10:00:00'); +const HODINA = 60 * 60 * 1000; + +/** Menu jednoho dne se všemi náležitostmi, aby nevznikala jiná varování. */ +const denniMenu = (nazev: string) => [ + { amount: '-', name: `Polévka ${nazev}`, price: '30 Kč', isSoup: true }, + { amount: '-', name: nazev, price: '150 Kč', isSoup: false }, +]; + +/** Naplní storage týdenním menu TechTower s předaným příznakem zastaralosti. */ +const seedWeekMenu = (isStale: boolean, lastUpdate: number) => { + const week = [0, 1, 2, 3, 4].map(i => ({ + TECHTOWER: { + lastUpdate, + closed: false, + isStale, + food: denniMenu(`Jídlo z minulého týdne ${i}`), + }, + })); + mockStorageData.set(getMenuKey(STREDA), week); +}; + +describe('getRestaurantMenu – obnovení dat z minulého týdne', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(STREDA); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + mockStorageData.clear(); + mockGetMenuTechTower.mockReset(); + }); + + test('zastaralá data se znovu načtou a varování zmizí', async () => { + seedWeekMenu(true, Date.now() - 2 * HODINA); + mockGetMenuTechTower.mockResolvedValue([0, 1, 2, 3, 4].map(i => denniMenu(`Aktuální jídlo ${i}`))); + + const menu = await getRestaurantMenu('TECHTOWER', STREDA); + + expect(mockGetMenuTechTower).toHaveBeenCalledTimes(1); + expect(menu.isStale).toBe(false); + expect(menu.food?.[1].name).toBe('Aktuální jídlo 2'); + expect(menu.warnings).not.toContain('Data jsou z minulého týdne'); + }); + + test('aktuální data se znovu nenačítají', async () => { + seedWeekMenu(false, Date.now() - 2 * HODINA); + + const menu = await getRestaurantMenu('TECHTOWER', STREDA); + + expect(mockGetMenuTechTower).not.toHaveBeenCalled(); + expect(menu.warnings).toEqual([]); + }); + + test('zastaralá data se neobnovují častěji než jednou za hodinu', async () => { + seedWeekMenu(true, Date.now() - 5 * 60 * 1000); + + const menu = await getRestaurantMenu('TECHTOWER', STREDA); + + expect(mockGetMenuTechTower).not.toHaveBeenCalled(); + expect(menu.warnings).toContain('Data jsou z minulého týdne'); + }); +});