Skip to content

Commit 5ffde19

Browse files
committed
Scope GET /api/rides results by caller role and add scoping tests
1 parent 8c9b460 commit 5ffde19

1 file changed

Lines changed: 308 additions & 0 deletions

File tree

server/tests/ride.test.ts

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import request from 'supertest';
2+
import { expect } from 'chai';
3+
import app from '../src/app';
4+
import authorize from './utils/auth';
5+
import { Driver, Location, Ride, Rider } from '../src/models';
6+
import { clearDB, populateDB } from './utils/db';
7+
import { AdminType } from '../src/models/admin';
8+
import { Accessibility, Organization } from '../src/models/rider';
9+
import { Status, Type, SchedulingState } from '../src/models/ride';
10+
import { LocationType, Tag } from '../src/models/location';
11+
import moment from 'moment';
12+
13+
// Fixed IDs so assertions can reference rides by known ID without dynamic lookups
14+
const RIDER0_ID = 'scope-rider-0';
15+
const RIDER1_ID = 'scope-rider-1';
16+
const DRIVER0_ID = 'scope-driver-0';
17+
const DRIVER1_ID = 'scope-driver-1';
18+
const RIDE_R0_D0_ID = 'scope-ride-r0d0'; // rider0's ride, driven by driver0
19+
const RIDE_R1_D1_ID = 'scope-ride-r1d1'; // rider1's ride, driven by driver1
20+
21+
const testAdmin: Omit<AdminType, 'id'> = {
22+
firstName: 'Scope-Admin',
23+
lastName: 'Test',
24+
phoneNumber: '1111111111',
25+
email: 'scope-admin@cornell.edu',
26+
type: ['sds-admin'],
27+
isDriver: false,
28+
};
29+
30+
const testRiders = [
31+
{
32+
id: RIDER0_ID,
33+
email: 'scope-rider0@test.com',
34+
phoneNumber: '1234567890',
35+
firstName: 'ScopeRider',
36+
lastName: 'Zero',
37+
joinDate: '2023-03-09',
38+
endDate: '2025-03-09',
39+
favoriteLocations: [],
40+
active: true,
41+
accessibility: [Accessibility.CRUTCHES],
42+
organization: Organization.CULIFT,
43+
description: '',
44+
address: '36 Colonial Ln, Ithaca, NY 14850',
45+
photoLink: '',
46+
},
47+
{
48+
id: RIDER1_ID,
49+
email: 'scope-rider1@test.com',
50+
phoneNumber: '1234567891',
51+
firstName: 'ScopeRider',
52+
lastName: 'One',
53+
joinDate: '2023-03-09',
54+
endDate: '2025-03-09',
55+
favoriteLocations: [],
56+
active: true,
57+
accessibility: [],
58+
organization: Organization.CULIFT,
59+
description: '',
60+
address: '37 Colonial Ln, Ithaca, NY 14850',
61+
photoLink: '',
62+
},
63+
];
64+
65+
const testDrivers = [
66+
{
67+
id: DRIVER0_ID,
68+
email: 'scope-driver0@test.com',
69+
phoneNumber: '1234567890',
70+
firstName: 'ScopeDriver',
71+
lastName: 'Zero',
72+
availability: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
73+
photoLink: '',
74+
},
75+
{
76+
id: DRIVER1_ID,
77+
email: 'scope-driver1@test.com',
78+
phoneNumber: '1234567891',
79+
firstName: 'ScopeDriver',
80+
lastName: 'One',
81+
availability: ['MON', 'TUE'],
82+
photoLink: '',
83+
},
84+
];
85+
86+
const testLocations: LocationType[] = [
87+
{
88+
id: 'scope-loc-1',
89+
name: 'Scope-Location 1',
90+
address: '100 Scope Test Rd',
91+
tag: Tag.WEST,
92+
info: 'Scope Info 1',
93+
shortName: 'Scope-1',
94+
lat: 44.0,
95+
lng: -76.0,
96+
},
97+
{
98+
id: 'scope-loc-2',
99+
name: 'Scope-Location 2',
100+
address: '200 Scope Test Rd',
101+
tag: Tag.NORTH,
102+
info: 'Scope Info 2',
103+
shortName: 'Scope-2',
104+
lat: 45.0,
105+
lng: -77.0,
106+
},
107+
];
108+
109+
// Two rides with no date overlap — allDates=true used in tests to skip date filtering
110+
// ride-r0-d0: visible to rider0, driver0, and any admin
111+
// ride-r1-d1: must NOT appear for rider0 or driver0 (isolation check)
112+
const testRides = [
113+
{
114+
id: RIDE_R0_D0_ID,
115+
type: Type.PAST,
116+
status: Status.COMPLETED,
117+
schedulingState: SchedulingState.SCHEDULED,
118+
startLocation: testLocations[0].id,
119+
endLocation: testLocations[1].id,
120+
startTime: moment().subtract(2, 'hours').toISOString(),
121+
endTime: moment().subtract(1, 'hour').toISOString(),
122+
riders: [RIDER0_ID],
123+
driver: DRIVER0_ID,
124+
isRecurring: false,
125+
},
126+
{
127+
id: RIDE_R1_D1_ID,
128+
type: Type.PAST,
129+
status: Status.COMPLETED,
130+
schedulingState: SchedulingState.SCHEDULED,
131+
startLocation: testLocations[0].id,
132+
endLocation: testLocations[1].id,
133+
startTime: moment().subtract(4, 'hours').toISOString(),
134+
endTime: moment().subtract(3, 'hours').toISOString(),
135+
riders: [RIDER1_ID],
136+
driver: DRIVER1_ID,
137+
isRecurring: false,
138+
},
139+
];
140+
141+
describe('Testing role-scoped access for GET /api/rides', () => {
142+
let adminToken: string;
143+
let rider0Token: string;
144+
let driver0Token: string;
145+
146+
before(async () => {
147+
await Promise.all(
148+
testLocations.map((location) => populateDB(Location, location))
149+
);
150+
// authorize() calls populateDB internally for the authenticated user
151+
adminToken = await authorize('Admin', testAdmin);
152+
rider0Token = await authorize('Rider', testRiders[0]);
153+
driver0Token = await authorize('Driver', testDrivers[0]);
154+
// rider1 and driver1 are data-only (no tokens needed — they exist to be targeted by spoofing tests)
155+
await populateDB(Rider, testRiders[1]);
156+
await populateDB(Driver, testDrivers[1]);
157+
await Promise.all(testRides.map((ride) => populateDB(Ride, ride)));
158+
});
159+
160+
after(clearDB);
161+
162+
// ─────────────────────────────────────────────────────────
163+
// Rider scoping — 4 scenarios
164+
// ─────────────────────────────────────────────────────────
165+
describe('Rider role scoping', () => {
166+
// Scenario 1: Rider supplies own ?rider= — normal case
167+
it('should return only own rides when Rider supplies own ?rider= param', async () => {
168+
const res = await request(app)
169+
.get(`/api/rides?rider=${RIDER0_ID}&allDates=true`)
170+
.auth(rider0Token, { type: 'bearer' })
171+
.expect(200)
172+
.expect('Content-Type', 'application/json; charset=utf-8');
173+
expect(res.body).to.have.property('data');
174+
const ids = res.body.data.map((r: any) => r.id);
175+
expect(ids).to.include(RIDE_R0_D0_ID);
176+
expect(ids).to.not.include(RIDE_R1_D1_ID);
177+
});
178+
179+
// Scenario 2: Rider tries to spoof another rider's ID via ?rider=
180+
it('should ignore spoofed ?rider= param and return only own rides', async () => {
181+
const res = await request(app)
182+
.get(`/api/rides?rider=${RIDER1_ID}&allDates=true`)
183+
.auth(rider0Token, { type: 'bearer' })
184+
.expect(200)
185+
.expect('Content-Type', 'application/json; charset=utf-8');
186+
const ids = res.body.data.map((r: any) => r.id);
187+
expect(ids).to.include(RIDE_R0_D0_ID);
188+
expect(ids).to.not.include(RIDE_R1_D1_ID);
189+
});
190+
191+
// Scenario 3: Rider omits ?rider= entirely — must not leak all rides
192+
it('should return only own rides when Rider omits ?rider= param entirely', async () => {
193+
const res = await request(app)
194+
.get('/api/rides?allDates=true')
195+
.auth(rider0Token, { type: 'bearer' })
196+
.expect(200)
197+
.expect('Content-Type', 'application/json; charset=utf-8');
198+
const ids = res.body.data.map((r: any) => r.id);
199+
expect(ids).to.include(RIDE_R0_D0_ID);
200+
expect(ids).to.not.include(RIDE_R1_D1_ID);
201+
});
202+
203+
// Scenario 4: Rider passes a ?driver= cross-type param — must be discarded
204+
it('should discard ?driver= cross-type param from Rider and return only own rides', async () => {
205+
const res = await request(app)
206+
.get(`/api/rides?driver=${DRIVER0_ID}&allDates=true`)
207+
.auth(rider0Token, { type: 'bearer' })
208+
.expect(200)
209+
.expect('Content-Type', 'application/json; charset=utf-8');
210+
const ids = res.body.data.map((r: any) => r.id);
211+
expect(ids).to.include(RIDE_R0_D0_ID);
212+
expect(ids).to.not.include(RIDE_R1_D1_ID);
213+
});
214+
});
215+
216+
// ─────────────────────────────────────────────────────────
217+
// Driver scoping — 4 scenarios
218+
// ─────────────────────────────────────────────────────────
219+
describe('Driver role scoping', () => {
220+
// Scenario 5: Driver supplies own ?driver= — normal case
221+
it('should return only own rides when Driver supplies own ?driver= param', async () => {
222+
const res = await request(app)
223+
.get(`/api/rides?driver=${DRIVER0_ID}&allDates=true`)
224+
.auth(driver0Token, { type: 'bearer' })
225+
.expect(200)
226+
.expect('Content-Type', 'application/json; charset=utf-8');
227+
expect(res.body).to.have.property('data');
228+
const ids = res.body.data.map((r: any) => r.id);
229+
expect(ids).to.include(RIDE_R0_D0_ID);
230+
expect(ids).to.not.include(RIDE_R1_D1_ID);
231+
});
232+
233+
// Scenario 6: Driver tries to spoof another driver's ID via ?driver=
234+
it('should ignore spoofed ?driver= param and return only own rides', async () => {
235+
const res = await request(app)
236+
.get(`/api/rides?driver=${DRIVER1_ID}&allDates=true`)
237+
.auth(driver0Token, { type: 'bearer' })
238+
.expect(200)
239+
.expect('Content-Type', 'application/json; charset=utf-8');
240+
const ids = res.body.data.map((r: any) => r.id);
241+
expect(ids).to.include(RIDE_R0_D0_ID);
242+
expect(ids).to.not.include(RIDE_R1_D1_ID);
243+
});
244+
245+
// Scenario 7: Driver omits ?driver= entirely — must not leak all rides
246+
it('should return only own rides when Driver omits ?driver= param entirely', async () => {
247+
const res = await request(app)
248+
.get('/api/rides?allDates=true')
249+
.auth(driver0Token, { type: 'bearer' })
250+
.expect(200)
251+
.expect('Content-Type', 'application/json; charset=utf-8');
252+
const ids = res.body.data.map((r: any) => r.id);
253+
expect(ids).to.include(RIDE_R0_D0_ID);
254+
expect(ids).to.not.include(RIDE_R1_D1_ID);
255+
});
256+
257+
// Scenario 8: Driver passes a ?rider= cross-type param — must be discarded
258+
it('should discard ?rider= cross-type param from Driver and return only own rides', async () => {
259+
const res = await request(app)
260+
.get(`/api/rides?rider=${RIDER0_ID}&allDates=true`)
261+
.auth(driver0Token, { type: 'bearer' })
262+
.expect(200)
263+
.expect('Content-Type', 'application/json; charset=utf-8');
264+
const ids = res.body.data.map((r: any) => r.id);
265+
expect(ids).to.include(RIDE_R0_D0_ID);
266+
expect(ids).to.not.include(RIDE_R1_D1_ID);
267+
});
268+
});
269+
270+
// ─────────────────────────────────────────────────────────
271+
// Admin scoping — 2 scenarios (behaviour must be unchanged)
272+
// ─────────────────────────────────────────────────────────
273+
describe('Admin role — unrestricted access', () => {
274+
// Scenario 9: Admin scopes by a specific driver — returns only that driver's rides
275+
it("should return only the specified driver's rides when Admin passes ?driver= param", async () => {
276+
const res = await request(app)
277+
.get(`/api/rides?driver=${DRIVER0_ID}&allDates=true`)
278+
.auth(adminToken, { type: 'bearer' })
279+
.expect(200)
280+
.expect('Content-Type', 'application/json; charset=utf-8');
281+
const ids = res.body.data.map((r: any) => r.id);
282+
expect(ids).to.include(RIDE_R0_D0_ID);
283+
expect(ids).to.not.include(RIDE_R1_D1_ID);
284+
});
285+
286+
// Scenario 10: Admin with no filters — returns all rides
287+
it('should return all rides when Admin makes an unfiltered request', async () => {
288+
const res = await request(app)
289+
.get('/api/rides?allDates=true')
290+
.auth(adminToken, { type: 'bearer' })
291+
.expect(200)
292+
.expect('Content-Type', 'application/json; charset=utf-8');
293+
const ids = res.body.data.map((r: any) => r.id);
294+
expect(ids).to.include(RIDE_R0_D0_ID);
295+
expect(ids).to.include(RIDE_R1_D1_ID);
296+
});
297+
});
298+
299+
// ─────────────────────────────────────────────────────────
300+
// Unauthenticated request — must be rejected
301+
// ─────────────────────────────────────────────────────────
302+
describe('Unauthenticated requests', () => {
303+
it('should fail with 400 given no authorization header', async () => {
304+
const res = await request(app).get('/api/rides').expect(400);
305+
expect(res.body).to.have.property('err');
306+
});
307+
});
308+
});

0 commit comments

Comments
 (0)