Merge pull request #21 from brendan-ch/feat/notifications-state

feat/notifications-state
This commit is contained in:
2025-02-12 20:04:01 -08:00
committed by GitHub
7 changed files with 146 additions and 2 deletions

View File

@@ -63,6 +63,8 @@ type Shuttle {
type Query {
systems: [System!]!
system(id: ID): System
isNotificationScheduled(input: NotificationInput!): Boolean
}
# Mutations

View File

@@ -15,6 +15,10 @@ export const QueryResolvers: Resolvers<ServerContext> = {
name: system.name,
id: system.id,
};
},
isNotificationScheduled: async (_parent, args, contextValue, _info) => {
const notificationData = args.input;
return contextValue.notificationService.isNotificationScheduled(notificationData);
}
},
}
}

View File

@@ -250,4 +250,28 @@ export class NotificationService {
}
return this.deviceIdsToDeliverTo[tuple.toString()].has(deviceId);
}
/**
* Return all scheduled notification for the given device ID.
* @param deviceId
*/
public async getAllScheduledNotificationsForDevice(deviceId: string): Promise<ScheduledNotificationData[]> {
const scheduledNotifications: ScheduledNotificationData[] = [];
for (const key of Object.keys(this.deviceIdsToDeliverTo)) {
if (this.deviceIdsToDeliverTo[key].has(deviceId)) {
const tupleKey = TupleKey.fromExistingStringKey(key);
const shuttleId = tupleKey.tuple[0]
const stopId = tupleKey.tuple[1];
scheduledNotifications.push({
shuttleId,
stopId,
deviceId,
});
}
}
return scheduledNotifications;
}
}

View File

@@ -16,4 +16,9 @@ export class TupleKey<T extends any[]> {
valueOf(): string {
return this.strKey;
}
static fromExistingStringKey(strKey: string) {
const tuple = strKey.split(separator);
return new TupleKey(...tuple);
}
}

View File

@@ -2,6 +2,8 @@ import { describe, expect, it } from "@jest/globals";
import { generateMockSystems } from "../testHelpers/mockDataGenerators";
import { setupTestServerContext, setupTestServerHolder } from "../testHelpers/apolloTestServerHelpers";
import assert = require("node:assert");
import { ScheduledNotificationData } from "../../src/services/NotificationService";
import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers";
// See Apollo documentation for integration test guide
// https://www.apollographql.com/docs/apollo-server/testing/testing
@@ -93,4 +95,63 @@ describe("QueryResolvers", () => {
expect(response.body.singleResult.data?.system).toBeNull();
});
});
describe("isNotificationScheduled", () => {
const query = `
query IsNotificationScheduled($input: NotificationInput!) {
isNotificationScheduled(input: $input)
}
`
it("returns true if the notification is scheduled", async () => {
// Arrange
const shuttle = await addMockShuttleToRepository(context.repository, "1");
const stop = await addMockStopToRepository(context.repository, "1")
const notification: ScheduledNotificationData = {
shuttleId: shuttle.id,
stopId: stop.id,
deviceId: "1",
};
await context.notificationService.scheduleNotification(notification);
// Act
const response = await holder.testServer.executeOperation({
query,
variables: {
input: {
...notification,
},
}
}, {
contextValue: context,
});
// Assert
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.isNotificationScheduled).toBe(true);
});
it("returns false if the notification isn't scheduled", async () => {
// Act
const response = await holder.testServer.executeOperation({
query,
variables: {
input: {
shuttleId: "1",
stopId: "1",
deviceId: "1",
},
}
}, {
contextValue: context,
});
// Assert
assert(response.body.kind === "single");
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.isNotificationScheduled).toBe(false);
});
});
});

View File

@@ -230,4 +230,38 @@ describe("NotificationService", () => {
expect(http2.connect as jest.Mock).toHaveBeenCalledTimes(0);
});
});
describe("getAllScheduledNotificationsForDevice", () => {
it("returns scheduled notifications for the device ID", async () => {
// Arrange
const shuttle1 = await addMockShuttleToRepository(repository, "1");
const stop = await addMockStopToRepository(repository, "1");
const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle1, stop);
await notificationService.scheduleNotification(notificationData1);
const shuttle2 = {
...shuttle1,
id: "2",
}
await repository.addOrUpdateShuttle(shuttle2);
const notificationData2 = {
...notificationData1,
shuttleId: shuttle2.id,
}
await notificationService.scheduleNotification(notificationData2);
// Act
const notifications = await notificationService.getAllScheduledNotificationsForDevice(notificationData1.deviceId);
// Assert
expect(notifications.length).toBe(2);
});
it("returns an empty array if there are no notifications", async () => {
// Act
const notifications = await notificationService.getAllScheduledNotificationsForDevice("1");
expect(notifications.length).toBe(0);
});
});
});

View File

@@ -28,4 +28,18 @@ describe("TupleKey", () => {
expect(sampleObject[tupleKey1.toString()]).toEqual("value1");
expect(sampleObject[(new TupleKey("1", "2")).toString()]).toEqual("value1");
});
});
describe("fromExistingStringKey", () => {
it("creates a new TupleKey from an existing string key", () => {
const strKey = "hello|there";
const tupleKey = TupleKey.fromExistingStringKey(strKey);
expect(tupleKey.toString()).toEqual(strKey);
});
it("creates an empty tuple if there is no string", () => {
const strKey = "";
const tupleKey = TupleKey.fromExistingStringKey(strKey);
expect(tupleKey.toString()).toEqual(strKey);
})
})
});