Skip to content

Commit cf91570

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(eloqua): bind selector destination to account
1 parent 2a8aedf commit cf91570

2 files changed

Lines changed: 97 additions & 11 deletions

File tree

apps/sim/lib/selectors/server/providers/eloqua.test.ts

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
*/
44
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
55

6-
const { mockFetch, mockResolveSelectorCredentialBundle } = vi.hoisted(() => ({
6+
const { mockFetch, mockGetCredential, mockResolveSelectorCredentialBundle } = vi.hoisted(() => ({
77
mockFetch: vi.fn(),
8+
mockGetCredential: vi.fn(),
89
mockResolveSelectorCredentialBundle: vi.fn(),
910
}))
1011

12+
vi.mock('@/lib/oauth/credential-service', () => ({
13+
getCredential: mockGetCredential,
14+
}))
15+
1116
vi.mock('@/lib/selectors/server/providers/credential-bundle', () => ({
1217
resolveSelectorCredentialBundle: mockResolveSelectorCredentialBundle,
1318
}))
@@ -30,7 +35,18 @@ function args(
3035
workspaceId: 'workspace-1',
3136
principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' },
3237
requesterUserId: 'user-1',
33-
credential: { suppliedId: 'credential-1' },
38+
credential: {
39+
suppliedId: 'credential-1',
40+
providerId: 'eloqua',
41+
access: {
42+
ok: true,
43+
credentialOwnerUserId: 'owner-1',
44+
workspaceId: 'workspace-1',
45+
resolvedCredentialId: 'account-1',
46+
credentialType: 'oauth',
47+
},
48+
signal,
49+
},
3450
references: new Map(),
3551
protectedValues: createSelectorProtectedValues(),
3652
signal,
@@ -41,9 +57,12 @@ describe('Oracle Eloqua server selector adapter', () => {
4157
beforeEach(() => {
4258
vi.clearAllMocks()
4359
vi.stubGlobal('fetch', mockFetch)
60+
mockGetCredential.mockResolvedValue({
61+
providerId: 'eloqua',
62+
scope: '__eloqua_instance__:https://secure.p03.eloqua.com,full',
63+
})
4464
mockResolveSelectorCredentialBundle.mockResolvedValue({
4565
accessToken: 'server-only-token',
46-
instanceUrl: 'https://secure.p03.eloqua.com',
4766
})
4867
})
4968

@@ -99,7 +118,14 @@ describe('Oracle Eloqua server selector adapter', () => {
99118
expect(firstUrl.searchParams.get('count')).toBe('100')
100119
expect(firstUrl.searchParams.get('page')).toBe('1')
101120
expect(secondUrl.searchParams.get('page')).toBe('2')
121+
expect(mockGetCredential).toHaveBeenCalledTimes(2)
122+
expect(mockGetCredential).toHaveBeenCalledWith('selector-execution', 'account-1', 'owner-1')
102123
expect(mockResolveSelectorCredentialBundle).toHaveBeenCalledTimes(2)
124+
expect(mockResolveSelectorCredentialBundle).toHaveBeenCalledWith(
125+
expect.objectContaining({
126+
credential: expect.objectContaining({ providerId: 'eloqua', suppliedId: 'account-1' }),
127+
})
128+
)
103129
})
104130

105131
it.each([
@@ -125,14 +151,55 @@ describe('Oracle Eloqua server selector adapter', () => {
125151
})
126152

127153
it('rejects a credential with an unsafe destination before fetching', async () => {
128-
mockResolveSelectorCredentialBundle.mockResolvedValue({
129-
accessToken: 'server-only-token',
130-
instanceUrl: 'https://evil.example',
154+
mockGetCredential.mockResolvedValue({
155+
providerId: 'eloqua',
156+
scope: '__eloqua_instance__:https://evil.example,full',
131157
})
132158

133159
await expect(
134160
eloquaSelectorAttachments['eloqua.forms'].execute(args({ kind: 'list' }, 'eloqua.forms'))
135161
).rejects.toBeInstanceOf(SelectorConnectionUnavailableError)
162+
expect(mockResolveSelectorCredentialBundle).not.toHaveBeenCalled()
163+
expect(mockFetch).not.toHaveBeenCalled()
164+
})
165+
166+
it('fails closed when the stored credential scope does not contain a pod', async () => {
167+
mockGetCredential.mockResolvedValue({
168+
providerId: 'eloqua',
169+
scope: 'full',
170+
})
171+
172+
await expect(
173+
eloquaSelectorAttachments['eloqua.forms'].execute(args({ kind: 'list' }, 'eloqua.forms'))
174+
).rejects.toBeInstanceOf(SelectorConnectionUnavailableError)
175+
expect(mockResolveSelectorCredentialBundle).not.toHaveBeenCalled()
176+
expect(mockFetch).not.toHaveBeenCalled()
177+
})
178+
179+
it('fails closed when the stored credential provider does not match Eloqua', async () => {
180+
mockGetCredential.mockResolvedValue({
181+
providerId: 'salesforce',
182+
scope: '__eloqua_instance__:https://secure.p03.eloqua.com,full',
183+
})
184+
185+
await expect(
186+
eloquaSelectorAttachments['eloqua.forms'].execute(args({ kind: 'list' }, 'eloqua.forms'))
187+
).rejects.toBeInstanceOf(SelectorConnectionUnavailableError)
188+
expect(mockResolveSelectorCredentialBundle).not.toHaveBeenCalled()
189+
expect(mockFetch).not.toHaveBeenCalled()
190+
})
191+
192+
it('does not begin token resolution when credential loading is cancelled', async () => {
193+
const controller = new AbortController()
194+
mockGetCredential.mockReturnValue(new Promise(() => undefined))
195+
196+
const pending = eloquaSelectorAttachments['eloqua.forms'].execute(
197+
args({ kind: 'list' }, 'eloqua.forms', controller.signal)
198+
)
199+
controller.abort()
200+
201+
await expect(pending).rejects.toMatchObject({ name: 'AbortError' })
202+
expect(mockResolveSelectorCredentialBundle).not.toHaveBeenCalled()
136203
expect(mockFetch).not.toHaveBeenCalled()
137204
})
138205

apps/sim/lib/selectors/server/providers/eloqua.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { z } from 'zod'
2-
import { normalizeEloquaInstanceUrl } from '@/lib/oauth/eloqua'
2+
import { getCredential } from '@/lib/oauth/credential-service'
3+
import { extractEloquaInstanceUrl, normalizeEloquaInstanceUrl } from '@/lib/oauth/eloqua'
34
import type { ServerSelectorKey } from '@/lib/selectors/manifest'
5+
import { waitForSelectorCredentialResolution } from '@/lib/selectors/server/credentials'
46
import {
57
SelectorConnectionUnavailableError,
68
SelectorContextUnavailableError,
@@ -93,17 +95,34 @@ function selectorOption(item: z.infer<typeof eloquaSelectorItemSchema>): SafeSel
9395
async function prepareEloquaDestination(
9496
args: ExecuteServerSelectorArgs
9597
): Promise<EloquaSelectorDestination> {
96-
if (!args.credential) throw new SelectorConnectionUnavailableError()
98+
const selectorCredential = args.credential
99+
const access = selectorCredential?.access
100+
if (!selectorCredential || !access?.credentialOwnerUserId || !access.resolvedCredentialId) {
101+
throw new SelectorConnectionUnavailableError()
102+
}
97103
try {
104+
selectorCredential.signal?.throwIfAborted()
105+
const credential = await waitForSelectorCredentialResolution(
106+
getCredential(
107+
'selector-execution',
108+
access.resolvedCredentialId,
109+
access.credentialOwnerUserId
110+
),
111+
selectorCredential.signal
112+
)
113+
if (!credential || credential.providerId !== 'eloqua') {
114+
throw new SelectorConnectionUnavailableError()
115+
}
116+
selectorCredential.signal?.throwIfAborted()
117+
const instanceUrl = normalizeEloquaInstanceUrl(extractEloquaInstanceUrl(credential.scope))
98118
const bundle = await resolveSelectorCredentialBundle({
99-
credential: args.credential,
119+
credential: { ...selectorCredential, suppliedId: access.resolvedCredentialId },
100120
protectedValues: args.protectedValues,
101121
})
102-
if (!bundle.instanceUrl) throw new SelectorConnectionUnavailableError()
103-
const instanceUrl = normalizeEloquaInstanceUrl(bundle.instanceUrl)
104122
args.protectedValues.add(instanceUrl, 'reference')
105123
return { accessToken: bundle.accessToken, instanceUrl }
106124
} catch (error) {
125+
if (selectorCredential.signal?.aborted) throw error
107126
if (error instanceof SelectorConnectionUnavailableError) throw error
108127
throw new SelectorConnectionUnavailableError()
109128
}

0 commit comments

Comments
 (0)