diff --git a/src/lib/commands/run-on-cloud.ts b/src/lib/commands/run-on-cloud.ts index 145605557..0f2455cb5 100644 --- a/src/lib/commands/run-on-cloud.ts +++ b/src/lib/commands/run-on-cloud.ts @@ -88,6 +88,17 @@ export async function* runActorOrTaskOnCloud(apifyClient: ApifyClient, options: throw new Error(`${type} ${actorOrTaskData.userFriendlyId} (${actorOrTaskData.id}) not found!`); } + if (err.type === 'full-permission-actor-not-approved') { + const approvalUrl: string | undefined = err.data?.approvalUrl; + const lines = [ + `${type} ${actorOrTaskData.userFriendlyId} requires full access to your Apify account and has not been approved yet.`, + ]; + if (approvalUrl) { + lines.push('', `Approve here: ${chalk.blue(approvalUrl)}`); + } + throw new Error(lines.join('\n')); + } + throw err; } diff --git a/test/local/lib/run-on-cloud.test.ts b/test/local/lib/run-on-cloud.test.ts new file mode 100644 index 000000000..c7b22cb43 --- /dev/null +++ b/test/local/lib/run-on-cloud.test.ts @@ -0,0 +1,57 @@ +import type { ApifyClient } from 'apify-client'; + +import { runActorOrTaskOnCloud } from '../../../src/lib/commands/run-on-cloud.js'; + +const fakeClientThatThrows = (error: unknown) => + ({ + actor: () => ({ + start: async () => { + throw error; + }, + }), + }) as unknown as ApifyClient; + +const callAndCatch = async (apiError: unknown) => { + const iterator = runActorOrTaskOnCloud(fakeClientThatThrows(apiError), { + actorOrTaskData: { id: 'abc', userFriendlyId: 'apify/test-actor' }, + runOptions: {}, + type: 'Actor', + silent: true, + }); + + try { + for await (const _ of iterator) { + // drain + } + } catch (err) { + return err as Error; + } + + throw new Error('Expected runActorOrTaskOnCloud to throw'); +}; + +describe('runActorOrTaskOnCloud', () => { + it('surfaces approval URL when Actor requires full account access', async () => { + const approvalUrl = 'https://console.apify.com/actors/abc?approvePermissions=true'; + const apiError = Object.assign(new Error('This Actor requires full access to your account.'), { + type: 'full-permission-actor-not-approved', + data: { approvalUrl }, + }); + + const err = await callAndCatch(apiError); + + expect(err.message).toMatch(/has not been approved yet/); + expect(err.message).toContain(approvalUrl); + }); + + it('falls back to bare message when API response has no approvalUrl', async () => { + const apiError = Object.assign(new Error('This Actor requires full access to your account.'), { + type: 'full-permission-actor-not-approved', + }); + + const err = await callAndCatch(apiError); + + expect(err.message).toMatch(/has not been approved yet/); + expect(err.message).not.toMatch(/Approve here/); + }); +});