Skip to content

Commit

Permalink
Initial booking questions implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
RogerHowellDfE committed Nov 27, 2023
1 parent 57efb80 commit 53c7526
Show file tree
Hide file tree
Showing 100 changed files with 7,099 additions and 170 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

namespace ServiceAssessmentService.WebApp.Test;

public class BasicWebTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;

public BasicWebTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}

public static IEnumerable<object[]> SimpleUrls =>
new List<object[]>
{
new object[] { "/" },
new object[] { "/AccessibilityStatement" },
new object[] { "/CookiePolicy" },
new object[] { "/Privacy" },
new object[] { "/Dashboard" },
new object[] { "/Book/BookingRequest/Index" },
// new object[] { "/Book/Request/AssessmentType/Index" },
};

[Theory]
[MemberData(nameof(SimpleUrls))]
public async Task StatusCodeMustIndicateSuccessResponse(string url)
{
// Arrange
var client = _factory.CreateClient();

// Act
var response = await client.GetAsync(url);

// Assert
using (new AssertionScope())
{
response.IsSuccessStatusCode.Should().BeTrue(); // Status Code 200-299

// Redundant additional checks, but provides extra information in case of failure.
int statusCode = (int)response.StatusCode;
statusCode.Should().BeGreaterThanOrEqualTo(200);
statusCode.Should().BeLessThanOrEqualTo(299);

response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
}
}

