Skip to content

Commit 41a108e

Browse files
committed
test: standardize unit tests across all services (31 tests)
Add coverage tooling (coverlet), create missing test classes and event fixtures, extend test cases for all 7 Lambda functions, and remove extra tests to ensure parity with Python, Java, and TypeScript runtimes.
1 parent 92d9f8f commit 41a108e

15 files changed

Lines changed: 944 additions & 109 deletions

Unicorn.Approvals/ApprovalsService.Tests/ApprovalsService.Tests.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
<PrivateAssets>all</PrivateAssets>
1414
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1515
</PackageReference>
16+
<PackageReference Include="coverlet.collector" Version="6.0.4">
17+
<PrivateAssets>all</PrivateAssets>
18+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
19+
</PackageReference>
1620
<PackageReference Include="xunit" Version="2.9.3" />
1721
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
1822
<PrivateAssets>all</PrivateAssets>
@@ -47,5 +51,8 @@
4751
<None Update="events\StreamEvents\contract_status_draft_waiting_for_approval.json">
4852
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
4953
</None>
54+
<None Update="events\StreamEvents\contract_status_missing_new_image.json">
55+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
56+
</None>
5057
</ItemGroup>
5158
</Project>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Amazon.DynamoDBv2.DataModel;
8+
using Amazon.Lambda.CloudWatchEvents;
9+
using NSubstitute;
10+
using NSubstitute.ExceptionExtensions;
11+
using Xunit;
12+
using Xunit.Abstractions;
13+
14+
namespace Unicorn.Approvals.ApprovalsService.Tests;
15+
16+
[Collection("Sequential")]
17+
public class ContractStatusChangedEventHandlerTest
18+
{
19+
private readonly ITestOutputHelper _testOutputHelper;
20+
21+
public ContractStatusChangedEventHandlerTest(ITestOutputHelper testOutputHelper)
22+
{
23+
_testOutputHelper = testOutputHelper;
24+
// Set env variable for Powertools Metrics
25+
Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE", "ContractService");
26+
Environment.SetEnvironmentVariable("CONTRACT_STATUS_TABLE", "test-contract-status-table");
27+
}
28+
29+
[Fact]
30+
public async Task Valid_event_saves_contract_status_to_dynamodb()
31+
{
32+
// Arrange
33+
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
34+
mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusChangedEvent>(), Arg.Any<CancellationToken>())
35+
.Returns(Task.CompletedTask);
36+
37+
var cloudWatchEvent = new CloudWatchEvent<ContractStatusChangedEvent>
38+
{
39+
Detail = new ContractStatusChangedEvent
40+
{
41+
PropertyId = "usa/anytown/main-street/111",
42+
ContractId = Guid.NewGuid(),
43+
ContractStatus = "DRAFT",
44+
ContractLastModifiedOn = DateTime.Today
45+
}
46+
};
47+
48+
var context = TestHelpers.NewLambdaContext();
49+
50+
// Act
51+
var function = new ContractStatusChangedEventHandler(mockDynamoDbContext);
52+
await function.FunctionHandler(cloudWatchEvent, context);
53+
54+
// Assert
55+
await mockDynamoDbContext.Received(1)
56+
.SaveAsync(Arg.Is<ContractStatusChangedEvent>(e =>
57+
e.PropertyId == "usa/anytown/main-street/111" &&
58+
e.ContractStatus == "DRAFT"), Arg.Any<CancellationToken>());
59+
}
60+
61+
[Fact]
62+
public async Task Malformed_event_throws_contract_status_changed_event_handler_exception()
63+
{
64+
// Arrange
65+
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
66+
mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusChangedEvent>(), Arg.Any<CancellationToken>())
67+
.ThrowsAsync(new ArgumentException("Invalid event data"));
68+
69+
var cloudWatchEvent = new CloudWatchEvent<ContractStatusChangedEvent>
70+
{
71+
Detail = new ContractStatusChangedEvent
72+
{
73+
PropertyId = "",
74+
ContractId = Guid.Empty,
75+
ContractStatus = "",
76+
ContractLastModifiedOn = DateTime.MinValue
77+
}
78+
};
79+
80+
var context = TestHelpers.NewLambdaContext();
81+
82+
// Act & Assert
83+
var function = new ContractStatusChangedEventHandler(mockDynamoDbContext);
84+
await Assert.ThrowsAsync<ContractStatusChangedEventHandlerException>(
85+
() => function.FunctionHandler(cloudWatchEvent, context));
86+
}
87+
88+
[Fact]
89+
public async Task DynamoDB_failure_throws_contract_status_changed_event_handler_exception()
90+
{
91+
// Arrange
92+
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
93+
mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusChangedEvent>(), Arg.Any<CancellationToken>())
94+
.ThrowsAsync(new Exception("DynamoDB service unavailable"));
95+
96+
var cloudWatchEvent = new CloudWatchEvent<ContractStatusChangedEvent>
97+
{
98+
Detail = new ContractStatusChangedEvent
99+
{
100+
PropertyId = "usa/anytown/main-street/222",
101+
ContractId = Guid.NewGuid(),
102+
ContractStatus = "APPROVED",
103+
ContractLastModifiedOn = DateTime.Today
104+
}
105+
};
106+
107+
var context = TestHelpers.NewLambdaContext();
108+
109+
// Act & Assert
110+
var function = new ContractStatusChangedEventHandler(mockDynamoDbContext);
111+
var exception = await Assert.ThrowsAsync<ContractStatusChangedEventHandlerException>(
112+
() => function.FunctionHandler(cloudWatchEvent, context));
113+
114+
Assert.Contains("DynamoDB service unavailable", exception.Message);
115+
}
116+
}

