mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-17 07:50:31 +00:00
258 lines
9.6 KiB
TypeScript
258 lines
9.6 KiB
TypeScript
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<ParkingStructureID, CircularQueue<IParkingStructureTimestampRecord>> = 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<IParkingStructureTimestampRecord>(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
|
|
});
|
|
});
|
|
});
|
|
});
|