diff --git a/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs b/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs index 210f61f..95b682b 100644 --- a/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs +++ b/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs @@ -9,9 +9,11 @@ namespace Dfe.ContentSupport.Web.Controllers; [Route("/content")] [AllowAnonymous] -public class ContentController(IContentService contentService, ILayoutService layoutService) +public class ContentController(IContentService contentService, ILayoutService layoutService, ILogger logger) : Controller { + public const string ErrorActionName = "error"; + public async Task Home() { var defaultModel = new CsPage @@ -28,24 +30,42 @@ public async Task Home() return View(defaultModel); } - - [HttpGet("{slug}/{page?}")] public async Task Index(string slug, string page = "", bool isPreview = false, [FromQuery] List? tags = null) { - if (!ModelState.IsValid) return RedirectToAction("error"); - if (string.IsNullOrEmpty(slug)) return RedirectToAction("error"); + if (!ModelState.IsValid) + { + logger.LogError("Invalid model state received for {Controller} {Action} with slug {Slug}", nameof(ContentController), nameof(Index), slug); + return RedirectToAction(ErrorActionName); + } + + if (string.IsNullOrEmpty(slug)) + { + logger.LogError("No slug received for C&S {Controller} {Action}", nameof(ContentController), nameof(Index)); + return RedirectToAction(ErrorActionName); + } - var resp = await contentService.GetContent(slug, isPreview); - if (resp is null) return RedirectToAction("error"); + try + { + var resp = await contentService.GetContent(slug, isPreview); + if (resp is null) + { + logger.LogError("Failed to load content for C&S page {Slug}; no content received.", slug); + return RedirectToAction(ErrorActionName); + } - resp = layoutService.GenerateLayout(resp, Request, page); - ViewBag.tags = tags; + resp = layoutService.GenerateLayout(resp, Request, page); + ViewBag.tags = tags; - return View("CsIndex", resp); + return View("CsIndex", resp); + } + catch (Exception ex) + { + logger.LogError(ex, "Error loading C&S content page {Slug}", slug); + return RedirectToAction(ErrorActionName); + } } - public IActionResult Privacy() { return View(); diff --git a/tests/Dfe.ContentSupport.Web.Tests/Controllers/ContentControllerTests.cs b/tests/Dfe.ContentSupport.Web.Tests/Controllers/ContentControllerTests.cs index eb5f3de..04b5d40 100644 --- a/tests/Dfe.ContentSupport.Web.Tests/Controllers/ContentControllerTests.cs +++ b/tests/Dfe.ContentSupport.Web.Tests/Controllers/ContentControllerTests.cs @@ -2,16 +2,18 @@ using Dfe.ContentSupport.Web.Models.Mapped; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Dfe.ContentSupport.Web.Tests.Controllers; public class ContentControllerTests { + private readonly Mock> _loggerMock = new(); private readonly Mock _contentServiceMock = new(); private ContentController GetController() { - return new ContentController(_contentServiceMock.Object, new LayoutService()); + return new ContentController(_contentServiceMock.Object, new LayoutService(), _loggerMock.Object); } @@ -36,6 +38,15 @@ public async Task Index_NoSlug_Returns_ErrorAction() result.Should().BeOfType(); (result as RedirectToActionResult)!.ActionName.Should().BeEquivalentTo("error"); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((o, t) => o.ToString() != null && o.ToString()!.StartsWith($"No slug received for C&S {nameof(ContentController)} {nameof(sut.Index)}")), + It.IsAny(), + It.IsAny>()), + Times.Once); } [Fact] @@ -53,15 +64,47 @@ public async Task Index_Calls_Service_GetContent() [Fact] public async Task Index_NullResponse_ReturnsErrorAction() { + var slug = "slug"; _contentServiceMock.Setup(o => o.GetContent(It.IsAny(), It.IsAny())) .ReturnsAsync((CsPage?)null); var sut = GetController(); - var result = await sut.Index("slug"); + var result = await sut.Index(slug); + + result.Should().BeOfType(); + (result as RedirectToActionResult)!.ActionName.Should().BeEquivalentTo("error"); + + _loggerMock.Verify(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((o, t) => o.ToString() != null && o.ToString()!.Equals($"Failed to load content for C&S page {slug}; no content received.")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task Index_ExceptionThrown_ReturnsErrorAction() + { + var slug = "slug"; + _contentServiceMock.Setup(o => o.GetContent(It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("An exception occurred loading content")); + + var sut = GetController(); + + var result = await sut.Index(slug); result.Should().BeOfType(); (result as RedirectToActionResult)!.ActionName.Should().BeEquivalentTo("error"); + + _loggerMock.Verify(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((o, t) => o.ToString() != null && o.ToString()!.Equals($"Error loading C&S content page {slug}")), + It.IsAny(), + It.IsAny>()), + Times.Once); } [Fact] diff --git a/tests/Dfe.ContentSupport.Web.Tests/Dfe.ContentSupport.Web.Tests.csproj b/tests/Dfe.ContentSupport.Web.Tests/Dfe.ContentSupport.Web.Tests.csproj index 3121e2b..e9ee0bd 100644 --- a/tests/Dfe.ContentSupport.Web.Tests/Dfe.ContentSupport.Web.Tests.csproj +++ b/tests/Dfe.ContentSupport.Web.Tests/Dfe.ContentSupport.Web.Tests.csproj @@ -11,20 +11,21 @@ - - - - - - + + + + + + + - + - +