diff --git a/src/resolvers/RouteResolvers.ts b/src/resolvers/RouteResolvers.ts index 94dc13f..0c71cfd 100644 --- a/src/resolvers/RouteResolvers.ts +++ b/src/resolvers/RouteResolvers.ts @@ -7,10 +7,10 @@ export const RouteResolvers: Resolvers = { const shuttles = await contextValue.repository.getShuttlesByRouteId(parent.id); return shuttles.map(({ - coordinates, - name, - id, - }) => ({ + coordinates, + name, + id, + }) => ({ coordinates: coordinates as Coordinates, name, route: parent, diff --git a/src/resolvers/SystemResolvers.ts b/src/resolvers/SystemResolvers.ts index b78e922..dbb98ca 100644 --- a/src/resolvers/SystemResolvers.ts +++ b/src/resolvers/SystemResolvers.ts @@ -7,23 +7,15 @@ export const SystemResolvers: Resolvers = { return await contextValue.repository.getRoutesBySystemId(parent.id); }, stops: async (parent, args, contextValue, info) => { - const stops = await contextValue.repository.getStopsBySystemId(parent.id); - return stops.map(({ - id, - name, - coordinates - }) => ({ - id, - name, - // Both ICoordinates and Coordinates have the same definition - coordinates: coordinates as Coordinates, - })); + return await contextValue.repository.getStopsBySystemId(parent.id); }, stop: async (parent, args, contextValue, info) => { if (!args.id) return null; const stop = await contextValue.repository.getStopById(args.id); if (stop === null) return null; + if (stop.systemId !== parent.id) return null; + return { id: stop.id, name: stop.name, @@ -35,6 +27,8 @@ export const SystemResolvers: Resolvers = { const route = await contextValue.repository.getRouteById(args.id); if (route === null) return null; + if (route.systemId !== parent.id) return null; + return { color: route.color, id: route.id, @@ -47,17 +41,12 @@ export const SystemResolvers: Resolvers = { const shuttle = await contextValue.repository.getShuttleById(args.id); if (shuttle === null) return null; + if (shuttle.systemId !== parent.id) return null; + return shuttle; }, shuttles: async (parent, args, contextValue, info) => { - const shuttles = await contextValue.repository.getShuttlesBySystemId(parent.id); - - return shuttles.map(shuttle => ({ - coordinates: shuttle.coordinates, - name: shuttle.name, - id: shuttle.id, - routeId: shuttle.routeId, - })); + return await contextValue.repository.getShuttlesBySystemId(parent.id); } }, } \ No newline at end of file diff --git a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts index 173f696..87cb896 100644 --- a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts @@ -7,15 +7,7 @@ import { fetchRouteDataSuccessfulResponse } from "../jsonSnapshots/fetchRouteDat import { fetchStopAndPolylineDataSuccessfulResponse } from "../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse"; -import { - generateMockEtas, - generateMockOrderedStops, - generateMockRoutes, - generateMockShuttles, - generateMockStops, - generateMockSystems -} from "../generators"; -import { IStop } from "../../src/entities/entities"; +import { generateMockRoutes, generateMockShuttles, generateMockStops, generateMockSystems } from "../testHelpers/mockDataGenerators"; import { fetchShuttleDataSuccessfulResponse } from "../jsonSnapshots/fetchShuttleData/fetchShuttleDataSuccessfulResponse"; @@ -24,7 +16,7 @@ import { resetGlobalFetchMockJson, updateGlobalFetchMockJson, updateGlobalFetchMockJsonToThrowSyntaxError -} from "../mockHelpers/fetchMockHelpers"; +} from "../testHelpers/fetchMockHelpers"; async function assertAsyncCallbackThrowsApiResponseError(callback: () => Promise) { await expect(callback).rejects.toThrow(ApiResponseError); diff --git a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts index 18f4616..9979a06 100644 --- a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader"; -import { resetGlobalFetchMockJson } from "../mockHelpers/fetchMockHelpers"; +import { resetGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; describe("TimedApiBasedRepositoryLoader", () => { diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index 906894b..f130ba5 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -7,7 +7,7 @@ import { generateMockShuttles, generateMockStops, generateMockSystems -} from "../generators"; +} from "../testHelpers/mockDataGenerators"; // For repositories created in the future, reuse core testing // logic from here and differentiate setup (e.g. creating mocks) diff --git a/test/resolvers/EtaResolverTests.test.ts b/test/resolvers/EtaResolverTests.test.ts new file mode 100644 index 0000000..73c9770 --- /dev/null +++ b/test/resolvers/EtaResolverTests.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it } from "@jest/globals"; +import { setupTestServerContext } from "../testHelpers/apolloTestServerHelpers"; +import { IEta, IShuttle, IStop, ISystem } from "../../src/entities/entities"; +import { + addMockEtaToRepository, addMockShuttleToRepository, + addMockStopToRepository, + addMockSystemToRepository +} from "../testHelpers/repositorySetupHelpers"; +import assert = require("node:assert"); + +describe("EtaResolvers", () => { + const context = setupTestServerContext(); + + let mockSystem: ISystem; + let mockShuttle: IShuttle; + let mockStop: IStop; + let expectedEta: IEta; + + beforeEach(async () => { + mockSystem = await addMockSystemToRepository(context.repository); + mockShuttle = await addMockShuttleToRepository(context.repository, mockSystem.id); + mockStop = await addMockStopToRepository(context.repository, mockSystem.id); + expectedEta = await addMockEtaToRepository(context.repository, mockStop.id, mockShuttle.id); + }); + + async function getResponseForEtaQuery(query: string) { + const response = await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + shuttleId: mockShuttle.id, + }, + }, { + contextValue: { + repository: context.repository, + } + }); + return response; + } + + describe("stop", () => { + const query = ` + query GetETAStop($systemId: ID!, $shuttleId: ID!) { + system(id: $systemId) { + shuttle(id: $shuttleId) { + etas { + stop { + id + } + } + } + } + } + `; + + it("returns the associated stop if it exists", async () => { + const response = await getResponseForEtaQuery(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const eta = (response.body.singleResult.data?.system as any).shuttle.etas[0]; + expect(eta.stop.id).toEqual(expectedEta.stopId); + }); + }); + + describe("shuttle", () => { + const query = ` + query GetETAShuttle($systemId: ID!, $shuttleId: ID!) { + system(id: $systemId) { + shuttle(id: $shuttleId) { + etas { + shuttle { + id + } + } + } + } + } + `; + + it("returns the associated shuttle if it exists", async () => { + const response = await getResponseForEtaQuery(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const eta = (response.body.singleResult.data?.system as any).shuttle.etas[0]; + expect(eta.shuttle.id).toEqual(expectedEta.shuttleId); + }); + }); +}); \ No newline at end of file diff --git a/test/resolvers/OrderedStopResolverTests.test.ts b/test/resolvers/OrderedStopResolverTests.test.ts new file mode 100644 index 0000000..11c46e4 --- /dev/null +++ b/test/resolvers/OrderedStopResolverTests.test.ts @@ -0,0 +1,288 @@ +import { beforeEach, describe, expect, it } from "@jest/globals"; +import { setupTestServerContext } from "../testHelpers/apolloTestServerHelpers"; +import { IRoute, IStop, ISystem } from "../../src/entities/entities"; +import { generateMockOrderedStops, generateMockStops } from "../testHelpers/mockDataGenerators"; +import { addMockRouteToRepository, addMockSystemToRepository } from "../testHelpers/repositorySetupHelpers"; +import assert = require("node:assert"); + +describe("OrderedStopResolvers", () => { + const context = setupTestServerContext(); + + let mockSystem: ISystem; + let mockRoute: IRoute; + let mockStops: IStop[]; + + beforeEach(async () => { + mockSystem = await addMockSystemToRepository(context.repository); + mockRoute = await addMockRouteToRepository(context.repository, mockSystem.id); + + mockStops = generateMockStops(); + await Promise.all(mockStops.map(async (mockStop) => { + mockStop.systemId = mockSystem.id; + await context.repository.addOrUpdateStop(mockStop); + })); + }); + + async function setUpOrderedStopsInRepository() { + const orderedStops = generateMockOrderedStops(); + + // Set up IDs and link stops together to work with the test query + orderedStops[0].routeId = mockRoute.id; + orderedStops[1].routeId = mockRoute.id; + + // Ensure that there is no duplication + orderedStops[0].stopId = mockStops[0].id; + orderedStops[1].stopId = mockStops[1].id; + + // Link the stops together + orderedStops[0].nextStop = orderedStops[1]; + orderedStops[1].previousStop = orderedStops[0]; + await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + await context.repository.addOrUpdateOrderedStop(orderedStops[1]); + return orderedStops; + } + + describe("nextStop", () => { + async function getResponseForNextStopQuery(stopId: string) { + const query = ` + query GetNextStop($systemId: ID!, $routeId: ID!, $stopId: ID!) { + system(id: $systemId) { + route(id: $routeId) { + orderedStop(forStopId: $stopId) { + nextStop { + routeId + stopId + } + } + } + } + } + `; + + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + routeId: mockRoute.id, + stopId, + }, + }, { + contextValue: { + repository: context.repository, + } + }); + } + + + it("returns the next stop if it exists", async () => { + // Arrange + const orderedStops = await setUpOrderedStopsInRepository(); + + // Act + const response = await getResponseForNextStopQuery(orderedStops[0].stopId); + + // Assert + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const nextStop = (response.body.singleResult.data?.system as any).route.orderedStop.nextStop; + expect(nextStop.stopId).toEqual(orderedStops[1].stopId); + expect(nextStop.routeId).toEqual(orderedStops[1].routeId); + }); + + it("returns null if there is no next stop in the repository", async () => { + const orderedStops = await setUpOrderedStopsInRepository(); + orderedStops[0].nextStop = undefined; + await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + + const response = await getResponseForNextStopQuery(orderedStops[0].stopId); + + assert(response.body.kind === "single"); + const nonexistentNextStop = (response.body.singleResult.data?.system as any).route.orderedStop.nextStop; + expect(nonexistentNextStop).toBeNull(); + }); + + it("returns null if the next stop object no longer exists", async () => { + const orderedStops = await setUpOrderedStopsInRepository(); + await context.repository.removeStopIfExists(orderedStops[1].stopId); + + const response = await getResponseForNextStopQuery(orderedStops[0].stopId); + + assert(response.body.kind === "single"); + const nonexistentNextStop = (response.body.singleResult.data?.system as any).route.orderedStop.nextStop; + expect(nonexistentNextStop).toBeNull(); + }); + }); + + describe("previousStop", () => { + async function getResponseForPreviousStopQuery(stopId: string) { + const query = ` + query GetNextStop($systemId: ID!, $routeId: ID!, $stopId: ID!) { + system(id: $systemId) { + route(id: $routeId) { + orderedStop(forStopId: $stopId) { + previousStop { + routeId + stopId + } + } + } + } + } + `; + + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + routeId: mockRoute.id, + stopId, + }, + }, { + contextValue: { + repository: context.repository, + } + }); + } + + it("returns the previous stop if it exists", async () => { + // Arrange + const orderedStops = await setUpOrderedStopsInRepository(); + + // Act + const response = await getResponseForPreviousStopQuery(orderedStops[1].stopId); + + // Assert + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const previousStop = (response.body.singleResult.data?.system as any).route.orderedStop.previousStop; + expect(previousStop.stopId).toEqual(orderedStops[0].stopId); + expect(previousStop.routeId).toEqual(orderedStops[0].routeId); + }); + + it("returns null if there is no previous stop in the repository", async () => { + const orderedStops = await setUpOrderedStopsInRepository(); + orderedStops[1].previousStop = undefined; + await context.repository.addOrUpdateOrderedStop(orderedStops[1]); + + const response = await getResponseForPreviousStopQuery(orderedStops[1].stopId); + + assert(response.body.kind === "single"); + const nonexistentPreviousStop = (response.body.singleResult.data?.system as any).route.orderedStop.previousStop; + expect(nonexistentPreviousStop).toBeNull(); + }); + + it("returns null if the current stop no longer exists", async () => { + const orderedStops = await setUpOrderedStopsInRepository(); + await context.repository.removeStopIfExists(orderedStops[0].stopId); + + const response = await getResponseForPreviousStopQuery(orderedStops[1].stopId); + + assert(response.body.kind === "single"); + const nonexistentPreviousStop = (response.body.singleResult.data?.system as any).route.orderedStop.previousStop; + expect(nonexistentPreviousStop).toBeNull(); + }); + }); + + describe("route", () => { + // Note that there is no `orderedStop(forRouteId)` resolver, + // so fetch all ordered stops for a stop instead. + // If we went through the route ID, it would've + // relied on the parent data within the route.orderedStop resolver + async function getResponseForRouteQuery(stopId: string) { + const query = ` + query GetNextStop($systemId: ID!, $stopId: ID!) { + system(id: $systemId) { + stop(id: $stopId) { + orderedStops { + route { + id + name + } + } + } + } + } + `; + + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + stopId, + } + }, { + contextValue: { + repository: context.repository, + } + }); + } + + it("returns the associated route if it exists", async () => { + const orderedStops = generateMockOrderedStops(); + orderedStops[0].routeId = mockRoute.id; + orderedStops[0].stopId = mockStops[0].id; + + // Add one stop only + await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + + const response = await getResponseForRouteQuery(orderedStops[1].stopId); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const route = (response.body.singleResult.data?.system as any).stop.orderedStops[0].route + + expect(route.id).toEqual(mockRoute.id); + expect(route.name).toEqual(mockRoute.name); + }); + + }); + + describe("stop", () => { + async function getResponseForStopQuery(stopId: string) { + const query = ` + query GetNextStop($systemId: ID!, $routeId: ID!, $stopId: ID!) { + system(id: $systemId) { + route(id: $routeId) { + orderedStop(forStopId: $stopId) { + stop { + name + id + } + } + } + } + } + `; + + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + routeId: mockRoute.id, + stopId, + } + }, { + contextValue: { + repository: context.repository, + } + }); + } + + it("returns the associated stop if it exists", async () => { + const orderedStops = await setUpOrderedStopsInRepository(); + orderedStops[0].stopId = mockStops[0].id; + await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + + const response = await getResponseForStopQuery(orderedStops[0].stopId); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const stop = (response.body.singleResult.data?.system as any).route.orderedStop.stop; + expect(stop.name).toEqual(mockStops[0].name); + expect(stop.id).toEqual(mockStops[0].id); + }); + }); +}); + diff --git a/test/resolvers/QueryResolverTests.test.ts b/test/resolvers/QueryResolverTests.test.ts new file mode 100644 index 0000000..73f8e83 --- /dev/null +++ b/test/resolvers/QueryResolverTests.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "@jest/globals"; +import { generateMockSystems } from "../testHelpers/mockDataGenerators"; +import { setupTestServerContext } from "../testHelpers/apolloTestServerHelpers"; +import assert = require("node:assert"); + +// See Apollo documentation for integration test guide +// https://www.apollographql.com/docs/apollo-server/testing/testing + +describe("QueryResolvers", () => { + const context = setupTestServerContext(); + + async function addMockSystems() { + const systems = generateMockSystems(); + await Promise.all(systems.map(async (system) => { + await context.repository.addOrUpdateSystem(system); + })); + return systems; + } + + describe("systems", () => { + it("returns systems from the repository", async () => { + const systems = await addMockSystems(); + + const query = ` + query GetSystems + { + systems { + name + } + } + `; + + const response = await context.testServer.executeOperation({ + query, + }, { + contextValue: { + repository: context.repository, + }, + }); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + expect(response.body.singleResult.data?.systems).toHaveLength(systems.length); + }); + }); + + describe("system", () => { + const query = ` + query GetSystem($id: ID!) + { + system(id: $id) { + name + } + } + `; + + it("returns a system for an ID from the repository", async () => { + const systems = await addMockSystems(); + const systemToGet = systems[1]; + + const response = await context.testServer.executeOperation({ + query, + variables: { + id: systemToGet.id, + } + }, { + contextValue: { + repository: context.repository, + } + }); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect(response.body.singleResult.data?.system).toBeDefined(); + }); + + it("returns null if there is no system", async () => { + const response = await context.testServer.executeOperation({ + query, + variables: { + id: "nonexistent-id", + } + }, { + contextValue: { + repository: context.repository, + } + }); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect(response.body.singleResult.data?.system).toBeNull(); + }); + }); +}); \ No newline at end of file diff --git a/test/resolvers/RouteResolverTests.test.ts b/test/resolvers/RouteResolverTests.test.ts new file mode 100644 index 0000000..ba5b403 --- /dev/null +++ b/test/resolvers/RouteResolverTests.test.ts @@ -0,0 +1,148 @@ +import { beforeEach, describe, expect, it } from "@jest/globals"; +import { setupTestServerContext } from "../testHelpers/apolloTestServerHelpers"; +import { + addMockRouteToRepository, + addMockStopToRepository, + addMockSystemToRepository +} from "../testHelpers/repositorySetupHelpers"; +import { generateMockOrderedStops, generateMockShuttles } from "../testHelpers/mockDataGenerators"; +import { IRoute, IStop, ISystem } from "../../src/entities/entities"; +import assert = require("node:assert"); + +describe("RouteResolvers", () => { + const context = setupTestServerContext(); + + let mockSystem: ISystem; + let mockRoute: IRoute; + let mockStop: IStop; + + beforeEach(async () => { + mockSystem = await addMockSystemToRepository(context.repository); + const systemId = mockSystem.id; + + mockRoute = await addMockRouteToRepository(context.repository, systemId); + mockStop = await addMockStopToRepository(context.repository, systemId); + }); + + + describe("shuttles", () => { + async function getResponseForShuttlesQuery() { + const query = ` + query GetRouteShuttles($systemId: ID!, $routeId: ID!) { + system(id: $systemId) { + route(id: $routeId) { + shuttles { + id + name + } + } + } + } + `; + + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + routeId: mockRoute.id, + }, + }, { + contextValue: { + repository: context.repository, + } + }); + } + + it("returns shuttle array if there are shuttles", async () => { + const expectedShuttles = generateMockShuttles(); + const expectedShuttle = expectedShuttles[0]; + expectedShuttle.systemId = mockSystem.id; + expectedShuttle.routeId = mockRoute.id; + await context.repository.addOrUpdateShuttle(expectedShuttle); + + const response = await getResponseForShuttlesQuery(); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined() + const shuttle = (response.body.singleResult.data as + any).system.route.shuttles[0]; + expect(shuttle.id).toEqual(expectedShuttle.id); + expect(shuttle.name).toEqual(expectedShuttle.name); + }); + + it("returns empty array if there are no shuttles", async () => { + const response = await getResponseForShuttlesQuery(); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined() + const shuttles = (response.body.singleResult.data as + any).system.route.shuttles; + expect(shuttles.length).toEqual(0); + }); + }); + + describe("orderedStop", () => { + async function getResponseForOrderedStopQuery() { + const query = ` + query GetRouteOrderedStop($systemId: ID!, $routeId: ID!, $stopId: ID!) { + system(id: $systemId) { + route(id: $routeId) { + orderedStop(forStopId: $stopId) { + stopId + } + } + } + } + `; + + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + routeId: mockRoute.id, + stopId: mockStop.id, + } + }, { + contextValue: { + repository: context.repository, + } + }); + } + + it("returns ordered stop using provided data", async () => { + const orderedStops = generateMockOrderedStops(); + const expectedOrderedStop = orderedStops[0]; + expectedOrderedStop.stopId = mockStop.id; + expectedOrderedStop.routeId = mockRoute.id; + await context.repository.addOrUpdateOrderedStop(expectedOrderedStop); + + const response = await getResponseForOrderedStopQuery(); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const orderedStop = (response.body.singleResult.data as + any).system.route.orderedStop; + expect(orderedStop.stopId).toEqual(expectedOrderedStop.stopId); + }); + + it("returns null if the stop doesn't exist", async () => { + const orderedStops = generateMockOrderedStops(); + const expectedOrderedStop = orderedStops[0]; + expectedOrderedStop.stopId = mockStop.id; + expectedOrderedStop.routeId = mockRoute.id; + await context.repository.addOrUpdateOrderedStop(expectedOrderedStop); + + await context.repository.removeStopIfExists(mockStop.id); + + const response = await getResponseForOrderedStopQuery(); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const orderedStop = (response.body.singleResult.data as + any).system.route.orderedStop; + expect(orderedStop).toBeNull(); + }); + }); +}); \ No newline at end of file diff --git a/test/resolvers/ShuttleResolverTests.test.ts b/test/resolvers/ShuttleResolverTests.test.ts new file mode 100644 index 0000000..0811c3b --- /dev/null +++ b/test/resolvers/ShuttleResolverTests.test.ts @@ -0,0 +1,200 @@ +import { beforeEach, describe, expect, it } from "@jest/globals"; +import { generateMockEtas, generateMockRoutes } from "../testHelpers/mockDataGenerators"; +import { IShuttle, ISystem } from "../../src/entities/entities"; +import { setupTestServerContext } from "../testHelpers/apolloTestServerHelpers"; +import { addMockShuttleToRepository, addMockSystemToRepository } from "../testHelpers/repositorySetupHelpers"; +import assert = require("node:assert"); + + +describe("ShuttleResolvers", () => { + const context = setupTestServerContext(); + + let mockSystem: ISystem; + let mockShuttle: IShuttle; + + beforeEach(async () => { + mockSystem = await addMockSystemToRepository(context.repository); + mockShuttle = await addMockShuttleToRepository(context.repository, + mockSystem.id); + }); + + + async function addMockEtas(shuttleId: string) { + const etas = generateMockEtas(); + await Promise.all(etas.map(async (eta) => { + eta.shuttleId = shuttleId; + await context.repository.addOrUpdateEta(eta); + })); + return etas; + } + + describe("eta", () => { + const query = ` + query GetShuttleETAs($systemId: ID!, $shuttleId: ID!, $stopId: ID!) + { + system(id: $systemId) { + shuttle(id: $shuttleId) { + eta(forStopId: $stopId) { + secondsRemaining + } + } + } + } + ` + it("returns ETA data for stop ID if exists", async () => { + const etas = await addMockEtas(mockShuttle.id); + + const mockEta = etas[1]; + + // Act + const response = await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + shuttleId: mockShuttle.id, + stopId: mockEta.stopId, + }, + }, { + contextValue: { + repository: context.repository, + }, + }); + + // Assert + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as + any).system.shuttle.eta.secondsRemaining).toEqual(mockEta.secondsRemaining); + }); + + it("returns null if it doesn't exist", async () => { + const response = await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + shuttleId: mockShuttle.id, + stopId: "nonexistent-stop", + } + }, { + contextValue: { + repository: context.repository, + } + }); + + // Assert + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as + any).system.shuttle.eta).toBeNull(); + }); + }); + + describe("etas", () => { + const query = ` + query GetShuttleETAs($systemId: ID!, $shuttleId: ID!) + { + system(id: $systemId) { + shuttle(id: $shuttleId) { + etas { + secondsRemaining + } + } + } + } + ` + + it("returns associated ETAs if they exist for the shuttle", async () => { + const etas = await addMockEtas(mockShuttle.id); + + const response = await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + shuttleId: mockShuttle.id, + }, + }, { + contextValue: { + repository: context.repository, + } + }); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as + any).system.shuttle.etas).toHaveLength(etas.length); + }); + + it("returns empty array if no ETAs exist", async () => { + const response = await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + shuttleId: mockShuttle.id, + }, + }, { + contextValue: { + repository: context.repository, + } + }); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as + any).system.shuttle.etas).toHaveLength(0); + + }); + }); + describe("route", () => { + const query = ` + query GetShuttleRoute($systemId: ID!, $shuttleId: ID!) { + system(id: $systemId) { + shuttle(id: $shuttleId) { + route { + color + id + name + polylineCoordinates { + latitude + longitude + } + } + } + } + } + ` + + async function getResponseForQuery() { + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + shuttleId: mockShuttle.id, + } + }, { + contextValue: { + repository: context.repository, + } + }); + } + + it("returns the route if it exists", async () => { + const mockRoute = generateMockRoutes()[0]; + await context.repository.addOrUpdateRoute(mockRoute); + + const response = await getResponseForQuery(); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as any).system.shuttle.route.id).toEqual(mockRoute.id); + }); + + it("returns null if there is no route", async () => { + const response = await getResponseForQuery(); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as any).system.shuttle.route).toBeNull(); + }); + + }); +}); \ No newline at end of file diff --git a/test/resolvers/StopResolverTests.test.ts b/test/resolvers/StopResolverTests.test.ts new file mode 100644 index 0000000..2a044b1 --- /dev/null +++ b/test/resolvers/StopResolverTests.test.ts @@ -0,0 +1,108 @@ +import { beforeEach, describe, expect, it } from "@jest/globals"; +import { setupTestServerContext } from "../testHelpers/apolloTestServerHelpers"; +import { generateMockEtas, generateMockOrderedStops } from "../testHelpers/mockDataGenerators"; +import { IStop, ISystem } from "../../src/entities/entities"; +import { addMockStopToRepository, addMockSystemToRepository } from "../testHelpers/repositorySetupHelpers"; +import assert = require("node:assert"); + +describe("StopResolvers", () => { + const context = setupTestServerContext(); + + let mockStop: IStop; + let mockSystem: ISystem; + + beforeEach(async () => { + mockSystem = await addMockSystemToRepository(context.repository); + mockStop = await addMockStopToRepository(context.repository, mockSystem.id); + }) + + async function getResponseForQuery(query: string) { + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + stopId: mockStop.id, + }, + }, { + contextValue: { + repository: context.repository, + } + }); + } + + describe("orderedStops", () => { + const query = ` + query GetOrderedStops($systemId: ID!, $stopId: ID!) { + system(id: $systemId) { + stop(id: $stopId) { + orderedStops { + routeId + stopId + } + } + } + } + ` + + + it("returns ordered stops if they exist for the stop ID", async () => { + let mockOrderedStops = generateMockOrderedStops(); + mockOrderedStops = mockOrderedStops.filter((orderedStop) => orderedStop.stopId === mockOrderedStops[0].stopId); + await Promise.all(mockOrderedStops.map(async orderedStop => { + orderedStop.stopId = mockStop.id; + await context.repository.addOrUpdateOrderedStop(orderedStop); + })); + + const response = await getResponseForQuery(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as any).system.stop.orderedStops).toHaveLength(mockOrderedStops.length); + }); + + it("returns empty array if no ordered stops exist", async () => { + const response = await getResponseForQuery(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as any).system.stop.orderedStops).toHaveLength(0); + }); + }); + + describe("etas", () => { + const query = ` + query GetEtas($systemId: ID!, $stopId: ID!) { + system(id: $systemId) { + stop(id: $stopId) { + etas { + secondsRemaining + } + } + } + } + ` + + it("returns ETAs if they exist for the stop ID", async () => { + let mockEtas = generateMockEtas(); + mockEtas = mockEtas.filter((eta) => eta.stopId === mockEtas[0].stopId); + await Promise.all(mockEtas.map(async eta => { + eta.stopId = mockStop.id; + await context.repository.addOrUpdateEta(eta); + })); + + const response = await getResponseForQuery(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as any).system.stop.etas).toHaveLength(mockEtas.length); + }); + + it("returns empty array if no ETAs exist", async () => { + const response = await getResponseForQuery(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + expect((response.body.singleResult.data as any).system.stop.etas).toHaveLength(0); + }); + }); +}); \ No newline at end of file diff --git a/test/resolvers/SystemResolverTests.test.ts b/test/resolvers/SystemResolverTests.test.ts new file mode 100644 index 0000000..7020075 --- /dev/null +++ b/test/resolvers/SystemResolverTests.test.ts @@ -0,0 +1,321 @@ +import { beforeEach, describe, expect, it } from "@jest/globals"; +import { setupTestServerContext } from "../testHelpers/apolloTestServerHelpers"; +import { generateMockRoutes, generateMockShuttles, generateMockStops } from "../testHelpers/mockDataGenerators"; +import { + addMockRouteToRepository, + addMockShuttleToRepository, + addMockStopToRepository, + addMockSystemToRepository +} from "../testHelpers/repositorySetupHelpers"; +import { ISystem } from "../../src/entities/entities"; +import assert = require("node:assert"); + +describe("SystemResolvers", () => { + const context = setupTestServerContext(); + + let mockSystem: ISystem; + + beforeEach(async () => { + mockSystem = await addMockSystemToRepository(context.repository); + }); + + // TODO: Consolidate these into one single method taking an object + async function getResponseFromQueryNeedingSystemId(query: string) { + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + }, + }, { + contextValue: { + repository: context.repository + }, + }); + } + + describe("routes", () => { + const query = ` + query GetSystemRoutes($systemId: ID!) { + system(id: $systemId) { + routes { + id + name + } + } + } + `; + + it("gets routes associated with system id", async () => { + const expectedRoutes = generateMockRoutes(); + await Promise.all(expectedRoutes.map(async (route) => { + route.systemId = mockSystem.id; + await context.repository.addOrUpdateRoute(route); + })); + + const response = await getResponseFromQueryNeedingSystemId(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined() + const routes = (response.body.singleResult.data as any).system.routes; + expect(routes.length === expectedRoutes.length); + }); + }); + + describe("stops", () => { + const query = ` + query GetSystemStops($systemId: ID!) { + system(id: $systemId) { + stops { + id + name + } + } + } + `; + + it("gets stops associated with system id", async () => { + const expectedStops = generateMockStops(); + await Promise.all(expectedStops.map(async (stop) => { + stop.systemId = mockSystem.id; + await context.repository.addOrUpdateStop(stop); + })); + + const response = await getResponseFromQueryNeedingSystemId(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined() + const stops = (response.body.singleResult.data as any).system.stops; + expect(stops.length === expectedStops.length); + }); + }); + + describe("stop", () => { + async function getResponseForStopQuery(stopId: string) { + const query = ` + query GetSystemStop($systemId: ID!, $stopId: ID!) { + system(id: $systemId) { + stop(id: $stopId) { + id + name + } + } + } + `; + + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + stopId: stopId, + }, + }, { + contextValue: { + repository: context.repository, + } + }); + } + + it("gets the stop with the correct id", async () => { + const mockStop = await addMockStopToRepository(context.repository, mockSystem.id); + + const response = await getResponseForStopQuery(mockStop.id); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const stop = (response.body.singleResult.data as any).system.stop; + expect(stop.id).toEqual(mockStop.id); + expect(stop.name).toEqual(mockStop.name); + }); + + it("returns null if the stop isn't associated with the system", async () => { + const updatedSystem = { + ...mockSystem, + id: "2", + } + await context.repository.addOrUpdateSystem(updatedSystem); + + const mockStop = await addMockStopToRepository(context.repository, updatedSystem.id); + + const response = await getResponseForStopQuery(mockStop.id); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const stop = (response.body.singleResult.data as any).system.stop; + expect(stop).toBeNull(); + }); + + it("returns null if there is no stop", async () => { + const response = await getResponseForStopQuery("1"); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const stop = (response.body.singleResult.data as any).system.stop; + expect(stop).toBeNull(); + }); + }); + + describe("route", () => { + async function getResponseForRouteQuery(routeId: string) { + const query = ` + query GetSystemRoute($systemId: ID!, $routeId: ID!) { + system(id: $systemId) { + route(id: $routeId) { + id + name + } + } + } + `; + + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + routeId, + }, + }, { + contextValue: { + repository: context.repository, + } + }); + } + + it("gets the route with the correct id", async () => { + const mockRoute = await addMockRouteToRepository(context.repository, mockSystem.id); + + const response = await getResponseForRouteQuery(mockRoute.id); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const route = (response.body.singleResult.data as any).system.route; + expect(route.id).toEqual(mockRoute.id); + expect(route.name).toEqual(mockRoute.name); + }); + + it("returns null if the route isn't associated with the system", async () => { + const updatedSystem = { + ...mockSystem, + id: "2", + } + await context.repository.addOrUpdateSystem(updatedSystem); + + const mockRoute = await addMockRouteToRepository(context.repository, updatedSystem.id); + + const response = await getResponseForRouteQuery(mockRoute.id); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const route = (response.body.singleResult.data as any).system.route; + expect(route).toBeNull(); + }); + + it("returns null if there is no route", async () => { + const response = await getResponseForRouteQuery("nonexistent-route-id"); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const route = (response.body.singleResult.data as any).system.route; + expect(route).toBeNull(); + }); + }); + + describe("shuttle", () => { + async function getResponseForShuttleQuery(shuttleId: string) { + const query = ` + query GetSystemShuttle($systemId: ID!, $shuttleId: ID!) { + system(id: $systemId) { + shuttle(id: $shuttleId) { + id + name + } + } + } + `; + + return await context.testServer.executeOperation({ + query, + variables: { + systemId: mockSystem.id, + shuttleId: shuttleId, + } + }, { + contextValue: { + repository: context.repository, + } + }); + } + + it("gets the shuttle with the correct id", async () => { + const mockShuttle = await addMockShuttleToRepository(context.repository, mockSystem.id); + + const response = await getResponseForShuttleQuery(mockShuttle.id); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + const shuttle = (response.body.singleResult.data as any).system.shuttle; + expect(shuttle.id).toEqual(mockShuttle.id); + expect(shuttle.name).toEqual(mockShuttle.name); + }); + + it("returns null if the shuttle isn't associated with the system", async () => { + const updatedSystem = { + ...mockSystem, + id: "2", + } + await context.repository.addOrUpdateSystem(updatedSystem); + + const mockShuttle = await addMockShuttleToRepository(context.repository, updatedSystem.id); + + const response = await getResponseForShuttleQuery(mockShuttle.id); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const shuttle = (response.body.singleResult.data as any).system.shuttle; + expect(shuttle).toBeNull(); + }); + + it("returns null if there is no shuttle", async () => { + const response = await getResponseForShuttleQuery("nonexistent-shuttle-id"); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined(); + + const shuttle = (response.body.singleResult.data as any).system.shuttle; + expect(shuttle).toBeNull(); + }); + }); + + describe("shuttles", () => { + const query = ` + query GetSystemShuttle($systemId: ID!) { + system(id: $systemId) { + shuttles { + id + name + } + } + } + `; + + it("gets shuttles associated with system id", async () => { + const expectedShuttles = generateMockShuttles(); + await Promise.all(expectedShuttles.map(async (shuttle) => { + shuttle.systemId = mockSystem.id; + await context.repository.addOrUpdateShuttle(shuttle); + })); + + const response = await getResponseFromQueryNeedingSystemId(query); + + assert(response.body.kind === "single"); + expect(response.body.singleResult.errors).toBeUndefined() + const shuttles = (response.body.singleResult.data as any).system.shuttles; + expect(shuttles.length === expectedShuttles.length); + }); + }); +}); \ No newline at end of file diff --git a/test/testHelpers/apolloTestServerHelpers.ts b/test/testHelpers/apolloTestServerHelpers.ts new file mode 100644 index 0000000..78b322f --- /dev/null +++ b/test/testHelpers/apolloTestServerHelpers.ts @@ -0,0 +1,30 @@ +import { readFileSync } from "fs"; +import { ApolloServer } from "@apollo/server"; +import { MergedResolvers } from "../../src/MergedResolvers"; +import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { beforeEach } from "@jest/globals"; +import { ServerContext } from "../../src/ServerContext"; + + +function setUpTestServer() { + // Leaving this separate from the main server in case + // configuration changes + const typeDefs = readFileSync("./schema.graphqls", "utf8"); + return new ApolloServer({ + typeDefs, + resolvers: MergedResolvers, + }); +} + +export function setupTestServerContext() { + // @ts-ignore + const context: { testServer: ApolloServer; repository: UnoptimizedInMemoryRepository } = {}; + + beforeEach(() => { + context.testServer = setUpTestServer(); + context.repository = new UnoptimizedInMemoryRepository(); + }); + + // Return a reference, not destructured values + return context; +} \ No newline at end of file diff --git a/test/mockHelpers/fetchMockHelpers.ts b/test/testHelpers/fetchMockHelpers.ts similarity index 100% rename from test/mockHelpers/fetchMockHelpers.ts rename to test/testHelpers/fetchMockHelpers.ts diff --git a/test/generators.ts b/test/testHelpers/mockDataGenerators.ts similarity index 98% rename from test/generators.ts rename to test/testHelpers/mockDataGenerators.ts index eac524a..e284c93 100644 --- a/test/generators.ts +++ b/test/testHelpers/mockDataGenerators.ts @@ -1,4 +1,4 @@ -import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../src/entities/entities"; +import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../../src/entities/entities"; // Use a single set of generators in case any of the // interfaces change in the future diff --git a/test/testHelpers/repositorySetupHelpers.ts b/test/testHelpers/repositorySetupHelpers.ts new file mode 100644 index 0000000..d9e48e3 --- /dev/null +++ b/test/testHelpers/repositorySetupHelpers.ts @@ -0,0 +1,54 @@ +import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { + generateMockEtas, + generateMockRoutes, + generateMockShuttles, + generateMockStops, + generateMockSystems +} from "./mockDataGenerators"; + +export async function addMockSystemToRepository(repository: UnoptimizedInMemoryRepository) { + const mockSystems = generateMockSystems(); + const mockSystem = mockSystems[0]; + mockSystem.id = "1"; + await repository.addOrUpdateSystem(mockSystem); + + return mockSystem; +} + +export async function addMockRouteToRepository(repository: UnoptimizedInMemoryRepository, systemId: string) { + const mockRoutes = generateMockRoutes(); + const mockRoute = mockRoutes[0]; + mockRoute.systemId = systemId; + await repository.addOrUpdateRoute(mockRoute); + + return mockRoute; +} + +export async function addMockStopToRepository(repository: UnoptimizedInMemoryRepository, systemId: string) { + const mockStops = generateMockStops(); + const mockStop = mockStops[0]; + mockStop.systemId = systemId; + await repository.addOrUpdateStop(mockStop); + + return mockStop; +} + +export async function addMockShuttleToRepository(repository: UnoptimizedInMemoryRepository, systemId: string) { + const mockShuttles = generateMockShuttles(); + const mockShuttle = mockShuttles[0]; + mockShuttle.systemId = systemId; + await repository.addOrUpdateShuttle(mockShuttle); + return mockShuttle; +} + +export async function addMockEtaToRepository(repository: UnoptimizedInMemoryRepository, stopId: string, shuttleId: string) { + const etas = generateMockEtas(); + const expectedEta = etas[0]; + expectedEta.stopId = stopId; + expectedEta.shuttleId = shuttleId; + await repository.addOrUpdateEta(expectedEta); + + return expectedEta; +} +