Skip to content

Commit

Permalink
test(Profile): add integration tests for GetPersonalInformation endpo…
Browse files Browse the repository at this point in the history
…int #38
  • Loading branch information
GenjiruSUchiwa committed Nov 13, 2024
1 parent f761233 commit 7f11947
Show file tree
Hide file tree
Showing 18 changed files with 845 additions and 26 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="NWebsec.AspNetCore.Core" Version="3.0.1" />
<PackageVersion Include="NWebsec.AspNetCore.Middleware" Version="3.0.0" />
<PackageVersion Include="Respawn" Version="6.2.1" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
Expand Down
7 changes: 7 additions & 0 deletions PlaceApi.sln
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Place.Core.Logging", "src\C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Place.Core.Database", "src\Common\Place.Core.Database\Place.Core.Database.csproj", "{FB0782E5-FA12-4030-B382-0311E004C166}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Place.Api.Integration.Tests", "tests\Place.Api.Integration.Tests\Place.Api.Integration.Tests.csproj", "{967AC2B2-7C7A-4037-B2F6-CF9792A69588}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -81,6 +83,7 @@ Global
{2AF91DCD-AABF-4EF0-844F-2319A1FAA4B5} = {25654B5F-D4E9-4019-B307-8759C38E94A8}
{C37B0761-0FF0-46F4-932B-B2C42C926E73} = {25654B5F-D4E9-4019-B307-8759C38E94A8}
{FB0782E5-FA12-4030-B382-0311E004C166} = {25654B5F-D4E9-4019-B307-8759C38E94A8}
{967AC2B2-7C7A-4037-B2F6-CF9792A69588} = {5B48E2F5-D7CC-48EA-94FE-A2F132650C8F}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1DFC8537-6B29-4BB5-8449-1910496DB479}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -141,5 +144,9 @@ Global
{FB0782E5-FA12-4030-B382-0311E004C166}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB0782E5-FA12-4030-B382-0311E004C166}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB0782E5-FA12-4030-B382-0311E004C166}.Release|Any CPU.Build.0 = Release|Any CPU
{967AC2B2-7C7A-4037-B2F6-CF9792A69588}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{967AC2B2-7C7A-4037-B2F6-CF9792A69588}.Debug|Any CPU.Build.0 = Debug|Any CPU
{967AC2B2-7C7A-4037-B2F6-CF9792A69588}.Release|Any CPU.ActiveCfg = Release|Any CPU
{967AC2B2-7C7A-4037-B2F6-CF9792A69588}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.201",
"version": "8.0.401",
"rollForward": "major",
"allowPrerelease": true
}
Expand Down
16 changes: 15 additions & 1 deletion src/Place.Api.Profile/Apis/V1/Endpoints/GetPersonalInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,21 @@ public async Task<

if (vm is not null)
{
return TypedResults.Ok(new PersonalInformationResponse(vm));
return TypedResults.Ok(
new PersonalInformationResponse
{
FirstName = vm.FirstName,
LastName = vm.LastName,
Email = vm.Email,
PhoneNumber = vm.PhoneNumber,
Street = vm.Street,
City = vm.City,
ZipCode = vm.ZipCode,
Country = vm.Country,
Gender = vm.Gender,
FormattedAddress = vm.FormattedAddress,
}
);
}

ProblemDetails problem =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,14 @@ namespace Place.Api.Profile.Apis.V1.Responses;

