import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { InMemoryParkingRepository, PARKING_LOGGING_INTERVAL_MS, ParkingStructureID } from "../../src/repositories/InMemoryParkingRepository"; import { IParkingStructure, IParkingStructureTimestampRecord } from "../../src/entities/ParkingRepositoryEntities"; import { CircularQueue } from "../../src/types/CircularQueue"; import { ParkingStructureCountOptions } from "../../src/repositories/ParkingGetterRepository"; describe("InMemoryParkingRepository", () => { let repository: InMemoryParkingRepository; const testStructure: IParkingStructure = { coordinates: { latitude: 33.794795, longitude: -117.850807, }, spotsAvailable: 0, id: "1", name: "Anderson Parking Structure", capacity: 100, address: "300 E Walnut Ave, Orange, CA 92867", updatedTime: new Date(), }; let historicalData: Map> = new Map(); beforeEach(() => { historicalData = new Map(); repository = new InMemoryParkingRepository(new Map(), historicalData); }); describe("addOrUpdateParkingStructure", () => { it("should add a new parking structure", async () => { await repository.addOrUpdateParkingStructure(testStructure); const result = await repository.getParkingStructureById(testStructure.id); expect(result).toEqual(testStructure); }); it("should update existing parking structure", async () => { await repository.addOrUpdateParkingStructure(testStructure); const updatedStructure = { ...testStructure, name: "Updated Garage" }; await repository.addOrUpdateParkingStructure(updatedStructure); const result = await repository.getParkingStructureById(testStructure.id); expect(result).toEqual(updatedStructure); }); it("should log historical data if past the logging interval", async () => { const now = Date.now(); jest .useFakeTimers() .setSystemTime(now); const expectedTimestampRecordMatcher = { spotsAvailable: testStructure.spotsAvailable, id: testStructure.id, timestampMs: now, } await repository.addOrUpdateParkingStructure(testStructure); jest.setSystemTime(now + PARKING_LOGGING_INTERVAL_MS + 60); await repository.addOrUpdateParkingStructure(testStructure); expect(historicalData.get(testStructure.id)?.get(0)).toEqual(expectedTimestampRecordMatcher); expect(historicalData.get(testStructure.id)?.get(1)).toEqual({ ...expectedTimestampRecordMatcher, timestampMs: now + PARKING_LOGGING_INTERVAL_MS + 60, }); }); it("should not log historical data if not past the logging interval", async () => { const now = Date.now(); jest .useFakeTimers() .setSystemTime(now); await repository.addOrUpdateParkingStructure(testStructure); jest.setSystemTime(now + 60); await repository.addOrUpdateParkingStructure(testStructure); expect(historicalData.get(testStructure.id)?.size()).toEqual(1); }); }); describe("removeParkingStructureIfExists", () => { it("should remove existing parking structure and return it", async () => { await repository.addOrUpdateParkingStructure(testStructure); const removed = await repository.removeParkingStructureIfExists(testStructure.id); expect(removed).toEqual(testStructure); const result = await repository.getParkingStructureById(testStructure.id); expect(result).toBeNull(); }); it("should return null when removing non-existent structure", async () => { const result = await repository.removeParkingStructureIfExists("non-existent"); expect(result).toBeNull(); }); }); describe("clearParkingStructureData", () => { it("should remove all parking structures", async () => { const structures = [ testStructure, { ...testStructure, id: "test-id-2", name: "Second Garage" } ]; for (const structure of structures) { await repository.addOrUpdateParkingStructure(structure); } await repository.clearParkingStructureData(); const result = await repository.getParkingStructures(); expect(result).toHaveLength(0); }); }); describe("getParkingStructures", () => { it("should return empty array when no structures exist", async () => { const result = await repository.getParkingStructures(); expect(result).toEqual([]); }); it("should return all added structures", async () => { const structures = [ testStructure, { ...testStructure, id: "test-id-2", name: "Second Garage" } ]; for (const structure of structures) { await repository.addOrUpdateParkingStructure(structure); } const result = await repository.getParkingStructures(); expect(result).toHaveLength(2); expect(result).toEqual(expect.arrayContaining(structures)); }); }); describe("getParkingStructureById", () => { it("should return null for non-existent structure", async () => { const result = await repository.getParkingStructureById("non-existent"); expect(result).toBeNull(); }); it("should return structure by id", async () => { await repository.addOrUpdateParkingStructure(testStructure); const result = await repository.getParkingStructureById(testStructure.id); expect(result).toEqual(testStructure); }); }); describe("getHistoricalAveragesOfParkingStructureCounts", () => { const sortingCallback = (a: IParkingStructureTimestampRecord, b: IParkingStructureTimestampRecord) => a.timestampMs - b.timestampMs; const setupHistoricalData = (records: IParkingStructureTimestampRecord[]) => { const queue = new CircularQueue(10); records.forEach(record => queue.appendWithSorting(record, sortingCallback)); historicalData.set(testStructure.id, queue); }; it("should return empty array for non-existent structure or no data", async () => { const options: ParkingStructureCountOptions = { startUnixEpochMs: 1000, endUnixEpochMs: 2000, intervalMs: 500 }; // Non-existent structure expect(await repository.getHistoricalAveragesOfParkingStructureCounts("non-existent", options)).toEqual([]); // Structure with no historical data await repository.addOrUpdateParkingStructure(testStructure); expect(await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, options)).toEqual([]); }); it("should calculate averages for single and multiple intervals", async () => { const records = [ { id: testStructure.id, spotsAvailable: 80, timestampMs: 1100 }, { id: testStructure.id, spotsAvailable: 70, timestampMs: 1200 }, { id: testStructure.id, spotsAvailable: 50, timestampMs: 1600 }, { id: testStructure.id, spotsAvailable: 40, timestampMs: 1700 } ]; setupHistoricalData(records); await repository.addOrUpdateParkingStructure(testStructure); // Single interval test const singleIntervalOptions: ParkingStructureCountOptions = { startUnixEpochMs: 1000, endUnixEpochMs: 1500, intervalMs: 500 }; const singleResult = await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, singleIntervalOptions); expect(singleResult).toHaveLength(1); expect(singleResult[0]).toEqual({ fromUnixEpochMs: 1000, toUnixEpochMs: 1500, averageSpotsAvailable: 75 // (80 + 70) / 2 }); // Multiple intervals test const multipleIntervalOptions: ParkingStructureCountOptions = { startUnixEpochMs: 1000, endUnixEpochMs: 2000, intervalMs: 500 }; const multipleResult = await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, multipleIntervalOptions); expect(multipleResult).toHaveLength(2); expect(multipleResult[0]).toEqual({ fromUnixEpochMs: 1000, toUnixEpochMs: 1500, averageSpotsAvailable: 75 // (80 + 70) / 2 }); expect(multipleResult[1]).toEqual({ fromUnixEpochMs: 1500, toUnixEpochMs: 2000, averageSpotsAvailable: 45 // (50 + 40) / 2 }); }); it("should handle edge cases: skipped intervals and boundaries", async () => { const records = [ { id: testStructure.id, spotsAvailable: 90, timestampMs: 1000 }, // start boundary - included { id: testStructure.id, spotsAvailable: 80, timestampMs: 1500 }, // interval boundary - included in second { id: testStructure.id, spotsAvailable: 60, timestampMs: 2100 } // skip interval, in third ]; setupHistoricalData(records); await repository.addOrUpdateParkingStructure(testStructure); const options: ParkingStructureCountOptions = { startUnixEpochMs: 1000, endUnixEpochMs: 2500, intervalMs: 500 }; const result = await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, options); expect(result).toHaveLength(3); expect(result[0]).toEqual({ fromUnixEpochMs: 1000, toUnixEpochMs: 1500, averageSpotsAvailable: 90 // only record at 1000ms }); expect(result[1]).toEqual({ fromUnixEpochMs: 1500, toUnixEpochMs: 2000, averageSpotsAvailable: 80 // only record at 1500ms }); expect(result[2]).toEqual({ fromUnixEpochMs: 2000, toUnixEpochMs: 2500, averageSpotsAvailable: 60 // only record at 2100ms }); }); }); });