Skip to content

Commit 44dd3a6

Browse files
committed
Phase 3: FunctionUrlConfig orphan cleanup and attribute switching
- Remove FunctionUrlConfig from template when [FunctionUrl] attribute is removed - Clean transition when switching from [FunctionUrl] to [HttpApi] or [RestApi] - 4 new unit tests for orphan cleanup and attribute switching scenarios
1 parent 4d37472 commit 44dd3a6

File tree

2 files changed

+78
-23
lines changed

2 files changed

+78
-23
lines changed

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
203203
{
204204
var currentSyncedEvents = new List<string>();
205205
var currentSyncedEventProperties = new Dictionary<string, List<string>>();
206+
var hasFunctionUrl = false;
206207

207208
foreach (var attributeModel in lambdaFunction.Attributes)
208209
{
@@ -223,10 +224,17 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
223224
break;
224225
case AttributeModel<FunctionUrlAttribute> functionUrlAttributeModel:
225226
ProcessFunctionUrlAttribute(lambdaFunction, functionUrlAttributeModel.Data);
227+
hasFunctionUrl = true;
226228
break;
227229
}
228230
}
229231

232+
// Remove FunctionUrlConfig if the attribute was removed
233+
if (!hasFunctionUrl)
234+
{
235+
_templateWriter.RemoveToken($"Resources.{lambdaFunction.ResourceName}.Properties.FunctionUrlConfig");
236+
}
237+
230238
SynchronizeEventsAndProperties(currentSyncedEvents, currentSyncedEventProperties, lambdaFunction);
231239
}
232240

Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/FunctionUrlTests.cs

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,19 @@ public partial class CloudFormationWriterTests
1515
[InlineData(CloudFormationTemplateFormat.Yaml)]
1616
public void FunctionUrlWithDefaultAuthType(CloudFormationTemplateFormat templateFormat)
1717
{
18-
// ARRANGE
1918
var mockFileManager = GetMockFileManager(string.Empty);
2019
var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler",
2120
"TestMethod", 30, 512, null, null);
2221
lambdaFunctionModel.Attributes = new List<AttributeModel>
2322
{
24-
new AttributeModel<FunctionUrlAttribute>
25-
{
26-
Data = new FunctionUrlAttribute()
27-
}
23+
new AttributeModel<FunctionUrlAttribute> { Data = new FunctionUrlAttribute() }
2824
};
2925
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
3026
var report = GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel });
3127
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
3228

33-
// ACT
3429
cloudFormationWriter.ApplyReport(report);
3530

36-
// ASSERT
3731
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
3832
Assert.Equal("NONE", templateWriter.GetToken<string>("Resources.TestMethod.Properties.FunctionUrlConfig.AuthType"));
3933
}
@@ -43,7 +37,6 @@ public void FunctionUrlWithDefaultAuthType(CloudFormationTemplateFormat template
4337
[InlineData(CloudFormationTemplateFormat.Yaml)]
4438
public void FunctionUrlWithIamAuth(CloudFormationTemplateFormat templateFormat)
4539
{
46-
// ARRANGE
4740
var mockFileManager = GetMockFileManager(string.Empty);
4841
var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler",
4942
"TestMethod", 30, 512, null, null);
@@ -58,10 +51,8 @@ public void FunctionUrlWithIamAuth(CloudFormationTemplateFormat templateFormat)
5851
var report = GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel });
5952
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
6053

61-
// ACT
6254
cloudFormationWriter.ApplyReport(report);
6355

64-
// ASSERT
6556
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
6657
Assert.Equal("AWS_IAM", templateWriter.GetToken<string>("Resources.TestMethod.Properties.FunctionUrlConfig.AuthType"));
6758
}
@@ -71,25 +62,19 @@ public void FunctionUrlWithIamAuth(CloudFormationTemplateFormat templateFormat)
7162
[InlineData(CloudFormationTemplateFormat.Yaml)]
7263
public void FunctionUrlDoesNotCreateEventEntry(CloudFormationTemplateFormat templateFormat)
7364
{
74-
// ARRANGE - FunctionUrl should NOT create an Events entry (unlike HttpApi/RestApi)
7565
var mockFileManager = GetMockFileManager(string.Empty);
7666
var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler",
7767
"TestMethod", 30, 512, null, null);
7868
lambdaFunctionModel.Attributes = new List<AttributeModel>
7969
{
80-
new AttributeModel<FunctionUrlAttribute>
81-
{
82-
Data = new FunctionUrlAttribute()
83-
}
70+
new AttributeModel<FunctionUrlAttribute> { Data = new FunctionUrlAttribute() }
8471
};
8572
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
8673
var report = GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel });
8774
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
8875

89-
// ACT
9076
cloudFormationWriter.ApplyReport(report);
9177

