Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
Expand All @@ -23,18 +27,6 @@
<ProjectReference Include="..\ApprovalsService\ApprovalsService.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="events\DynamoDbStreamEvents\contract_status_changed_draft.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="events\DynamoDbStreamEvents\contract_status_draft_waiting_for_approval.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="events\DynamoDbStreamEvents\contract_status_changed_approved_waiting_for_approval.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="events\DynamoDbStreamEvents\contract_status_changed_approved.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="events\StreamEvents\contract_status_changed_approved.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand All @@ -47,5 +39,8 @@
<None Update="events\StreamEvents\contract_status_draft_waiting_for_approval.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="events\StreamEvents\contract_status_missing_new_image.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

using System;
using System.Threading;
using System.Threading.Tasks;
using Amazon.DynamoDBv2.DataModel;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
using Xunit.Abstractions;

namespace Unicorn.Approvals.ApprovalsService.Tests;

[Collection("Sequential")]
public class ContractStatusChangedEventHandlerTest
{
private readonly ITestOutputHelper _testOutputHelper;

public ContractStatusChangedEventHandlerTest(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
// Set env variable for Powertools Metrics
Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE", "ContractService");
Environment.SetEnvironmentVariable("CONTRACT_STATUS_TABLE", "test-contract-status-table");
}

[Fact]
public async Task Valid_event_saves_contract_status_to_dynamodb()
{
// Arrange
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusChangedEvent>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);

var cloudWatchEvent = TestHelpers.NewContractStatusChangedEvent("usa/anytown/main-street/111", "DRAFT");

var context = TestHelpers.NewLambdaContext();

// Act
var function = new ContractStatusChangedEventHandler(mockDynamoDbContext);
await function.FunctionHandler(cloudWatchEvent, context);

// Assert
await mockDynamoDbContext.Received(1)
.SaveAsync(Arg.Is<ContractStatusChangedEvent>(e =>
e.PropertyId == "usa/anytown/main-street/111" &&
e.ContractStatus == "DRAFT"), Arg.Any<CancellationToken>());
}

[Fact]
public async Task Malformed_event_throws_contract_status_changed_event_handler_exception()
{
// Arrange
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusChangedEvent>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new ArgumentException("Invalid event data"));

var cloudWatchEvent = TestHelpers.NewContractStatusChangedEvent(
"", "", contractId: Guid.Empty, contractLastModifiedOn: DateTime.MinValue);

var context = TestHelpers.NewLambdaContext();

// Act & Assert
var function = new ContractStatusChangedEventHandler(mockDynamoDbContext);
await Assert.ThrowsAsync<ContractStatusChangedEventHandlerException>(
() => function.FunctionHandler(cloudWatchEvent, context));
}

[Fact]
public async Task DynamoDB_failure_throws_contract_status_changed_event_handler_exception()
{
// Arrange
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusChangedEvent>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new Exception("DynamoDB service unavailable"));

var cloudWatchEvent = TestHelpers.NewContractStatusChangedEvent("usa/anytown/main-street/222", "APPROVED");

var context = TestHelpers.NewLambdaContext();

// Act & Assert
var function = new ContractStatusChangedEventHandler(mockDynamoDbContext);
var exception = await Assert.ThrowsAsync<ContractStatusChangedEventHandlerException>(
() => function.FunctionHandler(cloudWatchEvent, context));

Assert.Contains("DynamoDB service unavailable", exception.Message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,34 +105,31 @@


[Fact]
public Task StatusIsDraftWithTokenSyncShouldNotSendTaskSuccess()
public Task StatusIsApprovedWithTokenSyncShouldSendTaskSuccess()
{
var ddbEvent =
TestHelpers.LoadDynamoDbEventSource(
"./events/StreamEvents/contract_status_draft_waiting_for_approval.json");
"./events/StreamEvents/contract_status_changed_approved_waiting_for_approval.json");

var mockStepFunctionsClient = Substitute.ForPartsOf<AmazonStepFunctionsClient>();
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Any<string>(), CancellationToken.None)

mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Any<string>(), Arg.Is(CancellationToken.None))
.Returns(new ContractStatusItem
{
PropertyId = "usa/anytown/main-street/999",
ContractId = Guid.NewGuid(),
ContractStatus = "DRAFT",
ContractStatus = "APPROVED",
ContractLastModifiedOn = DateTime.Today,
SfnWaitApprovedTaskToken = Token
});

var mockStepFunctionsClient = Substitute.ForPartsOf<AmazonStepFunctionsClient>();

var context = TestHelpers.NewLambdaContext();

var function =
new PropertiesApprovalSyncFunction(mockStepFunctionsClient, mockDynamoDbContext);

var handler = function.FunctionHandler(ddbEvent, context);

mockStepFunctionsClient.Received(0).
mockStepFunctionsClient.Received(1).
SendTaskSuccessAsync(Arg.Any<SendTaskSuccessRequest>(),
Arg.Any<CancellationToken>());

Expand All @@ -141,34 +138,28 @@


[Fact]
public Task StatusIsApprovedWithTokenSyncShouldSendTaskSuccess()
public async Task MissingNewImageShouldNotSendTaskSuccess()
{
var ddbEvent =
TestHelpers.LoadDynamoDbEventSource(
"./events/StreamEvents/contract_status_changed_approved_waiting_for_approval.json");
"./events/StreamEvents/contract_status_missing_new_image.json");

var mockStepFunctionsClient = Substitute.ForPartsOf<AmazonStepFunctionsClient>();
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();

mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Any<string>(), Arg.Is(CancellationToken.None))
.Returns(new ContractStatusItem
{
PropertyId = "usa/anytown/main-street/999",
ContractId = Guid.NewGuid(),
ContractStatus = "APPROVED",
ContractLastModifiedOn = DateTime.Today,
SfnWaitApprovedTaskToken = Token
});

mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns((ContractStatusItem)null);

var context = TestHelpers.NewLambdaContext();

var function =
new PropertiesApprovalSyncFunction(mockStepFunctionsClient, mockDynamoDbContext);
var handler = function.FunctionHandler(ddbEvent, context);

mockStepFunctionsClient.Received(1).
await Assert.ThrowsAsync<ContractStatusNotFoundException>(
() => function.FunctionHandler(ddbEvent, context));

mockStepFunctionsClient.Received(0).

Check warning on line 161 in Unicorn.Approvals/ApprovalsService.Tests/PropertiesApprovalSyncFunctionTest.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 161 in Unicorn.Approvals/ApprovalsService.Tests/PropertiesApprovalSyncFunctionTest.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
SendTaskSuccessAsync(Arg.Any<SendTaskSuccessRequest>(),
Arg.Any<CancellationToken>());

return Task.CompletedTask;
}
}
31 changes: 31 additions & 0 deletions Unicorn.Approvals/ApprovalsService.Tests/TestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Text;
using Amazon.Lambda.CloudWatchEvents;
using Amazon.Lambda.Core;
using Amazon.Lambda.DynamoDBEvents;
using Amazon.Lambda.Serialization.SystemTextJson;
Expand All @@ -13,6 +14,36 @@ namespace Unicorn.Approvals.ApprovalsService.Tests;