Unicorn.Approvals/ApprovalsService.Tests/PropertiesApprovalSyncFunctionTest.cs

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -105,34 +105,31 @@ public Task StatusIsApprovedNoTokenSyncShouldNotSendTaskSuccess()
105105

106106

107107
[Fact]
108-
public Task StatusIsDraftWithTokenSyncShouldNotSendTaskSuccess()
108+
public Task StatusIsApprovedWithTokenSyncShouldSendTaskSuccess()
109109
{
110110
var ddbEvent =
111111
TestHelpers.LoadDynamoDbEventSource(
112-
"./events/StreamEvents/contract_status_draft_waiting_for_approval.json");
112+
"./events/StreamEvents/contract_status_changed_approved_waiting_for_approval.json");
113113

114+
var mockStepFunctionsClient = Substitute.ForPartsOf<AmazonStepFunctionsClient>();
114115
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
115-
116-
mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Any<string>(), CancellationToken.None)
116+
117+
mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Any<string>(), Arg.Is(CancellationToken.None))
117118
.Returns(new ContractStatusItem
118119
{
119120
PropertyId = "usa/anytown/main-street/999",
120121
ContractId = Guid.NewGuid(),
121-
ContractStatus = "DRAFT",
122+
ContractStatus = "APPROVED",
122123
ContractLastModifiedOn = DateTime.Today,
123124
SfnWaitApprovedTaskToken = Token
124125
});
125-
126-
var mockStepFunctionsClient = Substitute.ForPartsOf<AmazonStepFunctionsClient>();
127-
128126
var context = TestHelpers.NewLambdaContext();
129127

130128
var function =
131129
new PropertiesApprovalSyncFunction(mockStepFunctionsClient, mockDynamoDbContext);
132-
133130
var handler = function.FunctionHandler(ddbEvent, context);
134131

135-
mockStepFunctionsClient.Received(0).
132+
mockStepFunctionsClient.Received(1).
136133
SendTaskSuccessAsync(Arg.Any<SendTaskSuccessRequest>(),
137134
Arg.Any<CancellationToken>());
138135

@@ -141,34 +138,28 @@ public Task StatusIsDraftWithTokenSyncShouldNotSendTaskSuccess()
141138

142139

