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..74d13596 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; @@ -46,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); @@ -60,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); @@ -75,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); @@ -89,12 +90,27 @@ public void Returns_ContentfulEnvironment_Value() var sut = new ApplicationConfiguration(); // act - var actual = sut.ContentfulEnvironment; + var actual = sut.ContentfulEnvironment.Value; // assert 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.Value; + + // assert + actual.Should().Be($"https://graphql.contentful.com/content/v1/spaces/{Value}/environments/{Value}"); + } + [Test] public void Returns_ContentfulPreviewHost_Value() { @@ -103,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); @@ -117,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); @@ -131,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); @@ -145,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); @@ -159,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); @@ -173,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); @@ -187,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); @@ -201,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); @@ -215,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); @@ -228,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); @@ -241,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); @@ -254,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); @@ -267,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); @@ -280,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); @@ -293,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); @@ -306,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); @@ -319,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); @@ -332,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); @@ -345,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); @@ -358,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); @@ -371,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 5d396496..8ac375ff 100644 --- a/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Configuration/ConfigurationInformationTests.cs @@ -12,32 +12,33 @@ 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("")] + [TestCase(" ")] [TestCase(null)] 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); @@ -45,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); @@ -79,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/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/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 new file mode 100644 index 00000000..9b3127f9 --- /dev/null +++ b/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs @@ -0,0 +1,192 @@ +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 Contentful.Core.Models; +using Contentful.Core.Search; +using FluentAssertions; +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; + +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.AzureEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.Development)); + _applicationConfiguration.ContentfulEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.Development)); + } + + [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); + } + + [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.ContentfulEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.PreProduction)); + + 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-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/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 @@ + + diff --git a/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs index 75a95734..3245d484 100644 --- a/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/ApplicationConfiguration.cs @@ -2,18 +2,38 @@ public class ApplicationConfiguration : IApplicationConfiguration { - private static string ValueOrStringEmpty(string key) => Environment.GetEnvironmentVariable(key) ?? string.Empty; - 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 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"); + 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")); + + public ApplicationConfiguration() + { + _contentfulGraphqlConnectionString = new StringConfigSetting(() => $"https://graphql.contentful.com/content/v1/spaces/{ContentfulSpaceId.Value}/environments/{ContentfulEnvironment.Value}"); + } + + 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 73b443c4..cff0b585 100644 --- a/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs +++ b/Childrens-Social-Care-CPD/Configuration/ConfigurationInformation.cs @@ -1,10 +1,9 @@ using System.Collections.ObjectModel; -using System.Data; using System.Reflection; 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 { @@ -13,7 +12,7 @@ public class ConfigurationInformation public ConfigurationInformation(IApplicationConfiguration applicationConfiguration) { - Environment = applicationConfiguration.AzureEnvironment; + Environment = applicationConfiguration.AzureEnvironment.Value; ExtractInfo(applicationConfiguration); } @@ -26,19 +25,20 @@ 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); + var isSet = IsSet(value); + var displayValue = GetDisplayValue(rule, isSet, value); // Don't add extraneous values that haven't been set - if (rule == null && !hasValue) continue; + if (rule == null && !isSet) continue; list.Add(new ConfigurationItemInfo( Name: property.Name, Required: rule != null, Obfuscated: rule?.Obfuscate ?? true, Hidden: rule?.Hidden ?? false, - HasValue: hasValue, + IsSet: isSet, Value: displayValue, Extraneous: rule == null)); } @@ -57,16 +57,22 @@ private static string GetDisplayValue(RequiredForEnvironmentAttribute rule, bool : value?.ToString(); } - private static bool HasValue(PropertyInfo propertyInfo, object value) + private static bool IsSet(object value) { if (value == null) { return false; } - if (propertyInfo.PropertyType == typeof(string)) + if (value is string) + { + var v = value as string; + return !(string.IsNullOrEmpty(v) || string.IsNullOrWhiteSpace(v)); + } + + if (value is IConfigurationSetting) { - return !string.IsNullOrEmpty(value as string); + return (value as IConfigurationSetting).IsSet; } return true; diff --git a/Childrens-Social-Care-CPD/Configuration/ConfigurationSetting.cs b/Childrens-Social-Care-CPD/Configuration/ConfigurationSetting.cs new file mode 100644 index 00000000..1ac8411a --- /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); + ArgumentNullException.ThrowIfNull(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/ContentfulConfiguration.cs b/Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs similarity index 53% rename from Childrens-Social-Care-CPD/ContentfulConfiguration.cs rename to Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs index 45c49839..248628b4 100644 --- a/Childrens-Social-Care-CPD/ContentfulConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/ContentfulConfiguration.cs @@ -1,29 +1,32 @@ -using System.Diagnostics.CodeAnalysis; -using Childrens_Social_Care_CPD.Configuration; +using Contentful.Core.Models.Management; +using System.Diagnostics.CodeAnalysis; -namespace Childrens_Social_Care_CPD; +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; } -} +} \ No newline at end of file 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 51b31779..99e7ef58 100644 --- a/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs +++ b/Childrens-Social-Care-CPD/Configuration/IApplicationConfiguration.cs @@ -4,46 +4,47 @@ namespace Childrens_Social_Care_CPD.Configuration; public interface IApplicationConfiguration { - [RequiredForEnvironment(ApplicationEnvironment.Test, Hidden = false)] - [RequiredForEnvironment(ApplicationEnvironment.PreProduction, Hidden = false)] - [RequiredForEnvironment(ApplicationEnvironment.Production, Hidden = false)] - string AppInsightsConnectionString { get; } + [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] + 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)] + IConfigurationSetting ContentfulEnvironment { get; } [RequiredForEnvironment(ApplicationEnvironment.All, Hidden = false)] - string ContentfulEnvironment { 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)] - bool DisableSecureCookies { get; } + [RequiredForEnvironment(ApplicationEnvironment.Integration, Hidden = false, Obfuscate = false)] + 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 cb64039f..85ea8498 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; @@ -22,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; @@ -30,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/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/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 new file mode 100644 index 00000000..f9af87d6 --- /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 = default); + Task FindByTags(IEnumerable tags, int skip, int take, CancellationToken cancellationToken = default); +} + +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 = ContentfulConfiguration.IsPreviewEnabled(applicationConfiguration); + } + + 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/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/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/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) {