Skip to content

Commit 610d259

Browse files
committed
fix: refactored RequestApproval function
1 parent 6dc0223 commit 610d259

File tree

2 files changed

+102
-96
lines changed

2 files changed

+102
-96
lines changed

Unicorn.Web/ApprovalService.Tests/RequestApprovalFunctionTest.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using System.Net;
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
4+
using System.Net;
25
using Amazon.DynamoDBv2.DataModel;
36
using Amazon.DynamoDBv2.DocumentModel;
47
using Amazon.EventBridge;
@@ -21,15 +24,13 @@ public RequestApprovalFunctionTest()
2124

2225
[Fact]
2326
public async Task RequestApprovalFunction_WhenPropertyFound_SendsApprovalRequest()
24-
{
27+
{
2528
// Arrange
2629
var request = TestHelpers.LoadApiGatewayProxyRequest("./events/request_approval_event.json");
2730
var context = TestHelpers.NewLambdaContext();
2831

2932
var dynamoDbContext = new Mock<IDynamoDBContext>();
3033
var eventBindingClient = new Mock<IAmazonEventBridge>();
31-
var eventBusName = Guid.NewGuid().ToString();
32-
var serviceNamespace = Guid.NewGuid().ToString();
3334

3435
var searchResult = new List<PropertyRecord>
3536
{
@@ -39,14 +40,19 @@ public async Task RequestApprovalFunction_WhenPropertyFound_SendsApprovalRequest
3940
City = "Anytown",
4041
Street = "Main Street",
4142
PropertyNumber = "123",
42-
Status = "NEW"
43+
ListPrice = 2000000.00M,
44+
Images = new() { "image1.jpg", "image2.jpg", "image3.jpg" }
4345
}
4446
};
4547

4648
dynamoDbContext.Setup(c =>
4749
c.FromQueryAsync<PropertyRecord>(It.IsAny<QueryOperationConfig>(), It.IsAny<DynamoDBOperationConfig>()))
4850
.Returns(TestHelpers.NewDynamoDBSearchResult(searchResult));
49-
51+
52+
// dynamoDbContext.Setup(c =>
53+
// c.SaveAsync(It.IsAny<PropertyRecord>(), It.IsAny<CancellationToken>()).ConfigureAwait(false))
54+
// .Returns(new ConfiguredTaskAwaitable());
55+
5056
eventBindingClient.Setup(c =>
5157
c.PutEventsAsync(It.IsAny<PutEventsRequest>(), It.IsAny<CancellationToken>()))
5258
.ReturnsAsync(new PutEventsResponse { FailedEntryCount = 0 });
@@ -62,8 +68,7 @@ public async Task RequestApprovalFunction_WhenPropertyFound_SendsApprovalRequest
6268
};
6369

6470
// Act
65-
var function = new RequestApprovalFunction(dynamoDbContext.Object, eventBindingClient.Object, eventBusName,
66-
serviceNamespace);
71+
var function = new RequestApprovalFunction(dynamoDbContext.Object, eventBindingClient.Object);
6772
var response = await function.FunctionHandler(request, context);
6873

6974
// Assert
Lines changed: 89 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: MIT-0
33

4+
using System.Net;
45
using System.Text.Json;
56
using System.Text.RegularExpressions;
67
using Amazon;
@@ -16,6 +17,7 @@
1617
using AWS.Lambda.Powertools.Logging;
1718
using AWS.Lambda.Powertools.Metrics;
1819
using AWS.Lambda.Powertools.Tracing;
20+
using Microsoft.Extensions.Diagnostics.HealthChecks;
1921
using Unicorn.Web.Common;
2022
using DynamoDBContextConfig = Amazon.DynamoDBv2.DataModel.DynamoDBContextConfig;
2123

@@ -28,9 +30,11 @@ public class RequestApprovalFunction
2830
{
2931
private readonly IDynamoDBContext _dynamoDbContext;
3032
private readonly IAmazonEventBridge _eventBindingClient;
31-
private readonly string _eventBusName;
32-
private readonly string _serviceNamespace;
33-
33+
private string? _dynamodbTable;
34+
private string? _eventBusName;
35+
private string? _serviceNamespace;
36+
private const string Pattern = @"[a-z-]+\/[a-z-]+\/[a-z][a-z0-9-]*\/[0-9-]+";
37+
3438
/// <summary>
3539
/// Default constructor. Initialises global variables for function.
3640
/// </summary>
@@ -40,23 +44,16 @@ public RequestApprovalFunction()
4044
// Instrument all AWS SDK calls
4145
AWSSDKHandler.RegisterXRayForAllServices();
4246

43-
var dynamodbTable = Environment.GetEnvironmentVariable("DYNAMODB_TABLE") ?? "";
44-
if (string.IsNullOrEmpty(dynamodbTable))
45-
throw new Exception("Environment variable DYNAMODB_TABLE is not defined.");
46-
47-
_eventBusName = Environment.GetEnvironmentVariable("EVENT_BUS") ?? "";
48-
if (string.IsNullOrEmpty(_eventBusName))
49-
throw new Exception("Environment variable EVENT_BUS is not defined.");
50-
51-
_serviceNamespace = Environment.GetEnvironmentVariable("SERVICE_NAMESPACE") ?? "";
52-
if (string.IsNullOrEmpty(_eventBusName))
53-
throw new Exception("Environment variable SERVICE_NAMESPACE is not defined.");
47+
// Validate and set environment variables
48+
SetEnvironmentVariables();
5449

50+
// Initialise DDB client
5551
AWSConfigsDynamoDB.Context.TypeMappings[typeof(PropertyRecord)] =
56-
new TypeMapping(typeof(PropertyRecord), dynamodbTable);
57-
52+
new TypeMapping(typeof(PropertyRecord), _dynamodbTable);
5853
var config = new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion.V2 };
5954
_dynamoDbContext = new DynamoDBContext(new AmazonDynamoDBClient(), config);
55+
56+
// Initialise EventBridge client
6057
_eventBindingClient = new AmazonEventBridgeClient();
6158
}
6259

