From 281603e2e4f90c1ac667ff06c70950efc604d6a7 Mon Sep 17 00:00:00 2001 From: cairnsj <51908793+cairnsj@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:18:39 +0100 Subject: [PATCH 01/10] Add GraphQL packages --- Childrens-Social-Care-CPD/Childrens-Social-Care-CPD.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Childrens-Social-Care-CPD/Childrens-Social-Care-CPD.csproj b/Childrens-Social-Care-CPD/Childrens-Social-Care-CPD.csproj index 6527a82c..723ad4a2 100644 --- a/Childrens-Social-Care-CPD/Childrens-Social-Care-CPD.csproj +++ b/Childrens-Social-Care-CPD/Childrens-Social-Care-CPD.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -14,6 +14,8 @@ + + From 783c3d3eb613b412d0a3591d1eecda85355409ec Mon Sep 17 00:00:00 2001 From: cairnsj <51908793+cairnsj@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:22:15 +0100 Subject: [PATCH 02/10] Setup GraphQL client in the application DI container --- .../Configuration/ApplicationConfiguration.cs | 1 + .../Configuration/IApplicationConfiguration.cs | 1 + .../WebApplicationBuilderExtensions.cs | 11 +++++++++++ 3 files changed, 13 insertions(+) diff --git a/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs index 75a95734..1d7ccbf4 100644 --- a/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs @@ -9,6 +9,7 @@ public class ApplicationConfiguration : IApplicationConfiguration public string ClarityProjectId => ValueOrStringEmpty("CPD_CLARITY"); public string ContentfulDeliveryApiKey => ValueOrStringEmpty("CPD_DELIVERY_KEY"); public string ContentfulEnvironment => ValueOrStringEmpty("CPD_CONTENTFUL_ENVIRONMENT"); + public string ContentfulGraphqlConnectionString => $"https://graphql.contentful.com/content/v1/spaces/{ContentfulSpaceId}/environments/{ContentfulEnvironment}"; public string ContentfulPreviewHost => "preview.contentful.com"; public string ContentfulPreviewId => ValueOrStringEmpty("CPD_PREVIEW_KEY"); public string ContentfulSpaceId => ValueOrStringEmpty("CPD_SPACE_ID"); diff --git a/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs index 07535fc0..cf61e383 100644 --- a/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs @@ -8,6 +8,7 @@ public interface IApplicationConfiguration string ClarityProjectId { get; } string ContentfulDeliveryApiKey { get; } string ContentfulEnvironment { get; } + string ContentfulGraphqlConnectionString { get; } string ContentfulPreviewHost { get; } string ContentfulPreviewId { get; } string ContentfulSpaceId { get; } diff --git a/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs b/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs index 6975fb9d..b4f7ed22 100644 --- a/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs +++ b/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs @@ -3,10 +3,14 @@ using Childrens_Social_Care_CPD.Contentful.Renderers; using Contentful.AspNetCore; using Contentful.Core.Configuration; +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Http; +using GraphQL.Client.Serializer.SystemTextJson; using Microsoft.ApplicationInsights.AspNetCore.Extensions; using Microsoft.Extensions.Logging.ApplicationInsights; using Microsoft.Extensions.Logging.AzureAppServices; using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; namespace Childrens_Social_Care_CPD; @@ -24,6 +28,13 @@ public static void AddDependencies(this WebApplicationBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddScoped(services => { + var config = services.GetService(); + var client = new GraphQLHttpClient(config.ContentfulGraphqlConnectionString, new SystemTextJsonSerializer()); + client.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", config.ContentfulDeliveryApiKey); + return client; + }); + // Register all the IRender implementations in the assembly System.Reflection.Assembly.GetExecutingAssembly() .GetTypes() From 22df26623d3acaaa39492f8501977515b5ba8311 Mon Sep 17 00:00:00 2001 From: cairnsj <51908793+cairnsj@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:55:50 +0100 Subject: [PATCH 03/10] Use GraphQL on the ResourcesController for the search --- .github/workflows/sonarqube.yml | 2 +- .../ApplicationConfigurationTests.cs | 16 ++ .../Controllers/ResourcesControllerTests.cs | 77 ++++----- .../DataAccess/ResourcesRepositoryTests.cs | 155 ++++++++++++++++++ .../Controllers/ResourcesController.cs | 89 ++++------ .../DataAccess/ResourcesRepository.cs | 47 ++++++ .../GraphQL/Queries/SearchResourcesByTags.cs | 99 +++++++++++ .../Models/ResourcesListViewModel.cs | 4 +- .../Views/Resources/Search.cshtml | 5 +- .../Views/Resources/_SearchResult.cshtml | 7 +- .../WebApplicationBuilderExtensions.cs | 4 +- 11 files changed, 398 insertions(+), 107 deletions(-) create mode 100644 Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs create mode 100644 Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs create mode 100644 Childrens-Social-Care-CPD/GraphQL/Queries/SearchResourcesByTags.cs diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index e2f5444d..2d0f6db6 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -57,7 +57,7 @@ /o:"dfe-digital" \ /d:sonar.qualitygate.wait=true \ /d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml \ - /d:sonar.exclusions="**/*.css,**/*.scss,**/Models/*,**/Program.cs,**/WebApplicationBuilderExtensions.cs" \ + /d:sonar.exclusions="**/*.css,**/*.scss,**/Models/*,**/Program.cs,**/WebApplicationBuilderExtensions.cs,**/GraphQL/Queries/*" \ /d:sonar.test.exclusions="Childrens-Social-Care-CPD-Tests/**/*" \ /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ /d:sonar.host.url="https://sonarcloud.io" diff --git a/Childrens-Social-Care-CPD-Tests/Configuration/ApplicationConfigurationTests.cs b/Childrens-Social-Care-CPD-Tests/Configuration/ApplicationConfigurationTests.cs index 752d3f98..bf1995e6 100644 --- a/Childrens-Social-Care-CPD-Tests/Configuration/ApplicationConfigurationTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Configuration/ApplicationConfigurationTests.cs @@ -1,4 +1,5 @@ using Childrens_Social_Care_CPD.Configuration; +using Contentful.Core.Models.Management; using FluentAssertions; using NUnit.Framework; using System; @@ -95,6 +96,21 @@ public void Returns_ContentfulEnvironment_Value() actual.Should().Be(Value); } + [Test] + public void Returns_ContentfulGraphqlConnectionString_Value() + { + // arrange + Environment.SetEnvironmentVariable("CPD_SPACE_ID", Value); + Environment.SetEnvironmentVariable("CPD_CONTENTFUL_ENVIRONMENT", Value); + var sut = new ApplicationConfiguration(); + + // act + var actual = sut.ContentfulGraphqlConnectionString; + + // assert + actual.Should().Be($"https://graphql.contentful.com/content/v1/spaces/{Value}/environments/{Value}"); + } + [Test] public void Returns_ContentfulPreviewHost_Value() { diff --git a/Childrens-Social-Care-CPD-Tests/Controllers/ResourcesControllerTests.cs b/Childrens-Social-Care-CPD-Tests/Controllers/ResourcesControllerTests.cs index 60ad48bf..82ca31e9 100644 --- a/Childrens-Social-Care-CPD-Tests/Controllers/ResourcesControllerTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Controllers/ResourcesControllerTests.cs @@ -1,10 +1,7 @@ -using Castle.Core.Logging; -using Childrens_Social_Care_CPD.Contentful; -using Childrens_Social_Care_CPD.Contentful.Models; +using Childrens_Social_Care_CPD.Contentful.Models; using Childrens_Social_Care_CPD.Controllers; +using Childrens_Social_Care_CPD.DataAccess; using Childrens_Social_Care_CPD.Models; -using Contentful.Core.Models; -using Contentful.Core.Search; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -12,47 +9,43 @@ using Microsoft.Extensions.Logging; using NSubstitute; using NUnit.Framework; -using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; +using static Childrens_Social_Care_CPD.GraphQL.Queries.SearchResourcesByTags; namespace Childrens_Social_Care_CPD_Tests.Controllers; public class ResourcesControllerTests { + private IResourcesRepository _resourcesRepository; + private ResourcesController _resourcesController; private IRequestCookieCollection _cookies; private HttpContext _httpContext; private HttpRequest _httpRequest; - private ICpdContentfulClient _contentfulClient; private ILogger _logger; private CancellationTokenSource _cancellationTokenSource; - private void SetContent(Content content, ContentfulCollection resourceCollection) + private void SetPageContent(Content content) { - resourceCollection ??= new (); - - _contentfulClient - .GetEntries(Arg.Any>(), Arg.Any()) - .Returns(resourceCollection); - - var contentCollection = new ContentfulCollection(); - - contentCollection.Items = content == null - ? new List() - : contentCollection.Items = new List { content }; - - _contentfulClient - .GetEntries(Arg.Any>(), Arg.Any()) - .Returns(contentCollection); + _resourcesRepository.FetchRootPage(Arg.Any()).Returns(content); + } - _cancellationTokenSource = new CancellationTokenSource(); + public void SetSearchResults(ResponseType content) + { + _resourcesRepository + .FindByTags(Arg.Any>(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(content); } [SetUp] public void SetUp() { + _cancellationTokenSource = new CancellationTokenSource(); + _resourcesRepository = Substitute.For(); + _logger = Substitute.For>(); _cookies = Substitute.For(); _httpContext = Substitute.For(); @@ -63,9 +56,7 @@ public void SetUp() _httpContext.Request.Returns(_httpRequest); controllerContext.HttpContext = _httpContext; - _contentfulClient = Substitute.For(); - - _resourcesController = new ResourcesController(_logger, _contentfulClient) + _resourcesController = new ResourcesController(_logger, _resourcesRepository) { ControllerContext = controllerContext, TempData = Substitute.For() @@ -75,9 +66,6 @@ public void SetUp() [Test] public async Task Search_With_Empty_Query_Returns_View() { - // arrange - SetContent(null, null); - // act var actual = await _resourcesController.Search(query: null, _cancellationTokenSource.Token) as ViewResult; @@ -91,7 +79,7 @@ public async Task Search_Page_Resource_Is_Passed_To_View() { // arrange var content = new Content(); - SetContent(content, null); + SetPageContent(content); // act var actual = (await _resourcesController.Search(query: null, _cancellationTokenSource.Token) as ViewResult)?.Model as ResourcesListViewModel; @@ -103,9 +91,6 @@ public async Task Search_Page_Resource_Is_Passed_To_View() [Test] public async Task Search_Sets_The_ViewState_ContextModel() { - // arrange - SetContent(null, null); - // act await _resourcesController.Search(null, _cancellationTokenSource.Token); var actual = _resourcesController.ViewData["ContextModel"] as ContextModel; @@ -121,7 +106,6 @@ public async Task Search_Sets_The_ViewState_ContextModel() public async Task Search_Selected_Tags_Are_Passed_Into_View() { // arrange - SetContent(null, null); var query = new ResourcesQuery { Page = 1, @@ -139,14 +123,20 @@ public async Task Search_Selected_Tags_Are_Passed_Into_View() public async Task Search_Page_Set_To_Be_In_Bounds() { // arrange - SetContent(null, new ContentfulCollection { - Items = new List { - new Resource(), - new Resource(), - new Resource(), - }, - Total = 3 - }); + var results = new ResponseType() + { + ResourceCollection = new ResourceCollection() + { + Total = 3, + Items = new Collection() + { + new SearchResult(), + new SearchResult(), + new SearchResult(), + } + } + }; + SetSearchResults(results); var query = new ResourcesQuery { Page = 2, @@ -164,7 +154,6 @@ public async Task Search_Page_Set_To_Be_In_Bounds() public async Task Search_Invalid_Tags_Logs_Warning() { // arrange - SetContent(null, null); var tags = new int[] { -1 }; var query = new ResourcesQuery { diff --git a/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs b/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs new file mode 100644 index 00000000..fd51cf58 --- /dev/null +++ b/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs @@ -0,0 +1,155 @@ +using Childrens_Social_Care_CPD.Configuration; +using Childrens_Social_Care_CPD.Contentful; +using Childrens_Social_Care_CPD.Contentful.Models; +using Childrens_Social_Care_CPD.DataAccess; +using Childrens_Social_Care_CPD.GraphQL.Queries; +using Contentful.Core.Models; +using Contentful.Core.Search; +using FluentAssertions; +using GraphQL.Client.Abstractions; +using GraphQL; +using GraphQL.Client.Abstractions.Websocket; +using NSubstitute; +using NUnit.Framework; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using static Childrens_Social_Care_CPD.GraphQL.Queries.SearchResourcesByTags; +using System.Collections.ObjectModel; +using System; +using Newtonsoft.Json.Linq; +using NSubstitute.Core; + +namespace Childrens_Social_Care_CPD_Tests.DataAccess; + +public class ResourcesRepositoryTests +{ + private CancellationTokenSource _cancellationTokenSource; + private IApplicationConfiguration _applicationConfiguration; + private ICpdContentfulClient _contentfulClient; + private IGraphQLWebSocketClient _gqlClient; + + [SetUp] + public void Setup() + { + _cancellationTokenSource = new CancellationTokenSource(); + _applicationConfiguration = Substitute.For(); + _contentfulClient = Substitute.For(); + _gqlClient = Substitute.For(); + + // By default we want the preview flag set to false + _applicationConfiguration.ContentfulPreviewId.Returns(string.Empty); + } + + [Test] + public async Task FetchRootPage_Returns_Root_Page_Returned_When_Found() + { + // arrange + var content = new Content(); + var collection = new ContentfulCollection + { + Items = new List + { + content + } + }; + _contentfulClient.GetEntries(Arg.Any>(), Arg.Any()).Returns(collection); + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + var result = await sut.FetchRootPage(_cancellationTokenSource.Token); + + // assert + result.Should().Be(content); + } + + [Test] + public async Task FetchRootPage_Returns_Null_When_Root_Page_Not_Found() + { + // arrange + var collection = new ContentfulCollection + { + Items = new List() + }; + _contentfulClient.GetEntries(Arg.Any>(), Arg.Any()).Returns(collection); + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + var result = await sut.FetchRootPage(_cancellationTokenSource.Token); + + // assert + result.Should().BeNull(); + } + + private void SetSearchResults(ResponseType responseType) + { + var response = Substitute.For>(); + response.Data = responseType; + _gqlClient.SendQueryAsync(Arg.Any(), Arg.Any()).Returns(response); + } + + [Test] + public async Task FindByTags_Returns_Results() + { + // arrange + var results = new ResponseType + { + ResourceCollection = new ResourceCollection + { + Total = 1, + Items = new Collection + { + new SearchResult() + } + } + }; + SetSearchResults(results); + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + var result = await sut.FindByTags(Array.Empty(), 0, 1, _cancellationTokenSource.Token); + + // assert + result.Should().Be(results); + } + + [Test] + public async Task FindByTags_Limits_Results() + { + // arrange + var results = new ResponseType(); + var response = Substitute.For>(); + response.Data = results; + GraphQLRequest request = null; + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + await sut.FindByTags(Array.Empty(), 0, 1, _cancellationTokenSource.Token); + + // assert + dynamic variables = request.Variables; + (variables.limit as object).Should().Be(1); + } + + [Test] + public async Task FindByTags_Skips_Results() + { + // arrange + var results = new ResponseType(); + var response = Substitute.For>(); + response.Data = results; + GraphQLRequest request = null; + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + await sut.FindByTags(Array.Empty(), 5, 1, _cancellationTokenSource.Token); + + // assert + dynamic variables = request.Variables; + (variables.skip as object).Should().Be(5); + } +} diff --git a/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs b/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs index a243e073..895ffad7 100644 --- a/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs +++ b/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs @@ -1,8 +1,6 @@ -using Childrens_Social_Care_CPD.Contentful; -using Childrens_Social_Care_CPD.Contentful.Models; +using Childrens_Social_Care_CPD.DataAccess; +using Childrens_Social_Care_CPD.GraphQL.Queries; using Childrens_Social_Care_CPD.Models; -using Contentful.Core.Models; -using Contentful.Core.Search; using Microsoft.AspNetCore.Mvc; namespace Childrens_Social_Care_CPD.Controllers; @@ -17,8 +15,9 @@ public class ResourcesQuery public partial class ResourcesController : Controller { + private const int PAGE_SIZE = 8; private readonly ILogger _logger; - private readonly ICpdContentfulClient _cpdClient; + private readonly IResourcesRepository _resourcesRepository; private static readonly List _tagInfos = new() { new TagInfo("Type", "Case studies", "caseStudies"), new TagInfo("Type", "CPD", "cpd"), @@ -30,10 +29,10 @@ public partial class ResourcesController : Controller }; private static readonly IEnumerable _allTags = _tagInfos.Select(x => x.TagName); - public ResourcesController(ILogger logger, ICpdContentfulClient cpdClient) + public ResourcesController(ILogger logger, IResourcesRepository resourcesRepository) { _logger = logger; - _cpdClient = cpdClient; + _resourcesRepository = resourcesRepository; } private IEnumerable GetQueryTags(int[] tags) @@ -52,55 +51,24 @@ private IEnumerable GetQueryTags(int[] tags) return tags.Select(x => { return _tagInfos[x].TagName; }); } - private Task> FetchResourcesContentAsync(CancellationToken cancellationToken) + private static Tuple CalculatePageStats(SearchResourcesByTags.ResponseType searchResults, int page) { - var queryBuilder = QueryBuilder.New - .ContentTypeIs("content") - .Include(10) - .FieldEquals("fields.id", "resources"); + var totalResults = searchResults?.ResourceCollection?.Total ?? 0; + var totalPages = (int)Math.Ceiling((decimal)totalResults / PAGE_SIZE); - return _cpdClient.GetEntries(queryBuilder, cancellationToken); - } - - private Task> FetchResourceSearchResultsAsync(int[] tags, CancellationToken cancellationToken, int skip = 0, int limit = 5) - { - var queryBuilder = QueryBuilder.New - .ContentTypeIs("resource") - .Include(1) - .FieldIncludes("metadata.tags.sys.id", GetQueryTags(tags)) - .OrderBy("-sys.createdAt") - .Skip(skip) - .Limit(limit); - - return _cpdClient.GetEntries(queryBuilder, cancellationToken); - } - - private async Task>> GetContentAsync(int[] tags, CancellationToken cancellationToken, int skip = 0, int limit = 5) - { - var pageContentTask = FetchResourcesContentAsync(cancellationToken); - var searchContentTask = FetchResourceSearchResultsAsync(tags, cancellationToken, skip, limit); - - await Task.WhenAll(pageContentTask, searchContentTask); - return Tuple.Create(pageContentTask.Result?.FirstOrDefault(), searchContentTask.Result); - } - - private static Tuple CalculatePaging(ResourcesQuery query) - { - var pageSize = 8; - var page = Math.Max(query.Page, 1); - var skip = (page - 1) * pageSize; - - return Tuple.Create(page, skip, pageSize); + return Tuple.Create(totalResults, totalPages, Math.Min(page, totalPages)); } private static string GetPagingFormatString(int[] tags) { - var tagStrings = tags.Select(x => $"tags={x}"); - var qsTags = string.Join("&", tagStrings); + if (tags.Any()) + { + var tagStrings = tags.Select(x => $"tags={x}"); + var allTags = string.Join("&", tagStrings); + return $"/resources?page={{0}}&{allTags}"; + } - return string.IsNullOrEmpty(qsTags) - ? $"/resources?page={{0}}" - : $"/resources?page={{0}}&{qsTags}"; + return $"/resources?page={{0}}"; } [Route("resources", Name = "Resource")] @@ -110,16 +78,27 @@ public async Task Search([FromQuery] ResourcesQuery query, Cancel query ??= new ResourcesQuery(); query.Tags ??= Array.Empty(); - (var page, var skip, var pageSize) = CalculatePaging(query); - (var pageContent, var contentCollection) = await GetContentAsync(query.Tags, cancellationToken, skip, pageSize); - - var totalPages = (int)Math.Ceiling((decimal)contentCollection.Total / pageSize); - page = Math.Min(page, totalPages); + var page = Math.Max(query.Page, 1); + var skip = (page - 1) * PAGE_SIZE; + var pageContentTask = _resourcesRepository.FetchRootPage(cancellationToken); + var searchResults = await _resourcesRepository.FindByTags(GetQueryTags(query.Tags), skip, PAGE_SIZE, cancellationToken); + var pageContent = await pageContentTask; + (var totalResults, var totalPages, var currentPage) = CalculatePageStats(searchResults, page); var contextModel = new ContextModel(string.Empty, "Resources", "Resources", "Resources", true, preferencesSet); ViewData["ContextModel"] = contextModel; - var viewModel = new ResourcesListViewModel(pageContent, contentCollection, _tagInfos, query.Tags, page, totalPages, contentCollection.Total, GetPagingFormatString(query.Tags)); + var viewModel = new ResourcesListViewModel( + pageContent, + searchResults?.ResourceCollection, + _tagInfos, + query.Tags, + currentPage, + totalPages, + totalResults, + GetPagingFormatString(query.Tags) + ); + return View(viewModel); } } \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs b/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs new file mode 100644 index 00000000..0ec4067c --- /dev/null +++ b/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs @@ -0,0 +1,47 @@ +using Childrens_Social_Care_CPD.Configuration; +using Childrens_Social_Care_CPD.Contentful; +using Childrens_Social_Care_CPD.Contentful.Models; +using Childrens_Social_Care_CPD.GraphQL.Queries; +using Contentful.Core.Search; +using GraphQL.Client.Abstractions.Websocket; + +namespace Childrens_Social_Care_CPD.DataAccess; + +public interface IResourcesRepository +{ + Task FetchRootPage(CancellationToken cancellationToken); + Task FindByTags(IEnumerable tags, int skip, int take, CancellationToken cancellationToken); +} + +public class ResourcesRepository : IResourcesRepository +{ + private readonly ICpdContentfulClient _cpdClient; + private readonly IGraphQLWebSocketClient _gqlClient; + private readonly bool _isPreview; + + public ResourcesRepository(IApplicationConfiguration applicationConfiguration, ICpdContentfulClient cpdClient, IGraphQLWebSocketClient gqlClient) + { + _cpdClient = cpdClient; + _gqlClient = gqlClient; + _isPreview = !string.IsNullOrEmpty(applicationConfiguration.ContentfulPreviewId); + } + + public Task FetchRootPage(CancellationToken cancellationToken = default) + { + var queryBuilder = QueryBuilder.New + .ContentTypeIs("content") + .Include(10) + .FieldEquals("fields.id", "resources"); + + return _cpdClient + .GetEntries(queryBuilder, cancellationToken) + .ContinueWith(x => x.Result.FirstOrDefault()); + } + + public Task FindByTags(IEnumerable tags, int skip, int take, CancellationToken cancellationToken = default) + { + return _gqlClient + .SendQueryAsync(SearchResourcesByTags.Query(tags, take, skip, _isPreview), cancellationToken) + .ContinueWith(x => x.Result.Data); + } +} diff --git a/Childrens-Social-Care-CPD/GraphQL/Queries/SearchResourcesByTags.cs b/Childrens-Social-Care-CPD/GraphQL/Queries/SearchResourcesByTags.cs new file mode 100644 index 00000000..ea8d8e45 --- /dev/null +++ b/Childrens-Social-Care-CPD/GraphQL/Queries/SearchResourcesByTags.cs @@ -0,0 +1,99 @@ +using GraphQL; +using System.Text.Json.Serialization; + +namespace Childrens_Social_Care_CPD.GraphQL.Queries; + +public class SearchResourcesByTags +{ + public static GraphQLRequest Query(IEnumerable tags, int limit, int skip, bool preview = false) + { + return new GraphQLRequest + { + Query = @" + query SearchResourcesByTags($searchTags: [String!], $limit: Int, $skip: Int, $preview: Boolean) { + resourceCollection(where: { + contentfulMetadata: { + tags_exists: true + tags: { + id_contains_some: $searchTags + } + } + }, limit: $limit, skip: $skip, preview: $preview) { + total + items { + title + from + searchSummary + type + sys { + publishedAt + firstPublishedAt + } + linkedFrom { + contentCollection { + items { + id + } + } + } + } + } + }", + OperationName = "SearchResourcesByTags", + Variables = new + { + searchTags = tags, + limit, + skip, + preview, + } + }; + } + + public class ResponseType + { + [JsonPropertyName("resourceCollection")] + public ResourceCollection ResourceCollection { get; set; } + } + + public class ResourceCollection + { + [JsonPropertyName("items")] + public ICollection Items { get; set; } + public int Total { get; set; } + } + + public class SearchResult + { + public string Title { get; set; } + public string From { get; set; } + public string SearchSummary { get; set; } + public ICollection Type { get; set; } + public PublishedInfo Sys { get; set; } + public LinkedFromContentCollection LinkedFrom { get; set; } + } + + public class PublishedInfo + { + public DateTime? PublishedAt { get; set; } + public DateTime? FirstPublishedAt { get; set; } + } + + public class LinkedFromContentCollection + { + public LinkedFrom ContentCollection { get; set; } + } + + public class LinkedFrom + { + public ICollection Items { get; set; } + } + + public class LinkedItem + { + public string Id { get; set; } + } +} + + + diff --git a/Childrens-Social-Care-CPD/Models/ResourcesListViewModel.cs b/Childrens-Social-Care-CPD/Models/ResourcesListViewModel.cs index 9f453d8f..03957f7a 100644 --- a/Childrens-Social-Care-CPD/Models/ResourcesListViewModel.cs +++ b/Childrens-Social-Care-CPD/Models/ResourcesListViewModel.cs @@ -1,12 +1,12 @@ using Childrens_Social_Care_CPD.Contentful.Models; using Childrens_Social_Care_CPD.Controllers; -using Contentful.Core.Models; +using Childrens_Social_Care_CPD.GraphQL.Queries; namespace Childrens_Social_Care_CPD.Models; public record ResourcesListViewModel( Content Content, - ContentfulCollection SearchResults, + SearchResourcesByTags.ResourceCollection Results, IEnumerable TagInfos, int[] SelectedTags, int CurrentPage = 0, diff --git a/Childrens-Social-Care-CPD/Views/Resources/Search.cshtml b/Childrens-Social-Care-CPD/Views/Resources/Search.cshtml index 467ef725..ff014f9c 100644 --- a/Childrens-Social-Care-CPD/Views/Resources/Search.cshtml +++ b/Childrens-Social-Care-CPD/Views/Resources/Search.cshtml @@ -123,8 +123,11 @@ } else { - foreach (var contentItem in Model.SearchResults) + foreach (var contentItem in Model.Results.Items) { + var linkedFrom = contentItem.LinkedFrom.ContentCollection.Items.FirstOrDefault(); + if (linkedFrom == null) continue; + } diff --git a/Childrens-Social-Care-CPD/Views/Resources/_SearchResult.cshtml b/Childrens-Social-Care-CPD/Views/Resources/_SearchResult.cshtml index d5b8736d..447b97af 100644 --- a/Childrens-Social-Care-CPD/Views/Resources/_SearchResult.cshtml +++ b/Childrens-Social-Care-CPD/Views/Resources/_SearchResult.cshtml @@ -1,13 +1,14 @@ @using Childrens_Social_Care_CPD.Contentful.Models; +@using Childrens_Social_Care_CPD.GraphQL.Queries; -@model Resource +@model SearchResourcesByTags.SearchResult
-

@Model.Title

+

@Model.Title

@Model.SearchSummary

  • From: @Model.From
  • -
  • Update:
  • +
  • Update:
  • Resource type: @string.Join(", ", Model.Type.ToArray())
\ No newline at end of file diff --git a/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs b/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs index b4f7ed22..03b079a8 100644 --- a/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs +++ b/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs @@ -1,6 +1,7 @@ using Childrens_Social_Care_CPD.Configuration; using Childrens_Social_Care_CPD.Contentful; using Childrens_Social_Care_CPD.Contentful.Renderers; +using Childrens_Social_Care_CPD.DataAccess; using Contentful.AspNetCore; using Contentful.Core.Configuration; using GraphQL.Client.Abstractions.Websocket; @@ -22,11 +23,12 @@ public static void AddDependencies(this WebApplicationBuilder builder) ArgumentNullException.ThrowIfNull(builder); builder.Services.AddSingleton(); - builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddScoped(services => { var config = services.GetService(); From fc76ff252bff55f523056537a3146574a8161fe5 Mon Sep 17 00:00:00 2001 From: cairnsj <51908793+cairnsj@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:36:57 +0100 Subject: [PATCH 04/10] Resource search now supports preview mode in Contentful --- .../ConfigurationInformationTests.cs | 1 + .../DataAccess/ResourcesRepositoryTests.cs | 40 +++++++++++++++++++ .../Configuration/ApplicationConfiguration.cs | 11 ++++- .../Configuration/ConfigurationInformation.cs | 4 +- .../IApplicationConfiguration.cs | 9 ++--- .../WebApplicationBuilderExtensions.cs | 4 +- 6 files changed, 60 insertions(+), 9 deletions(-) diff --git a/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs b/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs index 5d396496..a4f9ec44 100644 --- a/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs @@ -33,6 +33,7 @@ public void Required_Values_Are_Detected() } [TestCase("")] + [TestCase(" ")] [TestCase(null)] public void Missing_Values_Are_Detected(string value) { diff --git a/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs b/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs index fd51cf58..c4b5287a 100644 --- a/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs +++ b/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs @@ -152,4 +152,44 @@ public async Task FindByTags_Skips_Results() dynamic variables = request.Variables; (variables.skip as object).Should().Be(5); } + + [Test] + public async Task FindByTags_Preview_Flag_Is_False_By_Default() + { + // arrange + var response = Substitute.For>(); + response.Data = new ResponseType(); + GraphQLRequest request = null; + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + await sut.FindByTags(Array.Empty(), 5, 1, _cancellationTokenSource.Token); + + // assert + dynamic variables = request.Variables; + (variables.preview as object).Should().Be(false); + } + + [Test] + public async Task FindByTags_Sets_Preview_Flag() + { + // arrange + _applicationConfiguration.ContentfulPreviewId.Returns("foo"); + + var response = Substitute.For>(); + response.Data = new ResponseType(); + GraphQLRequest request = null; + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + await sut.FindByTags(Array.Empty(), 5, 1, _cancellationTokenSource.Token); + + // assert + dynamic variables = request.Variables; + (variables.preview as object).Should().Be(true); + } } diff --git a/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs index 1d7ccbf4..a764100d 100644 --- a/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs @@ -2,7 +2,16 @@ public class ApplicationConfiguration : IApplicationConfiguration { - private static string ValueOrStringEmpty(string key) => Environment.GetEnvironmentVariable(key) ?? string.Empty; + private static string ValueOrStringEmpty(string key) + { + var value = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + return value; + } public string AppInsightsConnectionString => ValueOrStringEmpty("CPD_INSTRUMENTATION_CONNECTIONSTRING"); public string AppVersion => ValueOrStringEmpty("VCS-TAG"); public string AzureEnvironment => ValueOrStringEmpty("CPD_AZURE_ENVIRONMENT"); diff --git a/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs b/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs index 73b443c4..305fc61b 100644 --- a/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs +++ b/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using System.Data; using System.Reflection; namespace Childrens_Social_Care_CPD.Configuration; @@ -66,7 +65,8 @@ private static bool HasValue(PropertyInfo propertyInfo, object value) if (propertyInfo.PropertyType == typeof(string)) { - return !string.IsNullOrEmpty(value as string); + var v = value as string; + return !(string.IsNullOrEmpty(v) || string.IsNullOrWhiteSpace(v)); } return true; diff --git a/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs index 9fff1982..119a3382 100644 --- a/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs @@ -4,9 +4,7 @@ namespace Childrens_Social_Care_CPD.Configuration; public interface IApplicationConfiguration { - [RequiredForEnvironment(ApplicationEnvironment.Test, Hidden = false)] - [RequiredForEnvironment(ApplicationEnvironment.PreProduction, Hidden = false)] - [RequiredForEnvironment(ApplicationEnvironment.Production, Hidden = false)] + [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] string AppInsightsConnectionString { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false, Obfuscate = false)] @@ -21,9 +19,10 @@ public interface IApplicationConfiguration [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] string ContentfulDeliveryApiKey { get; } - [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] + [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false, Obfuscate = false)] string ContentfulEnvironment { get; } + [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] string ContentfulGraphqlConnectionString { get; } [RequiredForEnvironment(ApplicationEnvironment.PreProduction, Hidden = false)] @@ -36,7 +35,7 @@ public interface IApplicationConfiguration string ContentfulSpaceId { get; } [DefaultValue(false)] - [RequiredForEnvironment(ApplicationEnvironment.Integration, Hidden = false)] + [RequiredForEnvironment(ApplicationEnvironment.Integration, Hidden = false, Obfuscate = false)] bool DisableSecureCookies { get; } [DefaultValue(0)] diff --git a/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs b/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs index 03b079a8..0d566b6f 100644 --- a/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs +++ b/Childrens-Social-Care-CPD/WebApplicationBuilderExtensions.cs @@ -33,7 +33,9 @@ public static void AddDependencies(this WebApplicationBuilder builder) builder.Services.AddScoped(services => { var config = services.GetService(); var client = new GraphQLHttpClient(config.ContentfulGraphqlConnectionString, new SystemTextJsonSerializer()); - client.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", config.ContentfulDeliveryApiKey); + var key = string.IsNullOrEmpty(config.ContentfulPreviewId) ? config.ContentfulDeliveryApiKey : config.ContentfulPreviewId; + + client.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key); return client; }); From 736ad9b707d02fb97009516318d34ec6638938f8 Mon Sep 17 00:00:00 2001 From: cairnsj <51908793+cairnsj@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:12:23 +0100 Subject: [PATCH 05/10] Update ConfigurationHealthCheck.cs --- Childrens-Social-Care-CPD/ConfigurationHealthCheck.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Childrens-Social-Care-CPD/ConfigurationHealthCheck.cs b/Childrens-Social-Care-CPD/ConfigurationHealthCheck.cs index cb64039f..231493b1 100644 --- a/Childrens-Social-Care-CPD/ConfigurationHealthCheck.cs +++ b/Childrens-Social-Care-CPD/ConfigurationHealthCheck.cs @@ -1,6 +1,5 @@ using Childrens_Social_Care_CPD.Configuration; using Microsoft.Extensions.Diagnostics.HealthChecks; -using System.Xml.Linq; namespace Childrens_Social_Care_CPD; From d172d5734fbbca66a1c327c056ec2287fbfcec22 Mon Sep 17 00:00:00 2001 From: cairnsj <51908793+cairnsj@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:33:57 +0100 Subject: [PATCH 06/10] Move the ContentfulConfiguration to the Configuration folder --- .../{ => Configuration}/ContentfulConfiguration.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) rename Childrens-Social-Care-CPD/{ => Configuration}/ContentfulConfiguration.cs (80%) diff --git a/Childrens-Social-Care-CPD/ContentfulConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs similarity index 80% rename from Childrens-Social-Care-CPD/ContentfulConfiguration.cs rename to Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs index 45c49839..c3ab202e 100644 --- a/Childrens-Social-Care-CPD/ContentfulConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using Childrens_Social_Care_CPD.Configuration; -namespace Childrens_Social_Care_CPD; +namespace Childrens_Social_Care_CPD.Configuration; [ExcludeFromCodeCoverage] public static class ContentfulConfiguration @@ -16,9 +15,9 @@ public static ConfigurationManager GetContentfulConfiguration(ConfigurationManag configuration["ContentfulOptions:DeliveryApiKey"] = applicationConfiguration.ContentfulDeliveryApiKey; var azureEnvironment = applicationConfiguration.AzureEnvironment; - if ((contentfulEnvironment.ToLower() != azureEnvironment.ToLower()) - && !String.IsNullOrEmpty(azureEnvironment) - && azureEnvironment!= LOADTESTAPPENVIRONMENT) + if (contentfulEnvironment.ToLower() != azureEnvironment.ToLower() + && !string.IsNullOrEmpty(azureEnvironment) + && azureEnvironment != LOADTESTAPPENVIRONMENT) { configuration["ContentfulOptions:host"] = applicationConfiguration.ContentfulPreviewHost; configuration["ContentfulOptions:UsePreviewApi"] = "true"; @@ -26,4 +25,4 @@ public static ConfigurationManager GetContentfulConfiguration(ConfigurationManag } return configuration; } -} +} \ No newline at end of file From dfabaf795208b3b37803e75febc9a233d98474de Mon Sep 17 00:00:00 2001 From: cairnsj <51908793+cairnsj@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:40:15 +0100 Subject: [PATCH 07/10] Update ResourcesRepository.cs --- Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs b/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs index 0ec4067c..20da2ffe 100644 --- a/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs +++ b/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs @@ -9,8 +9,8 @@ namespace Childrens_Social_Care_CPD.DataAccess; public interface IResourcesRepository { - Task FetchRootPage(CancellationToken cancellationToken); - Task FindByTags(IEnumerable tags, int skip, int take, CancellationToken cancellationToken); + Task FetchRootPage(CancellationToken cancellationToken = default); + Task FindByTags(IEnumerable tags, int skip, int take, CancellationToken cancellationToken = default); } public class ResourcesRepository : IResourcesRepository From b2027eebeb3bb439185a7f064ce286bcf1dfe3fc Mon Sep 17 00:00:00 2001 From: cairnsj <51908793+cairnsj@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:41:11 +0100 Subject: [PATCH 08/10] Make the IsPreview for Contentful part of the ContentfulConfiguration. --- .../ApplicationConfigurationTests.cs | 52 ++++----- .../ConfigurationInformationTests.cs | 20 ++-- .../ConfigurationSettingTests.cs | 103 ++++++++++++++++++ .../FeaturesConfigBackgroundServiceTests.cs | 6 +- .../ConfigurationHealthCheckTests.cs | 13 +-- .../Controllers/ApplicationControllerTests.cs | 8 +- .../CookieHelperTests.cs | 2 +- .../DataAccess/ResourcesRepositoryTests.cs | 9 +- .../MockApplicationConfiguration.cs | 54 +++++++++ .../Configuration/ApplicationConfiguration.cs | 54 +++++---- .../Configuration/ConfigurationInformation.cs | 12 +- .../Configuration/ConfigurationSetting.cs | 64 +++++++++++ .../Configuration/ContentfulConfiguration.cs | 25 +++-- .../FeaturesConfigBackgroundService.cs | 4 +- .../IApplicationConfiguration.cs | 30 ++--- .../ConfigurationHealthCheck.cs | 4 +- .../Controllers/ApplicationController.cs | 10 +- Childrens-Social-Care-CPD/CookieHelper.cs | 2 +- .../DataAccess/ResourcesRepository.cs | 2 +- .../Views/Application/Configuration.cshtml | 6 +- .../Shared/_GoogleAnalyticsPartial.cshtml | 4 +- .../WebApplicationBuilderExtensions.cs | 24 ++-- 22 files changed, 373 insertions(+), 135 deletions(-) create mode 100644 Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationSettingTests.cs create mode 100644 Childrens-Social-Care-CPD-Tests/MockApplicationConfiguration.cs create mode 100644 Childrens-Social-Care-CPD/Configuration/ConfigurationSetting.cs diff --git a/Childrens-Social-Care-CPD-Tests/Configuration/ApplicationConfigurationTests.cs b/Childrens-Social-Care-CPD-Tests/Configuration/ApplicationConfigurationTests.cs index bf1995e6..74d13596 100644 --- a/Childrens-Social-Care-CPD-Tests/Configuration/ApplicationConfigurationTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Configuration/ApplicationConfigurationTests.cs @@ -47,7 +47,7 @@ public void Returns_AzureEnvironment_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.AzureEnvironment; + var actual = sut.AzureEnvironment.Value; // assert actual.Should().Be(Value); @@ -61,7 +61,7 @@ public void Returns_ClarityProjectId_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ClarityProjectId; + var actual = sut.ClarityProjectId.Value; // assert actual.Should().Be(Value); @@ -76,7 +76,7 @@ public void Returns_ContentfulDeliveryApiKey_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulDeliveryApiKey; + var actual = sut.ContentfulDeliveryApiKey.Value; // assert actual.Should().Be(Value); @@ -90,7 +90,7 @@ public void Returns_ContentfulEnvironment_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulEnvironment; + var actual = sut.ContentfulEnvironment.Value; // assert actual.Should().Be(Value); @@ -105,7 +105,7 @@ public void Returns_ContentfulGraphqlConnectionString_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulGraphqlConnectionString; + var actual = sut.ContentfulGraphqlConnectionString.Value; // assert actual.Should().Be($"https://graphql.contentful.com/content/v1/spaces/{Value}/environments/{Value}"); @@ -119,7 +119,7 @@ public void Returns_ContentfulPreviewHost_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulPreviewHost; + var actual = sut.ContentfulPreviewHost.Value; // assert actual.Should().Be(value); @@ -133,7 +133,7 @@ public void Returns_ContentfulPreviewId_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulPreviewId; + var actual = sut.ContentfulPreviewId.Value; // assert actual.Should().Be(Value); @@ -147,7 +147,7 @@ public void Returns_ContentfulSpaceId_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulSpaceId; + var actual = sut.ContentfulSpaceId.Value; // assert actual.Should().Be(Value); @@ -161,7 +161,7 @@ public void Returns_GoogleTagManagerKey_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.GoogleTagManagerKey; + var actual = sut.GoogleTagManagerKey.Value; // assert actual.Should().Be(Value); @@ -175,7 +175,7 @@ public void Returns_AppInsightsConnectionString_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.AppInsightsConnectionString; + var actual = sut.AppInsightsConnectionString.Value; // assert actual.Should().Be(Value); @@ -189,7 +189,7 @@ public void Returns_GitHash_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.GitHash; + var actual = sut.GitHash.Value; // assert actual.Should().Be(Value); @@ -203,7 +203,7 @@ public void Returns_DisableSecureCookies_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.DisableSecureCookies; + var actual = sut.DisableSecureCookies.Value; // assert actual.Should().Be(false); @@ -217,7 +217,7 @@ public void Returns_AppVersionEnvironment_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.AppVersion; + var actual = sut.AppVersion.Value; // assert actual.Should().Be(Value); @@ -231,7 +231,7 @@ public void Returns_FeaturePollInterval_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.FeaturePollingInterval; + var actual = sut.FeaturePollingInterval.Value; // assert actual.Should().Be(10000); @@ -244,7 +244,7 @@ public void Returns_FeaturePollInterval_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.FeaturePollingInterval; + var actual = sut.FeaturePollingInterval.Value; // assert actual.Should().Be(0); @@ -257,7 +257,7 @@ public void Returns_AzureEnvironment_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.AzureEnvironment; + var actual = sut.AzureEnvironment.Value; // assert actual.Should().Be(string.Empty); @@ -270,7 +270,7 @@ public void Returns_ClarityProjectId_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ClarityProjectId; + var actual = sut.ClarityProjectId.Value; // assert actual.Should().Be(string.Empty); @@ -283,7 +283,7 @@ public void Returns_ContentfulDeliveryApiKey_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulDeliveryApiKey; + var actual = sut.ContentfulDeliveryApiKey.Value; // assert actual.Should().Be(string.Empty); @@ -296,7 +296,7 @@ public void Returns_ContentfulEnvironment_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulEnvironment; + var actual = sut.ContentfulEnvironment.Value; // assert actual.Should().Be(string.Empty); @@ -309,7 +309,7 @@ public void Returns_ContentfulPreviewId_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulPreviewId; + var actual = sut.ContentfulPreviewId.Value; // assert actual.Should().Be(string.Empty); @@ -322,7 +322,7 @@ public void Returns_ContentfulSpaceId_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulSpaceId; + var actual = sut.ContentfulSpaceId.Value; // assert actual.Should().Be(string.Empty); @@ -335,7 +335,7 @@ public void Returns_GoogleTagManagerKey_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.GoogleTagManagerKey; + var actual = sut.GoogleTagManagerKey.Value; // assert actual.Should().Be(string.Empty); @@ -348,7 +348,7 @@ public void Returns_AppInsightsConnectionString_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.AppInsightsConnectionString; + var actual = sut.AppInsightsConnectionString.Value; // assert actual.Should().Be(string.Empty); @@ -361,7 +361,7 @@ public void Returns_GitHash_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.GitHash; + var actual = sut.GitHash.Value; // assert actual.Should().Be(string.Empty); @@ -374,7 +374,7 @@ public void Returns_DisableSecureCookies_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.DisableSecureCookies; + var actual = sut.DisableSecureCookies.Value; // assert actual.Should().Be(false); @@ -387,7 +387,7 @@ public void Returns_AppVersionEnvironment_Default_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.AppVersion; + var actual = sut.AppVersion.Value; // assert actual.Should().Be(string.Empty); diff --git a/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs b/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs index a4f9ec44..8ac375ff 100644 --- a/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs @@ -12,24 +12,24 @@ public partial class ConfigurationInformationTests [SetUp] public void Setup() - { + { _applicationConfiguration = Substitute.For(); - _applicationConfiguration.AzureEnvironment.Returns("dev"); + _applicationConfiguration.AzureEnvironment.Value.Returns(ApplicationEnvironment.Development); } [Test] public void Required_Values_Are_Detected() { // arrange - _applicationConfiguration.AppVersion.Returns("foo"); - + _applicationConfiguration.AppVersion.Returns(new StringConfigSetting(() => "foo")); + // act var sut = new ConfigurationInformation(_applicationConfiguration); var actual = sut.ConfigurationInfo.Single(x => x.Name == "AppVersion"); // assert actual.Required.Should().BeTrue(); - actual.HasValue.Should().BeTrue(); + actual.IsSet.Should().BeTrue(); } [TestCase("")] @@ -38,7 +38,7 @@ public void Required_Values_Are_Detected() public void Missing_Values_Are_Detected(string value) { // arrange - _applicationConfiguration.AppVersion.Returns(value); + _applicationConfiguration.AppVersion.Returns(new StringConfigSetting(() => value)); // act var sut = new ConfigurationInformation(_applicationConfiguration); @@ -46,14 +46,14 @@ public void Missing_Values_Are_Detected(string value) // assert actual.Required.Should().BeTrue(); - actual.HasValue.Should().BeFalse(); + actual.IsSet.Should().BeFalse(); } [Test] public void Extraneous_Values_Are_Detected() { // arrange - _applicationConfiguration.ClarityProjectId.Returns("foo"); + _applicationConfiguration.ClarityProjectId.Returns(new StringConfigSetting(() => "foo")); // act var sut = new ConfigurationInformation(_applicationConfiguration); @@ -80,8 +80,8 @@ public void Sensitive_Values_Are_Obfuscated() { // arrange var value = "sensitive value"; - _applicationConfiguration.AzureEnvironment.Returns(ApplicationEnvironment.Production); - _applicationConfiguration.AppInsightsConnectionString.Returns(value); + _applicationConfiguration.AzureEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.Production)); + _applicationConfiguration.AppInsightsConnectionString.Returns(new StringConfigSetting(() => value)); // act var sut = new ConfigurationInformation(_applicationConfiguration); diff --git a/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationSettingTests.cs b/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationSettingTests.cs new file mode 100644 index 00000000..c0ea4605 --- /dev/null +++ b/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationSettingTests.cs @@ -0,0 +1,103 @@ +using Childrens_Social_Care_CPD.Configuration; +using FluentAssertions; +using NUnit.Framework; + +namespace Childrens_Social_Care_CPD_Tests.Configuration; + +public class ConfigurationSettingTests +{ + [Test] + public void Default_Value_Is_Set() + { + // arrange + var defaultValue = new object(); + + // act + var sut = new ConfigurationSetting(() => " ", x => new object(), defaultValue); + + // assert + sut.Value.Should().Be(defaultValue); + } + + [Test] + public void Value_Calls_Getter() + { + // arrange + var input = "x"; + var value = new object(); + var passedValue = string.Empty; + + // act + var sut = new ConfigurationSetting(() => input, x => { + passedValue = input; + return value; + }); + + // accessing Value causes the parser to be called + sut.Value.Should().NotBeNull(); + + // assert + passedValue.Should().BeEquivalentTo(input); + } + + [Test] + public void Value_Calls_Parser() + { + // arrange + var value = new object(); + + // act + var sut = new ConfigurationSetting(() => "x", x => value); + + // assert + sut.Value.Should().Be(value); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void IsSet_Returns_False_For_Invalid_Input(string value) + { + // act + var sut = new ConfigurationSetting(() => value, x => new object()); + + // assert + sut.IsSet.Should().BeFalse(); + } + + [Test] + public void IsSet_Returns_True_For_Valid_Input() + { + // act + var sut = new ConfigurationSetting(() => "x", x => new object()); + + // assert + sut.IsSet.Should().BeTrue(); + } + + [Test] + public void ConfigurationSetting_ToString_Returns_Value_Representation() + { + // arrange + var value = new object(); + + // act + var sut = new ConfigurationSetting(() => "x", x => "foo"); + + // assert + sut.ToString().Should().Be("foo"); + } + + [Test] + public void ConfigurationSetting_ToString_Should_Not_Return_Null() + { + // arrange + var value = new object(); + + // act + var sut = new ConfigurationSetting(() => "x", x => null); + + // assert + sut.ToString().Should().Be(string.Empty); + } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD-Tests/Configuration/FeaturesConfigBackgroundServiceTests.cs b/Childrens-Social-Care-CPD-Tests/Configuration/FeaturesConfigBackgroundServiceTests.cs index 5ef0409a..29ac884e 100644 --- a/Childrens-Social-Care-CPD-Tests/Configuration/FeaturesConfigBackgroundServiceTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Configuration/FeaturesConfigBackgroundServiceTests.cs @@ -26,7 +26,7 @@ public void Setup() public async Task Calls_Updater_At_Specified_Interval(int interval) { // arrange - _applicationConfiguration.FeaturePollingInterval.Returns(interval); + _applicationConfiguration.FeaturePollingInterval.Value.Returns(interval); var featuresConfigBackgroundService = new FeaturesConfigBackgroundService( _logger, _applicationConfiguration, @@ -37,7 +37,7 @@ public async Task Calls_Updater_At_Specified_Interval(int interval) using (var cancellationTokenSource = new CancellationTokenSource()) { var task = featuresConfigBackgroundService.StartAsync(cancellationTokenSource.Token); - await Task.Delay((int)(interval * 1100)); + await Task.Delay(interval * 1100); cancellationTokenSource.Cancel(); task.Wait(); } @@ -50,7 +50,7 @@ public async Task Calls_Updater_At_Specified_Interval(int interval) public async Task Returns_If_Interval_Is_Zero() { // arrange - _applicationConfiguration.FeaturePollingInterval.Returns(0); + _applicationConfiguration.FeaturePollingInterval.Value.Returns(0); var featuresConfigBackgroundService = new FeaturesConfigBackgroundService( _logger, _applicationConfiguration, diff --git a/Childrens-Social-Care-CPD-Tests/ConfigurationHealthCheckTests.cs b/Childrens-Social-Care-CPD-Tests/ConfigurationHealthCheckTests.cs index 6bcd3dda..9503c7f8 100644 --- a/Childrens-Social-Care-CPD-Tests/ConfigurationHealthCheckTests.cs +++ b/Childrens-Social-Care-CPD-Tests/ConfigurationHealthCheckTests.cs @@ -1,10 +1,8 @@ using Childrens_Social_Care_CPD; -using Childrens_Social_Care_CPD.Configuration; using FluentAssertions; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; using NSubstitute; -using NSubstitute.Extensions; using NUnit.Framework; using System.Threading.Tasks; @@ -13,20 +11,21 @@ namespace Childrens_Social_Care_CPD_Tests; public class ConfigurationHealthCheckTests { private ILogger _logger; - private IApplicationConfiguration _applicationConfiguration; + private MockApplicationConfiguration _applicationConfiguration; [SetUp] public void Setup() { _logger = Substitute.For>(); - _applicationConfiguration = Substitute.For(); + _applicationConfiguration = new MockApplicationConfiguration(); } [Test] public async Task Passes_When_All_Values_Set_And_Cookies_Are_Secured() { // arrange - _applicationConfiguration.ReturnsForAll("foo"); + _applicationConfiguration.SetAllValid(); + _applicationConfiguration._featurePollingInterval = "0"; var sut = new ConfigurationHealthCheck(_logger, _applicationConfiguration); // act @@ -40,8 +39,8 @@ public async Task Passes_When_All_Values_Set_And_Cookies_Are_Secured() public async Task Fails_When_Disable_Cookies_Is_True() { // arrange - _applicationConfiguration.ReturnsForAll("foo"); - _applicationConfiguration.DisableSecureCookies.Returns(true); + _applicationConfiguration.SetAllValid(); + _applicationConfiguration._disableSecureCookies = "true"; var sut = new ConfigurationHealthCheck(_logger, _applicationConfiguration); // act diff --git a/Childrens-Social-Care-CPD-Tests/Controllers/ApplicationControllerTests.cs b/Childrens-Social-Care-CPD-Tests/Controllers/ApplicationControllerTests.cs index b48dbc28..56be41a4 100644 --- a/Childrens-Social-Care-CPD-Tests/Controllers/ApplicationControllerTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Controllers/ApplicationControllerTests.cs @@ -32,7 +32,7 @@ public void ApplicationController_Includes_Contentful_Environment() { // arrange var value = "foo"; - _applicationConfiguration.ContentfulEnvironment.Returns(value); + _applicationConfiguration.ContentfulEnvironment.Value.Returns(value); // act var actual = _controller.AppInfo().Value as ApplicationInfo; @@ -46,7 +46,7 @@ public void ApplicationController_Includes_Azure_Environment() { // arrange var value = "foo"; - _applicationConfiguration.AzureEnvironment.Returns(value); + _applicationConfiguration.AzureEnvironment.Value.Returns(value); // act var actual = _controller.AppInfo().Value as ApplicationInfo; @@ -60,7 +60,7 @@ public void ApplicationController_Includes_Git_Hash() { // arrange var value = "foo"; - _applicationConfiguration.GitHash.Returns(value); + _applicationConfiguration.GitHash.Value.Returns(value); // act var actual = _controller.AppInfo().Value as ApplicationInfo; @@ -74,7 +74,7 @@ public void ApplicationController_Includes_App_Version() { // arrange var value = "foo"; - _applicationConfiguration.AppVersion.Returns(value); + _applicationConfiguration.AppVersion.Value.Returns(value); // act var actual = _controller.AppInfo().Value as ApplicationInfo; diff --git a/Childrens-Social-Care-CPD-Tests/CookieHelperTests.cs b/Childrens-Social-Care-CPD-Tests/CookieHelperTests.cs index 6c458059..04f4dc28 100644 --- a/Childrens-Social-Care-CPD-Tests/CookieHelperTests.cs +++ b/Childrens-Social-Care-CPD-Tests/CookieHelperTests.cs @@ -87,7 +87,7 @@ public void GetRequestAnalyticsCookieState(string cookieValue, AnalyticsConsentS public void Disables_Secure_Cookies_On_Config() { // arrange - _applicationConfiguration.DisableSecureCookies.Returns(false); + _applicationConfiguration.DisableSecureCookies.Returns(new BooleanConfigSetting(() => "false")); var httpContext = Substitute.For(); CookieOptions cookieOptions = null; httpContext.Response.Cookies.Append(CookieHelper.ANALYTICSCOOKIENAME, CookieHelper.ANALYTICSCOOKIEACCEPTED, Arg.Do(x => cookieOptions = x)); diff --git a/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs b/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs index c4b5287a..9b3127f9 100644 --- a/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs +++ b/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs @@ -2,11 +2,9 @@ using Childrens_Social_Care_CPD.Contentful; using Childrens_Social_Care_CPD.Contentful.Models; using Childrens_Social_Care_CPD.DataAccess; -using Childrens_Social_Care_CPD.GraphQL.Queries; using Contentful.Core.Models; using Contentful.Core.Search; using FluentAssertions; -using GraphQL.Client.Abstractions; using GraphQL; using GraphQL.Client.Abstractions.Websocket; using NSubstitute; @@ -17,8 +15,6 @@ using static Childrens_Social_Care_CPD.GraphQL.Queries.SearchResourcesByTags; using System.Collections.ObjectModel; using System; -using Newtonsoft.Json.Linq; -using NSubstitute.Core; namespace Childrens_Social_Care_CPD_Tests.DataAccess; @@ -38,7 +34,8 @@ public void Setup() _gqlClient = Substitute.For(); // By default we want the preview flag set to false - _applicationConfiguration.ContentfulPreviewId.Returns(string.Empty); + _applicationConfiguration.AzureEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.Development)); + _applicationConfiguration.ContentfulEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.Development)); } [Test] @@ -176,7 +173,7 @@ public async Task FindByTags_Preview_Flag_Is_False_By_Default() public async Task FindByTags_Sets_Preview_Flag() { // arrange - _applicationConfiguration.ContentfulPreviewId.Returns("foo"); + _applicationConfiguration.ContentfulEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.PreProduction)); var response = Substitute.For>(); response.Data = new ResponseType(); diff --git a/Childrens-Social-Care-CPD-Tests/MockApplicationConfiguration.cs b/Childrens-Social-Care-CPD-Tests/MockApplicationConfiguration.cs new file mode 100644 index 00000000..059a162e --- /dev/null +++ b/Childrens-Social-Care-CPD-Tests/MockApplicationConfiguration.cs @@ -0,0 +1,54 @@ +using Childrens_Social_Care_CPD.Configuration; + +namespace Childrens_Social_Care_CPD_Tests; + +public class MockApplicationConfiguration : IApplicationConfiguration +{ + public string _appInsightsConnectionString = null; + public string _appVersion = null; + public string _azureEnvironment = null; + public string _clarityProjectId = null; + public string _contentfulDeliveryApiKey = null; + public string _contentfulEnvironment = null; + public string _contentfulGraphqlConnectionString = null; + public string _contentfulPreviewHost = null; + public string _contentfulPreviewId = null; + public string _contentfulSpaceId = null; + public string _disableSecureCookies = null; + public string _featurePollingInterval = null; + public string _gitHash = null; + public string _googleTagManagerKey = null; + + public IConfigurationSetting AppInsightsConnectionString => new StringConfigSetting(() => _appInsightsConnectionString); + public IConfigurationSetting AppVersion => new StringConfigSetting(() => _appVersion); + public IConfigurationSetting AzureEnvironment => new StringConfigSetting(() => _azureEnvironment); + public IConfigurationSetting ClarityProjectId => new StringConfigSetting(() => _clarityProjectId); + public IConfigurationSetting ContentfulDeliveryApiKey => new StringConfigSetting(() => _contentfulDeliveryApiKey); + public IConfigurationSetting ContentfulEnvironment => new StringConfigSetting(() => _contentfulEnvironment); + public IConfigurationSetting ContentfulGraphqlConnectionString => new StringConfigSetting(() => _contentfulGraphqlConnectionString); + public IConfigurationSetting ContentfulPreviewHost => new StringConfigSetting(() => _contentfulPreviewHost); + public IConfigurationSetting ContentfulPreviewId => new StringConfigSetting(() => _contentfulPreviewId); + public IConfigurationSetting ContentfulSpaceId => new StringConfigSetting(() => _contentfulSpaceId); + public IConfigurationSetting DisableSecureCookies => new BooleanConfigSetting(() => _disableSecureCookies); + public IConfigurationSetting FeaturePollingInterval => new IntegerConfigSetting(() => _featurePollingInterval); + public IConfigurationSetting GitHash => new StringConfigSetting(() => _gitHash); + public IConfigurationSetting GoogleTagManagerKey => new StringConfigSetting(() => _googleTagManagerKey); + + public void SetAllValid(string value = "foo") + { + _appInsightsConnectionString = value; + _appVersion = value; + _azureEnvironment = value; + _clarityProjectId = value; + _contentfulDeliveryApiKey = value; + _contentfulEnvironment = value; + _contentfulGraphqlConnectionString = value; + _contentfulPreviewHost = value; + _contentfulPreviewId = value; + _contentfulSpaceId = value; + _disableSecureCookies = "false"; + _featurePollingInterval = "0"; + _gitHash = value; + _googleTagManagerKey = value; + } +} diff --git a/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs index a764100d..3245d484 100644 --- a/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs @@ -2,28 +2,38 @@ public class ApplicationConfiguration : IApplicationConfiguration { - private static string ValueOrStringEmpty(string key) - { - var value = Environment.GetEnvironmentVariable(key); - if (string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value)) - { - return string.Empty; - } + private readonly IConfigurationSetting _appInsightsConnectionString = new StringConfigSetting(() => Environment.GetEnvironmentVariable("CPD_INSTRUMENTATION_CONNECTIONSTRING")); + private readonly IConfigurationSetting _appVersion = new StringConfigSetting(() => Environment.GetEnvironmentVariable("VCS-TAG")); + private readonly IConfigurationSetting _azureEnvironment = new StringConfigSetting(() => Environment.GetEnvironmentVariable("CPD_AZURE_ENVIRONMENT")); + private readonly IConfigurationSetting _clarityProjectId = new StringConfigSetting(() => Environment.GetEnvironmentVariable("CPD_CLARITY")); + private readonly IConfigurationSetting _contentfulDeliveryApiKey = new StringConfigSetting(() => Environment.GetEnvironmentVariable("CPD_DELIVERY_KEY")); + private readonly IConfigurationSetting _contentfulEnvironment = new StringConfigSetting(() => Environment.GetEnvironmentVariable("CPD_CONTENTFUL_ENVIRONMENT")); + private readonly IConfigurationSetting _contentfulGraphqlConnectionString; + private readonly IConfigurationSetting _contentfulPreviewHost = new StringConfigSetting(() => "preview.contentful.com"); + private readonly IConfigurationSetting _contentfulPreviewId = new StringConfigSetting(() => Environment.GetEnvironmentVariable("CPD_PREVIEW_KEY")); + private readonly IConfigurationSetting _contentfulSpaceId = new StringConfigSetting(() => Environment.GetEnvironmentVariable("CPD_SPACE_ID")); + private readonly IConfigurationSetting _disableSecureCookies = new BooleanConfigSetting(() => Environment.GetEnvironmentVariable("CPD_DISABLE_SECURE_COOKIES"), false); + private readonly IConfigurationSetting _featurePollingInterval = new IntegerConfigSetting(() => Environment.GetEnvironmentVariable("CPD_FEATURE_POLLING_INTERVAL"), 0); + private readonly IConfigurationSetting _gitHash = new StringConfigSetting(() => Environment.GetEnvironmentVariable("VCS-REF")); + private readonly IConfigurationSetting _googleTagManagerKey = new StringConfigSetting(() => Environment.GetEnvironmentVariable("CPD_GOOGLEANALYTICSTAG")); - return value; + public ApplicationConfiguration() + { + _contentfulGraphqlConnectionString = new StringConfigSetting(() => $"https://graphql.contentful.com/content/v1/spaces/{ContentfulSpaceId.Value}/environments/{ContentfulEnvironment.Value}"); } - public string AppInsightsConnectionString => ValueOrStringEmpty("CPD_INSTRUMENTATION_CONNECTIONSTRING"); - public string AppVersion => ValueOrStringEmpty("VCS-TAG"); - public string AzureEnvironment => ValueOrStringEmpty("CPD_AZURE_ENVIRONMENT"); - public string ClarityProjectId => ValueOrStringEmpty("CPD_CLARITY"); - public string ContentfulDeliveryApiKey => ValueOrStringEmpty("CPD_DELIVERY_KEY"); - public string ContentfulEnvironment => ValueOrStringEmpty("CPD_CONTENTFUL_ENVIRONMENT"); - public string ContentfulGraphqlConnectionString => $"https://graphql.contentful.com/content/v1/spaces/{ContentfulSpaceId}/environments/{ContentfulEnvironment}"; - public string ContentfulPreviewHost => "preview.contentful.com"; - public string ContentfulPreviewId => ValueOrStringEmpty("CPD_PREVIEW_KEY"); - public string ContentfulSpaceId => ValueOrStringEmpty("CPD_SPACE_ID"); - public bool DisableSecureCookies => ValueOrStringEmpty("CPD_DISABLE_SECURE_COOKIES") == "true"; - public int FeaturePollingInterval => int.Parse(Environment.GetEnvironmentVariable("CPD_FEATURE_POLLING_INTERVAL") ?? "0"); - public string GitHash => ValueOrStringEmpty("VCS-REF"); - public string GoogleTagManagerKey => ValueOrStringEmpty("CPD_GOOGLEANALYTICSTAG"); + + public IConfigurationSetting AppInsightsConnectionString => _appInsightsConnectionString; + public IConfigurationSetting AppVersion => _appVersion; + public IConfigurationSetting AzureEnvironment => _azureEnvironment; + public IConfigurationSetting ClarityProjectId => _clarityProjectId; + public IConfigurationSetting ContentfulDeliveryApiKey => _contentfulDeliveryApiKey; + public IConfigurationSetting ContentfulEnvironment => _contentfulEnvironment; + public IConfigurationSetting ContentfulGraphqlConnectionString => _contentfulGraphqlConnectionString; + public IConfigurationSetting ContentfulPreviewHost => _contentfulPreviewHost; + public IConfigurationSetting ContentfulPreviewId => _contentfulPreviewId; + public IConfigurationSetting ContentfulSpaceId => _contentfulSpaceId; + public IConfigurationSetting DisableSecureCookies => _disableSecureCookies; + public IConfigurationSetting FeaturePollingInterval => _featurePollingInterval; + public IConfigurationSetting GitHash => _gitHash; + public IConfigurationSetting GoogleTagManagerKey => _googleTagManagerKey; } \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs b/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs index 305fc61b..84072be9 100644 --- a/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs +++ b/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs @@ -3,7 +3,7 @@ namespace Childrens_Social_Care_CPD.Configuration; -public record ConfigurationItemInfo(string Name, bool Required, bool Obfuscated, bool Hidden, bool HasValue, string Value, bool Extraneous); +public record ConfigurationItemInfo(string Name, bool Required, bool Obfuscated, bool Hidden, bool IsSet, string Value, bool Extraneous); public class ConfigurationInformation { @@ -12,7 +12,7 @@ public class ConfigurationInformation public ConfigurationInformation(IApplicationConfiguration applicationConfiguration) { - Environment = applicationConfiguration.AzureEnvironment; + Environment = applicationConfiguration.AzureEnvironment.Value; ExtractInfo(applicationConfiguration); } @@ -25,6 +25,7 @@ private void ExtractInfo(IApplicationConfiguration applicationConfiguration) { var property = propertyPair.Key; var rule = propertyPair.Value; + var value = property.GetValue(applicationConfiguration); var hasValue = HasValue(property, value); var displayValue = GetDisplayValue(rule, hasValue, value); @@ -37,7 +38,7 @@ private void ExtractInfo(IApplicationConfiguration applicationConfiguration) Required: rule != null, Obfuscated: rule?.Obfuscate ?? true, Hidden: rule?.Hidden ?? false, - HasValue: hasValue, + IsSet: hasValue, Value: displayValue, Extraneous: rule == null)); } @@ -69,6 +70,11 @@ private static bool HasValue(PropertyInfo propertyInfo, object value) return !(string.IsNullOrEmpty(v) || string.IsNullOrWhiteSpace(v)); } + if (propertyInfo.PropertyType.GetInterfaces().Any(x => x == typeof(IConfigurationSetting))) + { + return (value as IConfigurationSetting).IsSet; + } + return true; } } \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Configuration/ConfigurationSetting.cs b/Childrens-Social-Care-CPD/Configuration/ConfigurationSetting.cs new file mode 100644 index 00000000..7aa8a720 --- /dev/null +++ b/Childrens-Social-Care-CPD/Configuration/ConfigurationSetting.cs @@ -0,0 +1,64 @@ +namespace Childrens_Social_Care_CPD.Configuration; + +public interface IConfigurationSetting +{ + bool IsSet { get; } +} + +public interface IConfigurationSetting : IConfigurationSetting +{ + T Value { get; } +} + +public class ConfigurationSetting : IConfigurationSetting +{ + private readonly Func _valueGetter; + private readonly Func _valueParser; + private readonly T _defaultValue; + + public ConfigurationSetting(Func valueGetter, Func valueParser, T defaultValue = default) + { + ArgumentNullException.ThrowIfNull(valueGetter, nameof(valueGetter)); + ArgumentNullException.ThrowIfNull(valueParser, nameof(valueParser)); + + _valueGetter = valueGetter; + _valueParser = valueParser; + _defaultValue = defaultValue; + } + + private static bool CheckIfSet(string value) => !(string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value)); + + public T Value + { + get + { + var value = _valueGetter(); + return CheckIfSet(value) ? _valueParser(value) : _defaultValue; + } + } + + public bool IsSet => CheckIfSet(_valueGetter()); + + public override string ToString() + { + return Value?.ToString() ?? string.Empty; + } +} + +internal class BooleanConfigSetting : ConfigurationSetting +{ + public BooleanConfigSetting(Func valueGetter, bool defaultValue = false) : base(valueGetter, bool.Parse, defaultValue) + { } +} + +internal class StringConfigSetting : ConfigurationSetting +{ + public StringConfigSetting(Func valueGetter, string defaultValue = "") : base(valueGetter, x => x, defaultValue) + { } +} + +internal class IntegerConfigSetting : ConfigurationSetting +{ + public IntegerConfigSetting(Func valueGetter, int defaultValue = 0) : base(valueGetter, int.Parse, defaultValue) + { } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs index c3ab202e..511c9569 100644 --- a/Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs @@ -1,27 +1,32 @@ -using System.Diagnostics.CodeAnalysis; +using Contentful.Core.Models.Management; +using System.Diagnostics.CodeAnalysis; namespace Childrens_Social_Care_CPD.Configuration; [ExcludeFromCodeCoverage] public static class ContentfulConfiguration { - public const string LOADTESTAPPENVIRONMENT = "load-test"; + public static bool IsPreviewEnabled(IApplicationConfiguration applicationConfiguration) + { + var azureEnvironment = applicationConfiguration.AzureEnvironment; + return azureEnvironment.IsSet + && azureEnvironment.Value != ApplicationEnvironment.LoadTest + && applicationConfiguration.ContentfulEnvironment.Value.ToLower() != azureEnvironment.Value.ToLower(); + } public static ConfigurationManager GetContentfulConfiguration(ConfigurationManager configuration, IApplicationConfiguration applicationConfiguration) { var contentfulEnvironment = applicationConfiguration.ContentfulEnvironment; - configuration["ContentfulOptions:Environment"] = contentfulEnvironment; - configuration["ContentfulOptions:SpaceId"] = applicationConfiguration.ContentfulSpaceId; - configuration["ContentfulOptions:DeliveryApiKey"] = applicationConfiguration.ContentfulDeliveryApiKey; + configuration["ContentfulOptions:Environment"] = contentfulEnvironment.Value; + configuration["ContentfulOptions:SpaceId"] = applicationConfiguration.ContentfulSpaceId.Value; + configuration["ContentfulOptions:DeliveryApiKey"] = applicationConfiguration.ContentfulDeliveryApiKey.Value; var azureEnvironment = applicationConfiguration.AzureEnvironment; - if (contentfulEnvironment.ToLower() != azureEnvironment.ToLower() - && !string.IsNullOrEmpty(azureEnvironment) - && azureEnvironment != LOADTESTAPPENVIRONMENT) + if (IsPreviewEnabled(applicationConfiguration)) { - configuration["ContentfulOptions:host"] = applicationConfiguration.ContentfulPreviewHost; + configuration["ContentfulOptions:host"] = applicationConfiguration.ContentfulPreviewHost.Value; configuration["ContentfulOptions:UsePreviewApi"] = "true"; - configuration["ContentfulOptions:PreviewApiKey"] = applicationConfiguration.ContentfulPreviewId; + configuration["ContentfulOptions:PreviewApiKey"] = applicationConfiguration.ContentfulPreviewId.Value; } return configuration; } diff --git a/Childrens-Social-Care-CPD/Configuration/FeaturesConfigBackgroundService.cs b/Childrens-Social-Care-CPD/Configuration/FeaturesConfigBackgroundService.cs index 1a130ccf..e70e1e54 100644 --- a/Childrens-Social-Care-CPD/Configuration/FeaturesConfigBackgroundService.cs +++ b/Childrens-Social-Care-CPD/Configuration/FeaturesConfigBackgroundService.cs @@ -22,9 +22,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogInformation("Background polling task started"); stoppingToken.Register(() => _logger.LogInformation("Background polling task started")); - if (_applicationConfiguration.FeaturePollingInterval == 0) return; + if (_applicationConfiguration.FeaturePollingInterval.Value == 0) return; - var timer = new PeriodicTimer(TimeSpan.FromSeconds(_applicationConfiguration.FeaturePollingInterval)); + var timer = new PeriodicTimer(TimeSpan.FromSeconds(_applicationConfiguration.FeaturePollingInterval.Value)); while (await timer.WaitForNextTickAsync(stoppingToken)) { _logger.LogInformation("Polling at: {utcNow}", DateTime.UtcNow.ToShortTimeString()); diff --git a/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs index 119a3382..99e7ef58 100644 --- a/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs @@ -5,46 +5,46 @@ namespace Childrens_Social_Care_CPD.Configuration; public interface IApplicationConfiguration { [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] - string AppInsightsConnectionString { get; } + IConfigurationSetting AppInsightsConnectionString { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false, Obfuscate = false)] - string AppVersion { get; } + IConfigurationSetting AppVersion { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false, Obfuscate = false)] - string AzureEnvironment { get; } + IConfigurationSetting AzureEnvironment { get; } [RequiredForEnvironment(ApplicationEnvironment.Production, Hidden = false)] - string ClarityProjectId { get; } + IConfigurationSetting ClarityProjectId { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] - string ContentfulDeliveryApiKey { get; } + IConfigurationSetting ContentfulDeliveryApiKey { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false, Obfuscate = false)] - string ContentfulEnvironment { get; } + IConfigurationSetting ContentfulEnvironment { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] - string ContentfulGraphqlConnectionString { get; } - + IConfigurationSetting ContentfulGraphqlConnectionString { get; } + [RequiredForEnvironment(ApplicationEnvironment.PreProduction, Hidden = false)] - string ContentfulPreviewHost { get; } + IConfigurationSetting ContentfulPreviewHost { get; } [RequiredForEnvironment(ApplicationEnvironment.PreProduction, Hidden = false)] - string ContentfulPreviewId { get; } + IConfigurationSetting ContentfulPreviewId { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] - string ContentfulSpaceId { get; } + IConfigurationSetting ContentfulSpaceId { get; } [DefaultValue(false)] [RequiredForEnvironment(ApplicationEnvironment.Integration, Hidden = false, Obfuscate = false)] - bool DisableSecureCookies { get; } + IConfigurationSetting DisableSecureCookies { get; } [DefaultValue(0)] [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false, Obfuscate = false)] - int FeaturePollingInterval { get; } + IConfigurationSetting FeaturePollingInterval { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false, Obfuscate = false)] - string GitHash { get; } + IConfigurationSetting GitHash { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] - string GoogleTagManagerKey { get; } + IConfigurationSetting GoogleTagManagerKey { get; } } diff --git a/Childrens-Social-Care-CPD/ConfigurationHealthCheck.cs b/Childrens-Social-Care-CPD/ConfigurationHealthCheck.cs index 231493b1..85ea8498 100644 --- a/Childrens-Social-Care-CPD/ConfigurationHealthCheck.cs +++ b/Childrens-Social-Care-CPD/ConfigurationHealthCheck.cs @@ -21,7 +21,7 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc foreach (var item in configurationInformation.ConfigurationInfo) { - if (item.Required && !item.HasValue) + if (item.Required && !item.IsSet) { _logger.LogError("Configuration setting {propertyName} does not have a value", item.Name); healthy = false; @@ -29,7 +29,7 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc } // Specific check as this is super important. - if (_applicationConfiguration.DisableSecureCookies) + if (_applicationConfiguration.DisableSecureCookies.Value) { _logger.LogError("DisableSecureCookies should not be enabled for standard environments"); healthy = false; diff --git a/Childrens-Social-Care-CPD/Controllers/ApplicationController.cs b/Childrens-Social-Care-CPD/Controllers/ApplicationController.cs index 4d751c12..53fa658b 100644 --- a/Childrens-Social-Care-CPD/Controllers/ApplicationController.cs +++ b/Childrens-Social-Care-CPD/Controllers/ApplicationController.cs @@ -20,10 +20,10 @@ public JsonResult AppInfo() { var applicationInfo = new ApplicationInfo() { - Environment = _applicationConfiguration.AzureEnvironment, - ContentfulEnvironment = _applicationConfiguration.ContentfulEnvironment, - GitShortHash = _applicationConfiguration.GitHash, - Version = _applicationConfiguration.AppVersion, + Environment = _applicationConfiguration.AzureEnvironment.Value, + ContentfulEnvironment = _applicationConfiguration.ContentfulEnvironment.Value, + GitShortHash = _applicationConfiguration.GitHash.Value, + Version = _applicationConfiguration.AppVersion.Value, }; return Json(applicationInfo); @@ -38,7 +38,7 @@ public IActionResult Configuration() if (Request.Headers.Accept == MediaTypeNames.Application.Json) { var info = configurationInformation.ConfigurationInfo.Where(x => !x.Hidden); - return Json(info.Select(x => new { x.Name, x.Extraneous, x.HasValue, x.Value, x.Obfuscated })); + return Json(info.Select(x => new { x.Name, x.Extraneous, x.IsSet, x.Value, x.Obfuscated })); } return View(configurationInformation.ConfigurationInfo); diff --git a/Childrens-Social-Care-CPD/CookieHelper.cs b/Childrens-Social-Care-CPD/CookieHelper.cs index 17a10858..f8b53748 100644 --- a/Childrens-Social-Care-CPD/CookieHelper.cs +++ b/Childrens-Social-Care-CPD/CookieHelper.cs @@ -23,7 +23,7 @@ public void SetResponseAnalyticsCookieState(HttpContext httpContext, AnalyticsCo HttpOnly = true, SameSite = SameSiteMode.Strict, IsEssential = true, - Secure = !_applicationConfiguration.DisableSecureCookies + Secure = !_applicationConfiguration.DisableSecureCookies.Value }; switch (state) diff --git a/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs b/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs index 20da2ffe..f9af87d6 100644 --- a/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs +++ b/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs @@ -23,7 +23,7 @@ public ResourcesRepository(IApplicationConfiguration applicationConfiguration, I { _cpdClient = cpdClient; _gqlClient = gqlClient; - _isPreview = !string.IsNullOrEmpty(applicationConfiguration.ContentfulPreviewId); + _isPreview = ContentfulConfiguration.IsPreviewEnabled(applicationConfiguration); } public Task FetchRootPage(CancellationToken cancellationToken = default) diff --git a/Childrens-Social-Care-CPD/Views/Application/Configuration.cshtml b/Childrens-Social-Care-CPD/Views/Application/Configuration.cshtml index def8695d..cdc9f16d 100644 --- a/Childrens-Social-Care-CPD/Views/Application/Configuration.cshtml +++ b/Childrens-Social-Care-CPD/Views/Application/Configuration.cshtml @@ -42,13 +42,13 @@ foreach (var item in Model) { if (item.Hidden) continue; - if (!item.HasValue && item.Extraneous) continue; + if (!item.IsSet && item.Extraneous) continue; var className = ""; - if (item.Extraneous && item.HasValue) + if (item.Extraneous && item.IsSet) className = "extraneous"; - if (!item.Extraneous && !item.HasValue) + if (!item.Extraneous && !item.IsSet) className = "missing"; diff --git a/Childrens-Social-Care-CPD/Views/Shared/_GoogleAnalyticsPartial.cshtml b/Childrens-Social-Care-CPD/Views/Shared/_GoogleAnalyticsPartial.cshtml index d4b58902..d507bb51 100644 --- a/Childrens-Social-Care-CPD/Views/Shared/_GoogleAnalyticsPartial.cshtml +++ b/Childrens-Social-Care-CPD/Views/Shared/_GoogleAnalyticsPartial.cshtml @@ -3,7 +3,7 @@ @inject IApplicationConfiguration applicationConfiguration @{ - var googleAnalyticsKey = applicationConfiguration.GoogleTagManagerKey; + var googleAnalyticsKey = applicationConfiguration.GoogleTagManagerKey.Value; var googleAnalyticsUrl = string.Format("https://www.googletagmanager.com/gtag/js?id={0}", googleAnalyticsKey); var clarityProjectId = applicationConfiguration.ClarityProjectId; var pageName = (ViewBag.pageName ?? ViewBag.ContextModel?.Id) ?? "Homepage"; @@ -18,7 +18,7 @@ gtag('config', '@googleAnalyticsKey'); - if (applicationConfiguration.AzureEnvironment == ApplicationEnvironment.Test || applicationConfiguration.AzureEnvironment == ApplicationEnvironment.Production) + if (applicationConfiguration.AzureEnvironment.Value == ApplicationEnvironment.Test || applicationConfiguration.AzureEnvironment.Value == ApplicationEnvironment.Production) {