92-
// ASSERT
9378
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
9479
Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig"));
9580
Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.Events"));
@@ -100,7 +85,6 @@ public void FunctionUrlDoesNotCreateEventEntry(CloudFormationTemplateFormat temp
10085
[InlineData(CloudFormationTemplateFormat.Yaml)]
10186
public void FunctionUrlWithCors(CloudFormationTemplateFormat templateFormat)
10287
{
103-
// ARRANGE
10488
var mockFileManager = GetMockFileManager(string.Empty);
10589
var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler",
10690
"TestMethod", 30, 512, null, null);
@@ -123,10 +107,8 @@ public void FunctionUrlWithCors(CloudFormationTemplateFormat templateFormat)
123107
var report = GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel });
124108
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
125109

126-
// ACT
127110
cloudFormationWriter.ApplyReport(report);
128111

129-
// ASSERT
130112
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
131113
var corsPath = "Resources.TestMethod.Properties.FunctionUrlConfig.Cors";
132114
Assert.Equal(new List<string> { "https://example.com" }, templateWriter.GetToken<List<string>>($"{corsPath}.AllowOrigins"));
@@ -141,7 +123,6 @@ public void FunctionUrlWithCors(CloudFormationTemplateFormat templateFormat)
141123
[InlineData(CloudFormationTemplateFormat.Yaml)]
142124
public void FunctionUrlWithoutCorsDoesNotEmitCorsBlock(CloudFormationTemplateFormat templateFormat)
143125
{
144-
// ARRANGE - No CORS properties set, so no Cors block should be emitted
145126
var mockFileManager = GetMockFileManager(string.Empty);
146127
var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler",
147128
"TestMethod", 30, 512, null, null);
@@ -156,13 +137,79 @@ public void FunctionUrlWithoutCorsDoesNotEmitCorsBlock(CloudFormationTemplateFor
156137
var report = GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel });
157138
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
158139

159-
// ACT
160140
cloudFormationWriter.ApplyReport(report);
161141

162-
// ASSERT
163142
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
164143
Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig.AuthType"));
165144
Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig.Cors"));
166145
}
146+
147+
[Theory]
148+
[InlineData(CloudFormationTemplateFormat.Json)]
149+
[InlineData(CloudFormationTemplateFormat.Yaml)]
150+
public void FunctionUrlConfigRemovedWhenAttributeRemoved(CloudFormationTemplateFormat templateFormat)
151+
{
152+
// First pass: create FunctionUrlConfig
153+
var mockFileManager = GetMockFileManager(string.Empty);
154+
var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler",
155+
"TestMethod", 30, 512, null, null);
156+
lambdaFunctionModel.Attributes = new List<AttributeModel>
157+
{
158+
new AttributeModel<FunctionUrlAttribute>
159+
{
160+
Data = new FunctionUrlAttribute { AllowOrigins = new[] { "*" } }
161+
}
162+
};
163+
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
164+
var report = GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel });
165+
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
166+
167+
cloudFormationWriter.ApplyReport(report);
168+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
169+
Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig"));
170+
171+
// Second pass: remove the attribute, FunctionUrlConfig should be cleaned up
172+
lambdaFunctionModel.Attributes = new List<AttributeModel>();
173+
cloudFormationWriter.ApplyReport(GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel }));
174+
175+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
176+
Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig"));
177+
}
178+
179+
[Theory]
180+
[InlineData(CloudFormationTemplateFormat.Json)]
181+
[InlineData(CloudFormationTemplateFormat.Yaml)]
182+
public void SwitchFromFunctionUrlToHttpApi(CloudFormationTemplateFormat templateFormat)
183+
{
184+
// First pass: FunctionUrl
185+
var mockFileManager = GetMockFileManager(string.Empty);
186+
var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler",
187+
"TestMethod", 30, 512, null, null);
188+
lambdaFunctionModel.Attributes = new List<AttributeModel>
189+
{
190+
new AttributeModel<FunctionUrlAttribute> { Data = new FunctionUrlAttribute() }
191+
};
192+
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
193+
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
194+
195+
cloudFormationWriter.ApplyReport(GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel }));
196+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
197+
Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig"));
198+
199+
// Second pass: switch to HttpApi
200+
lambdaFunctionModel.Attributes = new List<AttributeModel>
201+
{
202+
new AttributeModel<HttpApiAttribute>
203+
{
204+
Data = new HttpApiAttribute(LambdaHttpMethod.Get, "/items")
205+
}
206+
};
207+
cloudFormationWriter.ApplyReport(GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel }));
208+
209+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
210+
Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig"));
211+
Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.Events.RootGet"));
212+
Assert.Equal("HttpApi", templateWriter.GetToken<string>("Resources.TestMethod.Properties.Events.RootGet.Type"));
213+
}
167214
}
168215
}

0 commit comments

Comments
 (0)