@@ -65,19 +62,59 @@ public RequestApprovalFunction()
6562
/// </summary>
6663
/// <param name="dynamoDbContext"></param>
6764
/// <param name="eventBindingClient"></param>
68-
/// <param name="eventBusName"></param>
69-
/// <param name="serviceNamespace"></param>
70-
public RequestApprovalFunction(IDynamoDBContext dynamoDbContext,
71-
IAmazonEventBridge eventBindingClient,
72-
string eventBusName,
73-
string serviceNamespace)
65+
public RequestApprovalFunction(IDynamoDBContext dynamoDbContext, IAmazonEventBridge eventBindingClient)
7466
{
67+
// Validate and set environment variables
68+
SetEnvironmentVariables();
69+
7570
_dynamoDbContext = dynamoDbContext;
7671
_eventBindingClient = eventBindingClient;
77-
_eventBusName = eventBusName;
78-
_serviceNamespace = serviceNamespace;
7972
}
80-
73+
74+
/// <summary>
75+
/// Validate and set environment variables
76+
/// </summary>
77+
/// <exception cref="Exception">Generic exception thrown if any of the required environment variables cannot be set.</exception>
78+
private void SetEnvironmentVariables()
79+
{
80+
_dynamodbTable = Environment.GetEnvironmentVariable("DYNAMODB_TABLE") ?? "";
81+
if (string.IsNullOrEmpty(_dynamodbTable))
82+
throw new Exception("Environment variable DYNAMODB_TABLE is not defined.");
83+
84+
_eventBusName = Environment.GetEnvironmentVariable("EVENT_BUS") ?? "";
85+
if (string.IsNullOrEmpty(_eventBusName))
86+
throw new Exception("Environment variable EVENT_BUS is not defined.");
87+
88+
_serviceNamespace = Environment.GetEnvironmentVariable("SERVICE_NAMESPACE") ?? "";
89+
if (string.IsNullOrEmpty(_eventBusName))
90+
throw new Exception("Environment variable SERVICE_NAMESPACE is not defined.");
91+
}
92+
93+
/// <summary>
94+
/// Helper method to generate APIGatewayProxyResponse
95+
/// </summary>
96+
/// <param name="message">The message to include in the payload.</param>
97+
/// <param name="statusCode">the HTTP status code</param>
98+
/// <returns>APIGatewayProxyResponse</returns>
99+
private static Task<APIGatewayProxyResponse> ApiResponse(string message, HttpStatusCode statusCode)
100+
{
101+
var body = new Dictionary<string, string>
102+
{
103+
{ "message", message }
104+
};
105+
106+
return Task.FromResult(new APIGatewayProxyResponse
107+
{
108+
Body = JsonSerializer.Serialize(body),
109+
StatusCode = (int)statusCode,
110+
Headers = new Dictionary<string, string>
111+
{
112+
{ "Content-Type", "application/json" },
113+
{ "X-Custom-Header", "application/json" }
114+
}
115+
});
116+
}
117+
81118
/// <summary>
82119
/// Lambda Handler for creating new Contracts.
83120
/// </summary>
@@ -90,18 +127,8 @@ public RequestApprovalFunction(IDynamoDBContext dynamoDbContext,
90127
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent,
91128
ILambdaContext context)
92129
{
93-
var response = new APIGatewayProxyResponse
94-
{
95-
Body = string.Empty,
96-
StatusCode = 200,
97-
Headers = new Dictionary<string, string>
98-
{
99-
{ "Content-Type", "application/json" },
100-
{ "X-Custom-Header", "application/json" }
101-
}
102-
};
103-
104130
string propertyId;
131+
105132
try
106133
{
107134
var request = JsonSerializer.Deserialize<RequestApprovalRequest>(apigProxyEvent.Body);
@@ -111,25 +138,14 @@ public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyReques
111138
catch (Exception e)
112139
{
113140
Logger.LogError(e);
114-
var body = new Dictionary<string, string>
115-
{
116-
{ "message", $"Unable to parse event input as JSON: {e.Message}" }
117-
};
118-
response.Body = JsonSerializer.Serialize(body);
119-
response.StatusCode = 400;
120-
return response;
141+
return await ApiResponse("Unable to parse event input as JSON", HttpStatusCode.BadRequest);
121142
}
122143

123-
var pattern = @"[a-z-]+\/[a-z-]+\/[a-z][a-z0-9-]*\/[0-9-]+";
124-
if (string.IsNullOrWhiteSpace(propertyId) || !Regex.Match(propertyId, pattern).Success)
144+
// Validate property ID
145+
if (string.IsNullOrWhiteSpace(propertyId) || !Regex.Match(propertyId, Pattern).Success)
125146
{
126-
var body = new Dictionary<string, string>
127-
{
128-
{ "message", $"Input invalid; must conform to regular expression: {pattern}" }
129-
};
130-
response.Body = JsonSerializer.Serialize(body);
131-
response.StatusCode = 400;
132-
return response;
147+
return await ApiResponse($"Input invalid; must conform to regular expression: {Pattern}",
148+
HttpStatusCode.BadRequest);
133149
}
134150

135151
var splitString = propertyId.Split('/');
@@ -144,54 +160,42 @@ public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyReques
144160
try
145161
{
146162
var properties = await QueryTableAsync(pk, sk).ConfigureAwait(false);
163+
147164
if (!properties.Any())
148165
{
149-
var body = new Dictionary<string, string>
150-
{
151-
{ "message", "No property found in database with the requested property id" }
152-
};
153-
response.Body = JsonSerializer.Serialize(body);
154-
response.StatusCode = 500;
155-
return response;
166+
return await ApiResponse("No property found in database with the requested property id",
167+
HttpStatusCode.InternalServerError);
156168
}
157169

158170
var property = properties.First();
159-
if (string.Equals(property.Status, PropertyStatus.Approved, StringComparison.CurrentCultureIgnoreCase) ||
160-
string.Equals(property.Status, PropertyStatus.Declined, StringComparison.CurrentCultureIgnoreCase) ||
161-
string.Equals(property.Status, PropertyStatus.Pending, StringComparison.CurrentCultureIgnoreCase))
171+
172+
// Do not approve properties in an approved state
173+
if (string.Equals(property.Status, PropertyStatus.Approved, StringComparison.CurrentCultureIgnoreCase))
162174
{
163-
response.Body = JsonSerializer.Serialize(new Dictionary<string, string>
164-
{
165-
{ "message", $"Property is already {property.Status}; no action taken" }
166-
});
167-
return response;
175+
return await ApiResponse($"Property is already {property.Status}; no action taken", HttpStatusCode.InternalServerError);
168176
}
169-
177+
// Reset status to pending, awaiting the result of the check.
170178
property.Status = PropertyStatus.Pending;
171-
172-
await SendEventAsync(propertyId, property).ConfigureAwait(false);
173179

180+
// Publish the event
181+
await SendEventAsync(propertyId, property).ConfigureAwait(false);
182+
183+
// Add custom metric for the number of approval requests
184+
Metrics.AddMetric("ApprovalsRequested", 1, MetricUnit.Count);
185+
174186
Logger.LogInformation($"Storing new property in DynamoDB with PK {pk} and SK {sk}");
187+
175188
await _dynamoDbContext.SaveAsync(property).ConfigureAwait(false);
176189
Logger.LogInformation($"Stored item in DynamoDB;");
177190
}
178191
catch (Exception e)
179192
{
180193
Logger.LogError(e);
181-
var body = new Dictionary<string, string>
182-
{
183-
{ "message", e.Message }
184-
};
185-
response.Body = JsonSerializer.Serialize(body);
186-
response.StatusCode = 500;
187-
return response;
194+
return await ApiResponse(e.Message, HttpStatusCode.InternalServerError);
195+
188196
}
189-
190-
response.Body = JsonSerializer.Serialize(new Dictionary<string, string>
191-
{
192-
{ "message", "Approval Requested" }
193-
});
194-
return response;
197+
198+
return await ApiResponse("Approval Requested", HttpStatusCode.OK);
195199
}
196200

197201
private async Task<List<PropertyRecord>> QueryTableAsync(string partitionKey, string sortKey)
@@ -204,7 +208,7 @@ private async Task<List<PropertyRecord>> QueryTableAsync(string partitionKey, st
204208
.GetRemainingAsync()
205209
.ConfigureAwait(false);
206210
}
207-
211+
208212
private async Task SendEventAsync(string propertyId, PropertyRecord property)
209213
{
210214
var requestApprovalEvent = new RequestApprovalEvent
@@ -244,9 +248,6 @@ private async Task SendEventAsync(string propertyId, PropertyRecord property)
244248

245249
Logger.LogInformation(
246250
$"Sent event to EventBridge; {response.FailedEntryCount} records failed; {response.Entries.Count} entries received");
247-
248-
Metrics.AddMetric("ApprovalsRequested", 1, MetricUnit.Count);
249-
}
250-
251251

252+
}
252253
}

0 commit comments

Comments
 (0)