From d75c34503145a7ce09ec74a396b7bf1d352ec70a Mon Sep 17 00:00:00 2001 From: "SHAKIR, Muhammad" Date: Fri, 2 Aug 2024 15:45:52 +0100 Subject: [PATCH 1/2] added integration tests --- .../ApiIntegrationTestBase.cs | 144 ++++++++++++++++++ ...mies.Academisation.SubcutaneousTest.csproj | 1 + .../ProjectGroup/ProjectGroupTests.cs | 36 +++++ .../Utils/HttpResponseMessageExtensions.cs | 25 +++ .../Utils/Request.cs | 82 ++++++++++ Dfe.Academies.Academisation.WebApi/Program.cs | 1 - 6 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 Dfe.Academies.Academisation.SubcutaneousTest/ApiIntegrationTestBase.cs create mode 100644 Dfe.Academies.Academisation.SubcutaneousTest/ProjectGroup/ProjectGroupTests.cs create mode 100644 Dfe.Academies.Academisation.SubcutaneousTest/Utils/HttpResponseMessageExtensions.cs create mode 100644 Dfe.Academies.Academisation.SubcutaneousTest/Utils/Request.cs diff --git a/Dfe.Academies.Academisation.SubcutaneousTest/ApiIntegrationTestBase.cs b/Dfe.Academies.Academisation.SubcutaneousTest/ApiIntegrationTestBase.cs new file mode 100644 index 000000000..ba4294c1e --- /dev/null +++ b/Dfe.Academies.Academisation.SubcutaneousTest/ApiIntegrationTestBase.cs @@ -0,0 +1,144 @@ +using System.Net.Http.Headers; +using System.Reflection; +using AutoFixture; +using Dfe.Academies.Academisation.Data; +using Dfe.Academies.Academisation.Service.Commands.ProjectGroup; +using Dfe.Academies.Academisation.WebApi; +using Dfe.Academies.Academisation.WebApi.Options; +using MediatR; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Dfe.Academies.Academisation.SubcutaneousTest +{ + public class ApiIntegrationTestBase + { + private readonly Fixture _fixture; + protected readonly string _apiKey; + private HttpClient _httpClient; + private IMediator _mediator; + private AcademisationContext _dbContext; + public ApiIntegrationTestBase() { + _fixture = new(); + _apiKey = Guid.NewGuid().ToString(); + _httpClient = Build(); + _mediator = ServiceProvider.GetRequiredService(); + _dbContext = ServiceProvider.GetRequiredService(); + } + + protected HttpClient CreateClient() { return _httpClient; } + + private WebApplicationFactory WebAppFactory { get; set; } = new(); + + protected Fixture Fixture => _fixture; + + + protected AcademisationContext GetDBContext() + { + _dbContext.Database.EnsureDeleted(); + _dbContext.Database.EnsureCreated(); + return _dbContext; + + } + protected static CancellationToken CancellationToken => CancellationToken.None; + + protected IServiceProvider ServiceProvider + { + get + { + return WebAppFactory.Services; + } + } + + protected ApiIntegrationTestBase GetHttpClient => this; + + private HttpClient Build() + { + WebAppFactory = new WebApplicationFactory() + .WithWebHostBuilder(builder => + { + builder.UseEnvironment("local"); + + builder.ConfigureLogging(x => + { + x.ClearProviders(); + x.SetMinimumLevel(LogLevel.Debug); + }); + + ConfigureAppConfiguration(builder, _apiKey); + + builder.ConfigureTestServices(services => + { + ConfigureInMemoryDatabase(services); + ConfigureServices(builder); + + services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(Assembly.GetAssembly(typeof(CreateProjectGroupCommandHandler))!)); + }); + }); + + WebAppFactory.Server.PreserveExecutionContext = true; + + return BuildHttpClient(); + } + + private HttpClient BuildHttpClient() + { + var httpClient = WebAppFactory.CreateClient(new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false + }); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test"); + httpClient.DefaultRequestHeaders.Add("x-api-key", _apiKey); + + return httpClient; + } + + private static void ConfigureServices(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => + { + // Bind the configuration section to the AuthenticationConfig class + var configuration = context.Configuration; + services.Configure(configuration.GetSection("AuthenticationConfig")); + }); + } + + private static void ConfigureInMemoryDatabase(IServiceCollection services) + { + // Replace database context with our own Integration database + var descriptor = services.SingleOrDefault(d => + d.ServiceType == typeof(DbContextOptions)); + if (descriptor != null) + { + services.Remove(descriptor); + } + + var connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + + services.AddDbContext(options => + { + options.UseSqlite(connection); + }); + } + private static void ConfigureAppConfiguration(IWebHostBuilder builder, string apiKey) + { + builder.ConfigureAppConfiguration((context, configBuilder) => + { + // Create an in-memory configuration with test values + var inMemorySettings = new List> + { + new ("AuthenticationConfig:ApiKeys:0", apiKey) + }; + + configBuilder.AddInMemoryCollection(inMemorySettings!); + }); + } + } +} diff --git a/Dfe.Academies.Academisation.SubcutaneousTest/Dfe.Academies.Academisation.SubcutaneousTest.csproj b/Dfe.Academies.Academisation.SubcutaneousTest/Dfe.Academies.Academisation.SubcutaneousTest.csproj index 7d4b9adac..20b9466df 100644 --- a/Dfe.Academies.Academisation.SubcutaneousTest/Dfe.Academies.Academisation.SubcutaneousTest.csproj +++ b/Dfe.Academies.Academisation.SubcutaneousTest/Dfe.Academies.Academisation.SubcutaneousTest.csproj @@ -11,6 +11,7 @@ + diff --git a/Dfe.Academies.Academisation.SubcutaneousTest/ProjectGroup/ProjectGroupTests.cs b/Dfe.Academies.Academisation.SubcutaneousTest/ProjectGroup/ProjectGroupTests.cs new file mode 100644 index 000000000..fe17c1957 --- /dev/null +++ b/Dfe.Academies.Academisation.SubcutaneousTest/ProjectGroup/ProjectGroupTests.cs @@ -0,0 +1,36 @@ +using AutoFixture; +using Dfe.Academies.Academisation.Data; +using Dfe.Academies.Academisation.IService.ServiceModels.ProjectGroup; +using Dfe.Academies.Academisation.Service.Commands.ProjectGroup; +using Dfe.Academies.Academisation.SubcutaneousTest.Utils; + +namespace Dfe.Academies.Academisation.SubcutaneousTest.ProjectGroup +{ + public class ProjectGroupTests : ApiIntegrationTestBase + { + private readonly HttpClient _client; + private readonly AcademisationContext _context; + + public ProjectGroupTests() + { + _client = CreateClient(); + _context = GetDBContext(); + } + + [Fact] + public async Task CreateProjectGroup_ShouldCreateSuccessfully() + { + // Arrange + var command = new CreateProjectGroupCommand(Fixture.Create()[..15], Fixture.Create()[..7], Fixture.Create()[..10], []); + + // Action + var httpResponseMessage = await _client.PostAsJsonAsync("project-group/create-project-group",command, CancellationToken); + + Assert.True(httpResponseMessage.IsSuccessStatusCode); + var response = await httpResponseMessage.ConvertResponseToTypeAsync(); + Assert.Equal(response.TrustName, command.TrustName); + Assert.NotEmpty(response.ReferenceNumber!); + Assert.Equal(response.TrustReferenceNumber, command.TrustReferenceNumber); + } + } +} diff --git a/Dfe.Academies.Academisation.SubcutaneousTest/Utils/HttpResponseMessageExtensions.cs b/Dfe.Academies.Academisation.SubcutaneousTest/Utils/HttpResponseMessageExtensions.cs new file mode 100644 index 000000000..272a42a66 --- /dev/null +++ b/Dfe.Academies.Academisation.SubcutaneousTest/Utils/HttpResponseMessageExtensions.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; +using System.Text.Json; + +namespace Dfe.Academies.Academisation.SubcutaneousTest.Utils +{ + internal static class HttpResponseMessageExtensions + { + private static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + internal static async Task ConvertResponseToTypeAsync(this HttpResponseMessage httpResponseMessage) + { + var content = await httpResponseMessage.Content.ReadAsStringAsync(); + if (string.IsNullOrEmpty(content)) + { + return default!; + } + return JsonSerializer.Deserialize(content, Options)!; + } + } +} diff --git a/Dfe.Academies.Academisation.SubcutaneousTest/Utils/Request.cs b/Dfe.Academies.Academisation.SubcutaneousTest/Utils/Request.cs new file mode 100644 index 000000000..9566ae10e --- /dev/null +++ b/Dfe.Academies.Academisation.SubcutaneousTest/Utils/Request.cs @@ -0,0 +1,82 @@ +using System.Globalization; +using System.Text; +using System.Text.Json; +using System.Web; + +namespace Dfe.Academies.Academisation.SubcutaneousTest.Utils +{ + internal static class Request + { + internal static StringContent ConvertRequestObjectToContent(object request) => new(JsonSerializer.Serialize(request), Encoding.Default, "application/json"); + + internal static string ComplexTypeToQueryString(object obj) + { + var encode = obj.GetType().GetProperties() + .Where(p => p.GetValue(obj) != null) + .Select(p => $"{ToCamelCase(p.Name)}={HttpUtility.UrlEncode(ToString(p.GetValue(obj)!))}"); + + return string.Join("&", encode); + } + + internal static string NestedComplexTypeToQueryString(this object obj, string prefix = "") + { + var properties = obj.GetType().GetProperties() + .Where(p => p.GetValue(obj) != null); + + return string.Join('&', + properties.Select(prop => + { + var name = HttpUtility.UrlEncode(ToCamelCase(prop.Name)); + if (prop.GetValue(obj) is System.Collections.IEnumerable enumerable) + { + return string.Join('&', + enumerable.Cast() + .Where(mem => mem != null) + .Select((mem, i) => + { + var (isBaseObject, formattedValue) = FormatValue(mem); + if (isBaseObject) + { + return prefix + name + "=" + HttpUtility.UrlEncode(formattedValue); + } + + return mem.NestedComplexTypeToQueryString($"{name}{HttpUtility.UrlEncode("[")}{i}{HttpUtility.UrlEncode("]")}."); + })); + } + + return prefix + name + '=' + HttpUtility.UrlEncode(ToString(prop.GetValue(obj)!)); + })); + } + private static string ToCamelCase(string s) => char.ToLowerInvariant(s[0]) + s[1..]; + + private static string ToString(object obj) + { + return obj switch + { + DateTimeOffset date => date.ToString("o"), + DateTime date => date.ToString("o", CultureInfo.InvariantCulture), + DateOnly date => date.ToString("o", CultureInfo.InvariantCulture), + TimeOnly time => time.ToString("o", CultureInfo.InvariantCulture), + TimeSpan time => time.ToString("o", CultureInfo.InvariantCulture), + _ => obj?.ToString() ?? "", + }; + } + + private static (bool isBaseObject, string formattedValue) FormatValue(object obj) + { + if (obj.GetType().IsPrimitive + || obj.GetType().IsEnum + || obj is string + || obj is DateTimeOffset + || obj is DateTime + || obj is DateOnly + || obj is TimeOnly + || obj is TimeSpan) + { + return (true, ToString(obj)); + } + + return (false, ""); + } + } +} diff --git a/Dfe.Academies.Academisation.WebApi/Program.cs b/Dfe.Academies.Academisation.WebApi/Program.cs index ca92ca481..bfdfc43d3 100644 --- a/Dfe.Academies.Academisation.WebApi/Program.cs +++ b/Dfe.Academies.Academisation.WebApi/Program.cs @@ -41,7 +41,6 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; using NetEscapades.AspNetCore.SecurityHeaders; using Newtonsoft.Json; using Newtonsoft.Json.Converters; From f58c8d76c75f21d1c57f38a6488c8e90d806c59a Mon Sep 17 00:00:00 2001 From: "SHAKIR, Muhammad" Date: Fri, 2 Aug 2024 15:58:58 +0100 Subject: [PATCH 2/2] Added API integration framework --- .../ProjectGroup/ProjectGroupTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dfe.Academies.Academisation.SubcutaneousTest/ProjectGroup/ProjectGroupTests.cs b/Dfe.Academies.Academisation.SubcutaneousTest/ProjectGroup/ProjectGroupTests.cs index fe17c1957..67cbe49a7 100644 --- a/Dfe.Academies.Academisation.SubcutaneousTest/ProjectGroup/ProjectGroupTests.cs +++ b/Dfe.Academies.Academisation.SubcutaneousTest/ProjectGroup/ProjectGroupTests.cs @@ -21,6 +21,7 @@ public ProjectGroupTests() public async Task CreateProjectGroup_ShouldCreateSuccessfully() { // Arrange + var notExpectedId = 0; var command = new CreateProjectGroupCommand(Fixture.Create()[..15], Fixture.Create()[..7], Fixture.Create()[..10], []); // Action @@ -28,8 +29,9 @@ public async Task CreateProjectGroup_ShouldCreateSuccessfully() Assert.True(httpResponseMessage.IsSuccessStatusCode); var response = await httpResponseMessage.ConvertResponseToTypeAsync(); - Assert.Equal(response.TrustName, command.TrustName); + Assert.Null(response.TrustName); Assert.NotEmpty(response.ReferenceNumber!); + Assert.NotEqual(response.Id, notExpectedId); Assert.Equal(response.TrustReferenceNumber, command.TrustReferenceNumber); } }