143140
[Fact]
144-
public Task StatusIsApprovedWithTokenSyncShouldSendTaskSuccess()
141+
public async Task MissingNewImageShouldNotSendTaskSuccess()
145142
{
146143
var ddbEvent =
147144
TestHelpers.LoadDynamoDbEventSource(
148-
"./events/StreamEvents/contract_status_changed_approved_waiting_for_approval.json");
145+
"./events/StreamEvents/contract_status_missing_new_image.json");
149146

150147
var mockStepFunctionsClient = Substitute.ForPartsOf<AmazonStepFunctionsClient>();
151148
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
152-
153-
mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Any<string>(), Arg.Is(CancellationToken.None))
154-
.Returns(new ContractStatusItem
155-
{
156-
PropertyId = "usa/anytown/main-street/999",
157-
ContractId = Guid.NewGuid(),
158-
ContractStatus = "APPROVED",
159-
ContractLastModifiedOn = DateTime.Today,
160-
SfnWaitApprovedTaskToken = Token
161-
});
149+
150+
mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Any<string>(), Arg.Any<CancellationToken>())
151+
.Returns((ContractStatusItem)null);
152+
162153
var context = TestHelpers.NewLambdaContext();
163154

164155
var function =
165156
new PropertiesApprovalSyncFunction(mockStepFunctionsClient, mockDynamoDbContext);
166-
var handler = function.FunctionHandler(ddbEvent, context);
167157

