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
+
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+