From a73dea9af9cffae4623d8f08c1066b826968e060 Mon Sep 17 00:00:00 2001 From: "Adrian Joshua Strutt (AI)" Date: Mon, 18 May 2026 16:07:15 +0000 Subject: [PATCH 1/2] fix(cli-internal): handle ResourceNotFoundException in fetchFunctionSchedule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a Lambda function has no CloudWatch Events rule (non-scheduled functions), the DescribeRuleCommand API throws ResourceNotFoundException. This crashed the entire gen2-migration generate command. Wrap the DescribeRuleCommand call in a try-catch that specifically catches ResourceNotFoundException and returns undefined (no schedule), matching the existing pattern for GetPolicyCommand. Add 7 unit tests covering all fetchFunctionSchedule scenarios. Closes #14883 --- Prompt: Fix issue #14883 — ResourceNotFoundException in fetchFunctionSchedule when function has no CloudWatch Events rule. --- .../generate/_infra/aws-fetcher.test.ts | 129 ++++++++++++++++++ .../gen2-migration/_common/aws-fetcher.ts | 11 +- 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/_infra/aws-fetcher.test.ts diff --git a/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/_infra/aws-fetcher.test.ts b/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/_infra/aws-fetcher.test.ts new file mode 100644 index 00000000000..81d6517c5f2 --- /dev/null +++ b/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/_infra/aws-fetcher.test.ts @@ -0,0 +1,129 @@ +import { AwsFetcher } from '../../../../../commands/gen2-migration/generate/_infra/aws-fetcher'; +import { AwsClients } from '../../../../../commands/gen2-migration/_infra/aws-clients'; + +function createMockClients(overrides: { lambdaSend?: jest.Mock; cloudWatchEventsSend?: jest.Mock }): AwsClients { + return { + lambda: { send: overrides.lambdaSend ?? jest.fn() }, + cloudWatchEvents: { send: overrides.cloudWatchEventsSend ?? jest.fn() }, + } as unknown as AwsClients; +} + +describe('AwsFetcher', () => { + describe('fetchFunctionSchedule()', () => { + it('returns the schedule expression when the rule exists', async () => { + const policy = { + Statement: [ + { + Condition: { + ArnLike: { + 'AWS:SourceArn': 'arn:aws:events:us-east-1:123456789:rule/myFunc-schedule', + }, + }, + }, + ], + }; + const lambdaSend = jest.fn().mockResolvedValue({ Policy: JSON.stringify(policy) }); + const cloudWatchEventsSend = jest.fn().mockResolvedValue({ ScheduleExpression: 'rate(1 hour)' }); + + const fetcher = new AwsFetcher(createMockClients({ lambdaSend, cloudWatchEventsSend })); + const result = await fetcher.fetchFunctionSchedule('myFunc'); + + expect(result).toBe('rate(1 hour)'); + }); + + it('returns undefined when GetPolicy throws ResourceNotFoundException', async () => { + const error = new Error('Resource not found'); + error.name = 'ResourceNotFoundException'; + const lambdaSend = jest.fn().mockRejectedValue(error); + + const fetcher = new AwsFetcher(createMockClients({ lambdaSend })); + const result = await fetcher.fetchFunctionSchedule('myFunc'); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when DescribeRule throws ResourceNotFoundException', async () => { + const policy = { + Statement: [ + { + Condition: { + ArnLike: { + 'AWS:SourceArn': 'arn:aws:events:us-east-1:123456789:rule/myFunc-schedule', + }, + }, + }, + ], + }; + const lambdaSend = jest.fn().mockResolvedValue({ Policy: JSON.stringify(policy) }); + const error = new Error('Rule myFunc-schedule does not exist on EventBus default.'); + error.name = 'ResourceNotFoundException'; + const cloudWatchEventsSend = jest.fn().mockRejectedValue(error); + + const fetcher = new AwsFetcher(createMockClients({ lambdaSend, cloudWatchEventsSend })); + const result = await fetcher.fetchFunctionSchedule('myFunc'); + + expect(result).toBeUndefined(); + }); + + it('propagates non-ResourceNotFoundException errors from GetPolicy', async () => { + const error = new Error('Access denied'); + error.name = 'AccessDeniedException'; + const lambdaSend = jest.fn().mockRejectedValue(error); + + const fetcher = new AwsFetcher(createMockClients({ lambdaSend })); + + await expect(fetcher.fetchFunctionSchedule('myFunc')).rejects.toThrow('Access denied'); + }); + + it('propagates non-ResourceNotFoundException errors from DescribeRule', async () => { + const policy = { + Statement: [ + { + Condition: { + ArnLike: { + 'AWS:SourceArn': 'arn:aws:events:us-east-1:123456789:rule/myFunc-schedule', + }, + }, + }, + ], + }; + const lambdaSend = jest.fn().mockResolvedValue({ Policy: JSON.stringify(policy) }); + const error = new Error('Internal failure'); + error.name = 'InternalException'; + const cloudWatchEventsSend = jest.fn().mockRejectedValue(error); + + const fetcher = new AwsFetcher(createMockClients({ lambdaSend, cloudWatchEventsSend })); + + await expect(fetcher.fetchFunctionSchedule('myFunc')).rejects.toThrow('Internal failure'); + }); + + it('returns undefined when the policy has no schedule rule reference', async () => { + const policy = { + Statement: [ + { + Condition: { + ArnLike: { + 'AWS:SourceArn': 'arn:aws:execute-api:us-east-1:123456789:abc/*/GET/', + }, + }, + }, + ], + }; + const lambdaSend = jest.fn().mockResolvedValue({ Policy: JSON.stringify(policy) }); + + const fetcher = new AwsFetcher(createMockClients({ lambdaSend })); + const result = await fetcher.fetchFunctionSchedule('myFunc'); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when the policy response has no Policy field', async () => { + const lambdaSend = jest.fn().mockResolvedValue({}); + + const fetcher = new AwsFetcher(createMockClients({ lambdaSend })); + const result = await fetcher.fetchFunctionSchedule('myFunc'); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/packages/amplify-cli/src/commands/gen2-migration/_common/aws-fetcher.ts b/packages/amplify-cli/src/commands/gen2-migration/_common/aws-fetcher.ts index 118c4685445..734260cd9cf 100644 --- a/packages/amplify-cli/src/commands/gen2-migration/_common/aws-fetcher.ts +++ b/packages/amplify-cli/src/commands/gen2-migration/_common/aws-fetcher.ts @@ -141,8 +141,15 @@ export class AwsFetcher { throw e; } if (!ruleName) return undefined; - const ruleResponse = await this.clients.cloudWatchEvents.send(new DescribeRuleCommand({ Name: ruleName })); - return ruleResponse.ScheduleExpression; + try { + const ruleResponse = await this.clients.cloudWatchEvents.send(new DescribeRuleCommand({ Name: ruleName })); + return ruleResponse.ScheduleExpression; + } catch (e: unknown) { + if (e instanceof Error && e.name === 'ResourceNotFoundException') { + return undefined; + } + throw e; + } } // ── Storage (S3) ──────────────────────────────────────────────── From 955a3ecfae4a213c041f6b6b4978937920f9f180 Mon Sep 17 00:00:00 2001 From: Adrian Joshua Strutt Date: Thu, 21 May 2026 17:43:59 +0000 Subject: [PATCH 2/2] fix: correct import paths in aws-fetcher test --- .../gen2-migration/generate/_infra/aws-fetcher.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/_infra/aws-fetcher.test.ts b/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/_infra/aws-fetcher.test.ts index 81d6517c5f2..0a4b9e36ffd 100644 --- a/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/_infra/aws-fetcher.test.ts +++ b/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/_infra/aws-fetcher.test.ts @@ -1,5 +1,5 @@ -import { AwsFetcher } from '../../../../../commands/gen2-migration/generate/_infra/aws-fetcher'; -import { AwsClients } from '../../../../../commands/gen2-migration/_infra/aws-clients'; +import { AwsFetcher } from '../../../../../commands/gen2-migration/_common/aws-fetcher'; +import { AwsClients } from '../../../../../commands/gen2-migration/_common/aws-clients'; function createMockClients(overrides: { lambdaSend?: jest.Mock; cloudWatchEventsSend?: jest.Mock }): AwsClients { return {