168-
mockStepFunctionsClient.Received(1).
158+
await Assert.ThrowsAsync<ContractStatusNotFoundException>(
159+
() => function.FunctionHandler(ddbEvent, context));
160+
161+
mockStepFunctionsClient.Received(0).
169162
SendTaskSuccessAsync(Arg.Any<SendTaskSuccessRequest>(),
170163
Arg.Any<CancellationToken>());
171-
172-
return Task.CompletedTask;
173164
}
174165
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
4+
using System;
5+
using System.Text.Json;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Amazon.DynamoDBv2.DataModel;
9+
using NSubstitute;
10+
using NSubstitute.ExceptionExtensions;
11+
using Xunit;
12+
using Xunit.Abstractions;
13+
14+
namespace Unicorn.Approvals.ApprovalsService.Tests;
15+
16+
[Collection("Sequential")]
17+
public class WaitForContractApprovalFunctionTest
18+
{
19+
private readonly ITestOutputHelper _testOutputHelper;
20+
21+
public WaitForContractApprovalFunctionTest(ITestOutputHelper testOutputHelper)
22+
{
23+
_testOutputHelper = testOutputHelper;
24+
// Set env variable for Powertools Metrics
25+
Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE", "ContractService");
26+
Environment.SetEnvironmentVariable("CONTRACT_STATUS_TABLE", "test-contract-status-table");
27+
}
28+
29+
[Fact]
30+
public async Task Contract_exists_stores_token_and_returns()
31+
{
32+
// Arrange
33+
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
34+
var propertyId = "usa/anytown/main-street/111";
35+
var taskToken = "test-task-token-abc123";
36+
37+
var existingItem = new ContractStatusItem
38+
{
39+
PropertyId = propertyId,
40+
ContractId = Guid.NewGuid(),
41+
ContractStatus = "DRAFT",
42+
ContractLastModifiedOn = DateTime.Today,
43+
SfnWaitApprovedTaskToken = null
44+
};
45+
46+
mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Is(propertyId), Arg.Any<CancellationToken>())
47+
.Returns(existingItem);
48+
49+
mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusItem>(), Arg.Any<CancellationToken>())
50+
.Returns(Task.CompletedTask);
51+
52+
var input = new
53+
{
54+
Input = new { PropertyId = propertyId },
55+
TaskToken = taskToken
56+
};
57+
58+
var context = TestHelpers.NewLambdaContext();
59+
60+
// Act
61+
var function = new WaitForContractApprovalFunction(mockDynamoDbContext);
62+
await function.FunctionHandler(input, context);
63+
64+
// Assert - token should have been stored and SaveAsync called
65+
await mockDynamoDbContext.Received(1)
66+
.SaveAsync(Arg.Is<ContractStatusItem>(item =>
67+
item.PropertyId == propertyId &&
68+
item.SfnWaitApprovedTaskToken == taskToken), Arg.Any<CancellationToken>());
69+
}
70+
71+
[Fact]
72+
public async Task Contract_not_found_throws_contract_status_not_found_exception()
73+
{
74+
// Arrange
75+
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
76+
var propertyId = "usa/anytown/main-street/999";
77+
var taskToken = "test-task-token-abc123";
78+
79+
mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Is(propertyId), Arg.Any<CancellationToken>())
80+
.Returns((ContractStatusItem?)null);
81+
82+
var input = new
83+
{
84+
Input = new { PropertyId = propertyId },
85+
TaskToken = taskToken
86+
};
87+
88+
var context = TestHelpers.NewLambdaContext();
89+
90+
// Act & Assert
91+
var function = new WaitForContractApprovalFunction(mockDynamoDbContext);
92+
await Assert.ThrowsAsync<ContractStatusNotFoundException>(
93+
() => function.FunctionHandler(input, context));
94+
}
95+
96+
[Fact]
97+
public async Task DynamoDB_save_failure_throws_exception()
98+
{
99+
// Arrange
100+
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
101+
var propertyId = "usa/anytown/main-street/333";
102+
var taskToken = "test-task-token-xyz789";
103+
104+
var existingItem = new ContractStatusItem
105+
{
106+
PropertyId = propertyId,
107+
ContractId = Guid.NewGuid(),
108+
ContractStatus = "DRAFT",
109+
ContractLastModifiedOn = DateTime.Today,
110+
SfnWaitApprovedTaskToken = null
111+
};
112+
113+
mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Is(propertyId), Arg.Any<CancellationToken>())
114+
.Returns(existingItem);
115+
116+
mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusItem>(), Arg.Any<CancellationToken>())
117+
.ThrowsAsync(new Exception("DynamoDB save failed"));
118+
119+
var input = new
120+
{
121+
Input = new { PropertyId = propertyId },
122+
TaskToken = taskToken
123+
};
124+
125+
var context = TestHelpers.NewLambdaContext();
126+
127+
// Act & Assert
128+
var function = new WaitForContractApprovalFunction(mockDynamoDbContext);
129+
await Assert.ThrowsAsync<Exception>(
130+
() => function.FunctionHandler(input, context));
131+
}
132+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"Records": [
3+
{
4+
"eventID": "1",
5+
"eventName": "REMOVE",
6+
"eventVersion": "1.1",
7+
"eventSource": "aws:dynamodb",
8+
"awsRegion": "ap-southeast-2",
9+
"dynamodb": {
10+
"ApproximateCreationDateTime": 1661269906.0,
11+
"Keys": {
12+
"PropertyId": {
13+
"S": "usa/anytown/main-street/999"
14+
}
15+
},
16+
"SequenceNumber": "100000000005391461882",
17+
"SizeBytes": 50,
18+
"StreamViewType": "NEW_AND_OLD_IMAGES"
19+
},
20+
"eventSourceARN": "arn:aws:dynamodb:ap-southeast-2:123456789012:table/test/stream/2022-08-23T15:46:44.107"
21+
}
22+
]
23+
}

Unicorn.Approvals/ApprovalsService/ContractStatusChangedEventHandler.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ public ContractStatusChangedEventHandler()
4545
}
4646

4747

48+
/// <summary>
49+
/// Testing constructor for ContractStatusChangedEventHandler
50+
/// </summary>
51+
/// <param name="dynamoDbContext">DynamoDB context</param>
52+
public ContractStatusChangedEventHandler(IDynamoDBContext dynamoDbContext)
53+
{
54+
_dynamoDbContext = dynamoDbContext;
55+
}
56+
4857
/// <summary>
4958
/// Event handler for ContractStatusChangedEvent
5059
/// </summary>

0 commit comments

Comments
 (0)