Skip to content

Commit 21ac7b1

Browse files
authored
improvement(tests): db-mock migration tranche 4 — billing, webhooks/execution/logs, routes/misc (final) (#5866)
* improvement(tests): db-mock migration tranche 4 — billing, webhooks/execution/logs, routes/misc (final) * fix(tests): route agent-handler MCP server rows through queueTableRows
1 parent 51307c4 commit 21ac7b1

43 files changed

Lines changed: 1091 additions & 2256 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/api/auth/sso/register/route.test.ts

Lines changed: 58 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,46 @@
11
/**
22
* @vitest-environment node
33
*/
4-
import { createEnvMock, createMockRequest } from '@sim/testing'
5-
import { beforeEach, describe, expect, it, vi } from 'vitest'
4+
import {
5+
createEnvMock,
6+
createMockRequest,
7+
dbChainMock,
8+
dbChainMockFns,
9+
queueTableRows,
10+
resetDbChainMock,
11+
schemaMock,
12+
} from '@sim/testing'
13+
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
614

715
const {
816
mockGetSession,
917
mockRegisterSSOProvider,
1018
mockHasSSOAccess,
1119
mockValidateUrlWithDNS,
1220
mockSecureFetchWithPinnedIP,
13-
dbState,
14-
memberTable,
15-
ssoProviderTable,
1621
} = vi.hoisted(() => ({
1722
mockGetSession: vi.fn(),
1823
mockRegisterSSOProvider: vi.fn(),
1924
mockHasSSOAccess: vi.fn(),
2025
mockValidateUrlWithDNS: vi.fn(),
2126
mockSecureFetchWithPinnedIP: vi.fn(),
22-
dbState: { members: [] as any[], providers: [] as any[] },
23-
memberTable: {
24-
userId: 'member.userId',
25-
organizationId: 'member.organizationId',
26-
role: 'member.role',
27-
},
28-
ssoProviderTable: {
29-
id: 'sso.id',
30-
providerId: 'sso.providerId',
31-
domain: 'sso.domain',
32-
issuer: 'sso.issuer',
33-
userId: 'sso.userId',
34-
organizationId: 'sso.organizationId',
35-
oidcConfig: 'sso.oidcConfig',
36-
samlConfig: 'sso.samlConfig',
37-
},
3827
}))
3928

40-
function makeBuilder(rows: any[]): any {
41-
const thenable: any = Promise.resolve(rows)
42-
thenable.where = (condition: any) => {
43-
const values = condition?.values
44-
if (Array.isArray(values) && values.length > 0) {
45-
const target = String(values[values.length - 1]).toLowerCase()
46-
return makeBuilder(rows.filter((r) => String(r.domain ?? '').toLowerCase() === target))
47-
}
48-
return makeBuilder(rows)
49-
}
50-
thenable.limit = () => Promise.resolve(rows)
51-
thenable.orderBy = () => Promise.resolve(rows)
52-
return thenable
29+
vi.mock('@sim/db', () => ({ ...dbChainMock, ...schemaMock }))
30+
31+
/** Queues the caller's org membership row(s) for the admin/owner check. */
32+
function queueMembers(rows: Array<Record<string, unknown>>) {
33+
queueTableRows(schemaMock.member, rows)
5334
}
5435

55-
vi.mock('@sim/db', () => ({
56-
db: {
57-
select: () => ({
58-
from: (table: unknown) =>
59-
makeBuilder(table === memberTable ? dbState.members : dbState.providers),
60-
}),
61-
},
62-
member: memberTable,
63-
ssoProvider: ssoProviderTable,
64-
}))
36+
/**
37+
* Queues existing SSO provider rows for BOTH domain-conflict lookups (the
38+
* pre-registration check and the post-registration re-check).
39+
*/
40+
function queueProviders(rows: Array<Record<string, unknown>>) {
41+
queueTableRows(schemaMock.ssoProvider, rows)
42+
queueTableRows(schemaMock.ssoProvider, rows)
43+
}
6544

6645
vi.mock('@/lib/auth', () => ({
6746
getSession: mockGetSession,
@@ -109,15 +88,18 @@ function request(body: Record<string, unknown>) {
10988
describe('POST /api/auth/sso/register', () => {
11089
beforeEach(() => {
11190
vi.clearAllMocks()
112-
dbState.members = []
113-
dbState.providers = []
91+
resetDbChainMock()
11492
mockGetSession.mockResolvedValue({ user: { id: 'u1' } })
11593
mockHasSSOAccess.mockResolvedValue(true)
11694
mockValidateUrlWithDNS.mockResolvedValue({ isValid: true, resolvedIP: '1.2.3.4' })
11795
mockSecureFetchWithPinnedIP.mockRejectedValue(new Error('discovery not mocked for this test'))
11896
mockRegisterSSOProvider.mockResolvedValue({ providerId: 'acme-oidc' })
11997
})
12098

99+
afterAll(() => {
100+
resetDbChainMock()
101+
})
102+
121103
it('rejects callers without an Enterprise plan', async () => {
122104
mockHasSSOAccess.mockResolvedValue(false)
123105
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
@@ -126,22 +108,22 @@ describe('POST /api/auth/sso/register', () => {
126108
})
127109

128110
it('rejects callers who are not an admin/owner of the target org', async () => {
129-
dbState.members = [{ organizationId: 'org1', role: 'member' }]
111+
queueMembers([{ organizationId: 'org1', role: 'member' }])
130112
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
131113
expect(res.status).toBe(403)
132114
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
133115
})
134116

135117
it('rejects an invalid domain', async () => {
136-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
118+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
137119
const res = await POST(request({ ...OIDC_BODY, domain: 'not-a-domain', orgId: 'org1' }))
138120
expect(res.status).toBe(400)
139121
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
140122
})
141123

142124
it('rejects a domain already registered by another organization', async () => {
143-
dbState.members = [{ organizationId: 'org-attacker', role: 'owner' }]
144-
dbState.providers = [{ domain: 'acme.com', userId: 'u-victim', organizationId: 'org-victim' }]
125+
queueMembers([{ organizationId: 'org-attacker', role: 'owner' }])
126+
queueProviders([{ domain: 'acme.com', userId: 'u-victim', organizationId: 'org-victim' }])
145127
const res = await POST(request({ ...OIDC_BODY, orgId: 'org-attacker' }))
146128
const json = await res.json()
147129
expect(res.status).toBe(409)
@@ -150,46 +132,51 @@ describe('POST /api/auth/sso/register', () => {
150132
})
151133

152134
it('matches conflicts across casing variants', async () => {
153-
dbState.members = [{ organizationId: 'org-attacker', role: 'owner' }]
154-
dbState.providers = [{ domain: 'ACME.com', userId: 'u-victim', organizationId: 'org-victim' }]
135+
queueMembers([{ organizationId: 'org-attacker', role: 'owner' }])
136+
queueProviders([{ domain: 'ACME.com', userId: 'u-victim', organizationId: 'org-victim' }])
155137
const res = await POST(request({ ...OIDC_BODY, orgId: 'org-attacker' }))
156138
expect(res.status).toBe(409)
157139
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
140+
// The conflict lookup itself must be case-insensitive: lower(domain) = <normalized domain>.
141+
const conflictWhere = dbChainMockFns.where.mock.calls.find(([condition]) =>
142+
condition?.strings?.join('?').includes('lower(')
143+
)
144+
expect(conflictWhere?.[0]?.values).toContain('acme.com')
158145
})
159146

160147
it('registers when the domain is unclaimed', async () => {
161-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
148+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
162149
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
163150
expect(res.status).toBe(200)
164151
expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1)
165152
})
166153

167154
it('allows the owning tenant to update its own provider for the same domain', async () => {
168-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
169-
dbState.providers = [{ domain: 'acme.com', userId: 'u1', organizationId: 'org1' }]
155+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
156+
queueProviders([{ domain: 'acme.com', userId: 'u1', organizationId: 'org1' }])
170157
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
171158
expect(res.status).toBe(200)
172159
expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1)
173160
})
174161

175162
it('lets an org admin adopt their own user-scoped provider for the same domain', async () => {
176-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
177-
dbState.providers = [{ domain: 'acme.com', userId: 'u1', organizationId: null }]
163+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
164+
queueProviders([{ domain: 'acme.com', userId: 'u1', organizationId: null }])
178165
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
179166
expect(res.status).toBe(200)
180167
expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1)
181168
})
182169

183170
it("still blocks an org admin from claiming another user's user-scoped domain", async () => {
184-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
185-
dbState.providers = [{ domain: 'acme.com', userId: 'someone-else', organizationId: null }]
171+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
172+
queueProviders([{ domain: 'acme.com', userId: 'someone-else', organizationId: null }])
186173
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
187174
expect(res.status).toBe(409)
188175
expect(mockRegisterSSOProvider).not.toHaveBeenCalled()
189176
})
190177

191178
it('normalizes the domain before persisting it', async () => {
192-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
179+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
193180
const res = await POST(request({ ...OIDC_BODY, domain: 'ACME.com', orgId: 'org1' }))
194181
expect(res.status).toBe(200)
195182
expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1)
@@ -198,23 +185,23 @@ describe('POST /api/auth/sso/register', () => {
198185
})
199186

200187
it('passes skipDiscovery since Sim already resolved and validated the OIDC endpoints', async () => {
201-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
188+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
202189
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
203190
expect(res.status).toBe(200)
204191
const config = mockRegisterSSOProvider.mock.calls[0][0].body
205192
expect(config.oidcConfig.skipDiscovery).toBe(true)
206193
})
207194

208195
it('omits userInfoEndpoint when skipUserInfoEndpoint is requested, forcing ID token claims', async () => {
209-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
196+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
210197
const res = await POST(request({ ...OIDC_BODY, skipUserInfoEndpoint: true, orgId: 'org1' }))
211198
expect(res.status).toBe(200)
212199
const config = mockRegisterSSOProvider.mock.calls[0][0].body
213200
expect(config.oidcConfig.userInfoEndpoint).toBeUndefined()
214201
})
215202

216203
it('does not SSRF-validate userInfoEndpoint when skipUserInfoEndpoint is requested', async () => {
217-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
204+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
218205
mockValidateUrlWithDNS.mockImplementation(async (url: string, label: string) => {
219206
if (label === 'OIDC userInfoEndpoint') {
220207
return { isValid: false, error: 'resolves to a private IP address' }
@@ -228,7 +215,7 @@ describe('POST /api/auth/sso/register', () => {
228215
})
229216

230217
it('does not SSRF-validate a discovered userinfo_endpoint when skipUserInfoEndpoint is requested', async () => {
231-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
218+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
232219
mockValidateUrlWithDNS.mockImplementation(async (url: string, label: string) => {
233220
if (label === 'OIDC userinfo_endpoint') {
234221
return { isValid: false, error: 'resolves to a private IP address' }
@@ -258,15 +245,15 @@ describe('POST /api/auth/sso/register', () => {
258245
})
259246

260247
it('keeps userInfoEndpoint when skipUserInfoEndpoint is not requested', async () => {
261-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
248+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
262249
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
263250
expect(res.status).toBe(200)
264251
const config = mockRegisterSSOProvider.mock.calls[0][0].body
265252
expect(config.oidcConfig.userInfoEndpoint).toBe('https://idp.acme.com/userinfo')
266253
})
267254

268255
it('selects tokenEndpointAuthentication from the discovery document when endpoints are auto-discovered', async () => {
269-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
256+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
270257
mockSecureFetchWithPinnedIP.mockResolvedValue({
271258
ok: true,
272259
json: async () => ({
@@ -290,7 +277,7 @@ describe('POST /api/auth/sso/register', () => {
290277
})
291278

292279
it('still selects tokenEndpointAuthentication from discovery when all endpoints are explicit', async () => {
293-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
280+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
294281
mockSecureFetchWithPinnedIP.mockResolvedValue({
295282
ok: true,
296283
json: async () => ({
@@ -305,7 +292,7 @@ describe('POST /api/auth/sso/register', () => {
305292
})
306293

307294
it('registers successfully when discovery is unreachable and all endpoints are explicit', async () => {
308-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
295+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
309296
mockSecureFetchWithPinnedIP.mockRejectedValue(new Error('ECONNREFUSED'))
310297
const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' }))
311298
expect(res.status).toBe(200)
@@ -316,7 +303,7 @@ describe('POST /api/auth/sso/register', () => {
316303
})
317304

318305
it('prefers client_secret_post over client_secret_basic when an IdP supports both', async () => {
319-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
306+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
320307
mockSecureFetchWithPinnedIP.mockResolvedValue({
321308
ok: true,
322309
json: async () => ({
@@ -330,7 +317,7 @@ describe('POST /api/auth/sso/register', () => {
330317
})
331318

332319
it('defaults to client_secret_post when discovery advertises no auth methods', async () => {
333-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
320+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
334321
mockSecureFetchWithPinnedIP.mockResolvedValue({
335322
ok: true,
336323
json: async () => ({}),
@@ -342,7 +329,7 @@ describe('POST /api/auth/sso/register', () => {
342329
})
343330

344331
it('surfaces the specific discovery failure reason when endpoints are missing', async () => {
345-
dbState.members = [{ organizationId: 'org1', role: 'owner' }]
332+
queueMembers([{ organizationId: 'org1', role: 'owner' }])
346333
mockValidateUrlWithDNS.mockImplementation(async (url: string, label: string) => {
347334
if (label === 'OIDC discovery URL') {
348335
return { isValid: false, error: 'resolves to a private IP address' }

0 commit comments

Comments
 (0)