diff --git a/.editorconfig b/.editorconfig index 801f4f7..732666a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,9 @@ dotnet_diagnostic.CS8604.severity = none # CS1591: Missing XML comment for publicly visible type or member dotnet_diagnostic.CS1591.severity = none + +# CS8629: Nullable value type may be null. +dotnet_diagnostic.CS8629.severity = silent + +# CS8602: Dereference of a possibly null reference. +dotnet_diagnostic.CS8602.severity = silent diff --git a/client/client.esproj b/client/client.esproj index 2fe46fe..5d60ada 100644 --- a/client/client.esproj +++ b/client/client.esproj @@ -1,10 +1,10 @@ - - 8fc072ca-af79-4930-b9b1-139230dc3426 - - - set BROWSER=none&&npm start - src\ - Jest - + + 8fc072ca-af79-4930-b9b1-139230dc3426 + + + set BROWSER=none&&npm start + src\ + Jest + \ No newline at end of file diff --git a/client/src/pages/home.js b/client/src/pages/home.js index 3cd59be..3605abc 100644 --- a/client/src/pages/home.js +++ b/client/src/pages/home.js @@ -55,6 +55,8 @@ export default function Home() { const handleTryAgain = async () => { + setErrorMessage(null); + if (errorMessage === 'Failed to load characters.') return await getCharacters(); } diff --git a/criminalmindsapi.sln b/criminalmindsapi.sln index 71e421e..c1fb9bb 100644 --- a/criminalmindsapi.sln +++ b/criminalmindsapi.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server.tests", "server.tests\server.tests.csproj", "{8B86105B-F6F4-49A9-B384-C2823F693F38}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC}.Debug|Any CPU.Build.0 = Debug|Any CPU {1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {1EF6EB3C-FCA0-4FF6-8A99-373AFAC6E4EC}.Release|Any CPU.Build.0 = Release|Any CPU + {8B86105B-F6F4-49A9-B384-C2823F693F38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B86105B-F6F4-49A9-B384-C2823F693F38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B86105B-F6F4-49A9-B384-C2823F693F38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B86105B-F6F4-49A9-B384-C2823F693F38}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/server.tests/Usings.cs b/server.tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/server.tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/server.tests/integrationTests/CharactersControllerIntegrationTests.cs b/server.tests/integrationTests/CharactersControllerIntegrationTests.cs new file mode 100644 index 0000000..9afc85a --- /dev/null +++ b/server.tests/integrationTests/CharactersControllerIntegrationTests.cs @@ -0,0 +1,159 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Testing; +using server.Models; +using System.Collections; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; + +namespace server.tests.integrationTests +{ + public class CharactersControllerIntegrationTests + { + private readonly HttpClient _client; + private readonly JsonSerializerOptions _serializerOptions; + private readonly string _endpoint; + + public CharactersControllerIntegrationTests() + { + var webAppFactory = new WebApplicationFactory(); + + _client = webAppFactory.CreateDefaultClient(); + + _client.DefaultRequestHeaders.Add("x-api-version", "1"); + + _serializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + _endpoint = "/api/characters"; + } + + [Fact] + public async Task GetCharactersAsync_AllCharacters_Returns200StatusCodeWithCharacters() + { + var response = await _client.GetAsync(_endpoint); + + var data = await response.Content.ReadAsStreamAsync(); + + var characters = JsonSerializer.Deserialize>(data); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.Should().ContainKey("X-Rate-Limit-Limit"); + response.Headers.Should().ContainKey("X-Rate-Limit-Remaining"); + response.Headers.Should().ContainKey("X-Rate-Limit-Reset"); + + characters.Should().NotBeNull(); + characters.Should().BeOfType>(); + characters.Should().HaveCountGreaterThan(0); + } + + [Fact] + public async Task GetCharactersAsync_SeasonOneCharacters_Returns200StatusCodeWithCharacters() + { + var seasonValue = 1; + + var url = $"{_endpoint}?season={seasonValue}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var characters = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.Should().ContainKey("X-Rate-Limit-Limit"); + response.Headers.Should().ContainKey("X-Rate-Limit-Remaining"); + response.Headers.Should().ContainKey("X-Rate-Limit-Reset"); + + characters.Should().NotBeNull(); + characters.Should().BeOfType>(); + characters.Should().HaveCountGreaterThan(0); + + foreach (var character in characters) + { + character.Seasons.Should().Contain(seasonValue); + } + } + + [Fact] + public async Task GetCharactersAsync_InvalidSeasonQueryParameter_Returns400StatusCodeWithValidationProblemDetails() + { + var seasonValue = "test"; + + var url = $"{_endpoint}?season={seasonValue}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + response.Headers.Should().ContainKey("X-Rate-Limit-Limit"); + response.Headers.Should().ContainKey("X-Rate-Limit-Remaining"); + response.Headers.Should().ContainKey("X-Rate-Limit-Reset"); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + + [Fact] + public async Task GetCharactersAsync_NameContainsJason_Returns200StatusCodeWithCharacters() + { + var nameValue = "jason"; + + var url = $"{_endpoint}?name={nameValue}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var characters = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.Should().ContainKey("X-Rate-Limit-Limit"); + response.Headers.Should().ContainKey("X-Rate-Limit-Remaining"); + response.Headers.Should().ContainKey("X-Rate-Limit-Reset"); + + characters.Should().NotBeNull(); + characters.Should().BeOfType>(); + characters.Should().HaveCountGreaterThan(0); + + foreach (var character in characters) + { + character.FullName.ToLower().Should().Contain(nameValue); + } + } + + [Fact] + public async Task GetCharactersAsync_ActorNameContainsMandy_Returns200StatusCodeWithCharacters() + { + var actorNameValue = "mandy"; + + var url = $"{_endpoint}?actorname={actorNameValue}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var characters = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Headers.Should().ContainKey("X-Rate-Limit-Limit"); + response.Headers.Should().ContainKey("X-Rate-Limit-Remaining"); + response.Headers.Should().ContainKey("X-Rate-Limit-Reset"); + + characters.Should().NotBeNull(); + characters.Should().BeOfType>(); + characters.Should().HaveCountGreaterThan(0); + + foreach (var character in characters) + { + character.ActorFullName.ToLower().Should().Contain(actorNameValue); + } + } + } +} diff --git a/server.tests/server.tests.csproj b/server.tests/server.tests.csproj new file mode 100644 index 0000000..a00c304 --- /dev/null +++ b/server.tests/server.tests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/server.tests/unitTests/CharacterControllerUnitTests.cs b/server.tests/unitTests/CharacterControllerUnitTests.cs new file mode 100644 index 0000000..929857c --- /dev/null +++ b/server.tests/unitTests/CharacterControllerUnitTests.cs @@ -0,0 +1,70 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Moq; +using server.Controllers.v1; +using server.Models; +using server.Persistence.Repositories; +using System.Net; + +namespace server.tests.unitTests +{ + public class CharacterControllerUnitTests + { + private readonly Mock _mockRepo; + private readonly CharactersController _controller; + + public CharacterControllerUnitTests() + { + _mockRepo = new Mock(); + _controller = new CharactersController(_mockRepo.Object); + } + + [Fact] + public async Task GetCharactersAsync_AllCharacters_Returns200StatusCodeWithCharactersCollection() + { + var filter = new CharacterFilter(); + + var characters = new List {new Character(), new Character()}; + + _mockRepo + .Setup(repo => repo.GetCharactersAsync(filter)) + .ReturnsAsync(characters); + + var response = await _controller.GetCharactersAsync(filter) as ObjectResult; + var data = response.Value as List; + + _mockRepo.Verify(repo => repo.GetCharactersAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.OK); + + data.Should().NotBeNull(); + data.Should().BeOfType>(); + data.Should().HaveCount(2); + } + + [Fact] + public async Task GetCharactersAsync_RepoThrowsException_Returns500StatusCodeWithProblemDetails() + { + var filter = new CharacterFilter(); + + _mockRepo + .Setup(repo => repo.GetCharactersAsync(filter)) + .Throws(new Exception()); + + var response = await _controller.GetCharactersAsync(filter) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetCharactersAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.InternalServerError); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + } +} diff --git a/server/Controllers/v1/CharactersController.cs b/server/Controllers/v1/CharactersController.cs index 9e1c3e6..ddc3fdf 100644 --- a/server/Controllers/v1/CharactersController.cs +++ b/server/Controllers/v1/CharactersController.cs @@ -31,7 +31,7 @@ namespace server.Controllers.v1 [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public async Task>> GetCharactersAsync([FromQuery] CharacterFilter? filter) + public async Task GetCharactersAsync([FromQuery] CharacterFilter? filter) { try { @@ -60,7 +60,7 @@ namespace server.Controllers.v1 [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public async Task> GetCharacterByIdAsync(string id) + public async Task GetCharacterByIdAsync(string id) { if (!ObjectId.TryParse(id, out _)) diff --git a/server/Controllers/v1/EpisodesController.cs b/server/Controllers/v1/EpisodesController.cs index dbb131c..7ee0864 100644 --- a/server/Controllers/v1/EpisodesController.cs +++ b/server/Controllers/v1/EpisodesController.cs @@ -30,7 +30,7 @@ namespace server.Controllers.v1 [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public async Task>> GetEpisodesAsync([FromQuery] EpisodeFilter? filter) + public async Task GetEpisodesAsync([FromQuery] EpisodeFilter? filter) { try { @@ -59,7 +59,7 @@ namespace server.Controllers.v1 [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public async Task> GetEpisodeByNumberAsync(int number) + public async Task GetEpisodeByNumberAsync(int number) { try { diff --git a/server/Controllers/v1/QuotesController.cs b/server/Controllers/v1/QuotesController.cs index e8f838c..a5a18d6 100644 --- a/server/Controllers/v1/QuotesController.cs +++ b/server/Controllers/v1/QuotesController.cs @@ -32,7 +32,7 @@ namespace server.Controllers.v1 [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public async Task>> GetQuotesAsync([FromQuery] QuoteFilter? filter) + public async Task GetQuotesAsync([FromQuery] QuoteFilter? filter) { try { @@ -61,7 +61,7 @@ namespace server.Controllers.v1 [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public async Task> GetQuoteByIdAsync(string id) + public async Task GetQuoteByIdAsync(string id) { if (!ObjectId.TryParse(id, out _)) diff --git a/server/Controllers/v1/SeasonsController.cs b/server/Controllers/v1/SeasonsController.cs index fd84db7..5dfdd41 100644 --- a/server/Controllers/v1/SeasonsController.cs +++ b/server/Controllers/v1/SeasonsController.cs @@ -27,7 +27,7 @@ namespace server.Controllers.v1 [HttpGet] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public async Task>> GetSeasonsAsync() + public async Task GetSeasonsAsync() { try { @@ -53,7 +53,7 @@ namespace server.Controllers.v1 [ProducesResponseType(typeof(Season), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public async Task> GetSeasonByNumberAsync(int number) + public async Task GetSeasonByNumberAsync(int number) { try { diff --git a/server/server.csproj b/server/server.csproj index e3944a6..d48351d 100644 --- a/server/server.csproj +++ b/server/server.csproj @@ -1,22 +1,27 @@  - - net6.0 - enable - enable - True - + + net6.0 + enable + enable + True + - - - + + + - - - - - - - + + + + + + + + + + + +