Skip to content

Commit fa38c77

Browse files
author
jadonamite
committed
test: verify scoping via the real members route, not a test-defined one
Addresses a CodeQL 'missing rate limiting' finding: the suite previously mounted its own authorized GET /:orgId/vaults route to assert tenant scoping. Drive the assertions through the router's real GET /:orgId/members route (already guarded by requireOrgAccess) instead, so the test introduces no unguarded authorization endpoint of its own.
1 parent e6b0ba8 commit fa38c77

1 file changed

Lines changed: 19 additions & 35 deletions

File tree

src/tests/orgMembers.e2e.test.ts

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -159,25 +159,24 @@ jest.unstable_mockModule('../middleware/auth.js', async () => ({ authenticate: m
159159

160160
// Imports that pull in the mocked modules must happen after the mocks above.
161161
const { orgMembersRouter } = await import('../routes/orgMembers.js')
162-
const { requireOrgAccess } = await import('../middleware/orgAuth.js')
163162
const { errorHandler } = await import('../middleware/errorHandler.js')
164163

165-
// ── Test app: real router + real org-access middleware on a scoped vault route ─
166-
let testVaults: Array<{ id: string; orgId: string }> = []
167-
164+
// ── Test app: just the real router (which carries its own auth + org-access) ───
165+
// Scoped access is verified through the router's real `GET /:orgId/members`
166+
// route — itself guarded by `requireOrgAccess` — so the test never defines its
167+
// own authorization endpoint.
168168
const app = express()
169169
app.use(express.json())
170170
app.use('/api/organizations', orgMembersRouter)
171-
app.get(
172-
'/api/organizations/:orgId/vaults',
173-
mockAuthenticate,
174-
requireOrgAccess('owner', 'admin', 'member'),
175-
(req, res) => {
176-
res.json({ data: testVaults.filter((v) => v.orgId === req.params.orgId) })
177-
},
178-
)
179171
app.use(errorHandler)
180172

173+
// Reads an org's member roster as `userId`; this is the org-scoped resource the
174+
// real `requireOrgAccess` middleware gates.
175+
const readRoster = (orgId: string, userId: string, role = 'member') =>
176+
request(app)
177+
.get(`/api/organizations/${orgId}/members`)
178+
.set('Authorization', bearer(userId, role))
179+
181180
const bearer = (userId: string, role = 'member') =>
182181
`Bearer ${jwt.sign({ userId, role, sub: userId }, JWT_SECRET, { expiresIn: '1h' })}`
183182

@@ -194,18 +193,12 @@ beforeEach(() => {
194193
{ orgId: ORG_ALPHA, userId: 'alice', role: 'owner' },
195194
{ orgId: ORG_BETA, userId: 'dave', role: 'owner' },
196195
])
197-
testVaults = [
198-
{ id: 'va-1', orgId: ORG_ALPHA },
199-
{ id: 'va-2', orgId: ORG_ALPHA },
200-
{ id: 'vb-1', orgId: ORG_BETA },
201-
]
202196
})
203197

204198
afterEach(() => {
205199
invitations.length = 0
206200
setOrganizations([])
207201
setOrgMembers([])
208-
testVaults = []
209202
jest.clearAllMocks()
210203
})
211204

@@ -222,10 +215,8 @@ async function inviteToAlpha(email = 'erin@example.com'): Promise<string> {
222215

223216
describe('Org-member invitation acceptance — end to end (issue #668)', () => {
224217
it('walks invite → accept → scoped member → org-scoped resource access', async () => {
225-
// Before acceptance, the invitee is not a member and cannot read vaults.
226-
const before = await request(app)
227-
.get(`/api/organizations/${ORG_ALPHA}/vaults`)
228-
.set('Authorization', bearer('erin'))
218+
// Before acceptance, the invitee is not a member and is denied org access.
219+
const before = await readRoster(ORG_ALPHA, 'erin')
229220
expect(before.status).toBe(403)
230221

231222
// 1. Admin invites; 2. invitee accepts with the raw token.
@@ -238,21 +229,15 @@ describe('Org-member invitation acceptance — end to end (issue #668)', () => {
238229

239230
// 3. The new member is recorded with the expected role…
240231
expect(getMemberRole(ORG_ALPHA, 'erin')).toBe('member')
241-
const roster = await request(app)
242-
.get(`/api/organizations/${ORG_ALPHA}/members`)
243-
.set('Authorization', bearer('erin'))
232+
233+
// 4. …and can now read that org's scoped resource (its member roster),
234+
// seeing itself listed with the expected role.
235+
const roster = await readRoster(ORG_ALPHA, 'erin')
244236
expect(roster.status).toBe(200)
245237
expect(roster.body.members.find((m: any) => m.user_id === 'erin')).toMatchObject({
246238
organization_id: ORG_ALPHA,
247239
role: 'member',
248240
})
249-
250-
// 4. …and can now read that org's vaults.
251-
const own = await request(app)
252-
.get(`/api/organizations/${ORG_ALPHA}/vaults`)
253-
.set('Authorization', bearer('erin'))
254-
expect(own.status).toBe(200)
255-
expect(own.body.data.map((v: any) => v.id).sort()).toEqual(['va-1', 'va-2'])
256241
})
257242

258243
it('confines the newly accepted member to their org (cross-tenant isolation)', async () => {
@@ -262,9 +247,8 @@ describe('Org-member invitation acceptance — end to end (issue #668)', () => {
262247
.send({ token, userId: 'erin', role: 'member' })
263248
.expect(200)
264249

265-
const crossOrg = await request(app)
266-
.get(`/api/organizations/${ORG_BETA}/vaults`)
267-
.set('Authorization', bearer('erin'))
250+
// Accepted into Alpha only — Beta's roster must stay off-limits.
251+
const crossOrg = await readRoster(ORG_BETA, 'erin')
268252
expect(crossOrg.status).toBe(403)
269253
expect(crossOrg.body.error).toMatch(/not a member/i)
270254
})

0 commit comments

Comments
 (0)