public static class TestHelpers
{
/// <summary>
/// Builds a CloudWatchEvent wrapping a ContractStatusChangedEvent detail for ContractStatusChangedEventHandler tests
/// </summary>
public static CloudWatchEvent<ContractStatusChangedEvent> NewContractStatusChangedEvent(
string propertyId, string contractStatus, Guid? contractId = null, DateTime? contractLastModifiedOn = null)
{
return new CloudWatchEvent<ContractStatusChangedEvent>
{
Detail = new ContractStatusChangedEvent
{
PropertyId = propertyId,
ContractId = contractId ?? Guid.NewGuid(),
ContractStatus = contractStatus,
ContractLastModifiedOn = contractLastModifiedOn ?? DateTime.Today
}
};
}

/// <summary>
/// Builds the Step Functions task-token input payload for WaitForContractApprovalFunction tests
/// </summary>
public static object NewWaitForContractApprovalInput(string propertyId, string taskToken)
{
return new
{
Input = new { PropertyId = propertyId },
TaskToken = taskToken
};
}

public static DynamoDBEvent LoadDynamoDbEventSource(string filename)
{
var serializer = Activator.CreateInstance(typeof(DefaultLambdaJsonSerializer)) as ILambdaSerializer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

using System;
using System.Threading;
using System.Threading.Tasks;
using Amazon.DynamoDBv2.DataModel;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
using Xunit.Abstractions;

namespace Unicorn.Approvals.ApprovalsService.Tests;

[Collection("Sequential")]
public class WaitForContractApprovalFunctionTest
{
private readonly ITestOutputHelper _testOutputHelper;

public WaitForContractApprovalFunctionTest(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
// Set env variable for Powertools Metrics
Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE", "ContractService");
Environment.SetEnvironmentVariable("CONTRACT_STATUS_TABLE", "test-contract-status-table");
}

[Fact]
public async Task Contract_exists_stores_token_and_returns()
{
// Arrange
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
var propertyId = "usa/anytown/main-street/111";
var taskToken = "test-task-token-abc123";

var existingItem = new ContractStatusItem
{
PropertyId = propertyId,
ContractId = Guid.NewGuid(),
ContractStatus = "DRAFT",
ContractLastModifiedOn = DateTime.Today,
SfnWaitApprovedTaskToken = null
};

mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Is(propertyId), Arg.Any<CancellationToken>())
.Returns(existingItem);

mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusItem>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);

var input = TestHelpers.NewWaitForContractApprovalInput(propertyId, taskToken);

var context = TestHelpers.NewLambdaContext();

// Act
var function = new WaitForContractApprovalFunction(mockDynamoDbContext);
await function.FunctionHandler(input, context);

// Assert - token should have been stored and SaveAsync called
await mockDynamoDbContext.Received(1)
.SaveAsync(Arg.Is<ContractStatusItem>(item =>
item.PropertyId == propertyId &&
item.SfnWaitApprovedTaskToken == taskToken), Arg.Any<CancellationToken>());
}

[Fact]
public async Task Contract_not_found_throws_contract_status_not_found_exception()
{
// Arrange
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
var propertyId = "usa/anytown/main-street/999";
var taskToken = "test-task-token-abc123";

mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Is(propertyId), Arg.Any<CancellationToken>())
.Returns((ContractStatusItem?)null);

Check warning on line 75 in Unicorn.Approvals/ApprovalsService.Tests/WaitForContractApprovalFunctionTest.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 75 in Unicorn.Approvals/ApprovalsService.Tests/WaitForContractApprovalFunctionTest.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

var input = TestHelpers.NewWaitForContractApprovalInput(propertyId, taskToken);

var context = TestHelpers.NewLambdaContext();

// Act & Assert
var function = new WaitForContractApprovalFunction(mockDynamoDbContext);
await Assert.ThrowsAsync<ContractStatusNotFoundException>(
() => function.FunctionHandler(input, context));
}

[Fact]
public async Task DynamoDB_save_failure_throws_exception()
{
// Arrange
var mockDynamoDbContext = Substitute.For<IDynamoDBContext>();
var propertyId = "usa/anytown/main-street/333";
var taskToken = "test-task-token-xyz789";

var existingItem = new ContractStatusItem
{
PropertyId = propertyId,
ContractId = Guid.NewGuid(),
ContractStatus = "DRAFT",
ContractLastModifiedOn = DateTime.Today,
SfnWaitApprovedTaskToken = null
};

mockDynamoDbContext.LoadAsync<ContractStatusItem>(Arg.Is(propertyId), Arg.Any<CancellationToken>())
.Returns(existingItem);

mockDynamoDbContext.SaveAsync(Arg.Any<ContractStatusItem>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new Exception("DynamoDB save failed"));

var input = TestHelpers.NewWaitForContractApprovalInput(propertyId, taskToken);

var context = TestHelpers.NewLambdaContext();

// Act & Assert
var function = new WaitForContractApprovalFunction(mockDynamoDbContext);
await Assert.ThrowsAsync<Exception>(
() => function.FunctionHandler(input, context));
}
}
Loading
Loading