mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-17 07:50:31 +00:00
Merge pull request #83 from brendan-ch/chore/sort-resolver-data
chore/sort-resolver-data
This commit is contained in:
102
AGENTS.md
Normal file
102
AGENTS.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to coding agents (e.g., Codex CLI, Claude Code, and other AI coding assistants) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Core Development
|
||||
```bash
|
||||
# Start development server with hot reloading
|
||||
docker compose run dev
|
||||
|
||||
# Run comprehensive test suite
|
||||
docker compose run test
|
||||
|
||||
# Generate GraphQL TypeScript types
|
||||
npm run generate
|
||||
|
||||
# Build for development (install, codegen, tsc)
|
||||
npm run build:dev
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests via npm
|
||||
npm test
|
||||
|
||||
# Run specific test file
|
||||
npm test -- --testPathPattern=<test-file-name>
|
||||
|
||||
# Run tests with coverage
|
||||
npm test -- --coverage
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Project Inter Server is a GraphQL-based backend for college transit tracking with real-time shuttle data, parking availability, and push notifications.
|
||||
|
||||
- InterchangeSystem: Central orchestrator for shuttles, parking, and notifications
|
||||
- Use `InterchangeSystem.build()` in production
|
||||
- Use `InterchangeSystem.buildForTesting()` in tests
|
||||
|
||||
- Repository Pattern (data access abstraction)
|
||||
- Shuttle: `UnoptimizedInMemoryShuttleRepository`
|
||||
- Parking: `InMemoryParkingRepository`
|
||||
- Notifications: `RedisNotificationRepository` (prod) / `InMemoryNotificationRepository` (test)
|
||||
|
||||
- Data Loaders (external API integration)
|
||||
- `ApiBasedShuttleRepositoryLoader` – Passio GO! API
|
||||
- `ChapmanApiBasedParkingRepositoryLoader` – Parking data
|
||||
- `TimedApiBasedRepositoryLoader` – Periodic refresh wrapper
|
||||
|
||||
- Notification System
|
||||
- `ETANotificationScheduler` – Shuttle arrival notifications
|
||||
- `AppleNotificationSender` – APNS integration
|
||||
|
||||
## GraphQL
|
||||
- Schema definition: `schema.graphqls`
|
||||
- Generated types: `src/generated/`
|
||||
- Resolvers: `src/resolvers/`
|
||||
- Resolver merge: `src/MergedResolvers.ts`
|
||||
|
||||
## Directory Structure
|
||||
- `src/entities/` – Core business logic
|
||||
- `src/repositories/` – Data access layer
|
||||
- `src/loaders/` – External API integrations
|
||||
- `src/notifications/` – Push notification system
|
||||
- `src/resolvers/` – GraphQL resolvers and tests
|
||||
- `testHelpers/` – Test utilities and mock data
|
||||
|
||||
## Docker Services
|
||||
- `dev` – Development server with hot reload
|
||||
- `test` – Unit/integration tests
|
||||
- `redis` – Persistent Redis
|
||||
- `redis-no-persistence` – Ephemeral Redis for tests
|
||||
|
||||
## Testing Patterns
|
||||
- Prefer `buildForTesting()` to construct systems in tests.
|
||||
- Mock external APIs using JSON snapshots under `testHelpers/jsonSnapshots`.
|
||||
- Use in-memory repositories for speed where possible.
|
||||
- When adding features that affect API output, add focused resolver tests in the corresponding `src/resolvers/__tests__` file.
|
||||
- Separate unit tests from integration tests where practical to keep feedback fast and failures well-scoped.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### General Guidelines
|
||||
- Use test-driven development where possible. Write tests before implementation and run them before and after changes.
|
||||
- Use Docker Compose for tests. Run `docker compose run test` so you can see full output.
|
||||
|
||||
### Git Workflow
|
||||
- Name pull requests after their branch name.
|
||||
|
||||
### Code Style
|
||||
- Prefer arrow functions, especially within classes.
|
||||
- Keep changes minimal and focused; avoid unrelated refactors. Mention incidental issues separately.
|
||||
- Respect existing interfaces and types; use non-destructive edits.
|
||||
|
||||
### Agent Tips
|
||||
- Write or update tests alongside changes; validate with `docker compose run test`.
|
||||
- When touching GraphQL resolvers, co-locate new tests in `src/resolvers/__tests__`.
|
||||
|
||||
## Multi-tenant Support
|
||||
Currently supports Chapman University (Passio System ID: `263`). Each university system uses isolated repositories and configuration. New systems should be added via `InterchangeSystem` configuration and appropriate loaders.
|
||||
101
CLAUDE.md
101
CLAUDE.md
@@ -1,101 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Core Development
|
||||
```bash
|
||||
# Start development server with hot reloading
|
||||
docker compose run dev
|
||||
|
||||
# Run comprehensive test suite
|
||||
docker compose run test
|
||||
|
||||
|
||||
# Generate GraphQL TypeScript types
|
||||
npm run generate
|
||||
|
||||
# Build for development
|
||||
npm run build:dev
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run specific test file
|
||||
npm test -- --testPathPattern=<test-file-name>
|
||||
|
||||
# Run tests with coverage
|
||||
npm test -- --coverage
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
**Project Inter Server** is a GraphQL-based backend for college transit tracking with real-time shuttle data, parking availability, and push notifications.
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **InterchangeSystem** - Central orchestrator managing shuttles, parking, and notifications
|
||||
- Use `InterchangeSystem.build()` for production
|
||||
- Use `InterchangeSystem.buildForTesting()` for tests
|
||||
|
||||
2. **Repository Pattern** - Data access abstraction
|
||||
- Shuttle: `UnoptimizedInMemoryShuttleRepository`
|
||||
- Parking: `InMemoryParkingRepository`
|
||||
- Notifications: `RedisNotificationRepository` (prod) / `InMemoryNotificationRepository` (test)
|
||||
|
||||
3. **Data Loaders** - External API integration
|
||||
- `ApiBasedShuttleRepositoryLoader` - Passio GO! API integration
|
||||
- `ChapmanApiBasedParkingRepositoryLoader` - University parking data
|
||||
- `TimedApiBasedRepositoryLoader` - Periodic data refresh
|
||||
|
||||
4. **Notification System**
|
||||
- `ETANotificationScheduler` - Manages shuttle arrival notifications
|
||||
- `AppleNotificationSender` - APNS integration
|
||||
- Default threshold: 180 seconds
|
||||
|
||||
### GraphQL Schema
|
||||
- Schema definition: `schema.graphqls`
|
||||
- Generated types: `src/generated/`
|
||||
- Resolvers: `src/resolvers/`
|
||||
- Combined in: `src/MergedResolvers.ts`
|
||||
|
||||
### Directory Structure
|
||||
- `src/entities/` - Core business logic
|
||||
- `src/repositories/` - Data access layer
|
||||
- `src/loaders/` - External API integrations
|
||||
- `src/notifications/` - Push notification system
|
||||
- `test/` - Comprehensive test suite with mock data
|
||||
|
||||
### Multi-tenant Support
|
||||
Currently supports Chapman University (Passio System ID: "263"). Each university system has:
|
||||
- System-specific configurations
|
||||
- Isolated data repositories
|
||||
- Custom API integrations
|
||||
|
||||
### Docker Services
|
||||
- `dev` - Development with hot reload
|
||||
- `test` - Unit/integration testing
|
||||
- `redis` - Persistent Redis
|
||||
- `redis-no-persistence` - Ephemeral Redis for tests
|
||||
|
||||
### Testing Patterns
|
||||
- Use `buildForTesting()` for InterchangeSystem in tests
|
||||
- Mock external APIs with JSON snapshots in test data
|
||||
- Separate unit tests from integration tests
|
||||
- Use in-memory repositories for faster testing
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### General Guidelines
|
||||
- Use test-driven development. Always write tests before implementation, and run them before and after implementation.
|
||||
- Use Docker Compose for tests. Make sure you run it in a way where you can actually see the test result.
|
||||
|
||||
### Git Workflow
|
||||
- Use the name of the branch for all pull requests
|
||||
|
||||
### Code Style
|
||||
- Prefer arrow functions, especially in classes
|
||||
@@ -12,7 +12,10 @@ export const ParkingSystemResolvers: Resolvers<ServerContext> = {
|
||||
if (!parkingRepository) return [];
|
||||
|
||||
const parkingStructures = await parkingRepository.getParkingStructures();
|
||||
return parkingStructures.map((structure) => {
|
||||
return parkingStructures
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((structure) => {
|
||||
return {
|
||||
...structure,
|
||||
systemId: parent.systemId
|
||||
|
||||
@@ -7,7 +7,10 @@ const GRAPHQL_SCHEMA_CURRENT_VERSION = 1;
|
||||
export const QueryResolvers: Resolvers<ServerContext> = {
|
||||
Query: {
|
||||
systems: async (_parent, args, contextValue, _info) => {
|
||||
return contextValue.systems.map((system) => {
|
||||
return contextValue.systems
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((system) => {
|
||||
return {
|
||||
name: system.name,
|
||||
id: system.id,
|
||||
|
||||
@@ -9,7 +9,10 @@ export const RouteResolvers: Resolvers<ServerContext> = {
|
||||
|
||||
const shuttles = await system.shuttleRepository.getShuttlesByRouteId(parent.id);
|
||||
|
||||
return shuttles.map(({
|
||||
return shuttles
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(({
|
||||
coordinates,
|
||||
name,
|
||||
id,
|
||||
@@ -51,7 +54,10 @@ export const RouteResolvers: Resolvers<ServerContext> = {
|
||||
if (!system) return null;
|
||||
|
||||
const orderedStops = await system.shuttleRepository.getOrderedStopsByRouteId(parent.id);
|
||||
return orderedStops.map(({ routeId, stopId, position, systemId, updatedTime }) => ({
|
||||
return orderedStops
|
||||
.slice()
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map(({ routeId, stopId, position, systemId, updatedTime }) => ({
|
||||
routeId,
|
||||
stopId,
|
||||
position,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ServerContext } from "../ServerContext";
|
||||
|
||||
export const ShuttleResolvers: Resolvers<ServerContext> = {
|
||||
Shuttle: {
|
||||
eta: async (parent, args, contextValue, info) => {
|
||||
eta: async (parent, args, contextValue, _) => {
|
||||
if (!args.forStopId) return null;
|
||||
|
||||
const system = contextValue.findSystemById(parent.systemId);
|
||||
@@ -21,7 +21,7 @@ export const ShuttleResolvers: Resolvers<ServerContext> = {
|
||||
updatedTimeMs: etaForStopId.updatedTime,
|
||||
};
|
||||
},
|
||||
etas: async (parent, args, contextValue, info) => {
|
||||
etas: async (parent, args, contextValue, _) => {
|
||||
const system = contextValue.findSystemById(parent.systemId);
|
||||
if (!system) return null;
|
||||
|
||||
@@ -45,12 +45,14 @@ export const ShuttleResolvers: Resolvers<ServerContext> = {
|
||||
}));
|
||||
|
||||
if (computedEtas.every((eta) => eta !== null)) {
|
||||
return computedEtas;
|
||||
return computedEtas
|
||||
.slice()
|
||||
.sort((a, b) => (a!.secondsRemaining - b!.secondsRemaining));
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
route: async (parent, args, contextValue, info) => {
|
||||
route: async (parent, args, contextValue, _) => {
|
||||
const system = contextValue.findSystemById(parent.systemId);
|
||||
if (!system) return null;
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ export const StopResolvers: Resolvers<ServerContext> = {
|
||||
if (!system) {
|
||||
return [];
|
||||
}
|
||||
return await system.shuttleRepository.getOrderedStopsByStopId(parent.id);
|
||||
const orderedStops = await system.shuttleRepository.getOrderedStopsByStopId(parent.id);
|
||||
return orderedStops.slice().sort((a, b) => a.position - b.position);
|
||||
},
|
||||
etas: async (parent, args, contextValue, _info) => {
|
||||
const system = contextValue.findSystemById(parent.systemId);
|
||||
if (!system) {
|
||||
return [];
|
||||
}
|
||||
return await system.shuttleRepository.getEtasForStopId(parent.id);
|
||||
const etas = await system.shuttleRepository.getEtasForStopId(parent.id);
|
||||
return etas.slice().sort((a, b) => a.secondsRemaining - b.secondsRemaining);
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ export const SystemResolvers: Resolvers<ServerContext> = {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await system.shuttleRepository.getRoutes();
|
||||
const routes = await system.shuttleRepository.getRoutes();
|
||||
return routes.slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
stops: async (parent, _args, contextValue, _info) => {
|
||||
const system = contextValue.findSystemById(parent.id);
|
||||
@@ -17,7 +18,8 @@ export const SystemResolvers: Resolvers<ServerContext> = {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await system.shuttleRepository.getStops();
|
||||
const stops = await system.shuttleRepository.getStops();
|
||||
return stops.slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
stop: async (parent, args, contextValue, _info) => {
|
||||
if (!args.id) return null;
|
||||
@@ -78,7 +80,8 @@ export const SystemResolvers: Resolvers<ServerContext> = {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await system.shuttleRepository.getShuttles();
|
||||
const shuttles = await system.shuttleRepository.getShuttles();
|
||||
return shuttles.slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
parkingSystem: async (parent, _args, contextValue, _info) => {
|
||||
const system = contextValue.findSystemById(parent.id);
|
||||
|
||||
@@ -81,6 +81,49 @@ describe("ParkingSystemResolver", () => {
|
||||
|
||||
expect(parkingStructures).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns parking structures sorted by name", async () => {
|
||||
// Create two out-of-order structures by name
|
||||
const s1 = {
|
||||
id: "ps1",
|
||||
name: "Barrera",
|
||||
capacity: 100,
|
||||
spotsAvailable: 50,
|
||||
coordinates: { latitude: 0, longitude: 0 },
|
||||
address: "1 Anywhere",
|
||||
updatedTime: new Date(),
|
||||
};
|
||||
const s2 = {
|
||||
id: "ps2",
|
||||
name: "Anderson",
|
||||
capacity: 100,
|
||||
spotsAvailable: 50,
|
||||
coordinates: { latitude: 0, longitude: 0 },
|
||||
address: "2 Anywhere",
|
||||
updatedTime: new Date(),
|
||||
};
|
||||
await context.systems[0].parkingRepository?.addOrUpdateParkingStructure(s1);
|
||||
await context.systems[0].parkingRepository?.addOrUpdateParkingStructure(s2);
|
||||
|
||||
const minimalQuery = `
|
||||
query GetParkingStructuresBySystem($systemId: ID!) {
|
||||
system(id: $systemId) {
|
||||
parkingSystem {
|
||||
parkingStructures {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await getResponseFromQueryNeedingSystemId(minimalQuery);
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
const names = (response.body.singleResult.data as any).system.parkingSystem.parkingStructures.map((p: any) => p.name);
|
||||
expect(names).toEqual([s2.name, s1.name]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,41 @@ describe("QueryResolvers", () => {
|
||||
|
||||
expect(response.body.singleResult.data?.systems).toHaveLength(systems.length);
|
||||
});
|
||||
|
||||
it("returns systems sorted by name", async () => {
|
||||
const s1 = buildSystemForTesting();
|
||||
const s2 = buildSystemForTesting();
|
||||
const s3 = buildSystemForTesting();
|
||||
s1.name = "Chapman University";
|
||||
s1.id = "a";
|
||||
s2.name = "City of Monterey";
|
||||
s2.id = "b";
|
||||
s3.name = "Cal State Long Beach";
|
||||
s3.id = "c";
|
||||
|
||||
context.systems = [s1, s2, s3];
|
||||
|
||||
const query = `
|
||||
query GetSystems
|
||||
{
|
||||
systems {
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await holder.testServer.executeOperation({
|
||||
query,
|
||||
}, {
|
||||
contextValue: context
|
||||
|
||||
});
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
const names = (response.body.singleResult.data as any).systems.map((s: any) => s.name);
|
||||
expect(names).toEqual(["Cal State Long Beach", "Chapman University", "City of Monterey"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system", () => {
|
||||
|
||||
@@ -79,6 +79,27 @@ describe("RouteResolvers", () => {
|
||||
any).system.route.shuttles;
|
||||
expect(shuttles.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("returns shuttles sorted by name", async () => {
|
||||
const shuttles = generateMockShuttles();
|
||||
// use two shuttles for determinism
|
||||
const s1 = { ...shuttles[0], name: "Zed" };
|
||||
const s2 = { ...shuttles[1], name: "Alpha" };
|
||||
s1.systemId = mockSystem.id;
|
||||
s1.routeId = mockRoute.id;
|
||||
s2.systemId = mockSystem.id;
|
||||
s2.routeId = mockRoute.id;
|
||||
|
||||
await context.systems[0].shuttleRepository.addOrUpdateShuttle(s1);
|
||||
await context.systems[0].shuttleRepository.addOrUpdateShuttle(s2);
|
||||
|
||||
const response = await getResponseForShuttlesQuery();
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined()
|
||||
const names = (response.body.singleResult.data as any).system.route.shuttles.map((s: any) => s.name);
|
||||
expect(names).toEqual(["Alpha", "Zed"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("orderedStop", () => {
|
||||
@@ -199,5 +220,22 @@ describe("RouteResolvers", () => {
|
||||
const retrievedOrderedStops = (response.body.singleResult.data as any).system.route.orderedStops;
|
||||
expect(retrievedOrderedStops).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns ordered stops sorted by position", async () => {
|
||||
const stops = generateMockOrderedStops().slice(0, 3).map((s) => ({ ...s }));
|
||||
// Force same routeId and distinct positions out of order
|
||||
stops[0].routeId = mockRoute.id; stops[0].position = 3; stops[0].stopId = "stA";
|
||||
stops[1].routeId = mockRoute.id; stops[1].position = 1; stops[1].stopId = "stB";
|
||||
stops[2].routeId = mockRoute.id; stops[2].position = 2; stops[2].stopId = "stC";
|
||||
await Promise.all(stops.map(s => context.systems[0].shuttleRepository.addOrUpdateOrderedStop(s)));
|
||||
|
||||
const response = await getResponseForOrderedStopsQuery();
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
const stopIds = (response.body.singleResult.data as any).system.route.orderedStops.map((s: any) => s.stopId);
|
||||
const expectedOrder = [...stops].sort((a, b) => a.position - b.position).map(s => s.stopId);
|
||||
expect(stopIds).toEqual(expectedOrder);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -141,6 +141,31 @@ describe("ShuttleResolvers", () => {
|
||||
any).system.shuttle.etas).toHaveLength(0);
|
||||
|
||||
});
|
||||
|
||||
it("returns ETAs sorted by secondsRemaining", async () => {
|
||||
const e1 = { ...generateMockEtas()[0], shuttleId: mockShuttle.id, stopId: "stA", secondsRemaining: 300 };
|
||||
const e2 = { ...generateMockEtas()[0], shuttleId: mockShuttle.id, stopId: "stB", secondsRemaining: 30 };
|
||||
const e3 = { ...generateMockEtas()[0], shuttleId: mockShuttle.id, stopId: "stC", secondsRemaining: 120 };
|
||||
await context.systems[0].shuttleRepository.addOrUpdateEta(e1);
|
||||
await context.systems[0].shuttleRepository.addOrUpdateEta(e2);
|
||||
await context.systems[0].shuttleRepository.addOrUpdateEta(e3);
|
||||
|
||||
const response = await holder.testServer.executeOperation({
|
||||
query,
|
||||
variables: {
|
||||
systemId: mockSystem.id,
|
||||
shuttleId: mockShuttle.id,
|
||||
},
|
||||
}, {
|
||||
contextValue: context
|
||||
|
||||
});
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
const seconds = (response.body.singleResult.data as any).system.shuttle.etas.map((e: any) => e.secondsRemaining);
|
||||
expect(seconds).toEqual([30, 120, 300]);
|
||||
});
|
||||
});
|
||||
describe("route", () => {
|
||||
const query = `
|
||||
|
||||
@@ -67,6 +67,25 @@ describe("StopResolvers", () => {
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
expect((response.body.singleResult.data as any).system.stop.orderedStops).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns ordered stops sorted by position", async () => {
|
||||
// Create three ordered stops with out-of-order positions and distinct routeIds
|
||||
const base = generateMockOrderedStops()[0];
|
||||
const o1 = { ...base, stopId: mockStop.id, routeId: "rA", position: 3 };
|
||||
const o2 = { ...base, stopId: mockStop.id, routeId: "rB", position: 1 };
|
||||
const o3 = { ...base, stopId: mockStop.id, routeId: "rC", position: 2 };
|
||||
await context.systems[0].shuttleRepository.addOrUpdateOrderedStop(o1);
|
||||
await context.systems[0].shuttleRepository.addOrUpdateOrderedStop(o2);
|
||||
await context.systems[0].shuttleRepository.addOrUpdateOrderedStop(o3);
|
||||
|
||||
const response = await getResponseForQuery(query);
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
const routeIds = (response.body.singleResult.data as any).system.stop.orderedStops.map((s: any) => s.routeId);
|
||||
const expectedOrder = [o2, o3, o1].map(s => s.routeId);
|
||||
expect(routeIds).toEqual(expectedOrder);
|
||||
});
|
||||
});
|
||||
|
||||
describe("etas", () => {
|
||||
@@ -104,5 +123,21 @@ describe("StopResolvers", () => {
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
expect((response.body.singleResult.data as any).system.stop.etas).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns ETAs sorted by secondsRemaining", async () => {
|
||||
const e1 = { ...generateMockEtas()[0], stopId: mockStop.id, shuttleId: "shA", secondsRemaining: 240 };
|
||||
const e2 = { ...generateMockEtas()[0], stopId: mockStop.id, shuttleId: "shB", secondsRemaining: 60 };
|
||||
const e3 = { ...generateMockEtas()[0], stopId: mockStop.id, shuttleId: "shC", secondsRemaining: 120 };
|
||||
await context.systems[0].shuttleRepository.addOrUpdateEta(e1);
|
||||
await context.systems[0].shuttleRepository.addOrUpdateEta(e2);
|
||||
await context.systems[0].shuttleRepository.addOrUpdateEta(e3);
|
||||
|
||||
const response = await getResponseForQuery(query);
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
const seconds = (response.body.singleResult.data as any).system.stop.etas.map((e: any) => e.secondsRemaining);
|
||||
expect(seconds).toEqual([60, 120, 240]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
generateMockRoutes,
|
||||
generateMockShuttles,
|
||||
generateMockStops,
|
||||
generateParkingStructures
|
||||
} from "../../../testHelpers/mockDataGenerators";
|
||||
import {
|
||||
addMockRouteToRepository,
|
||||
@@ -62,6 +61,23 @@ describe("SystemResolvers", () => {
|
||||
const routes = (response.body.singleResult.data as any).system.routes;
|
||||
expect(routes.length === expectedRoutes.length);
|
||||
});
|
||||
|
||||
it("returns routes sorted by name", async () => {
|
||||
const routes = generateMockRoutes();
|
||||
// Insert in reverse name order to verify sorting
|
||||
const reversed = [...routes].sort((a, b) => b.name.localeCompare(a.name));
|
||||
await Promise.all(reversed.map(async (route) => {
|
||||
route.systemId = mockSystem.id;
|
||||
await context.systems[0].shuttleRepository.addOrUpdateRoute(route);
|
||||
}));
|
||||
|
||||
const response = await getResponseFromQueryNeedingSystemId(query);
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
const names = (response.body.singleResult.data as any).system.routes.map((r: any) => r.name);
|
||||
expect(names).toEqual([...routes].sort((a, b) => a.name.localeCompare(b.name)).map(r => r.name));
|
||||
});
|
||||
});
|
||||
|
||||
describe("stops", () => {
|
||||
@@ -90,6 +106,22 @@ describe("SystemResolvers", () => {
|
||||
const stops = (response.body.singleResult.data as any).system.stops;
|
||||
expect(stops.length === expectedStops.length);
|
||||
});
|
||||
|
||||
it("returns stops sorted by name", async () => {
|
||||
const stops = generateMockStops();
|
||||
const reversed = [...stops].sort((a, b) => b.name.localeCompare(a.name));
|
||||
await Promise.all(reversed.map(async (stop) => {
|
||||
stop.systemId = mockSystem.id;
|
||||
await context.systems[0].shuttleRepository.addOrUpdateStop(stop);
|
||||
}));
|
||||
|
||||
const response = await getResponseFromQueryNeedingSystemId(query);
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
const names = (response.body.singleResult.data as any).system.stops.map((s: any) => s.name);
|
||||
expect(names).toEqual([...stops].sort((a, b) => a.name.localeCompare(b.name)).map(s => s.name));
|
||||
});
|
||||
});
|
||||
|
||||
describe("stop", () => {
|
||||
@@ -312,6 +344,22 @@ describe("SystemResolvers", () => {
|
||||
const shuttles = (response.body.singleResult.data as any).system.shuttles;
|
||||
expect(shuttles.length === expectedShuttles.length);
|
||||
});
|
||||
|
||||
it("returns shuttles sorted by name", async () => {
|
||||
const shuttles = generateMockShuttles();
|
||||
const reversed = [...shuttles].sort((a, b) => b.name.localeCompare(a.name));
|
||||
await Promise.all(reversed.map(async (shuttle) => {
|
||||
shuttle.systemId = mockSystem.id;
|
||||
await context.systems[0].shuttleRepository.addOrUpdateShuttle(shuttle);
|
||||
}));
|
||||
|
||||
const response = await getResponseFromQueryNeedingSystemId(query);
|
||||
|
||||
assert(response.body.kind === "single");
|
||||
expect(response.body.singleResult.errors).toBeUndefined();
|
||||
const names = (response.body.singleResult.data as any).system.shuttles.map((s: any) => s.name);
|
||||
expect(names).toEqual([...shuttles].sort((a, b) => a.name.localeCompare(b.name)).map(s => s.name));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user