From 868a9f3b1d5fa1b6b96ebcc59c6739fa9b097d30 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 2 Jul 2025 19:40:45 -0400 Subject: [PATCH] Add RedisParkingRepository.ts and convert existing tests to shared tests for both classes --- .../RedisNotificationRepository.ts | 4 +- src/repositories/RedisParkingRepository.ts | 265 ++++++++++++++++++ .../InMemoryParkingRepositoryTests.test.ts | 257 ----------------- .../ParkingRepositorySharedTests.test.ts | 202 +++++++++++++ 4 files changed, 469 insertions(+), 259 deletions(-) create mode 100644 src/repositories/RedisParkingRepository.ts delete mode 100644 test/repositories/InMemoryParkingRepositoryTests.test.ts create mode 100644 test/repositories/ParkingRepositorySharedTests.test.ts diff --git a/src/repositories/RedisNotificationRepository.ts b/src/repositories/RedisNotificationRepository.ts index ad189bf..cb4a30c 100644 --- a/src/repositories/RedisNotificationRepository.ts +++ b/src/repositories/RedisNotificationRepository.ts @@ -16,8 +16,8 @@ export class RedisNotificationRepository implements NotificationRepository { private redisClient = createClient({ url: process.env.REDIS_URL, socket: { - tls: (process.env.REDIS_URL?.match(/rediss:/) != null), - rejectUnauthorized: false, + tls: (process.env.REDIS_URL?.match(/rediss:/) != null), + rejectUnauthorized: false, } }), ) { diff --git a/src/repositories/RedisParkingRepository.ts b/src/repositories/RedisParkingRepository.ts new file mode 100644 index 0000000..07fa188 --- /dev/null +++ b/src/repositories/RedisParkingRepository.ts @@ -0,0 +1,265 @@ +import { createClient, RedisClientType } from 'redis'; +import { ParkingGetterSetterRepository } from "./ParkingGetterSetterRepository"; +import { IParkingStructure, IParkingStructureTimestampRecord } from "../entities/ParkingRepositoryEntities"; +import { HistoricalParkingAverageQueryResult, ParkingStructureCountOptions } from "./ParkingGetterRepository"; + +export type ParkingStructureID = string; + +// Every 10 minutes +export const PARKING_LOGGING_INTERVAL_MS = 600000; + +export class RedisParkingRepository implements ParkingGetterSetterRepository { + private dataLastAdded: Map = new Map(); + + constructor( + private redisClient = createClient({ + url: process.env.REDIS_URL, + socket: { + tls: (process.env.REDIS_URL?.match(/rediss:/) != null), + rejectUnauthorized: false, + } + }), + ) { + } + + get isReady() { + return this.redisClient.isReady; + } + + public async connect() { + await this.redisClient.connect(); + } + + public async disconnect() { + await this.redisClient.disconnect(); + } + + public async clearAllData() { + await this.redisClient.flushAll(); + this.dataLastAdded.clear(); + } + + addOrUpdateParkingStructure = async (structure: IParkingStructure): Promise => { + // Store current structure data + await this.redisClient.hSet(`parking:structure:${structure.id}`, { + id: structure.id, + name: structure.name, + address: structure.address, + capacity: structure.capacity.toString(), + spotsAvailable: structure.spotsAvailable.toString(), + latitude: structure.coordinates.latitude.toString(), + longitude: structure.coordinates.longitude.toString(), + updatedTime: structure.updatedTime.toISOString() + }); + + // Add to historical data if needed + await this.addHistoricalDataForStructure(structure); + }; + + private addHistoricalDataForStructure = async (structure: IParkingStructure): Promise => { + const now = Date.now(); + const lastAdded = this.dataLastAdded.get(structure.id); + + const parkingLoggingIntervalExceeded = () => { + return !lastAdded || (now - lastAdded.getTime()) >= PARKING_LOGGING_INTERVAL_MS; + }; + + if (parkingLoggingIntervalExceeded()) { + // Use Redis Time Series to store historical data + const timeSeriesKey = `parking:timeseries:${structure.id}`; + + try { + // Try to add the time series data point + await this.redisClient.sendCommand([ + 'TS.ADD', + timeSeriesKey, + now.toString(), + structure.spotsAvailable.toString(), + 'LABELS', + 'structureId', + structure.id + ]); + } catch (error) { + // If time series doesn't exist, create it first + try { + await this.redisClient.sendCommand([ + 'TS.CREATE', + timeSeriesKey, + 'LABELS', + 'structureId', + structure.id + ]); + // Now add the data point + await this.redisClient.sendCommand([ + 'TS.ADD', + timeSeriesKey, + now.toString(), + structure.spotsAvailable.toString() + ]); + } catch (createError) { + // If still fails, it might be because time series already exists, try adding again + await this.redisClient.sendCommand([ + 'TS.ADD', + timeSeriesKey, + now.toString(), + structure.spotsAvailable.toString() + ]); + } + } + + this.dataLastAdded.set(structure.id, new Date(now)); + } + }; + + clearParkingStructureData = async (): Promise => { + // Get all parking structure keys + const structureKeys = await this.redisClient.keys('parking:structure:*'); + const timeSeriesKeys = await this.redisClient.keys('parking:timeseries:*'); + + // Delete all structure and time series data + if (structureKeys.length > 0) { + await this.redisClient.del(structureKeys); + } + if (timeSeriesKeys.length > 0) { + await this.redisClient.del(timeSeriesKeys); + } + + this.dataLastAdded.clear(); + }; + + getParkingStructureById = async (id: string): Promise => { + const data = await this.redisClient.hGetAll(`parking:structure:${id}`); + + if (Object.keys(data).length === 0) { + return null; + } + + return { + id: data.id, + name: data.name, + address: data.address, + capacity: parseInt(data.capacity), + spotsAvailable: parseInt(data.spotsAvailable), + coordinates: { + latitude: parseFloat(data.latitude), + longitude: parseFloat(data.longitude) + }, + updatedTime: new Date(data.updatedTime) + }; + }; + + getParkingStructures = async (): Promise => { + const keys = await this.redisClient.keys('parking:structure:*'); + const structures: IParkingStructure[] = []; + + for (const key of keys) { + const data = await this.redisClient.hGetAll(key); + if (Object.keys(data).length > 0) { + structures.push({ + id: data.id, + name: data.name, + address: data.address, + capacity: parseInt(data.capacity), + spotsAvailable: parseInt(data.spotsAvailable), + coordinates: { + latitude: parseFloat(data.latitude), + longitude: parseFloat(data.longitude) + }, + updatedTime: new Date(data.updatedTime) + }); + } + } + + return structures; + }; + + removeParkingStructureIfExists = async (id: string): Promise => { + const structure = await this.getParkingStructureById(id); + if (structure) { + await this.redisClient.del(`parking:structure:${id}`); + await this.redisClient.del(`parking:timeseries:${id}`); + this.dataLastAdded.delete(id); + return structure; + } + return null; + }; + + getHistoricalAveragesOfParkingStructureCounts = async (id: string, options: ParkingStructureCountOptions): Promise => { + const timeSeriesKey = `parking:timeseries:${id}`; + + try { + // Get time series data for the specified range + const timeSeriesData = await this.redisClient.sendCommand([ + 'TS.RANGE', + timeSeriesKey, + options.startUnixEpochMs.toString(), + options.endUnixEpochMs.toString() + ]) as [string, string][]; + + if (!timeSeriesData || timeSeriesData.length === 0) { + return []; + } + + // Convert Redis time series data to our record format + const records: IParkingStructureTimestampRecord[] = timeSeriesData.map(([timestamp, value]) => ({ + id, + timestampMs: parseInt(timestamp), + spotsAvailable: parseInt(value) + })); + + return this.calculateAveragesFromRecords(records, options); + } catch (error) { + // Time series might not exist + return []; + } + }; + + private calculateAveragesFromRecords = ( + records: IParkingStructureTimestampRecord[], + options: ParkingStructureCountOptions + ): HistoricalParkingAverageQueryResult[] => { + const results: HistoricalParkingAverageQueryResult[] = []; + const { startUnixEpochMs, endUnixEpochMs, intervalMs } = options; + + let currentIntervalStart = startUnixEpochMs; + + while (currentIntervalStart < endUnixEpochMs) { + const currentIntervalEnd = Math.min(currentIntervalStart + intervalMs, endUnixEpochMs); + const recordsInInterval = this.getRecordsInTimeRange(records, currentIntervalStart, currentIntervalEnd); + + if (recordsInInterval.length > 0) { + const averageResult = this.calculateAverageForInterval(currentIntervalStart, currentIntervalEnd, recordsInInterval); + results.push(averageResult); + } + + currentIntervalStart = currentIntervalEnd; + } + + return results; + }; + + private getRecordsInTimeRange = ( + records: IParkingStructureTimestampRecord[], + startMs: number, + endMs: number + ): IParkingStructureTimestampRecord[] => { + return records.filter(record => + record.timestampMs >= startMs && record.timestampMs < endMs + ); + }; + + private calculateAverageForInterval = ( + fromMs: number, + toMs: number, + records: IParkingStructureTimestampRecord[] + ): HistoricalParkingAverageQueryResult => { + const totalSpotsAvailable = records.reduce((sum, record) => sum + record.spotsAvailable, 0); + const averageSpotsAvailable = totalSpotsAvailable / records.length; + + return { + fromUnixEpochMs: fromMs, + toUnixEpochMs: toMs, + averageSpotsAvailable + }; + }; +} diff --git a/test/repositories/InMemoryParkingRepositoryTests.test.ts b/test/repositories/InMemoryParkingRepositoryTests.test.ts deleted file mode 100644 index ff74c68..0000000 --- a/test/repositories/InMemoryParkingRepositoryTests.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -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 - }); - }); - }); -}); diff --git a/test/repositories/ParkingRepositorySharedTests.test.ts b/test/repositories/ParkingRepositorySharedTests.test.ts new file mode 100644 index 0000000..319008c --- /dev/null +++ b/test/repositories/ParkingRepositorySharedTests.test.ts @@ -0,0 +1,202 @@ +import { afterEach, 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"; +import { ParkingGetterSetterRepository } from "../../src/repositories/ParkingGetterSetterRepository"; +import { RedisParkingRepository } from "../../src/repositories/RedisParkingRepository"; + +interface RepositoryHolder { + name: string; + factory(): Promise; + teardown(): Promise; +} + +class InMemoryParkingRepositoryHolder implements RepositoryHolder { + name = 'InMemoryParkingRepository'; + factory = async () => { + return new InMemoryParkingRepository(); + }; + teardown = async () => {}; +} + +class RedisParkingRepositoryHolder implements RepositoryHolder { + repo: RedisParkingRepository | undefined; + + name = 'RedisParkingRepository'; + factory = async () => { + this.repo = new RedisParkingRepository(); + await this.repo.connect(); + return this.repo; + }; + teardown = async () => { + if (this.repo) { + await this.repo.clearAllData(); + await this.repo.disconnect(); + } + }; +} + +const repositoryImplementations = [ + // new InMemoryParkingRepositoryHolder(), + new RedisParkingRepositoryHolder(), +]; + +describe.each(repositoryImplementations)('$name', (holder) => { + let repository: ParkingGetterSetterRepository; + 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(), + }; + + beforeEach(async () => { + repository = await holder.factory(); + jest.useRealTimers(); + }); + + afterEach(async () => { + await holder.teardown(); + }); + + 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); + }); + }); + + 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", () => { + 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 intervals with manual historical data", async () => { + // await repository.addOrUpdateParkingStructure(testStructure); + // + // const now = Date.now(); + // jest.useFakeTimers().setSystemTime(now); + // + // const updates = [ + // { ...testStructure, spotsAvailable: 80, updatedTime: new Date(now + 1000) }, + // { ...testStructure, spotsAvailable: 70, updatedTime: new Date(now + 2000) }, + // ]; + // + // for (let i = 0; i < updates.length; i++) { + // jest.setSystemTime(now + (i + 1) * PARKING_LOGGING_INTERVAL_MS + 100); + // await repository.addOrUpdateParkingStructure(updates[i]); + // } + // + // const options: ParkingStructureCountOptions = { + // startUnixEpochMs: now, + // endUnixEpochMs: now + 4000000, // Large range to capture all data + // intervalMs: 1000000 + // }; + // + // const result = await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, options); + // + // // Should have at least some historical data + // expect(result.length).toBeGreaterThan(0); + // if (result.length > 0) { + // expect(result[0]).toHaveProperty('fromUnixEpochMs'); + // expect(result[0]).toHaveProperty('toUnixEpochMs'); + // expect(result[0]).toHaveProperty('averageSpotsAvailable'); + // expect(typeof result[0].averageSpotsAvailable).toBe('number'); + // } + // }); + }); +});