diff --git a/src/artifacts/resourceAttributes/DeletionPolicyPropertyDocs.ts b/src/artifacts/resourceAttributes/DeletionPolicyPropertyDocs.ts new file mode 100644 index 00000000..32da0dcf --- /dev/null +++ b/src/artifacts/resourceAttributes/DeletionPolicyPropertyDocs.ts @@ -0,0 +1,118 @@ +export const deletionPolicyValueDocsMap: ReadonlyMap = getDeletionPolicyValueDocsMap(); + +function getDeletionPolicyValueDocsMap(): Map { + const docsMap = new Map(); + + docsMap.set( + 'Delete', + [ + '**Delete**', + '\n', + '---', + 'CloudFormation deletes the resource and all its content if applicable during stack deletion. ', + 'You can add this deletion policy to any resource type. ', + "By default, if you don't specify a `DeletionPolicy`, CloudFormation deletes your resources. ", + 'However, be aware of the following considerations:', + '\n', + '- For `AWS::RDS::DBCluster` resources, the default policy is `Snapshot`. ', + "- For `AWS::RDS::DBInstance` resources that don't specify the DBClusterIdentifier property, the default policy is `Snapshot`.", + '- For Amazon S3 buckets, you must delete all objects in the bucket for deletion to succeed. ', + '- The default behavior of CloudFormation is to delete the secret with the ForceDeleteWithoutRecovery flag. ', + '\n', + '[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)', + ].join('\n'), + ); + + docsMap.set( + 'Retain', + [ + '**Retain**', + '\n', + '---', + 'CloudFormation keeps the resource without deleting the resource or its contents when its stack is deleted. ', + 'You can add this deletion policy to any resource type. ', + 'When CloudFormation completes the stack deletion, the stack will be in `Delete_Complete` state; however, resources that are retained continue to exist and continue to incur applicable charges until you delete those resources. ', + '\n', + 'For update operations, the following considerations apply: ', + '\n', + "- If a resource is deleted, the `DeletionPolicy` retains the physical resource but ensures that it's deleted from CloudFormation's scope. ", + "- If a resource is updated such that a new physical resource is created to replace the old resource, then the old resource is completely deleted, including from CloudFormation's scope. ", + '\n', + '[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)', + ].join('\n'), + ); + + docsMap.set( + 'RetainExceptOnCreate', + [ + '**RetainExceptOnCreate**', + '\n', + '---', + '`RetainExceptOnCreate` behaves like `Retain` for stack operations, except for the stack operation that initially created the resource.', + 'If the stack operation that created the resource is rolled back, CloudFormation deletes the resource. ', + 'For all other stack operations, such as stack deletion, CloudFormation retains the resource and its contents. ', + 'The result is that new, empty, and unused resources are deleted, while in-use resources and their data are retained. ', + 'Refer to the [UpdateStack](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_UpdateStack.html) API documentation to use this deletion policy as an API parameter without updating your template.', + '\n', + '[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)', + ].join('\n'), + ); + + docsMap.set( + 'Snapshot', + [ + '**Snapshot**', + '\n', + '---', + 'For resources that support snapshots, CloudFormation creates a snapshot for the resource before deleting it. ', + 'When CloudFormation completes the stack deletion, the stack will be in the `Delete_Complete` state; however, the snapshots that are created with this policy continue to exist and continue to incur applicable charges until you delete those snapshots. ', + 'Resources that support snapshots include: ', + '\n', + '- [AWS::DocDB::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-docdb-dbcluster.html)', + '- [AWS::EC2::Volume](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-ec2-volume.html)', + '- [AWS::ElastiCache::CacheCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-elasticache-cachecluster.html)', + '- [AWS::ElastiCache::ReplicationGroup](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-elasticache-replicationgroup.html)', + '- [AWS::Neptune::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-neptune-dbcluster.html)', + '- [AWS::RDS::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-rds-dbcluster.html)', + '- [AWS::RDS::DBInstance](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-rds-dbinstance.html)', + '- [AWS::Redshift::Cluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-redshift-cluster.html)', + '\n', + '[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)', + ].join('\n'), + ); + + return docsMap; +} + +export const SNAPSHOT_SUPPORTED_RESOURCE_TYPES: ReadonlyArray = [ + 'AWS::DocDB::DBCluster', + 'AWS::EC2::Volume', + 'AWS::ElastiCache::CacheCluster', + 'AWS::ElastiCache::ReplicationGroup', + 'AWS::Neptune::DBCluster', + 'AWS::RDS::DBCluster', + 'AWS::RDS::DBInstance', + 'AWS::Redshift::Cluster', +]; + +export const DEFAULT_SNAPSHOT_RESOURCE_TYPES: ReadonlyArray = [ + 'AWS::RDS::DBCluster', + 'AWS::RDS::DBInstance', // Only when DBClusterIdentifier is not specified +]; + +export const DELETION_POLICY_VALUES: ReadonlyArray = ['Delete', 'Retain', 'RetainExceptOnCreate', 'Snapshot']; + +export function supportsSnapshot(resourceType: string): boolean { + return SNAPSHOT_SUPPORTED_RESOURCE_TYPES.includes(resourceType); +} + +export function getDefaultDeletionPolicy(resourceType: string): string { + if (DEFAULT_SNAPSHOT_RESOURCE_TYPES.includes(resourceType)) { + return 'Snapshot'; + } + return 'Delete'; +} + +export function isValidDeletionPolicyValue(value: string): boolean { + return DELETION_POLICY_VALUES.includes(value); +} diff --git a/src/context/Context.ts b/src/context/Context.ts index 612c305b..a0504633 100644 --- a/src/context/Context.ts +++ b/src/context/Context.ts @@ -142,6 +142,27 @@ export class Context { return this.propertyPath.length > resourceAttributeIndex + 1; } + public isResourceAttributeValue(): boolean { + if (this.section !== TopLevelSection.Resources || !this.hasLogicalId) { + return false; + } + + if (this.propertyPath.length !== 3) { + return false; + } + + const attributeName = this.propertyPath[2] as string; + if (!ResourceAttributesSet.has(attributeName)) { + return false; + } + + if (this.text === attributeName) { + return false; + } + + return this.isValue(); + } + public getResourceAttributePropertyPath(): string[] { const resourceAttributeIndex = this.propertyPath.findIndex((segment) => ResourceAttributesSet.has(segment as string), diff --git a/src/hover/HoverFormatter.ts b/src/hover/HoverFormatter.ts index a09f8ee7..b392c7cf 100644 --- a/src/hover/HoverFormatter.ts +++ b/src/hover/HoverFormatter.ts @@ -1,3 +1,5 @@ +import { deletionPolicyValueDocsMap } from '../artifacts/resourceAttributes/DeletionPolicyPropertyDocs'; +import { ResourceAttribute } from '../context/ContextType'; import { Condition, Entity, Mapping, Parameter, Resource } from '../context/semantic/Entity'; import { EntityType } from '../context/semantic/SemanticTypes'; import { PropertyType } from '../schema/ResourceSchema'; @@ -1138,3 +1140,18 @@ export function formatResourceHover(resource: Resource): string { const result = doc.filter((item) => item.trim() !== '').join('\n\n'); return result; } + +/** + * Gets documentation for resource attribute values based on the attribute type and text. + */ +export function getResourceAttributeValueDoc(attributeName: ResourceAttribute, text: string): string | undefined { + switch (attributeName) { + case ResourceAttribute.DeletionPolicy: { + return deletionPolicyValueDocsMap.get(text); + } + //TODO: add other ResourceAttribute Values as needed + default: { + return undefined; + } + } +} diff --git a/src/hover/IntrinsicFunctionArgumentHoverProvider.ts b/src/hover/IntrinsicFunctionArgumentHoverProvider.ts index 829ca7d4..fa2f8925 100644 --- a/src/hover/IntrinsicFunctionArgumentHoverProvider.ts +++ b/src/hover/IntrinsicFunctionArgumentHoverProvider.ts @@ -1,7 +1,7 @@ import { Context } from '../context/Context'; -import { IntrinsicFunction } from '../context/ContextType'; +import { IntrinsicFunction, ResourceAttribute, ResourceAttributesSet } from '../context/ContextType'; import { ContextWithRelatedEntities } from '../context/ContextWithRelatedEntities'; -import { formatIntrinsicArgumentHover } from './HoverFormatter'; +import { formatIntrinsicArgumentHover, getResourceAttributeValueDoc } from './HoverFormatter'; import { HoverProvider } from './HoverProvider'; export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider { @@ -18,7 +18,11 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider { return undefined; } - // Handle different intrinsic function types + const resourceAttributeValueDoc = this.getResourceAttributeValueDoc(context); + if (resourceAttributeValueDoc) { + return resourceAttributeValueDoc; + } + switch (intrinsicFunction.type) { case IntrinsicFunction.Ref: { return this.handleRefArgument(context); @@ -60,4 +64,20 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider { private buildSchemaAndFormat(relatedContext: Context): string | undefined { return formatIntrinsicArgumentHover(relatedContext.entity); } + + /** + * Check if we're inside an intrinsic function that's providing a value for a resource attribute + * and return documentation for that value if applicable. + */ + private getResourceAttributeValueDoc(context: Context): string | undefined { + // Find the resource attribute in the property path + for (const pathSegment of context.propertyPath) { + if (ResourceAttributesSet.has(pathSegment as string)) { + const attributeName = pathSegment as ResourceAttribute; + return getResourceAttributeValueDoc(attributeName, context.text); + } + } + + return undefined; + } } diff --git a/src/hover/ResourceSectionHoverProvider.ts b/src/hover/ResourceSectionHoverProvider.ts index a3869b59..aababd64 100644 --- a/src/hover/ResourceSectionHoverProvider.ts +++ b/src/hover/ResourceSectionHoverProvider.ts @@ -1,12 +1,13 @@ import { resourceAttributeDocsMap } from '../artifacts/ResourceAttributeDocs'; import { creationPolicyPropertyDocsMap } from '../artifacts/resourceAttributes/CreationPolicyPropertyDocs'; +import { deletionPolicyValueDocsMap } from '../artifacts/resourceAttributes/DeletionPolicyPropertyDocs'; import { Context } from '../context/Context'; import { ResourceAttribute, TopLevelSection } from '../context/ContextType'; import { Resource } from '../context/semantic/Entity'; import { ResourceSchema } from '../schema/ResourceSchema'; import { SchemaRetriever } from '../schema/SchemaRetriever'; import { templatePathToJsonPointerPath } from '../utils/PathUtils'; -import { propertyTypesToMarkdown, formatResourceHover } from './HoverFormatter'; +import { propertyTypesToMarkdown, formatResourceHover, getResourceAttributeValueDoc } from './HoverFormatter'; import { HoverProvider } from './HoverProvider'; export class ResourceSectionHoverProvider implements HoverProvider { @@ -33,6 +34,9 @@ export class ResourceSectionHoverProvider implements HoverProvider { if (context.isResourceAttributeProperty()) { return this.getResourceAttributePropertyDoc(context, resource); } + if (context.isResourceAttributeValue()) { + return this.getResourceAttributeValueDoc(context); + } if (context.isResourceAttribute && resource[context.text] !== undefined) { return this.getResourceAttributeDoc(context.text); } @@ -104,6 +108,9 @@ export class ResourceSectionHoverProvider implements HoverProvider { case ResourceAttribute.CreationPolicy: { return this.getCreationPolicyPropertyDoc(propertyPath); } + case ResourceAttribute.DeletionPolicy: { + return this.getDeletionPolicyPropertyDoc(propertyPath); + } default: { return undefined; } @@ -114,4 +121,17 @@ export class ResourceSectionHoverProvider implements HoverProvider { const propertyPathString = propertyPath.join('.'); return creationPolicyPropertyDocsMap.get(propertyPathString); } + + private getDeletionPolicyPropertyDoc(propertyPath: ReadonlyArray): string | undefined { + if (propertyPath.length === 2) { + const deletionPolicyValue = propertyPath[1]; + return deletionPolicyValueDocsMap.get(deletionPolicyValue); + } + return undefined; + } + + private getResourceAttributeValueDoc(context: Context): string | undefined { + const attributeName = context.propertyPath[2] as ResourceAttribute; + return getResourceAttributeValueDoc(attributeName, context.text); + } } diff --git a/tst/e2e/hover/Hover.test.ts b/tst/e2e/hover/Hover.test.ts index a9a69a5c..7306de02 100644 --- a/tst/e2e/hover/Hover.test.ts +++ b/tst/e2e/hover/Hover.test.ts @@ -5,6 +5,7 @@ import { parameterAttributeDocsMap } from '../../../src/artifacts/ParameterAttri import { pseudoParameterDocsMap } from '../../../src/artifacts/PseudoParameterDocs'; import { resourceAttributeDocsMap } from '../../../src/artifacts/ResourceAttributeDocs'; import { creationPolicyPropertyDocsMap } from '../../../src/artifacts/resourceAttributes/CreationPolicyPropertyDocs'; +import { deletionPolicyValueDocsMap } from '../../../src/artifacts/resourceAttributes/DeletionPolicyPropertyDocs'; import { templateSectionDocsMap } from '../../../src/artifacts/TemplateSectionDocs'; import { TopLevelSection, @@ -1265,6 +1266,30 @@ Resources:`, .build(), }, }, + { + action: 'type', + content: ``, + position: { line: 302, character: 31 }, + description: 'Hover Snapshot in DeletionPolicy Resource Attribute', + verification: { + position: { line: 286, character: 44 }, + expectation: HoverExpectationBuilder.create() + .expectContent(deletionPolicyValueDocsMap.get('Snapshot')) + .build(), + }, + }, + { + action: 'type', + content: ``, + position: { line: 302, character: 31 }, + description: 'Hover Delete in DeletionPolicy Resource Attribute', + verification: { + position: { line: 286, character: 52 }, + expectation: HoverExpectationBuilder.create() + .expectContent(deletionPolicyValueDocsMap.get('Delete')) + .build(), + }, + }, { action: 'type', content: ` diff --git a/tst/unit/context/Context.test.ts b/tst/unit/context/Context.test.ts index 390d9db6..1c1deefe 100644 --- a/tst/unit/context/Context.test.ts +++ b/tst/unit/context/Context.test.ts @@ -286,6 +286,34 @@ describe('Context', () => { }); }); + describe('isResourceAttributeValue method', () => { + it('should return true when positioned at resource attribute value', () => { + const context = getContextAt(94, 20); // Position at "Retain" in "DeletionPolicy: Retain" + + expect(context).toBeDefined(); + expect(context!.section).toBe(TopLevelSection.Resources); + expect(context!.hasLogicalId).toBe(true); + expect(context!.isResourceAttributeValue()).toBe(true); + }); + + it('should return false when positioned at resource attribute key', () => { + const context = getContextAt(94, 4); // Position at "DeletionPolicy:" + + expect(context).toBeDefined(); + expect(context!.section).toBe(TopLevelSection.Resources); + expect(context!.text).toBe('DeletionPolicy'); + expect(context!.isResourceAttributeValue()).toBe(false); + }); + + it('should return false when not in Resources section', () => { + const context = getContextAt(21, 4); // Position at "EnvironmentType:" in Parameters section + + expect(context).toBeDefined(); + expect(context!.section).toBe(TopLevelSection.Parameters); + expect(context!.isResourceAttributeValue()).toBe(false); + }); + }); + describe('Comprehensive Resource Entity with All Attributes', () => { it('should create comprehensive resource entity with all resource attributes', () => { const context = getContextAt(88, 4); // ComprehensiveResource diff --git a/tst/unit/hover/IntrinsicFunctionArgumentHoverProvider.test.ts b/tst/unit/hover/IntrinsicFunctionArgumentHoverProvider.test.ts index 74b20182..d7437034 100644 --- a/tst/unit/hover/IntrinsicFunctionArgumentHoverProvider.test.ts +++ b/tst/unit/hover/IntrinsicFunctionArgumentHoverProvider.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import { deletionPolicyValueDocsMap } from '../../../src/artifacts/resourceAttributes/DeletionPolicyPropertyDocs'; import { IntrinsicFunction, TopLevelSection } from '../../../src/context/ContextType'; import { IntrinsicFunctionArgumentHoverProvider } from '../../../src/hover/IntrinsicFunctionArgumentHoverProvider'; import { createResourceContext, createParameterContext } from '../../utils/MockContext'; @@ -196,6 +197,138 @@ describe('IntrinsicFunctionArgumentHoverProvider', () => { }); }); + describe('DeletionPolicy values inside If intrinsic functions', () => { + it('should return documentation for valid DeletionPolicy value "Delete" inside !If', () => { + const provider = new IntrinsicFunctionArgumentHoverProvider(); + + const mockContext = createResourceContext('MyBucket', { + text: 'Delete', + data: { Type: 'AWS::S3::Bucket', DeletionPolicy: 'Delete' }, + propertyPath: [TopLevelSection.Resources, 'MyBucket', 'DeletionPolicy'], + }); + + // Mock the intrinsicContext for an If function + mockContext.intrinsicContext.inIntrinsic = () => true; + mockContext.intrinsicContext.intrinsicFunction = () => + ({ + type: IntrinsicFunction.If, + }) as any; + + const result = provider.getInformation(mockContext); + const expectedDoc = deletionPolicyValueDocsMap.get('Delete'); + + expect(result).toBe(expectedDoc); + }); + + it('should return documentation for valid DeletionPolicy value "Retain" inside !If', () => { + const provider = new IntrinsicFunctionArgumentHoverProvider(); + + const mockContext = createResourceContext('MyBucket', { + text: 'Retain', + data: { Type: 'AWS::S3::Bucket', DeletionPolicy: 'Retain' }, + propertyPath: [TopLevelSection.Resources, 'MyBucket', 'DeletionPolicy'], + }); + + // Mock the intrinsicContext for an If function + mockContext.intrinsicContext.inIntrinsic = () => true; + mockContext.intrinsicContext.intrinsicFunction = () => + ({ + type: IntrinsicFunction.If, + }) as any; + + const result = provider.getInformation(mockContext); + const expectedDoc = deletionPolicyValueDocsMap.get('Retain'); + + expect(result).toBe(expectedDoc); + }); + + it('should return documentation for valid DeletionPolicy value "Snapshot" inside !If', () => { + const provider = new IntrinsicFunctionArgumentHoverProvider(); + + const mockContext = createResourceContext('MyBucket', { + text: 'Snapshot', + data: { Type: 'AWS::S3::Bucket', DeletionPolicy: 'Snapshot' }, + propertyPath: [TopLevelSection.Resources, 'MyBucket', 'DeletionPolicy'], + }); + + // Mock the intrinsicContext for an If function + mockContext.intrinsicContext.inIntrinsic = () => true; + mockContext.intrinsicContext.intrinsicFunction = () => + ({ + type: IntrinsicFunction.If, + }) as any; + + const result = provider.getInformation(mockContext); + const expectedDoc = deletionPolicyValueDocsMap.get('Snapshot'); + + expect(result).toBe(expectedDoc); + }); + + it('should return documentation for valid DeletionPolicy value "RetainExceptOnCreate" inside !If', () => { + const provider = new IntrinsicFunctionArgumentHoverProvider(); + + const mockContext = createResourceContext('MyBucket', { + text: 'RetainExceptOnCreate', + data: { Type: 'AWS::S3::Bucket', DeletionPolicy: 'RetainExceptOnCreate' }, + propertyPath: [TopLevelSection.Resources, 'MyBucket', 'DeletionPolicy'], + }); + + // Mock the intrinsicContext for an If function + mockContext.intrinsicContext.inIntrinsic = () => true; + mockContext.intrinsicContext.intrinsicFunction = () => + ({ + type: IntrinsicFunction.If, + }) as any; + + const result = provider.getInformation(mockContext); + const expectedDoc = deletionPolicyValueDocsMap.get('RetainExceptOnCreate'); + + expect(result).toBe(expectedDoc); + }); + + it('should return undefined for invalid DeletionPolicy value inside !If', () => { + const provider = new IntrinsicFunctionArgumentHoverProvider(); + + const mockContext = createResourceContext('MyBucket', { + text: 'InvalidValue', + data: { Type: 'AWS::S3::Bucket', DeletionPolicy: 'InvalidValue' }, + propertyPath: [TopLevelSection.Resources, 'MyBucket', 'DeletionPolicy'], + }); + + // Mock the intrinsicContext for an If function + mockContext.intrinsicContext.inIntrinsic = () => true; + mockContext.intrinsicContext.intrinsicFunction = () => + ({ + type: IntrinsicFunction.If, + }) as any; + + const result = provider.getInformation(mockContext); + + expect(result).toBeUndefined(); + }); + + it('should return undefined when not in DeletionPolicy context inside !If', () => { + const provider = new IntrinsicFunctionArgumentHoverProvider(); + + const mockContext = createResourceContext('MyBucket', { + text: 'Retain', + data: { Type: 'AWS::S3::Bucket' }, + propertyPath: [TopLevelSection.Resources, 'MyBucket', 'Properties', 'BucketName'], + }); + + // Mock the intrinsicContext for an If function + mockContext.intrinsicContext.inIntrinsic = () => true; + mockContext.intrinsicContext.intrinsicFunction = () => + ({ + type: IntrinsicFunction.If, + }) as any; + + const result = provider.getInformation(mockContext); + + expect(result).toBeUndefined(); + }); + }); + describe('Edge cases', () => { it('should return undefined when context is not ContextWithRelatedEntities', () => { const provider = new IntrinsicFunctionArgumentHoverProvider(); diff --git a/tst/unit/hover/ResourceSectionHoverProvider.test.ts b/tst/unit/hover/ResourceSectionHoverProvider.test.ts index 96e9f3b9..804733a6 100644 --- a/tst/unit/hover/ResourceSectionHoverProvider.test.ts +++ b/tst/unit/hover/ResourceSectionHoverProvider.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, afterAll, beforeAll } from 'vitest'; import { creationPolicyPropertyDocsMap } from '../../../src/artifacts/resourceAttributes/CreationPolicyPropertyDocs'; +import { deletionPolicyValueDocsMap } from '../../../src/artifacts/resourceAttributes/DeletionPolicyPropertyDocs'; import { Context } from '../../../src/context/Context'; import { ContextManager } from '../../../src/context/ContextManager'; import { @@ -652,5 +653,123 @@ describe('ResourceSectionHoverProvider', () => { expect(result).toBeUndefined(); }); }); + + describe('DeletionPolicy Value Hover Documentation', () => { + it('should return documentation for DeletionPolicy Retain value', () => { + // Create a mock context for Retain value + const mockContext = createResourceContext('S3Bucket', { + text: 'Retain', + data: { + Type: 'AWS::S3::Bucket', + DeletionPolicy: 'Retain', + }, + propertyPath: [TopLevelSection.Resources, 'S3Bucket', 'DeletionPolicy'], + }); + + expect(mockContext.isResourceAttributeValue()).toBe(true); + + const result = hoverProvider.getInformation(mockContext); + const expectedDoc = deletionPolicyValueDocsMap.get('Retain'); + + expect(result).toBeDefined(); + expect(result).toBe(expectedDoc); + }); + + it('should return documentation for DeletionPolicy Delete value', () => { + // Create a mock context for Delete value + const mockContext = createResourceContext('S3Bucket', { + text: 'Delete', + data: { + Type: 'AWS::S3::Bucket', + DeletionPolicy: 'Delete', + }, + propertyPath: [TopLevelSection.Resources, 'S3Bucket', 'DeletionPolicy'], + }); + + expect(mockContext.isResourceAttributeValue()).toBe(true); + + const result = hoverProvider.getInformation(mockContext); + const expectedDoc = deletionPolicyValueDocsMap.get('Delete'); + + expect(result).toBeDefined(); + expect(result).toBe(expectedDoc); + }); + + it('should return documentation for DeletionPolicy Snapshot value', () => { + // Create a mock context for Snapshot value + const mockContext = createResourceContext('S3Bucket', { + text: 'Snapshot', + data: { + Type: 'AWS::S3::Bucket', + DeletionPolicy: 'Snapshot', + }, + propertyPath: [TopLevelSection.Resources, 'S3Bucket', 'DeletionPolicy'], + }); + + expect(mockContext.isResourceAttributeValue()).toBe(true); + + const result = hoverProvider.getInformation(mockContext); + const expectedDoc = deletionPolicyValueDocsMap.get('Snapshot'); + + expect(result).toBeDefined(); + expect(result).toBe(expectedDoc); + }); + + it('should return documentation for DeletionPolicy RetainExceptOnCreate value', () => { + // Create a mock context for RetainExceptOnCreate value + const mockContext = createResourceContext('S3Bucket', { + text: 'RetainExceptOnCreate', + data: { + Type: 'AWS::S3::Bucket', + DeletionPolicy: 'RetainExceptOnCreate', + }, + propertyPath: [TopLevelSection.Resources, 'S3Bucket', 'DeletionPolicy'], + }); + + expect(mockContext.isResourceAttributeValue()).toBe(true); + + const result = hoverProvider.getInformation(mockContext); + const expectedDoc = deletionPolicyValueDocsMap.get('RetainExceptOnCreate'); + + expect(result).toBeDefined(); + expect(result).toBe(expectedDoc); + }); + + it('should return undefined for invalid DeletionPolicy values', () => { + // Create a mock context for invalid value + const mockContext = createResourceContext('S3Bucket', { + text: 'InvalidValue', + data: { + Type: 'AWS::S3::Bucket', + DeletionPolicy: 'InvalidValue', + }, + propertyPath: [TopLevelSection.Resources, 'S3Bucket', 'DeletionPolicy'], + }); + + expect(mockContext.isResourceAttributeValue()).toBe(true); + + const result = hoverProvider.getInformation(mockContext); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for other resource attribute values', () => { + // Create a mock context for Condition value (not DeletionPolicy) + const mockContext = createResourceContext('S3Bucket', { + text: 'CreateProdResources', + data: { + Type: 'AWS::S3::Bucket', + Condition: 'CreateProdResources', + }, + propertyPath: [TopLevelSection.Resources, 'S3Bucket', 'Condition'], + }); + + expect(mockContext.isResourceAttributeValue()).toBe(true); + + const result = hoverProvider.getInformation(mockContext); + + expect(result).toBeUndefined(); + }); + }); }); });