public record PersonalInformationResponse
{
public string? FirstName { get; }
public string? LastName { get; }
public string Email { get; }
public string? PhoneNumber { get; }
public string? Street { get; }
public string? City { get; }
public string? ZipCode { get; }
public string? Country { get; }
public string? Gender { get; }
public string FormattedAddress { get; }

public PersonalInformationResponse(PersonalInformationViewModel vm)
{
FirstName = vm.FirstName;
LastName = vm.LastName;
Email = vm.Email;
PhoneNumber = vm.PhoneNumber;
Street = vm.Street;
City = vm.City;
ZipCode = vm.ZipCode;
Country = vm.Country;
Gender = vm.Gender;
FormattedAddress = vm.FormattedAddress;
}
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string Email { get; set; } = null!;
public string? PhoneNumber { get; set; }
public string? Street { get; set; }
public string? City { get; set; }
public string? ZipCode { get; set; }
public string? Country { get; set; }
public string? Gender { get; set; }
public string FormattedAddress { get; set; } = null!;
}
3 changes: 3 additions & 0 deletions src/Place.Api.Profile/IAssemblyMarker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Place.Api.Profile;

public interface IAssemblyMarker { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using FluentAssertions.Execution;
using Place.Api.Profile.Apis.V1.Responses;
using Place.Api.Profile.Infrastructure.Persistence.EF.Models;

namespace Place.Api.Integration.Tests.Apis.Extensions;

public static class AddressValidationExtensions
{
public static Task ValidateAddress(
this PersonalInformationResponse response,
ProfileReadModel profile
)
{
using AssertionScope scope = new();

response.FormattedAddress.Should().NotBeNull("FormattedAddress should never be null");

if (IsCompleteAddress(profile))
{
ValidateCompleteAddress(response.FormattedAddress, profile);
return Task.CompletedTask;
}

ValidatePartialAddress(response.FormattedAddress, profile);
return Task.CompletedTask;
}

private static bool IsCompleteAddress(ProfileReadModel profile) =>
!string.IsNullOrEmpty(profile.Street)
&& !string.IsNullOrEmpty(profile.City)
&& !string.IsNullOrEmpty(profile.ZipCode)
&& !string.IsNullOrEmpty(profile.Country);

private static void ValidateCompleteAddress(string formattedAddress, ProfileReadModel profile)
{
string expectedAddress =
$"{profile.Street}, {profile.ZipCode} {profile.City}, {profile.Country}";

formattedAddress
.Should()
.Be(expectedAddress)
.And.Contain(profile.Street!)
.And.Contain(profile.ZipCode!)
.And.Contain(profile.City!)
.And.Contain(profile.Country!);

ValidateAddressOrder(formattedAddress, profile);
}

private static void ValidateAddressOrder(string formattedAddress, ProfileReadModel profile)
{
int streetIndex = formattedAddress.IndexOf(profile.Street!, StringComparison.Ordinal);
int zipIndex = formattedAddress.IndexOf(profile.ZipCode!, StringComparison.Ordinal);
int cityIndex = formattedAddress.IndexOf(profile.City!, StringComparison.Ordinal);
int countryIndex = formattedAddress.IndexOf(profile.Country!, StringComparison.Ordinal);

streetIndex.Should().BeLessThan(zipIndex, "Street should come before zip code");
zipIndex.Should().BeLessThan(countryIndex, "Zip code should come before country");
cityIndex.Should().BeLessThan(countryIndex, "City should come before country");
}

private static void ValidatePartialAddress(string formattedAddress, ProfileReadModel profile)
{
string?[] components = [profile.Street, profile.City, profile.ZipCode, profile.Country];

foreach (string? component in components.Where(c => !string.IsNullOrEmpty(c)))
{
formattedAddress.Should().Contain(component!);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using FluentAssertions.Execution;
using Place.Api.Profile.Apis.V1.Responses;
using Place.Api.Profile.Domain.Profile;
using Place.Api.Profile.Infrastructure.Persistence.EF.Models;

namespace Place.Api.Integration.Tests.Apis.Extensions;

public static class PersonalInformationAssertions
{
public static Task ShouldReturnValidProfileAsync(
this (HttpResponseMessage Response, PersonalInformationResponse? Content) result,
ProfileReadModel expectedProfile
)
{
using AssertionScope scope = new();

ValidateHttpResponse(result.Response);
ValidateContent(result.Content, expectedProfile);
ValidateContactInformation(result.Content!, expectedProfile);
result.Content!.ValidateAddress(expectedProfile);

return Task.CompletedTask;
}

public static Task ShouldReturnPartialAddressAsync(
this (HttpResponseMessage Response, PersonalInformationResponse? Content) result,
string? expectedCity,
string? expectedCountry,
string expectedFormattedAddress
)
{
using AssertionScope scope = new();

ValidateHttpResponse(result.Response);
ValidatePartialAddress(
result.Content!,
expectedCity,
expectedCountry,
expectedFormattedAddress
);

return Task.CompletedTask;
}

private static void ValidateHttpResponse(HttpResponseMessage response)
{
response.Should().NotBeNull();
response
.StatusCode.Should()
.Be(HttpStatusCode.OK, "a valid profile should return OK status");
response.Content.Headers.ContentType?.MediaType.Should().Be("application/json");
}

private static void ValidateContent(
PersonalInformationResponse? content,
ProfileReadModel expected
)
{
content.Should().NotBeNull("response content should not be null for a valid profile");

content!.FirstName.Should().Be(expected.FirstName, "FirstName should match exactly");
content.LastName.Should().Be(expected.LastName, "LastName should match exactly");
content.Street.Should().Be(expected.Street, "Street should match exactly");
content.City.Should().Be(expected.City, "City should match exactly");
content.ZipCode.Should().Be(expected.ZipCode, "ZipCode should match exactly");
content.Country.Should().Be(expected.Country, "Country should match exactly");

ValidateNonEmptyFields(content, expected);
}

private static void ValidateContactInformation(
PersonalInformationResponse content,
ProfileReadModel expected
)
{
ValidateEmail(content.Email);
ValidatePhoneNumber(content.PhoneNumber);
ValidateGender(content.Gender, expected.Gender);
}

private static void ValidateNonEmptyFields(
PersonalInformationResponse content,
ProfileReadModel expected
)
{
if (expected.FirstName != null)
content.FirstName.Should().NotBeEmpty("FirstName should not be empty when provided");

if (expected.LastName != null)
content.LastName.Should().NotBeEmpty("LastName should not be empty when provided");

if (expected.Street != null)
content.Street.Should().NotBeEmpty("Street should not be empty when provided");
}

private static void ValidateEmail(string email)
{
email
.Should()
.NotBeNullOrWhiteSpace("Email is required")
.And.Contain("@", "Email should be in valid format")
.And.Contain(".", "Email should be in valid format");
}

private static void ValidatePhoneNumber(string? phoneNumber)
{
if (phoneNumber != null)
{
phoneNumber
.Should()
.StartWith("+", "Phone number should start with country code")
.And.HaveLength(12, "Phone number should be in international format");
}
}

private static void ValidateGender(string? actual, Gender? expected)
{
if (expected.HasValue)
{
actual.Should().NotBeNull().And.Be(expected.ToString(), "Gender should match exactly");
}
else
{
actual.Should().BeNull("Gender should be null when not provided");
}
}

private static void ValidatePartialAddress(
PersonalInformationResponse content,
string? expectedCity,
string? expectedCountry,
string expectedFormattedAddress
)
{
content.City.Should().Be(expectedCity);
content.Country.Should().Be(expectedCountry);
content.FormattedAddress.Should().Be(expectedFormattedAddress);

if (expectedCity == null)
content.City.Should().BeNull("City should be null when not provided");

if (expectedCountry == null)
content.Country.Should().BeNull("Country should be null when not provided");

content.FormattedAddress.Should().NotBeNull("FormattedAddress should never be null");

if (string.IsNullOrWhiteSpace(expectedCity) && string.IsNullOrWhiteSpace(expectedCountry))
{
content
.FormattedAddress.Should()
.BeEmpty(
"FormattedAddress should be empty when no address components are provided"
);
}
}
}
Loading

0 comments on commit 7f11947

Please sign in to comment.