diff --git a/.editorconfig b/.editorconfig index 732666a..fc1783b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,6 @@ dotnet_diagnostic.CS8629.severity = silent # CS8602: Dereference of a possibly null reference. dotnet_diagnostic.CS8602.severity = silent + +# CS8620: Argument cannot be used for parameter due to differences in the nullability of reference types. +dotnet_diagnostic.CS8620.severity = silent diff --git a/client/package-lock.json b/client/package-lock.json index 6fe2545..4665093 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -23,11 +23,13 @@ "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "terser": ">=5.14.2", "web-vitals": "^2.1.4" }, "devDependencies": { "eslint-config-react-app": "^7.0.1", - "jest-editor-support": "^30.1.0" + "jest-editor-support": "^30.1.0", + "terser": ">=5.14.2" } }, "node_modules/@ampproject/remapping": { @@ -15455,9 +15457,9 @@ } }, "node_modules/terser": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz", - "integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", @@ -27758,9 +27760,9 @@ } }, "terser": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz", - "integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "requires": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", diff --git a/client/package.json b/client/package.json index 441fb34..8887f33 100644 --- a/client/package.json +++ b/client/package.json @@ -22,10 +22,12 @@ "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "terser": ">=5.14.2" }, "scripts": { "start": "set HTTPS=true&&react-scripts start", + "dev": "start npm start && cd ../server && start dotnet run", "build": "CI=false && react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", @@ -51,6 +53,7 @@ }, "devDependencies": { "eslint-config-react-app": "^7.0.1", - "jest-editor-support": "^30.1.0" + "jest-editor-support": "^30.1.0", + "terser": ">=5.14.2" } } diff --git a/server.tests/Helpers/AssertHelper.cs b/server.tests/Helpers/AssertHelper.cs new file mode 100644 index 0000000..bfa085a --- /dev/null +++ b/server.tests/Helpers/AssertHelper.cs @@ -0,0 +1,15 @@ +using FluentAssertions; +using System.Net.Http.Headers; + +namespace server.tests.Helpers +{ + internal static class AssertHelper + { + public static void CheckForRateLimitingHeaders(HttpResponseHeaders headers) + { + headers.Should().ContainKey("X-Rate-Limit-Limit"); + headers.Should().ContainKey("X-Rate-Limit-Remaining"); + headers.Should().ContainKey("X-Rate-Limit-Reset"); + } + } +} diff --git a/server.tests/Http/HttpClientFactory.cs b/server.tests/Http/HttpClientFactory.cs new file mode 100644 index 0000000..fbfb464 --- /dev/null +++ b/server.tests/Http/HttpClientFactory.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc.Testing; + +namespace server.tests.Http +{ + internal static class HttpClientFactory + { + public static HttpClient GetClient(int version) + { + var webAppFactory = new WebApplicationFactory(); + + var client = webAppFactory.CreateDefaultClient(); + + client.DefaultRequestHeaders.Add("x-api-version", version.ToString()); + + return client; + } + } +} diff --git a/server.tests/Options/OptionsFactory.cs b/server.tests/Options/OptionsFactory.cs new file mode 100644 index 0000000..bf62a4c --- /dev/null +++ b/server.tests/Options/OptionsFactory.cs @@ -0,0 +1,15 @@ +using System.Text.Json; + +namespace server.tests.Options +{ + internal static class OptionsFactory + { + public static JsonSerializerOptions GetJsonSerializerOptions() + { + return new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + } + } +} diff --git a/server.tests/integrationTests/CharactersControllerIntegrationTests.cs b/server.tests/integrationTests/CharactersControllerIntegrationTests.cs deleted file mode 100644 index 9afc85a..0000000 --- a/server.tests/integrationTests/CharactersControllerIntegrationTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -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/unitTests/CharacterControllerUnitTests.cs b/server.tests/unitTests/CharacterControllerUnitTests.cs deleted file mode 100644 index 929857c..0000000 --- a/server.tests/unitTests/CharacterControllerUnitTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -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.tests/v1/integrationTests/CharactersControllerIntegrationTests.cs b/server.tests/v1/integrationTests/CharactersControllerIntegrationTests.cs new file mode 100644 index 0000000..7d81348 --- /dev/null +++ b/server.tests/v1/integrationTests/CharactersControllerIntegrationTests.cs @@ -0,0 +1,214 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; +using server.Models; +using server.tests.Helpers; +using server.tests.Http; +using server.tests.Options; +using System.Net; +using System.Text.Json; + +namespace server.tests.v1.IntegrationTests +{ + public class CharactersControllerIntegrationTests + { + private readonly HttpClient _client; + private readonly JsonSerializerOptions _serializerOptions; + private readonly string _endpoint; + + public CharactersControllerIntegrationTests() + { + _client = HttpClientFactory.GetClient(1); + + _serializerOptions = OptionsFactory.GetJsonSerializerOptions(); + + _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, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + characters.Should().NotBeNull(); + characters.Should().BeOfType>(); + characters.Should().HaveCountGreaterThan(0); + } + + [Fact] + public async Task GetCharactersAsync_SeasonOneCharacters_Returns200StatusCodeWithCharacters() + { + var season = 1; + + var url = $"{_endpoint}?{nameof(season)}={season}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var characters = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + characters.Should().NotBeNull(); + characters.Should().BeOfType>(); + characters.Should().HaveCountGreaterThan(0); + + foreach (var character in characters) + { + character.Seasons.Should().Contain(season); + } + } + + [Fact] + public async Task GetCharactersAsync_InvalidSeasonQueryParameter_Returns400StatusCodeWithValidationProblemDetails() + { + var season = "test"; + + var url = $"{_endpoint}?{nameof(season)}={season}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetCharactersAsync_NameContainsJason_Returns200StatusCodeWithCharacters() + { + var name = "jason"; + + var url = $"{_endpoint}?{nameof(name)}={name}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var characters = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + characters.Should().NotBeNull(); + characters.Should().BeOfType>(); + characters.Should().HaveCountGreaterThan(0); + + foreach (var character in characters) + { + character.FullName.ToLower().Should().Contain(name); + } + } + + [Fact] + public async Task GetCharactersAsync_ActorNameContainsMandy_Returns200StatusCodeWithCharacters() + { + var actorName = "mandy"; + + var url = $"{_endpoint}?{nameof(actorName)}={actorName}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var characters = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + characters.Should().NotBeNull(); + characters.Should().BeOfType>(); + characters.Should().HaveCountGreaterThan(0); + + foreach (var character in characters) + { + character.ActorFullName.ToLower().Should().Contain(actorName); + } + } + + [Fact] + public async Task GetCharacterByIdAsync_ValidCharacterId_Returns200StatusCodeWithCharacter() + { + var response = await _client.GetAsync(_endpoint); + + var data = await response.Content.ReadAsStreamAsync(); + + var characters = JsonSerializer.Deserialize>(data, _serializerOptions); + + var characterId = characters[0].Id; + + var url = $"{_endpoint}/{characterId}"; + + response = await _client.GetAsync(url); + + data = await response.Content.ReadAsStreamAsync(); + + var character = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + character.Should().NotBeNull(); + character.Should().BeOfType(); + character.Id.Should().Be(characterId); + } + + [Fact] + public async Task GetCharacterByIdAsync_InvalidCharacterId_Returns400StatusCodeWithValidationProblemDetails() + { + var characterId = "1"; + + var url = $"{_endpoint}/{characterId}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Detail.Should().NotBeNull(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetCharacterByIdAsync_ValidCharacterIdForNonExistentCharacter_Returns404StatusCodeWithProblemDetails() + { + var characterId = ObjectId.GenerateNewId(); + + var url = $"{_endpoint}/{characterId}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Detail.Should().NotBeNull(); + } + } +} diff --git a/server.tests/v1/integrationTests/EpisodesControllerIntegrationTests.cs b/server.tests/v1/integrationTests/EpisodesControllerIntegrationTests.cs new file mode 100644 index 0000000..0abd42b --- /dev/null +++ b/server.tests/v1/integrationTests/EpisodesControllerIntegrationTests.cs @@ -0,0 +1,437 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using server.Models; +using server.tests.Helpers; +using server.tests.Http; +using server.tests.Options; +using System.Net; +using System.Text.Json; + +namespace server.tests.v1.IntegrationTests +{ + public class EpisodesControllerIntegrationTests + { + private readonly HttpClient _client; + private readonly JsonSerializerOptions _serializerOptions; + private readonly string _endpoint; + + public EpisodesControllerIntegrationTests() + { + _client = HttpClientFactory.GetClient(1); + + _serializerOptions = OptionsFactory.GetJsonSerializerOptions(); + + _endpoint = "/api/episodes"; + } + + [Fact] + public async Task GetEpisodesAsync_AllEpisodes_Returns200StatusCodeWithEpisodes() + { + var response = await _client.GetAsync(_endpoint); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + } + + [Fact] + public async Task GetEpisodesAsync_SeasonOneEpisodes_Returns200StatusCodeWithEpisodes() + { + var season = 1; + + var url = $"{_endpoint}?{nameof(season)}={season}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + + foreach (var episode in episodes) + { + episode.Season.Should().Be(season); + } + } + + [Fact] + public async Task GetEpisodesAsync_InvalidSeasonQueryParameter_Returns400StatusCodeWithValidationProblemDetails() + { + var season = "test"; + + var url = $"{_endpoint}?{nameof(season)}={season}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetEpisodesAsync_EpisodesAfterJanuary2020_Returns200StatusCodeWithEpisodes() + { + var startDate = new DateTime(2020, 1, 1); + + var url = $"{_endpoint}?{nameof(startDate)}={startDate}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + + foreach (var episode in episodes) + { + episode.AirDate.Should().BeOnOrAfter(startDate); + } + } + + [Fact] + public async Task GetEpisodesAsync_InvalidStartDateQueryParam_Returns400StatusCodeWithValidationProblemDetails() + { + var startDate = "test"; + + var url = $"{_endpoint}?{nameof(startDate)}={startDate}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetEpisodesAsync_EpisodesBeforeJanuary2020_Returns200StatusCodeWithEpisodes() + { + var endDate = new DateTime(2020, 1, 1); + + var url = $"{_endpoint}?{nameof(endDate)}={endDate}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + + foreach (var episode in episodes) + { + episode.AirDate.Should().BeOnOrBefore(endDate); + } + } + + [Fact] + public async Task GetEpisodesAsync_InvalidEndDateQueryParam_Returns400StatusCodeWithValidationProblemDetails() + { + var endDate = "test"; + + var url = $"{_endpoint}?{nameof(endDate)}={endDate}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetEpisodesAsync_EpisodeTitlesThatContainThe_Returns200StatusCodeWithEpisodes() + { + var title = "the"; + + var url = $"{_endpoint}?{nameof(title)}={title}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + + foreach (var episode in episodes) + { + episode.Title.ToLower().Should().Contain(title); + } + } + + [Fact] + public async Task GetEpisodesAsync_EpisodeSummariesThatContainFoyet_Returns200StatusCodeWithEpisodes() + { + var summaryKeyword = "foyet"; + + var url = $"{_endpoint}?{nameof(summaryKeyword)}={summaryKeyword}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + + foreach (var episode in episodes) + { + episode.Summary.ToLower().Should().Contain(summaryKeyword); + } + } + + [Fact] + public async Task GetEpisodesAsync_EpisodesDirectedByCharles_Returns200StatusCodeWithEpisodes() + { + var directedBy = "charles"; + + var url = $"{_endpoint}?{nameof(directedBy)}={directedBy}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + + foreach (var episode in episodes) + { + episode.DirectedBy.ToLower().Should().Contain(directedBy); + } + } + + [Fact] + public async Task GetEpisodesAsync_EpisodesWrittenByBreen_Returns200StatusCodeWithEpisodes() + { + var writtenBy = "breen"; + + var url = $"{_endpoint}?{nameof(writtenBy)}={writtenBy}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + + foreach (var episode in episodes) + { + episode.WrittenBy.Any(writer => writer.ToLower().Contains(writtenBy)).Should().BeTrue(); + } + } + + [Fact] + public async Task GetEpisodesAsync_EpisodesWithGreaterThan12MillionUsViewers_Returns200StatusCodeWithEpisodes() + { + var viewersRangeStart = 12.0; + + var url = $"{_endpoint}?{nameof(viewersRangeStart)}={viewersRangeStart}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + + foreach (var episode in episodes) + { + episode.UsViewersInMillions.Should().BeGreaterThanOrEqualTo(viewersRangeStart); + } + } + + [Fact] + public async Task GetEpisodesAsync_InvalidViewersRangeStartQueryParam_Returns400StatusCodeWithValidationProblemDetails() + { + var viewersRangeStart = "test"; + + var url = $"{_endpoint}?{nameof(viewersRangeStart)}={viewersRangeStart}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetEpisodesAsync_EpisodesWithLessThan12MillionUsViewers_Returns200StatusCodeWithEpisodes() + { + var viewersRangeEnd = 12.0; + + var url = $"{_endpoint}?{nameof(viewersRangeEnd)}={viewersRangeEnd}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episodes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episodes.Should().NotBeNull(); + episodes.Should().BeOfType>(); + episodes.Should().HaveCountGreaterThan(0); + + foreach (var episode in episodes) + { + episode.UsViewersInMillions.Should().BeLessThanOrEqualTo(viewersRangeEnd); + } + } + + [Fact] + public async Task GetEpisodesAsync_InvalidViewersRangeEndQueryParam_Returns400StatusCodeWithValidationProblemDetails() + { + var viewersRangeEnd = "test"; + + var url = $"{_endpoint}?{nameof(viewersRangeEnd)}={viewersRangeEnd}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetEpisodeByNumberAsync_ValidEpisodeNumber_Returns200StatusCodeWithEpisode() + { + var episodeNumber = 1; + + var url = $"{_endpoint}/{episodeNumber}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var episode = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + episode.Should().NotBeNull(); + episode.Should().BeOfType(); + episode.NumberInSeries.Should().Be(episodeNumber); + } + + [Fact] + public async Task GetEpisodeByNumberAsync_InvalidEpisodeNumber_Returns404StatusCode() + { + var episodeNumber = "test"; + + var url = $"{_endpoint}/{episodeNumber}"; + + var response = await _client.GetAsync(url); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + } + + [Fact] + public async Task GetEpisodeByNumberAsync_ValidEpisodeNumberForNonExistentEpisode_Returns404StatusCodeWithProblemDetails() + { + var episodeNumber = 10000; + + var url = $"{_endpoint}/{episodeNumber}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + } +} diff --git a/server.tests/v1/integrationTests/QuotesControllerIntegrationTests.cs b/server.tests/v1/integrationTests/QuotesControllerIntegrationTests.cs new file mode 100644 index 0000000..c3c029b --- /dev/null +++ b/server.tests/v1/integrationTests/QuotesControllerIntegrationTests.cs @@ -0,0 +1,287 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; +using server.Models; +using server.tests.Helpers; +using server.tests.Http; +using server.tests.Options; +using System.Net; +using System.Text.Json; + +namespace server.tests.v1.IntegrationTests +{ + public class QuotesControllerIntegrationTests + { + private readonly HttpClient _client; + private readonly JsonSerializerOptions _serializerOptions; + private readonly string _endpoint; + + public QuotesControllerIntegrationTests() + { + _client = HttpClientFactory.GetClient(1); + + _serializerOptions = OptionsFactory.GetJsonSerializerOptions(); + + _endpoint = "/api/quotes"; + } + + [Fact] + public async Task GetQuotesAsync_AllQuotes_Returns200StatusCodeWithQuotes() + { + var response = await _client.GetAsync(_endpoint); + + var data = await response.Content.ReadAsStreamAsync(); + + var characters = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + characters.Should().NotBeNull(); + characters.Should().BeOfType>(); + characters.Should().HaveCountGreaterThan(0); + } + + [Fact] + public async Task GetQuotesAsync_SeasonOneQuotes_Returns200StatusCodeWithQoutes() + { + var season = 1; + + var url = $"{_endpoint}?{nameof(season)}={season}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var quotes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + quotes.Should().NotBeNull(); + quotes.Should().BeOfType>(); + quotes.Should().HaveCountGreaterThan(0); + + foreach (var quote in quotes) + { + quote.Season.Should().Be(season); + } + } + + [Fact] + public async Task GetQuotesAsync_InvalidSeasonQueryParameter_Returns400StatusCodeWithValidationProblemDetails() + { + var season = "test"; + + var url = $"{_endpoint}?{nameof(season)}={season}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetQuotesAsync_EpisodeOneQuotes_Returns200StatusCodeWithQuotes() + { + var episode = 1; + + var url = $"{_endpoint}?{nameof(episode)}={episode}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var quotes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + quotes.Should().NotBeNull(); + quotes.Should().BeOfType>(); + quotes.Should().HaveCountGreaterThan(0); + + foreach (var quote in quotes) + { + quote.Episode.Should().Be(episode); + } + } + + [Fact] + public async Task GetQuotesAsync_InvalidEpisodeQueryParameter_Returns400StatusCodeWithValidationProblemDetails() + { + var episode = "test"; + + var url = $"{_endpoint}?{nameof(episode)}={episode}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetQuotesAsync_QuotesThatContainEvil_Returns200StatusCodeWithQuotes() + { + var textKeyword = "evil"; + + var url = $"{_endpoint}?{nameof(textKeyword)}={textKeyword}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var quotes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + quotes.Should().NotBeNull(); + quotes.Should().BeOfType>(); + quotes.Should().HaveCountGreaterThan(0); + + foreach (var quote in quotes) + { + quote.Text.ToLower().Should().Contain(textKeyword); + } + } + + [Fact] + public async Task GetQuotesAsync_QuotesByYoda_Returns200StatusCodeWithQuotes() + { + var source = "yoda"; + + var url = $"{_endpoint}?{nameof(source)}={source}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var quotes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + quotes.Should().NotBeNull(); + quotes.Should().BeOfType>(); + quotes.Should().HaveCountGreaterThan(0); + + foreach (var quote in quotes) + { + quote.Source.ToLower().Should().Contain(source); + } + } + + [Fact] + public async Task GetQuotesAsync_QuotesNarratedByJason_Returns200StatusCodeWithQuotes() + { + var narrator = "jason"; + + var url = $"{_endpoint}?{nameof(narrator)}={narrator}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var quotes = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + quotes.Should().NotBeNull(); + quotes.Should().BeOfType>(); + quotes.Should().HaveCountGreaterThan(0); + + foreach (var quote in quotes) + { + quote.Narrator.ToLower().Should().Contain(narrator); + } + } + + [Fact] + public async Task GetQuotesById_ValidQuoteId_Returns200StatusCodeWithQuote() + { + var response = await _client.GetAsync(_endpoint); + + var data = await response.Content.ReadAsStreamAsync(); + + var quotes = JsonSerializer.Deserialize>(data, _serializerOptions); + + var quoteId = quotes[0].Id; + + var url = $"{_endpoint}/{quoteId}"; + + response = await _client.GetAsync(url); + + data = await response.Content.ReadAsStreamAsync(); + + var quote = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + quote.Should().NotBeNull(); + quote.Should().BeOfType(); + quote.Id.Should().Be(quoteId); + } + + [Fact] + public async Task GetQuotesById_InvalidQuoteId_Returns400StatusCodeWithValidationProblemDetails() + { + var quoteId = "1"; + + var url = $"{_endpoint}/{quoteId}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Detail.Should().NotBeNull(); + details.Errors.Should().NotBeNull(); + } + + [Fact] + public async Task GetQuoteByIdAsync_ValidQuoteIdForNonExistentQuote_Returns404StatusCodeWithProblemDetails() + { + var quoteId = ObjectId.GenerateNewId(); + + var url = $"{_endpoint}/{quoteId}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + details.Detail.Should().NotBeNull(); + } + } +} diff --git a/server.tests/v1/integrationTests/SeasonsControllerIntegrationTests.cs b/server.tests/v1/integrationTests/SeasonsControllerIntegrationTests.cs new file mode 100644 index 0000000..d4917c6 --- /dev/null +++ b/server.tests/v1/integrationTests/SeasonsControllerIntegrationTests.cs @@ -0,0 +1,99 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using server.Models; +using server.tests.Helpers; +using server.tests.Http; +using server.tests.Options; +using System.Net; +using System.Text.Json; + +namespace server.tests.v1.IntegrationTests +{ + public class SeasonsControllerIntegrationTests + { + private readonly HttpClient _client; + private readonly JsonSerializerOptions _serializerOptions; + private readonly string _endpoint; + + public SeasonsControllerIntegrationTests() + { + _client = HttpClientFactory.GetClient(1); + + _serializerOptions = OptionsFactory.GetJsonSerializerOptions(); + + _endpoint = "/api/seasons"; + } + + [Fact] + public async Task GetSeasonsAsync_AllSeasons_Returns200StatusCodeWithSeasons() + { + var response = await _client.GetAsync(_endpoint); + + var data = await response.Content.ReadAsStreamAsync(); + + var seasons = JsonSerializer.Deserialize>(data, _serializerOptions); + + response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + seasons.Should().NotBeNull(); + seasons.Should().BeOfType>(); + seasons.Should().HaveCountGreaterThan(0); + } + + [Fact] + public async Task GetSeasonByNumberAsync_SeasonOne_Returns200StatusCodeWithSeason() + { + var seasonNumber = 1; + + var url = $"{_endpoint}/{seasonNumber}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var season = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + season.Should().NotBeNull(); + season.Should().BeOfType(); + season.SeasonNumber.Should().Be(seasonNumber); + } + + [Fact] + public async Task GetSeasonByNumberAsync_InvalidSeason_Returns404StatusCode() + { + var seasonNumber = "test"; + + var url = $"{_endpoint}/{seasonNumber}"; + + var response = await _client.GetAsync(url); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + } + + [Fact] + public async Task GetSeasonByNumberAsync_ValidSeasonNumberForNonExistentSeason_Returns404StatusCode() + { + var seasonNumber = 20; + + var url = $"{_endpoint}/{seasonNumber}"; + + var response = await _client.GetAsync(url); + + var data = await response.Content.ReadAsStreamAsync(); + + var details = JsonSerializer.Deserialize(data, _serializerOptions); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + AssertHelper.CheckForRateLimitingHeaders(response.Headers); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + + } +} diff --git a/server.tests/v1/unitTests/CharactersControllerUnitTests.cs b/server.tests/v1/unitTests/CharactersControllerUnitTests.cs new file mode 100644 index 0000000..43dcab7 --- /dev/null +++ b/server.tests/v1/unitTests/CharactersControllerUnitTests.cs @@ -0,0 +1,140 @@ +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.v1.UnitTests +{ + public class CharactersControllerUnitTests + { + private readonly Mock _mockRepo; + private readonly CharactersController _controller; + + public CharactersControllerUnitTests() + { + _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(); + } + + [Fact] + public async Task GetCharacterByIdAsync_ValidCharacterId_Returns200StatusCodeWithCharacter() + { + var characterId = "62b7d5506c1b407771829926"; + + _mockRepo + .Setup(repo => repo.GetCharacterByIdAsync(characterId)) + .ReturnsAsync(new Character()); + + var response = await _controller.GetCharacterByIdAsync(characterId) as ObjectResult; + + var character = response.Value; + + _mockRepo.Verify(repo => repo.GetCharacterByIdAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.OK); + + character.Should().NotBeNull(); + character.Should().BeOfType(); + } + + [Fact] + public async Task GetCharacterByIdAsync_ValidCharacterIdForNonExistentCharacter_Returns404StatusCodeWithProblemDetails() + { + var characterId = "62b7d5506c1b407771829926"; + + _mockRepo + .Setup(repo => repo.GetCharacterByIdAsync(characterId)) + .ReturnsAsync(null as Character); + + var response = await _controller.GetCharacterByIdAsync(characterId) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetCharacterByIdAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.NotFound); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + + + [Fact] + public async Task GetCharacterByIdAsync_RepoThrowsException_Returns500StatusCodeWithProblemDetails() + { + var characterId = "62b7d5506c1b407771829926"; + + _mockRepo + .Setup(repo => repo.GetCharacterByIdAsync(characterId)) + .Throws(new Exception()); + + var response = await _controller.GetCharacterByIdAsync(characterId) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetCharacterByIdAsync(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.tests/v1/unitTests/EpisodesControllerUnitTests.cs b/server.tests/v1/unitTests/EpisodesControllerUnitTests.cs new file mode 100644 index 0000000..669c393 --- /dev/null +++ b/server.tests/v1/unitTests/EpisodesControllerUnitTests.cs @@ -0,0 +1,140 @@ +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.v1.UnitTests +{ + public class EpisodesControllerUnitTests + { + private readonly Mock _mockRepo; + private readonly EpisodesController _controller; + + public EpisodesControllerUnitTests() + { + _mockRepo = new Mock(); + _controller = new EpisodesController(_mockRepo.Object); + } + + [Fact] + public async Task GetEpisodesAsync_AllEpisodes_Returns200StatusCodeWithEpisodesCollection() + { + var filter = new EpisodeFilter(); + + var episodes = new List { new Episode(), new Episode() }; + + _mockRepo + .Setup(repo => repo.GetEpisodesAsync(filter)) + .ReturnsAsync(episodes); + + var response = await _controller.GetEpisodesAsync(filter) as ObjectResult; + + var data = response.Value as List; + + _mockRepo.Verify(repo => repo.GetEpisodesAsync(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 GetEpisodesAsync_RepoThrowsException_Returns500StatusCodeWithProblemDetails() + { + var filter = new EpisodeFilter(); + + _mockRepo + .Setup(repo => repo.GetEpisodesAsync(filter)) + .Throws(new Exception()); + + var response = await _controller.GetEpisodesAsync(filter) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetEpisodesAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.InternalServerError); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + + [Fact] + public async Task GetEpisodeByNumberAsync_ValidEpisodeNumber_Returns200StatusWithEpisode() + { + var episodeNumber = 1; + + _mockRepo + .Setup(repo => repo.GetEpisodeByNumberAsync(episodeNumber)) + .ReturnsAsync(new Episode()); + + var response = await _controller.GetEpisodeByNumberAsync(episodeNumber) as ObjectResult; + + var episode = response.Value; + + _mockRepo.Verify(repo => repo.GetEpisodeByNumberAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.OK); + + episode.Should().NotBeNull(); + episode.Should().BeOfType(); + } + + [Fact] + public async Task GetEpisodeByNumberAsync_ValidEpisodeNumberForNonExistentEpisode_Returns404StatusWithProblemDetails() + { + var episodeNumber = 3000; + + _mockRepo + .Setup(repo => repo.GetEpisodeByNumberAsync(episodeNumber)) + .ReturnsAsync(null as Episode); + + var response = await _controller.GetEpisodeByNumberAsync(episodeNumber) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetEpisodeByNumberAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.NotFound); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + + [Fact] + public async Task GetEpisodeByNumberAsync_RepoThrowsException_Returns500StatusCodeWithProblemDetails() + { + var episodeNumber = 1; + + _mockRepo + .Setup(repo => repo.GetEpisodeByNumberAsync(episodeNumber)) + .Throws(new Exception()); + + var response = await _controller.GetEpisodeByNumberAsync(episodeNumber) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetEpisodeByNumberAsync(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.tests/v1/unitTests/QuotesControllerUnitTests.cs b/server.tests/v1/unitTests/QuotesControllerUnitTests.cs new file mode 100644 index 0000000..d38ef20 --- /dev/null +++ b/server.tests/v1/unitTests/QuotesControllerUnitTests.cs @@ -0,0 +1,140 @@ +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.v1.UnitTests +{ + public class QuotesControllerUnitTests + { + private readonly Mock _mockRepo; + private readonly QuotesController _controller; + + public QuotesControllerUnitTests() + { + _mockRepo = new Mock(); + _controller = new QuotesController(_mockRepo.Object); + } + + [Fact] + public async Task GetQuotesAsync_AllQuotes_Returns200StatusCodeWithQuotesCollection() + { + var filter = new QuoteFilter(); + + var quotes = new List { new Quote(), new Quote() }; + + _mockRepo + .Setup(repo => repo.GetQuotesAsync(filter)) + .ReturnsAsync(quotes); + + var response = await _controller.GetQuotesAsync(filter) as ObjectResult; + + var data = response.Value as List; + + _mockRepo.Verify(repo => repo.GetQuotesAsync(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 GetQuotesAsync_RepoThrowsException_Returns500StatusCodeWithProblemDetails() + { + var filter = new QuoteFilter(); + + _mockRepo + .Setup(repo => repo.GetQuotesAsync(filter)) + .Throws(new Exception()); + + var response = await _controller.GetQuotesAsync(filter) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetQuotesAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.InternalServerError); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + + + [Fact] + public async Task GetQuoteByIdAsync_ValidId_Returns200StatusCodeWithQuote() + { + var quoteId = "62b7d5506c1b407771829926"; + + _mockRepo + .Setup(repo => repo.GetQuoteByIdAsync(quoteId)) + .ReturnsAsync(new Quote()); + + var response = await _controller.GetQuoteByIdAsync(quoteId) as ObjectResult; + + var character = response.Value; + + _mockRepo.Verify(repo => repo.GetQuoteByIdAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.OK); + + character.Should().NotBeNull(); + character.Should().BeOfType(); + } + + public async Task GetQuoteByIdAsync_ValidIdForNonExistentQuote_Returns404StatusCodeWithProblemDetails() + { + var quoteId = "62b7d5506c1b407771829926"; + + _mockRepo + .Setup(repo => repo.GetQuoteByIdAsync(quoteId)) + .ReturnsAsync(null as Quote); + + var response = await _controller.GetQuoteByIdAsync(quoteId) as ObjectResult; + + var quote = response.Value; + + _mockRepo.Verify(repo => repo.GetQuoteByIdAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.NotFound); + + quote.Should().NotBeNull(); + quote.Should().BeOfType(); + } + + [Fact] + public async Task GetQuoteByIdAsync_RepoThrowsException_Returns500StatusCodeWithProblemDetails() + { + var quoteId = "62b7d5506c1b407771829926"; + + _mockRepo + .Setup(repo => repo.GetQuoteByIdAsync(quoteId)) + .Throws(new Exception()); + + var response = await _controller.GetQuoteByIdAsync(quoteId) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetQuoteByIdAsync(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.tests/v1/unitTests/SeasonsControllerUnitTests.cs b/server.tests/v1/unitTests/SeasonsControllerUnitTests.cs new file mode 100644 index 0000000..e5373a2 --- /dev/null +++ b/server.tests/v1/unitTests/SeasonsControllerUnitTests.cs @@ -0,0 +1,136 @@ +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.v1.UnitTests +{ + public class SeasonsControllerUnitTests + { + private readonly Mock _mockRepo; + private readonly SeasonsController _controller; + + public SeasonsControllerUnitTests() + { + _mockRepo = new Mock(); + _controller = new SeasonsController(_mockRepo.Object); + } + + [Fact] + public async Task GetSeasonsAsync_AllSeasons_Returns200StatusCodeWithSeasonsCollection() + { + var seasons = new List { new Season(), new Season() }; + + _mockRepo + .Setup(repo => repo.GetSeasonsAsync()) + .ReturnsAsync(seasons); + + var response = await _controller.GetSeasonsAsync() as ObjectResult; + + var data = response.Value as List; + + _mockRepo.Verify(repo => repo.GetSeasonsAsync(), 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 GetSeasonsAsync_RepoThrowsException_Returns500StatusCodeWithProblemDetails() + { + _mockRepo + .Setup(repo => repo.GetSeasonsAsync()) + .Throws(new Exception()); + + var response = await _controller.GetSeasonsAsync() as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetSeasonsAsync(), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.InternalServerError); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + + [Fact] + public async Task GetSeasonByNumberAsync_ValidSeasonNumber_Returns200StatusCodeWithSeason() + { + var seasonNumber = 1; + + _mockRepo + .Setup(repo => repo.GetSeasonByNumberAsync(seasonNumber)) + .ReturnsAsync(new Season()); + + var response = await _controller.GetSeasonByNumberAsync(seasonNumber) as ObjectResult; + + var season = response.Value; + + _mockRepo.Verify(repo => repo.GetSeasonByNumberAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.OK); + + season.Should().NotBeNull(); + season.Should().BeOfType(); + } + + [Fact] + public async Task GetSeasonByNumberAsync_ValidSeasonNumberForNonExistentSeason_Returns404StatusCodeWithProblemDetails() + { + var seasonNumber = 20; + + _mockRepo + .Setup(repo => repo.GetSeasonByNumberAsync(seasonNumber)) + .ReturnsAsync(null as Season); + + var response = await _controller.GetSeasonByNumberAsync(seasonNumber) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetSeasonByNumberAsync(It.IsAny()), Times.Once()); + + response.Should().NotBeNull(); + response.Should().BeOfType(); + response.StatusCode.Should().Be((int)HttpStatusCode.NotFound); + + details.Should().NotBeNull(); + details.Should().BeOfType(); + } + + [Fact] + public async Task GetSeasonByNumberAsync_RepoThrowsException_Returns500StatusCodeWithProblemDetails() + { + var seasonNumber = 1; + + _mockRepo + .Setup(repo => repo.GetSeasonByNumberAsync(seasonNumber)) + .Throws(new Exception()); + + var response = await _controller.GetSeasonByNumberAsync(seasonNumber) as ObjectResult; + + var details = response.Value; + + _mockRepo.Verify(repo => repo.GetSeasonByNumberAsync(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 ddc3fdf..0cf465a 100644 --- a/server/Controllers/v1/CharactersController.cs +++ b/server/Controllers/v1/CharactersController.cs @@ -66,7 +66,7 @@ namespace server.Controllers.v1 if (!ObjectId.TryParse(id, out _)) { ModelState.AddModelError(nameof(id), $"{id} is not a valid id"); - return ValidationProblem(); + return ValidationProblem(detail: "Invalid request"); } try diff --git a/server/Controllers/v1/EpisodesController.cs b/server/Controllers/v1/EpisodesController.cs index 7ee0864..988ab5b 100644 --- a/server/Controllers/v1/EpisodesController.cs +++ b/server/Controllers/v1/EpisodesController.cs @@ -49,14 +49,12 @@ namespace server.Controllers.v1 /// /// The number of the episode in the series. /// Returns the episode requested. - /// Not a valid request. /// Unable to find an episode with the provided number. /// Failed to get episode. /// Returns the episode requested. [MapToApiVersion("1.0")] [HttpGet("{number:int}")] [ProducesResponseType(typeof(Episode), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task GetEpisodeByNumberAsync(int number) diff --git a/server/Controllers/v1/QuotesController.cs b/server/Controllers/v1/QuotesController.cs index a5a18d6..a9ed1ba 100644 --- a/server/Controllers/v1/QuotesController.cs +++ b/server/Controllers/v1/QuotesController.cs @@ -67,7 +67,7 @@ namespace server.Controllers.v1 if (!ObjectId.TryParse(id, out _)) { ModelState.AddModelError(nameof(id), $"{id} is not a valid id"); - return ValidationProblem(); + return ValidationProblem(detail: "Invalid request"); } try