[Theory]
[MemberData(nameof(SimpleUrls))]
public async Task ContentTypeMustBeTextHtmlUtf8(string url)
{
// Arrange
var client = _factory.CreateClient();

// Act
var response = await client.GetAsync(url);

// Assert
response.Content?.Headers?.ContentType?.ToString()
.Should().Be("text/html; charset=utf-8");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using ServiceAssessmentService.WebApp.Core;
using ServiceAssessmentService.WebApp.Models;
using System.Diagnostics.CodeAnalysis;
using ServiceAssessmentService.WebApp.Services.Book;

namespace ServiceAssessmentService.WebApp.Test.Book;

/// <summary>
/// Stub fake booking request read service.
/// All implementations of interface methods throw NotImplementedException,
/// and are virtual so that they may be overridden as required.
/// </summary>
[ExcludeFromCodeCoverage]
public class FakeNotImplementedBookingRequestReadService : IBookingRequestReadService
{
public Task<IEnumerable<IncompleteBookingRequest>> GetAllAssessments()
{
throw new NotImplementedException();
}

public virtual Task<IncompleteBookingRequest?> GetByIdAsync(BookingRequestId id)
{
throw new NotImplementedException();
}

public Task<List<DateOnly>> AvailableReviewDates(BookingRequestId id)
{
throw new NotImplementedException();
}

public virtual Task<IEnumerable<Phase>> GetPhases()
{
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using ServiceAssessmentService.WebApp.Core;
using ServiceAssessmentService.WebApp.Models;
using System.Diagnostics.CodeAnalysis;
using ServiceAssessmentService.WebApp.Services.Book;

namespace ServiceAssessmentService.WebApp.Test.Book;

/// <summary>
/// Stub fake booking request write service.
/// All implementations of interface methods throw NotImplementedException,
/// and are virtual so that they may be overridden as required.
/// </summary>
[ExcludeFromCodeCoverage]
public class FakeNotImplementedBookingRequestWriteService : IBookingRequestWriteService
{

public virtual Task<IncompleteBookingRequest> CreateRequestAsync(Phase phaseConcluding, AssessmentType assessmentType)
{
throw new NotImplementedException();
}

public virtual Task<ChangeRequestModel> UpdateRequestName(BookingRequestId id, string proposedName)
{
throw new NotImplementedException();
}

public virtual Task<ChangeRequestModel> UpdateDescription(BookingRequestId id, string proposedDescription)
{
throw new NotImplementedException();
}

public virtual Task<ChangeRequestModel> UpdateProjectCode(BookingRequestId id, bool? isProjectCodeKnown,
string proposedProjectCode)
{
throw new NotImplementedException();
}

public Task<ChangeRequestModel> UpdateStartDate(BookingRequestId id, string? proposedYear, string? proposedMonth, string? proposedDayOfMonth)
{
throw new NotImplementedException();
}

public Task<ChangeRequestModel> UpdateEndDate(BookingRequestId id, bool? isEndDateKnown, string? proposedYear, string? proposedMonth,
string? proposedDayOfMonth)
{
throw new NotImplementedException();
}

public Task<ChangeRequestModel> UpdateReviewDates(BookingRequestId id, List<DateOnly> proposedReviewDates)
{
throw new NotImplementedException();
}

public Task<ChangeRequestModel> UpdateReviewDate(BookingRequestId bookingRequestId, DateOnly? proposedReviewDate)
{
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using ServiceAssessmentService.WebApp.Models;
using System.Diagnostics.CodeAnalysis;
using ServiceAssessmentService.WebApp.Services.Lookups;

namespace ServiceAssessmentService.WebApp.Test.Book;

/// <summary>
/// Stub fake booking request read service.
/// All implementations of interface methods throw NotImplementedException,
/// and are virtual so that they may be overridden as required.
/// </summary>
[ExcludeFromCodeCoverage]
public class FakeNotImplementedLookupsReadService : ILookupsReadService
{
public virtual Task<IEnumerable<Phase>> GetPhases()
{
throw new NotImplementedException();
}

public virtual Task<IEnumerable<AssessmentType>> GetAssessmentTypes()
{
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using ServiceAssessmentService.WebApp.Controllers.Book;
using ServiceAssessmentService.WebApp.Models;
using ServiceAssessmentService.WebApp.Services.Lookups;
using ServiceAssessmentService.WebApp.Test.TestHelpers;
using Xunit;

namespace ServiceAssessmentService.WebApp.Test.Book.Request;

public class QuestionAssessmentTypePostTests : IClassFixture<WebApplicationFactory<Program>>
{
private const string Url = "/Book/BookingRequest/AssessmentType";

private readonly WebApplicationFactory<Program> _factory;

private static readonly IEnumerable<AssessmentType> SampleAssessmentTypes = new List<AssessmentType>
{
new() { Id = "assessment_type_30", DisplayNameLowerCase = "Assessment Type 30", IsEnabled = false, SortOrder = 30},
new() { Id = "assessment_type_10", DisplayNameLowerCase = "Assessment Type 10", IsEnabled = true, SortOrder = 10},
new() { Id = "assessment_type_20", DisplayNameLowerCase = "Assessment Type 20", IsEnabled = false, SortOrder = 20},
};

public QuestionAssessmentTypePostTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}


private WebApplicationFactory<Program> FactoryWithFakeApiReturningThreeAssessmentTypes => _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<ILookupsReadService, FakeLookupsReadServiceThreeAssessmentTypes>();
});
});



[Fact]
public async Task GivenApiDefiningAssessmentTypes_WhenSubmittingValidValues_ResponseCodeMustBeRedirect()
{
// Arrange
var client = FactoryWithFakeApiReturningThreeAssessmentTypes
.CreateClient(
new WebApplicationFactoryClientOptions
{
// On form submission, should receive POST and redirect -- do not follow this redirect
AllowAutoRedirect = false,
}
);

// Must perform post after a get, to first generate/fetch/include the CSRF token (used within the subsequent post)
var responseGet = await client.GetAsync(Url);
var contentGet = await HtmlHelpers.GetDocumentAsync(responseGet);

// Get the (single) form/submit button from within the page body (ignoring, e.g., forms within the header/footer).
// If more than one is found, this is a problem and should be flagged as an error.
var mainPageContentSection = contentGet.QuerySelectorAll(".main--content").Single();
var formElement = mainPageContentSection.QuerySelectorAll<IHtmlFormElement>("form").Single();
var submitButtonElement = formElement.QuerySelectorAll<IHtmlButtonElement>("button[type='submit']").Single();

// Set one of the values to "checked" (i.e., radio button is selected)
var arbitraryIdFromApi = SampleAssessmentTypes.Where(x => x.IsEnabled).Take(1).Single().Id;
var radioElements = formElement.QuerySelectorAll<IHtmlInputElement>("input[name='" + AssessmentTypeDto.FormName + "']");
var peerReviewRadioElement = radioElements.Single(x => arbitraryIdFromApi.Equals(x.GetAttribute("value"), StringComparison.Ordinal));
peerReviewRadioElement.IsChecked = true;


// Act
var postResponse = await client.SendAsync(formElement, submitButtonElement);

// Assert
// TODO: Consider extracting this out to helper method, to avoid duplication across tests?
using (new AssertionScope())
{
postResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.Redirect, "because a successful form submission should accept/process the POST then redirect to the next page (preventing the user from refreshing the browser and re-submitting data)");

// Redundant additional checks, but provides extra information in case of failure.
int statusCode = (int)postResponse.StatusCode;
statusCode.Should().NotBe(400, "because if this is returned, it indicates the form value was not received (likely developer/test error, a mistake in setting up the fake API responses perhaps? note that if the radio/option is disabled, the value does not get submitted with the form)");
statusCode.Should().NotBe(422, "because if this is returned, an unexpected/unprocessable form value was submitted (likely developer/test error)");
}
}

// TODO: Validate where the form redirects to on a successful submit
// - should be the next page in the flow, e.g., the "request started" question page or the "select project phase" question page (if implemented)

// TODO: Consider side-effects (trigger of a write API request)
// - Current flow is that a successful submit creates a new assessment request
// - Next implementations will ask about the phase (which may/may not defer creating a new assessment request)

// TODO: Error cases
// - No form value
// - Wholly inappropriate value
// - Normally valid but disabled value
// - Check what error messages get returned in each of the above cases
// - Missing CSRF token
// - Invalid CSRF token
// - Re-used CSRF token



public class FakeLookupsReadServiceThreeAssessmentTypes : FakeNotImplementedLookupsReadService
{
public override Task<IEnumerable<AssessmentType>> GetAssessmentTypes()
{
return Task.FromResult(SampleAssessmentTypes);
}
}

}
Loading

0 comments on commit 53c7526

Please sign in to comment.