Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[draft/reference for smaller PRs] Initial booking questions implementation #126

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,12 @@ root = true
[*.cshtml]
indent_style = space
indent_size = 4
insert_final_newline = true
insert_final_newline = true

[*.cs]
indent_style = space
indent_size = 4
insert_final_newline = true

## CA1848: For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])'
dotnet_diagnostic.CA1848.severity = none
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ jobs:

- name: Test
working-directory: ./src/ServiceAssessmentService
run: dotnet test --no-build --verbosity normal
run: dotnet test --no-build --configuration Release --verbosity normal


- name: End SonarCloud analysis and upload results (end after all build/test steps)
Expand Down
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,69 @@
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> UpdatePortfolio(BookingRequestId bookingRequestId, string dtoValue)
{
throw new NotImplementedException();
}

public Task<ChangeRequestModel> UpdateDeputyDirector(BookingRequestId bookingRequestId, string proposedDeputyDirectorName,
string proposedDeputyDirectorEmail)
{
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,29 @@
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();
}

public Task<IEnumerable<Portfolio>> GetPortfolioOptions()
{
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