diff --git a/src/handlers/TemplateHandler.ts b/src/handlers/TemplateHandler.ts index 42a10894..296c7f3e 100644 --- a/src/handlers/TemplateHandler.ts +++ b/src/handlers/TemplateHandler.ts @@ -1,4 +1,4 @@ -import { ServerRequestHandler, ResponseError, ErrorCodes } from 'vscode-languageserver'; +import { ResponseError, ErrorCodes, RequestHandler } from 'vscode-languageserver'; import { TopLevelSection } from '../context/ContextType'; import { getEntityMap } from '../context/SectionContextBuilder'; import { Parameter } from '../context/semantic/Entity'; @@ -6,10 +6,12 @@ import { parseIdentifiable } from '../protocol/LspParser'; import { Identifiable } from '../protocol/LspTypes'; import { ServerComponents } from '../server/ServerComponents'; import { LoggerFactory } from '../telemetry/LoggerFactory'; -import { parseTemplateActionParams, parseGetParametersParams } from '../templates/TemplateParser'; +import { analyzeCapabilities } from '../templates/CapabilityAnalyzer'; +import { parseTemplateActionParams, parseTemplateMetadataParams } from '../templates/TemplateParser'; import { - GetParametersParams, + TemplateMetadataParams, GetParametersResult, + GetCapabilitiesResult, TemplateActionParams, TemplateActionResult, TemplateStatusResult, @@ -21,12 +23,12 @@ const log = LoggerFactory.getLogger('TemplateHandler'); export function templateParametersHandler( components: ServerComponents, -): ServerRequestHandler { - return (rawParams, _token, _workDoneProgress, _resultProgress) => { +): RequestHandler { + return (rawParams) => { log.debug({ Handler: 'TemplateParameters', rawParams }); try { - const params = parseWithPrettyError(parseGetParametersParams, rawParams); + const params = parseWithPrettyError(parseTemplateMetadataParams, rawParams); const syntaxTree = components.syntaxTreeManager.getSyntaxTree(params.uri); if (syntaxTree) { const parametersMap = getEntityMap(syntaxTree, TopLevelSection.Parameters); @@ -49,7 +51,7 @@ export function templateParametersHandler( export function templateValidationCreateHandler( components: ServerComponents, -): ServerRequestHandler { +): RequestHandler { return async (rawParams) => { log.debug({ Handler: 'TemplateValidationCreate', rawParams }); @@ -64,7 +66,7 @@ export function templateValidationCreateHandler( export function templateDeploymentCreateHandler( components: ServerComponents, -): ServerRequestHandler { +): RequestHandler { return async (rawParams) => { log.debug({ Handler: 'TemplateDeploymentCreate', rawParams }); @@ -79,7 +81,7 @@ export function templateDeploymentCreateHandler( export function templateValidationStatusHandler( components: ServerComponents, -): ServerRequestHandler { +): RequestHandler { return (rawParams) => { log.debug({ Handler: 'TemplateValidationStatus', rawParams }); @@ -94,7 +96,7 @@ export function templateValidationStatusHandler( export function templateDeploymentStatusHandler( components: ServerComponents, -): ServerRequestHandler { +): RequestHandler { return (rawParams) => { log.debug({ Handler: 'TemplateDeploymentStatus', rawParams }); @@ -107,6 +109,28 @@ export function templateDeploymentStatusHandler( }; } +export function templateCapabilitiesHandler( + components: ServerComponents, +): RequestHandler { + return async (rawParams) => { + log.debug({ Handler: 'TemplateCapabilities', rawParams }); + + try { + const params = parseWithPrettyError(parseTemplateMetadataParams, rawParams); + const document = components.documentManager.get(params.uri); + if (!document) { + throw new ResponseError(ErrorCodes.InvalidRequest, 'Template body document not available'); + } + + const capabilities = await analyzeCapabilities(document, components.cfnService); + + return { capabilities }; + } catch (error) { + handleTemplateError(error, 'Failed to analyze template capabilities'); + } + }; +} + function handleTemplateError(error: unknown, contextMessage: string): never { if (error instanceof ResponseError) { throw error; diff --git a/src/protocol/LspTemplateHandlers.ts b/src/protocol/LspTemplateHandlers.ts index 38376d2e..003a0e4f 100644 --- a/src/protocol/LspTemplateHandlers.ts +++ b/src/protocol/LspTemplateHandlers.ts @@ -1,4 +1,4 @@ -import { Connection, ServerRequestHandler } from 'vscode-languageserver'; +import { Connection, RequestHandler } from 'vscode-languageserver'; import { TemplateActionParams, TemplateActionResult, @@ -7,32 +7,38 @@ import { TemplateDeploymentStatusRequest, TemplateStatusResult, TemplateValidationStatusRequest, - GetParametersParams, + TemplateMetadataParams, GetParametersRequest, GetParametersResult, + GetCapabilitiesRequest, + GetCapabilitiesResult, } from '../templates/TemplateRequestType'; import { Identifiable } from './LspTypes'; export class LspTemplateHandlers { constructor(private readonly connection: Connection) {} - onTemplateValidationCreate(handler: ServerRequestHandler) { + onTemplateValidationCreate(handler: RequestHandler) { this.connection.onRequest(TemplateValidationCreateRequest.method, handler); } - onTemplateDeploymentCreate(handler: ServerRequestHandler) { + onTemplateDeploymentCreate(handler: RequestHandler) { this.connection.onRequest(TemplateDeploymentCreateRequest.method, handler); } - onTemplateValidationStatus(handler: ServerRequestHandler) { + onTemplateValidationStatus(handler: RequestHandler) { this.connection.onRequest(TemplateValidationStatusRequest.method, handler); } - onTemplateDeploymentStatus(handler: ServerRequestHandler) { + onTemplateDeploymentStatus(handler: RequestHandler) { this.connection.onRequest(TemplateDeploymentStatusRequest.method, handler); } - onGetParameters(handler: ServerRequestHandler) { + onGetParameters(handler: RequestHandler) { this.connection.onRequest(GetParametersRequest.method, handler); } + + onGetCapabilities(handler: RequestHandler) { + this.connection.onRequest(GetCapabilitiesRequest.method, handler); + } } diff --git a/src/server/CfnServer.ts b/src/server/CfnServer.ts index 2c037029..faf514ec 100644 --- a/src/server/CfnServer.ts +++ b/src/server/CfnServer.ts @@ -30,6 +30,7 @@ import { templateValidationStatusHandler, templateDeploymentStatusHandler, templateParametersHandler, + templateCapabilitiesHandler, } from '../handlers/TemplateHandler'; import { LspFeatures } from '../protocol/LspConnection'; import { ServerComponents } from './ServerComponents'; @@ -69,6 +70,7 @@ export class CfnServer { this.features.authHandlers.onSsoTokenChanged(ssoTokenChangedHandler(this.components)); this.features.templateHandlers.onGetParameters(templateParametersHandler(this.components)); + this.features.templateHandlers.onGetCapabilities(templateCapabilitiesHandler(this.components)); this.features.templateHandlers.onTemplateValidationCreate(templateValidationCreateHandler(this.components)); this.features.templateHandlers.onTemplateDeploymentCreate(templateDeploymentCreateHandler(this.components)); this.features.templateHandlers.onTemplateValidationStatus(templateValidationStatusHandler(this.components)); diff --git a/src/services/CfnService.ts b/src/services/CfnService.ts index 6eff4144..7be979f9 100644 --- a/src/services/CfnService.ts +++ b/src/services/CfnService.ts @@ -34,6 +34,9 @@ import { StackResourceDriftStatus, Parameter, RegistryType, + ValidateTemplateCommand, + ValidateTemplateInput, + ValidateTemplateOutput, Visibility, TypeSummary, DescribeTypeOutput, @@ -275,6 +278,10 @@ export class CfnService { }); } + public async validateTemplate(params: ValidateTemplateInput): Promise { + return await this.withClient((client) => client.send(new ValidateTemplateCommand(params))); + } + static create(components: ServerComponents) { return new CfnService(components.awsClient); } diff --git a/src/templates/CapabilityAnalyzer.ts b/src/templates/CapabilityAnalyzer.ts new file mode 100644 index 00000000..6f42cfba --- /dev/null +++ b/src/templates/CapabilityAnalyzer.ts @@ -0,0 +1,26 @@ +import { Capability } from '@aws-sdk/client-cloudformation'; +import { Document } from '../document/Document'; +import { CfnService } from '../services/CfnService'; +import { LoggerFactory } from '../telemetry/LoggerFactory'; + +const log = LoggerFactory.getLogger('CapabilityAnalyzer'); + +export async function analyzeCapabilities(document: Document, cfnService: CfnService): Promise { + try { + const validationResult = await cfnService.validateTemplate({ TemplateBody: document.getText() }); + + if (!validationResult.Capabilities) { + return []; + } + + // ValidateTemplate cannot process transforms, assume all capabilities are required if a transform is detected + if (validationResult.Capabilities.includes(Capability.CAPABILITY_AUTO_EXPAND)) { + return [Capability.CAPABILITY_IAM, Capability.CAPABILITY_NAMED_IAM, Capability.CAPABILITY_AUTO_EXPAND]; + } + + return validationResult.Capabilities; + } catch (error) { + log.warn({ error }, 'Capability Analysis failed, assuming all capabilities are required'); + return [Capability.CAPABILITY_IAM, Capability.CAPABILITY_NAMED_IAM, Capability.CAPABILITY_AUTO_EXPAND]; + } +} diff --git a/src/templates/TemplateParser.ts b/src/templates/TemplateParser.ts index 7afe36ac..89d76aa0 100644 --- a/src/templates/TemplateParser.ts +++ b/src/templates/TemplateParser.ts @@ -1,6 +1,6 @@ import { Capability } from '@aws-sdk/client-cloudformation'; import { z } from 'zod'; -import { TemplateActionParams, GetParametersParams } from './TemplateRequestType'; +import { TemplateActionParams, TemplateMetadataParams } from './TemplateRequestType'; const CapabilitySchema = z.enum([ Capability.CAPABILITY_AUTO_EXPAND, @@ -31,6 +31,6 @@ export function parseTemplateActionParams(input: unknown): TemplateActionParams return TemplateActionParamsSchema.parse(input); } -export function parseGetParametersParams(input: unknown): GetParametersParams { +export function parseTemplateMetadataParams(input: unknown): TemplateMetadataParams { return GetParametersParamsSchema.parse(input); } diff --git a/src/templates/TemplateRequestType.ts b/src/templates/TemplateRequestType.ts index 1d2ffbd2..c4988067 100644 --- a/src/templates/TemplateRequestType.ts +++ b/src/templates/TemplateRequestType.ts @@ -16,7 +16,7 @@ export type TemplateActionResult = Identifiable & { stackName: string; }; -export type GetParametersParams = { +export type TemplateMetadataParams = { uri: string; }; @@ -76,6 +76,14 @@ export const TemplateDeploymentStatusRequest = new RequestType( +export const GetParametersRequest = new RequestType( 'aws/cfn/template/parameters', ); + +export type GetCapabilitiesResult = { + capabilities: Capability[]; +}; + +export const GetCapabilitiesRequest = new RequestType( + 'aws/cfn/template/capabilities', +); diff --git a/tst/unit/handlers/TemplateHandler.test.ts b/tst/unit/handlers/TemplateHandler.test.ts index 7418922d..592bbd27 100644 --- a/tst/unit/handlers/TemplateHandler.test.ts +++ b/tst/unit/handlers/TemplateHandler.test.ts @@ -1,26 +1,25 @@ +import { Capability } from '@aws-sdk/client-cloudformation'; import { StubbedInstance } from 'ts-sinon'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { - CancellationToken, - WorkDoneProgressReporter, - ResultProgressReporter, - ResponseError, - ErrorCodes, -} from 'vscode-languageserver'; +import { CancellationToken, ResponseError, ErrorCodes } from 'vscode-languageserver'; import { Context } from '../../../src/context/Context'; import * as SectionContextBuilder from '../../../src/context/SectionContextBuilder'; import { SyntaxTree } from '../../../src/context/syntaxtree/SyntaxTree'; import { SyntaxTreeManager } from '../../../src/context/syntaxtree/SyntaxTreeManager'; +import { Document } from '../../../src/document/Document'; import { templateParametersHandler, + templateCapabilitiesHandler, templateValidationCreateHandler, templateDeploymentCreateHandler, templateValidationStatusHandler, templateDeploymentStatusHandler, } from '../../../src/handlers/TemplateHandler'; +import { analyzeCapabilities } from '../../../src/templates/CapabilityAnalyzer'; import { - GetParametersParams, + TemplateMetadataParams, GetParametersResult, + GetCapabilitiesResult, TemplateStatus, WorkflowResult, } from '../../../src/templates/TemplateRequestType'; @@ -41,20 +40,22 @@ vi.mock('../../../src/protocol/LspParser', () => ({ vi.mock('../../../src/templates/TemplateParser', () => ({ parseTemplateActionParams: vi.fn((input) => input), - parseGetParametersParams: vi.fn((input) => input), + parseTemplateMetadataParams: vi.fn((input) => input), })); vi.mock('../../../src/utils/ZodErrorWrapper', () => ({ parseWithPrettyError: vi.fn((parser, input) => parser(input)), })); +vi.mock('../../../src/templates/CapabilityAnalyzer', () => ({ + analyzeCapabilities: vi.fn(), +})); + describe('TemplateHandler', () => { let mockComponents: MockedServerComponents; let syntaxTreeManager: StubbedInstance; let getEntityMapSpy: any; const mockToken = {} as CancellationToken; - const mockWorkDoneProgress = {} as WorkDoneProgressReporter; - const mockResultProgress = {} as ResultProgressReporter; beforeEach(() => { syntaxTreeManager = createMockSyntaxTreeManager(); @@ -64,34 +65,35 @@ describe('TemplateHandler', () => { mockComponents.validationWorkflowService.getStatus.reset(); mockComponents.deploymentWorkflowService.start.reset(); mockComponents.deploymentWorkflowService.getStatus.reset(); + vi.clearAllMocks(); }); describe('templateParametersHandler', () => { it('returns empty array when no syntax tree found', () => { - const params: GetParametersParams = { uri: 'test://template.yaml' }; + const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; syntaxTreeManager.getSyntaxTree.withArgs(params.uri).returns(undefined); const handler = templateParametersHandler(mockComponents); - const result = handler(params, mockToken, mockWorkDoneProgress, mockResultProgress) as GetParametersResult; + const result = handler(params, mockToken) as GetParametersResult; expect(result).toEqual({ parameters: [] }); }); it('returns empty array when getEntityMap returns undefined', () => { - const params: GetParametersParams = { uri: 'test://template.yaml' }; + const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; const mockSyntaxTree = {} as SyntaxTree; syntaxTreeManager.getSyntaxTree.withArgs(params.uri).returns(mockSyntaxTree); getEntityMapSpy.mockReturnValue(undefined); const handler = templateParametersHandler(mockComponents); - const result = handler(params, mockToken, mockWorkDoneProgress, mockResultProgress) as GetParametersResult; + const result = handler(params, mockToken) as GetParametersResult; expect(result).toEqual({ parameters: [] }); }); it('returns parameters when parameters section exists', () => { - const params: GetParametersParams = { uri: 'test://template.yaml' }; + const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; const mockSyntaxTree = {} as SyntaxTree; const mockParam1 = { name: 'param1', type: 'String' }; const mockParam2 = { name: 'param2', type: 'Number' }; @@ -106,7 +108,7 @@ describe('TemplateHandler', () => { getEntityMapSpy.mockReturnValue(parametersMap); const handler = templateParametersHandler(mockComponents); - const result = handler(params, mockToken, mockWorkDoneProgress, mockResultProgress) as GetParametersResult; + const result = handler(params, mockToken) as GetParametersResult; expect(result.parameters).toHaveLength(2); expect(result.parameters[0]).toBe(mockParam1); @@ -114,6 +116,31 @@ describe('TemplateHandler', () => { }); }); + describe('templateCapabilitiesHandler', () => { + it('should return capabilities when document is available', async () => { + const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; + const mockDocument = { getText: vi.fn().mockReturnValue('template content') } as unknown as Document; + const mockCapabilities = ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'] as Capability[]; + + mockComponents.documentManager.get.withArgs(params.uri).returns(mockDocument); + vi.mocked(analyzeCapabilities).mockResolvedValue(mockCapabilities); + + const handler = templateCapabilitiesHandler(mockComponents); + const result = (await handler(params, mockToken)) as GetCapabilitiesResult; + + expect(result.capabilities).toEqual(mockCapabilities); + }); + + it('should throw error when document is not available', async () => { + const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; + mockComponents.documentManager.get.withArgs(params.uri).returns(undefined); + + const handler = templateCapabilitiesHandler(mockComponents); + + await expect(handler(params, mockToken)).rejects.toThrow(ResponseError); + }); + }); + describe('templateValidationCreateHandler', () => { it('should delegate to validation service', async () => { const mockResult = { id: 'test-id', changeSetName: 'cs-123', stackName: 'test-stack' }; @@ -122,7 +149,7 @@ describe('TemplateHandler', () => { const handler = templateValidationCreateHandler(mockComponents); const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - const result = await handler(params, {} as any, {} as any); + const result = await handler(params, {} as any); expect(mockComponents.validationWorkflowService.start.calledWith(params)).toBe(true); expect(result).toEqual(mockResult); @@ -135,7 +162,7 @@ describe('TemplateHandler', () => { const handler = templateValidationCreateHandler(mockComponents); const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - await expect(handler(params, {} as any, {} as any)).rejects.toThrow(responseError); + await expect(handler(params, {} as any)).rejects.toThrow(responseError); }); it('should wrap other errors as InternalError', async () => { @@ -144,7 +171,7 @@ describe('TemplateHandler', () => { const handler = templateValidationCreateHandler(mockComponents); const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - await expect(handler(params, {} as any, {} as any)).rejects.toThrow(ResponseError); + await expect(handler(params, {} as any)).rejects.toThrow(ResponseError); }); }); @@ -156,7 +183,7 @@ describe('TemplateHandler', () => { const handler = templateDeploymentCreateHandler(mockComponents); const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - const result = await handler(params, {} as any, {} as any); + const result = await handler(params, {} as any); expect(mockComponents.deploymentWorkflowService.start.calledWith(params)).toBe(true); expect(result).toEqual(mockResult); @@ -175,7 +202,7 @@ describe('TemplateHandler', () => { const handler = templateValidationStatusHandler(mockComponents); const params = { id: 'test-id' }; - const result = await handler(params, {} as any, {} as any); + const result = await handler(params, {} as any); expect(mockComponents.validationWorkflowService.getStatus.calledWith(params)).toBe(true); expect(result).toEqual(mockResult); @@ -194,7 +221,7 @@ describe('TemplateHandler', () => { const handler = templateDeploymentStatusHandler(mockComponents); const params = { id: 'test-id' }; - const result = await handler(params, {} as any, {} as any); + const result = await handler(params, {} as any); expect(mockComponents.deploymentWorkflowService.getStatus.calledWith(params)).toBe(true); expect(result).toEqual(mockResult); diff --git a/tst/unit/protocol/LspTemplateHandlers.test.ts b/tst/unit/protocol/LspTemplateHandlers.test.ts index 4a38750a..5ae59030 100644 --- a/tst/unit/protocol/LspTemplateHandlers.test.ts +++ b/tst/unit/protocol/LspTemplateHandlers.test.ts @@ -1,12 +1,14 @@ import { StubbedInstance, stubInterface } from 'ts-sinon'; import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { Connection, ServerRequestHandler } from 'vscode-languageserver/node'; +import { Connection, RequestHandler } from 'vscode-languageserver/node'; import { LspTemplateHandlers } from '../../../src/protocol/LspTemplateHandlers'; import { Identifiable } from '../../../src/protocol/LspTypes'; import { - GetParametersParams, + TemplateMetadataParams, GetParametersResult, GetParametersRequest, + GetCapabilitiesResult, + GetCapabilitiesRequest, TemplateActionParams, TemplateActionResult, TemplateValidationCreateRequest, @@ -26,15 +28,23 @@ describe('LspTemplateHandlers', () => { }); it('should register onGetParameters handler', () => { - const mockHandler: ServerRequestHandler = vi.fn(); + const mockHandler: RequestHandler = vi.fn(); templateHandlers.onGetParameters(mockHandler); expect(connection.onRequest.calledWith(GetParametersRequest.method)).toBe(true); }); + it('should register onGetCapabilities handler', () => { + const mockHandler: RequestHandler = vi.fn(); + + templateHandlers.onGetCapabilities(mockHandler); + + expect(connection.onRequest.calledWith(GetCapabilitiesRequest.method)).toBe(true); + }); + it('should register onTemplateValidate handler', () => { - const mockHandler: ServerRequestHandler = vi.fn(); + const mockHandler: RequestHandler = vi.fn(); templateHandlers.onTemplateValidationCreate(mockHandler); @@ -42,7 +52,7 @@ describe('LspTemplateHandlers', () => { }); it('should register onTemplateDeploy handler', () => { - const mockHandler: ServerRequestHandler = vi.fn(); + const mockHandler: RequestHandler = vi.fn(); templateHandlers.onTemplateDeploymentCreate(mockHandler); @@ -50,7 +60,7 @@ describe('LspTemplateHandlers', () => { }); it('should register onTemplateValidatePoll handler', () => { - const mockHandler: ServerRequestHandler = vi.fn(); + const mockHandler: RequestHandler = vi.fn(); templateHandlers.onTemplateValidationStatus(mockHandler); @@ -58,7 +68,7 @@ describe('LspTemplateHandlers', () => { }); it('should register onTemplateDeployPoll handler', () => { - const mockHandler: ServerRequestHandler = vi.fn(); + const mockHandler: RequestHandler = vi.fn(); templateHandlers.onTemplateDeploymentStatus(mockHandler); diff --git a/tst/unit/templates/CapabilityAnalyzer.test.ts b/tst/unit/templates/CapabilityAnalyzer.test.ts new file mode 100644 index 00000000..0b468c47 --- /dev/null +++ b/tst/unit/templates/CapabilityAnalyzer.test.ts @@ -0,0 +1,90 @@ +import { Capability } from '@aws-sdk/client-cloudformation'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Document } from '../../../src/document/Document'; +import { CfnService } from '../../../src/services/CfnService'; +import { analyzeCapabilities } from '../../../src/templates/CapabilityAnalyzer'; + +describe('analyzeCapabilities', () => { + const mockDocument = { + getText: vi.fn(), + } as unknown as Document; + + const mockCfnService = { + validateTemplate: vi.fn(), + } as unknown as CfnService; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return empty array when validateTemplate returns undefined capabilities', async () => { + (mockDocument.getText as any).mockReturnValue('template content'); + (mockCfnService.validateTemplate as any).mockResolvedValue({ Capabilities: undefined }); + + const result = await analyzeCapabilities(mockDocument, mockCfnService); + + expect(result).toEqual([]); + expect(mockCfnService.validateTemplate).toHaveBeenCalledWith({ TemplateBody: 'template content' }); + }); + + it('should return all capabilities when CAPABILITY_AUTO_EXPAND is present', async () => { + (mockDocument.getText as any).mockReturnValue('template content'); + (mockCfnService.validateTemplate as any).mockResolvedValue({ + Capabilities: [Capability.CAPABILITY_AUTO_EXPAND, Capability.CAPABILITY_IAM], + }); + + const result = await analyzeCapabilities(mockDocument, mockCfnService); + + expect(result).toEqual([ + Capability.CAPABILITY_IAM, + Capability.CAPABILITY_NAMED_IAM, + Capability.CAPABILITY_AUTO_EXPAND, + ]); + }); + + it('should return the exact capabilities when no AUTO_EXPAND', async () => { + (mockDocument.getText as any).mockReturnValue('template content'); + (mockCfnService.validateTemplate as any).mockResolvedValue({ + Capabilities: [Capability.CAPABILITY_IAM], + }); + + const result = await analyzeCapabilities(mockDocument, mockCfnService); + + expect(result).toEqual([Capability.CAPABILITY_IAM]); + }); + + it('should return both IAM capabilities when both are present', async () => { + (mockDocument.getText as any).mockReturnValue('template content'); + (mockCfnService.validateTemplate as any).mockResolvedValue({ + Capabilities: [Capability.CAPABILITY_IAM, Capability.CAPABILITY_NAMED_IAM], + }); + + const result = await analyzeCapabilities(mockDocument, mockCfnService); + + expect(result).toEqual([Capability.CAPABILITY_IAM, Capability.CAPABILITY_NAMED_IAM]); + }); + + it('should return all capabilities when validateTemplate throws error', async () => { + (mockDocument.getText as any).mockReturnValue('template content'); + (mockCfnService.validateTemplate as any).mockRejectedValue(new Error('Validation failed')); + + const result = await analyzeCapabilities(mockDocument, mockCfnService); + + expect(result).toEqual([ + Capability.CAPABILITY_IAM, + Capability.CAPABILITY_NAMED_IAM, + Capability.CAPABILITY_AUTO_EXPAND, + ]); + }); + + it('should return empty array when validateTemplate returns no capabilities', async () => { + (mockDocument.getText as any).mockReturnValue('template content'); + (mockCfnService.validateTemplate as any).mockResolvedValue({ + Capabilities: [], + }); + + const result = await analyzeCapabilities(mockDocument, mockCfnService); + + expect(result).toEqual([]); + }); +}); diff --git a/tst/unit/templates/TemplateParser.test.ts b/tst/unit/templates/TemplateParser.test.ts index c988430f..b5cc0ce3 100644 --- a/tst/unit/templates/TemplateParser.test.ts +++ b/tst/unit/templates/TemplateParser.test.ts @@ -1,7 +1,7 @@ import { Capability } from '@aws-sdk/client-cloudformation'; import { describe, it, expect } from 'vitest'; import { ZodError } from 'zod'; -import { parseTemplateActionParams, parseGetParametersParams } from '../../../src/templates/TemplateParser'; +import { parseTemplateActionParams, parseTemplateMetadataParams } from '../../../src/templates/TemplateParser'; describe('TemplateParser', () => { describe('parseTemplateActionParams', () => { @@ -164,7 +164,7 @@ describe('TemplateParser', () => { uri: 'file:///test.yaml', }; - const result = parseGetParametersParams(input); + const result = parseTemplateMetadataParams(input); expect(result).toEqual({ uri: 'file:///test.yaml', @@ -176,21 +176,21 @@ describe('TemplateParser', () => { uri: '', }; - expect(() => parseGetParametersParams(input)).toThrow(ZodError); + expect(() => parseTemplateMetadataParams(input)).toThrow(ZodError); }); it('should throw ZodError for missing uri', () => { const input = {}; - expect(() => parseGetParametersParams(input)).toThrow(ZodError); + expect(() => parseTemplateMetadataParams(input)).toThrow(ZodError); }); it('should throw ZodError for null input', () => { - expect(() => parseGetParametersParams(null)).toThrow(ZodError); + expect(() => parseTemplateMetadataParams(null)).toThrow(ZodError); }); it('should throw ZodError for undefined input', () => { - expect(() => parseGetParametersParams(undefined)).toThrow(ZodError); + expect(() => parseTemplateMetadataParams(undefined)).toThrow(ZodError); }); }); });