Skip to content

Commit 3ed291f

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(bitbucket): harden provider edge cases
1 parent f5cf56c commit 3ed291f

29 files changed

Lines changed: 391 additions & 58 deletions

apps/sim/app/api/tools/bitbucket/repositories/route.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ vi.mock('@/lib/oauth/credential-service', () => ({
3232
import { POST } from '@/app/api/tools/bitbucket/repositories/route'
3333

3434
const URL = 'http://localhost:3000/api/tools/bitbucket/repositories'
35-
const FIRST_PAGE_URL = 'https://api.bitbucket.org/2.0/repositories/acme-platform?pagelen=100'
35+
const FIRST_PAGE_URL =
36+
'https://api.bitbucket.org/2.0/repositories/acme-platform?pagelen=100&fields=values.slug%2Cvalues.uuid%2Cvalues.name%2Cvalues.full_name%2Cnext'
3637
const SECOND_PAGE_URL =
3738
'https://api.bitbucket.org/2.0/repositories/acme-platform?page=2&pagelen=100'
3839
const REQUEST_BODY = {
@@ -190,7 +191,6 @@ describe('POST /api/tools/bitbucket/repositories', () => {
190191
values: [
191192
{
192193
uuid: '{repository-uuid}',
193-
name: 'Payments API',
194194
full_name: 'acme-platform/payments-api',
195195
links: { html: { href: 'https://bitbucket.org/acme-platform/payments-api' } },
196196
},
@@ -214,7 +214,7 @@ describe('POST /api/tools/bitbucket/repositories', () => {
214214
{
215215
slug: 'payments-api',
216216
uuid: '{repository-uuid}',
217-
name: 'Payments API',
217+
name: 'payments-api',
218218
fullName: 'acme-platform/payments-api',
219219
},
220220
],

apps/sim/app/api/tools/bitbucket/repositories/route.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const dynamic = 'force-dynamic'
1919
const logger = createLogger('BitbucketRepositoriesAPI')
2020
const BITBUCKET_PROVIDER_ID = 'bitbucket'
2121
const BITBUCKET_REPOSITORIES_URL = 'https://api.bitbucket.org/2.0/repositories'
22+
const BITBUCKET_REPOSITORY_FIELDS = 'values.slug,values.uuid,values.name,values.full_name,next'
2223
const SELECTOR_REQUEST_MAX_BYTES = 8 * 1024
2324
const PROVIDER_RESPONSE_MAX_BYTES = 1024 * 1024
2425

@@ -98,9 +99,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
9899
)
99100
}
100101

101-
const providerUrl =
102-
cursor ??
103-
`${BITBUCKET_REPOSITORIES_URL}/${encodeURIComponent(workspaceSlug)}?pagelen=${BITBUCKET_SELECTOR_PAGE_SIZE}`
102+
const firstPage = new URL(`${BITBUCKET_REPOSITORIES_URL}/${encodeURIComponent(workspaceSlug)}`)
103+
firstPage.searchParams.set('pagelen', String(BITBUCKET_SELECTOR_PAGE_SIZE))
104+
firstPage.searchParams.set('fields', BITBUCKET_REPOSITORY_FIELDS)
105+
const providerUrl = cursor ?? firstPage.toString()
104106

105107
let response: Response
106108
try {
@@ -160,7 +162,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
160162
repositories: page.data.values.map((repository) => ({
161163
slug: repository.slug,
162164
uuid: repository.uuid,
163-
name: repository.name,
165+
name: repository.name ?? repository.slug,
164166
fullName: repository.full_name,
165167
})),
166168
...(page.data.next ? { nextCursor: page.data.next } : {}),

apps/sim/app/api/tools/bitbucket/workspaces/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { POST } from '@/app/api/tools/bitbucket/workspaces/route'
3333

3434
const URL = 'http://localhost:3000/api/tools/bitbucket/workspaces'
3535
const FIRST_PAGE_URL =
36-
'https://api.bitbucket.org/2.0/user/workspaces?pagelen=100&fields=%2Bvalues.workspace.name'
36+
'https://api.bitbucket.org/2.0/user/workspaces?pagelen=100&fields=values.administrator%2Cvalues.workspace.slug%2Cvalues.workspace.uuid%2Cvalues.workspace.name%2Cnext'
3737
const SECOND_PAGE_URL = 'https://api.bitbucket.org/2.0/user/workspaces?page=2&pagelen=100'
3838
const REQUEST_BODY = { credential: 'credential-1', workflowId: 'workflow-1' } as const
3939

apps/sim/app/api/tools/bitbucket/workspaces/route.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export const dynamic = 'force-dynamic'
1919
const logger = createLogger('BitbucketWorkspacesAPI')
2020
const BITBUCKET_PROVIDER_ID = 'bitbucket'
2121
const BITBUCKET_WORKSPACES_URL = 'https://api.bitbucket.org/2.0/user/workspaces'
22-
const BITBUCKET_WORKSPACE_FIELDS = '+values.workspace.name'
22+
const BITBUCKET_WORKSPACE_FIELDS =
23+
'values.administrator,values.workspace.slug,values.workspace.uuid,values.workspace.name,next'
2324
const SELECTOR_REQUEST_MAX_BYTES = 8 * 1024
2425
const PROVIDER_RESPONSE_MAX_BYTES = 1024 * 1024
2526

@@ -101,10 +102,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
101102

102103
const firstPage = new URL(BITBUCKET_WORKSPACES_URL)
103104
firstPage.searchParams.set('pagelen', String(BITBUCKET_SELECTOR_PAGE_SIZE))
104-
/**
105-
* The current endpoint returns a `workspace_base` by default, whose documented
106-
* sample omits `name`; additive fields keep the default shape and request it.
107-
*/
108105
firstPage.searchParams.set('fields', BITBUCKET_WORKSPACE_FIELDS)
109106
const providerUrl = cursor ?? firstPage.toString()
110107

apps/sim/lib/api/contracts/selectors/bitbucket.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const bitbucketRepositoryProviderSchema = z
115115
.object({
116116
slug: bitbucketRepositorySlugSchema.optional(),
117117
uuid: bitbucketUuidSchema,
118-
name: bitbucketNameSchema,
118+
name: bitbucketNameSchema.optional(),
119119
full_name: bitbucketNameSchema,
120120
})
121121
.passthrough()

apps/sim/lib/auth/connectors/providers.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,22 +1531,32 @@ export function buildConnectorProviders(): GenericOAuthConfig[] {
15311531
},
15321532
getUserInfo: async (tokens) => {
15331533
try {
1534+
const signal = AbortSignal.timeout(15_000)
15341535
const response = await fetch('https://api.bitbucket.org/2.0/user', {
15351536
headers: {
15361537
Authorization: `Bearer ${tokens.accessToken}`,
15371538
},
1539+
signal,
15381540
})
15391541

15401542
if (!response.ok) {
1541-
await response.text().catch(() => {})
1543+
await readResponseTextWithLimit(response, {
1544+
maxBytes: 1024 * 1024,
1545+
label: 'Bitbucket OAuth user info error response',
1546+
signal,
1547+
}).catch(() => {})
15421548
logger.error('Error fetching Bitbucket user info:', {
15431549
status: response.status,
15441550
statusText: response.statusText,
15451551
})
15461552
return null
15471553
}
15481554

1549-
const data: BitbucketCurrentUserResponse = await response.json()
1555+
const data = await readResponseJsonWithLimit<BitbucketCurrentUserResponse>(response, {
1556+
maxBytes: 1024 * 1024,
1557+
label: 'Bitbucket OAuth user info response',
1558+
signal,
1559+
})
15501560
const stableId = data.account_id ?? data.uuid
15511561
if (!stableId) {
15521562
logger.error('Bitbucket user info did not include an account_id or uuid')

apps/sim/lib/oauth/oauth.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,27 @@ describe('Bitbucket OAuth Connector', () => {
209209
expect(uuidIdentity?.email).toBe('bitbucket-uuid-456@connectors.sim.invalid')
210210
expect(uuidIdentity?.name).toBe('grace')
211211
})
212+
213+
it('bounds Bitbucket user-info responses and supplies a provider deadline', async () => {
214+
const getUserInfo = getBitbucketConnector().getUserInfo
215+
if (!getUserInfo) throw new Error('Bitbucket connector must define getUserInfo')
216+
const mockFetch = vi.fn(async (_url: string, init?: RequestInit) => {
217+
expect(init?.signal).toBeInstanceOf(AbortSignal)
218+
return new Response('{}', {
219+
headers: {
220+
'content-length': String(1024 * 1024 + 1),
221+
'content-type': 'application/json',
222+
},
223+
})
224+
})
225+
226+
await expect(
227+
withMockFetch(mockFetch, () =>
228+
getUserInfo(getOAuth2Tokens({ access_token: 'bitbucket_access_token' }))
229+
)
230+
).resolves.toBeNull()
231+
expect(mockFetch).toHaveBeenCalledOnce()
232+
})
212233
})
213234

214235
describe('OAuth Provider Branding', () => {

apps/sim/tools/bitbucket/delete_branch.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ export const bitbucketDeleteBranchTool: ToolConfig<
3333
method: 'DELETE',
3434
headers: (params) => bitbucketHeaders(params.accessToken),
3535
},
36-
transformResponse: async () => ({ success: true, output: { deleted: true } }),
36+
transformResponse: async (response) => {
37+
if (response.status !== 204) {
38+
throw new Error(`Bitbucket branch deletion returned unexpected HTTP ${response.status}`)
39+
}
40+
return { success: true, output: { deleted: true } }
41+
},
3742
outputs: { deleted: { type: 'boolean', description: 'Whether the branch was deleted' } },
3843
errorExtractor: BITBUCKET_ERROR_EXTRACTOR,
3944
}

apps/sim/tools/bitbucket/get_merge_task_status.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ export const bitbucketGetMergeTaskStatusTool: ToolConfig<
7878
const result = record(data.merge_result)
7979
if (!result) throw new Error('Bitbucket successful merge task omitted merge_result')
8080
mergeResult = normalizeBitbucketPullRequest(result)
81-
} else if (data.merge_result !== undefined && data.merge_result !== null) {
82-
throw new Error('Bitbucket pending merge task returned an unexpected merge_result')
8381
}
8482

8583
return {

apps/sim/tools/bitbucket/get_pull_request_diffstat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const bitbucketGetPullRequestDiffstatTool: ToolConfig<
6060
const initialUrl = pullRequestDiffstatUrl(params)
6161
const headers = bitbucketHeaders(params.accessToken)
6262
let response: Response
63-
if (params.nextUrl) {
63+
if (params.nextUrl !== undefined) {
6464
const continuation = validateBitbucketPullRequestRedirect(
6565
params.nextUrl,
6666
params.workspaceSlug,

0 commit comments

Comments
 (0)