diff --git a/BookStoreAPI.Tests/BookStoreAPI.Tests.csproj b/BookStoreAPI.Tests/BookStoreAPI.Tests.csproj new file mode 100644 index 0000000..e02d13a --- /dev/null +++ b/BookStoreAPI.Tests/BookStoreAPI.Tests.csproj @@ -0,0 +1,42 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Always + + + diff --git a/BookStoreAPI.Tests/BookStoreWebApplicationFactory.cs b/BookStoreAPI.Tests/BookStoreWebApplicationFactory.cs new file mode 100644 index 0000000..30ac323 --- /dev/null +++ b/BookStoreAPI.Tests/BookStoreWebApplicationFactory.cs @@ -0,0 +1,46 @@ +using System.Data.Common; +using BookStoreAPI.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace BookStoreAPI.Tests; + +public class BookStoreWebApplicationFactory : WebApplicationFactory where TProgram : class +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(services => + { + var dbContextDescriptor = + services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); + + services.Remove(dbContextDescriptor); + + var dbConnectionDescriptor = services.SingleOrDefault( + d => d.ServiceType == typeof(DbConnection)); + + services.Remove(dbConnectionDescriptor); + + services.AddSingleton(container => + { + var connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + + return connection; + }); + + services.AddDbContext((container, options) => + { + var connection = container.GetRequiredService(); + options.UseSqlite(connection); + }); + + services.AddAutoMapper(typeof(Program)); + }); + + builder.UseEnvironment("Development"); + } +} \ No newline at end of file diff --git a/BookStoreAPI.Tests/IntegrationTests/Books/BooksTests.cs b/BookStoreAPI.Tests/IntegrationTests/Books/BooksTests.cs new file mode 100644 index 0000000..6e7fc9d --- /dev/null +++ b/BookStoreAPI.Tests/IntegrationTests/Books/BooksTests.cs @@ -0,0 +1,233 @@ +using System.Net; +using System.Text; +using AutoMapper; +using BookStoreAPI.Functions.Commands.Book.Create; +using BookStoreAPI.Functions.Commands.Book.Patch; +using BookStoreAPI.Models; +using BookStoreAPI.Tests.Utils; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Xunit; + +namespace BookStoreAPI.Tests.IntegrationTests.Books; + +public class BooksTests : IClassFixture> +{ + private readonly BookStoreWebApplicationFactory _factory; + private readonly IMapper _mapper; + private readonly IServiceProvider _serviceProvider; + + public BooksTests(BookStoreWebApplicationFactory factory) + { + _factory = factory; + + _serviceProvider = _factory.Services; + _mapper = _serviceProvider.GetRequiredService(); + + Data.SeedData(_serviceProvider); + } + + [Fact] + public async Task Get_ReturnsListOfAllBooksFromDatabase() + { + using var scope = _factory.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var client = _factory.CreateClient(); + + var response = await client.GetAsync("/api/BookStore"); + response.EnsureSuccessStatusCode(); + var contentString = await response.Content.ReadAsStringAsync(); + var books = JsonConvert.DeserializeObject>(contentString); + + var currentBooks = await dbContext.Books.Include(b => b.RentalHistory).Include(b => b.Genre).ToListAsync(); + var currentBooksDto = _mapper.Map>(currentBooks); + + Assert.NotNull(books); + Assert.NotEmpty(books); + Assert.Equivalent(currentBooksDto, books); + } + + [Fact] + public async Task Get_ReturnsBookFromDatabase() + { + using var scope = _factory.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var client = _factory.CreateClient(); + + var firstBook = await dbContext.Books.FirstAsync(); + + var response = await client.GetAsync($"api/BookStore/{firstBook.Id}"); + response.EnsureSuccessStatusCode(); + + var contentString = await response.Content.ReadAsStringAsync(); + var book = JsonConvert.DeserializeObject(contentString); + var firstBookDto = _mapper.Map(firstBook); + + Assert.NotNull(book); + Assert.Equivalent(firstBookDto, book); + } + + [Fact] + public async Task Get_ReturnsNotFoundIfBookDoesntExist() + { + var client = _factory.CreateClient(); + + var responseInvalid = await client.GetAsync("api/BookStore/11111"); + Assert.Equal(HttpStatusCode.NotFound, responseInvalid.StatusCode); + } + + [Fact] + public async Task Post_AddsBookToDatabase() + { + var client = _factory.CreateClient(); + + var book = new CreateBookCommand + { + Title = "Dummy Title", + Author = "Dummy Author", + ReleaseDate = DateTime.Now, + GenreId = 1 + }; + var bookSerialized = JsonConvert.SerializeObject(book); + + + var response = await client.PostAsync("api/BookStore", + new StringContent(bookSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + [Fact] + public async Task Post_ReturnsErrorIfNonExistentBookGenre() + { + var client = _factory.CreateClient(); + + var book = new CreateBookCommand + { + Title = "Dummy Title", + Author = "Dummy Author", + ReleaseDate = DateTime.Now, + GenreId = 123 + }; + var bookSerialized = JsonConvert.SerializeObject(book); + + var response = await client.PostAsync("api/BookStore", + new StringContent(bookSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + } + + [Fact] + public async Task Put_UpdateBookInDatabase() + { + var client = _factory.CreateClient(); + + var book = new BookDto + { + Title = "Dammy Title", + Author = "Dammy Author", + ReleaseDate = DateTime.Now, + GenreId = 1 + }; + var bookSerialized = JsonConvert.SerializeObject(book); + + var response = await client.PutAsync("api/BookStore/1", + new StringContent(bookSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + [Fact] + public async Task Put_ReturnsErrorIfInvalidBookGenre() + { + var client = _factory.CreateClient(); + + var book = new BookDto + { + Title = "Dammy Title", + Author = "Dammy Author", + ReleaseDate = DateTime.Now, + GenreId = 123 + }; + var bookSerialized = JsonConvert.SerializeObject(book); + + var response = await client.PutAsync("api/BookStore/1", + new StringContent(bookSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + } + + [Fact] + public async Task Put_ReturnsNotFoundIfInvalidBookId() + { + var client = _factory.CreateClient(); + + var book = new BookDto + { + Title = "Dammy Title", + Author = "Dammy Author", + ReleaseDate = DateTime.Now, + GenreId = 123 + }; + var bookSerialized = JsonConvert.SerializeObject(book); + + var response = await client.PutAsync("api/BookStore/11111", + new StringContent(bookSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Delete_DeleteBookFromDatabase() + { + var client = _factory.CreateClient(); + + var response = await client.DeleteAsync("api/BookStore/1"); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + [Fact] + public async Task Delete_ReturnsNotFoundIfInvalidBookId() + { + var client = _factory.CreateClient(); + + var response = await client.DeleteAsync("api/BookStore/11111"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Patch_UpdateBookInDatabase() + { + using var scope = _factory.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var client = _factory.CreateClient(); + + var command = new PatchBookCommand + { + Title = "Diummy Book" + }; + var commandSerialized = JsonConvert.SerializeObject(command); + + var response = await client.PatchAsync("api/BookStore/2", + new StringContent(commandSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + var book = await dbContext.Books.FirstOrDefaultAsync(b => b.Id == 2); + Assert.Equal("Diummy Book", book.Title); + } + + [Fact] + public async Task Patch_ReturnsNotFoundIfInvalidBookId() + { + var client = _factory.CreateClient(); + + var command = new PatchBookCommand + { + Title = "Diummy Book" + }; + var commandSerialized = JsonConvert.SerializeObject(command); + + var response = await client.PatchAsync("api/BookStore/111111", + new StringContent(commandSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} \ No newline at end of file diff --git a/BookStoreAPI.Tests/IntegrationTests/Clients/ClientsTest.cs b/BookStoreAPI.Tests/IntegrationTests/Clients/ClientsTest.cs new file mode 100644 index 0000000..6e3e57d --- /dev/null +++ b/BookStoreAPI.Tests/IntegrationTests/Clients/ClientsTest.cs @@ -0,0 +1,235 @@ +using System.Net; +using System.Text; +using AutoMapper; +using BookStoreAPI.Commands; +using BookStoreAPI.Functions.Commands.Client.Create; +using BookStoreAPI.Functions.Commands.Client.Patch; +using BookStoreAPI.Models; +using BookStoreAPI.Tests.Utils; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Xunit; + +namespace BookStoreAPI.Tests.IntegrationTests.Clients; + +public class ClientsTest : IClassFixture> +{ + private readonly BookStoreWebApplicationFactory _factory; + private readonly IMapper _mapper; + private readonly IServiceProvider _serviceProvider; + + public ClientsTest(BookStoreWebApplicationFactory factory) + { + _factory = factory; + + _serviceProvider = _factory.Services; + _mapper = _serviceProvider.GetRequiredService(); + + Data.SeedData(_serviceProvider); + } + + [Fact] + public async Task Get_ReturnsListOfAllClientsFromDatabase() + { + using var scope = _factory.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var client = _factory.CreateClient(); + + var response = await client.GetAsync("api/Client"); + response.EnsureSuccessStatusCode(); + + var contentString = await response.Content.ReadAsStringAsync(); + var clients = JsonConvert.DeserializeObject>(contentString); + + var currentClients = await dbContext.Clients.Include(c => c.RentedBooks).ThenInclude(b => b.RentalHistory) + .ToListAsync(); + var currentClientsDto = _mapper.Map>(currentClients); + + Assert.NotNull(clients); + Assert.NotEmpty(clients); + Assert.Equivalent(currentClientsDto, clients); + } + + [Fact] + public async Task Get_ReturnsClientFromDatabase() + { + using var scope = _factory.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var client = _factory.CreateClient(); + + var firstClient = await dbContext.Clients.FirstAsync(); + + var response = await client.GetAsync($"api/Client/{firstClient.Id}"); + response.EnsureSuccessStatusCode(); + + var contentString = await response.Content.ReadAsStringAsync(); + var DbClient = JsonConvert.DeserializeObject(contentString); + var firstClientDto = _mapper.Map(firstClient); + + Assert.NotNull(DbClient); + Assert.Equivalent(firstClientDto, DbClient); + } + + [Fact] + public async Task Get_ReturnsNotFoundIfInvalidClient() + { + var client = _factory.CreateClient(); + + var response = await client.GetAsync("api/Client/11111"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Post_AddsClientToDatabase() + { + var client = _factory.CreateClient(); + + var clientCommand = new CreateClientCommand + { + FirstName = "John", + LastName = "Doe", + DateOfBirth = DateTime.Now + }; + var clientCommandSerialized = JsonConvert.SerializeObject(clientCommand); + + + var response = await client.PostAsync("api/Client", + new StringContent(clientCommandSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + [Fact] + public async Task Post_ReturnsBadRequestIfInvalidBody() + { + var client = _factory.CreateClient(); + + var clientCommand = new CreateClientCommand + { + FirstName = + "Johnaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + LastName = "Doe", + DateOfBirth = DateTime.Now + }; + var clientCommandSerialized = JsonConvert.SerializeObject(clientCommand); + + var response = await client.PostAsync("api/Client", + new StringContent(clientCommandSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Put_UpdateClientInDatabase() + { + var client = _factory.CreateClient(); + + var clientCommand = new UpdateClientCommand + { + FirstName = "John", + LastName = "Doe", + DateOfBirth = DateTime.Now + }; + var clientCommandSerialized = JsonConvert.SerializeObject(clientCommand); + + + var response = await client.PutAsync("api/Client/1", + new StringContent(clientCommandSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + [Fact] + public async Task Put_ReturnsBadRequestIfInvalidBody() + { + var client = _factory.CreateClient(); + + var clientCommand = new UpdateClientCommand + { + FirstName = + "Johnaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + LastName = "Doe", + DateOfBirth = DateTime.Now + }; + var clientCommandSerialized = JsonConvert.SerializeObject(clientCommand); + + var response = await client.PutAsync("api/Client/1", + new StringContent(clientCommandSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Put_ReturnsNotFoundIfInvalidClient() + { + var client = _factory.CreateClient(); + + var clientCommand = new UpdateClientCommand + { + FirstName = + "John", + LastName = "Doe", + DateOfBirth = DateTime.Now + }; + var clientCommandSerialized = JsonConvert.SerializeObject(clientCommand); + + var response = await client.PutAsync("api/Client/11111111", + new StringContent(clientCommandSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Delete_DeleteClientFromDatabase() + { + var client = _factory.CreateClient(); + + var responseCorrect = await client.DeleteAsync("api/Client/1"); + Assert.Equal(HttpStatusCode.NoContent, responseCorrect.StatusCode); + } + + [Fact] + public async Task Delete_ReturnsNotFoundIfInvalidClient() + { + var client = _factory.CreateClient(); + + var response = await client.DeleteAsync("api/Client/11111"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Patch_UpdateClientInDatabase() + { + using var scope = _factory.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var client = _factory.CreateClient(); + + var command = new PatchClientCommand + { + FirstName = "Adam" + }; + var commandSerialized = JsonConvert.SerializeObject(command); + + var response = await client.PatchAsync("api/Client/2", + new StringContent(commandSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + var dbClient = await dbContext.Clients.FirstOrDefaultAsync(c => c.Id == 2); + Assert.Equal("Adam", dbClient.FirstName); + } + + [Fact] + public async Task Patch_ReturnsNotFoundIfInvalidClient() + { + var client = _factory.CreateClient(); + + var command = new PatchClientCommand + { + FirstName = "Adam" + }; + var commandSerialized = JsonConvert.SerializeObject(command); + + var response = await client.PatchAsync("api/Client/111111", + new StringContent(commandSerialized, Encoding.UTF8, "application/json")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} \ No newline at end of file diff --git a/BookStoreAPI.Tests/Utils/Data.cs b/BookStoreAPI.Tests/Utils/Data.cs new file mode 100644 index 0000000..470eaa7 --- /dev/null +++ b/BookStoreAPI.Tests/Utils/Data.cs @@ -0,0 +1,63 @@ +using BookStoreAPI.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace BookStoreAPI.Tests.Utils; + +public class Data +{ + public static void SeedData(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + dbContext.Database.EnsureCreated(); + + var genre = new Genre + { + Name = "DummyGenre" + }; + dbContext.Genres.Add(genre); + + + dbContext.SaveChanges(); + + var genreId = genre.Id; + + dbContext.Books.AddRange(new List + { + new() + { + Title = "Test Book 1", + Author = "DummyAuthor1", + ReleaseDate = DateTime.Now, + GenreId = genreId + }, + new() + { + Title = "Test Book 2", + Author = "DummyAuthor2", + ReleaseDate = DateTime.Now, + GenreId = genreId + } + }); + + + dbContext.Clients.AddRange(new List + { + new() + { + FirstName = "John", + LastName = "Doe", + DateOfBirth = DateTime.Now + }, + new() + { + FirstName = "Joe", + LastName = "Doe", + DateOfBirth = DateTime.Now + } + }); + + dbContext.SaveChanges(); + } +} \ No newline at end of file diff --git a/BookStoreAPI.sln b/BookStoreAPI.sln index 296a36e..e1a2eea 100644 --- a/BookStoreAPI.sln +++ b/BookStoreAPI.sln @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookStoreAPI", "BookStoreAPI\BookStoreAPI.csproj", "{1ED86459-A2B3-43E6-A024-5C692500442B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookStoreAPI.Tests", "BookStoreAPI.Tests\BookStoreAPI.Tests.csproj", "{15778276-08FB-4736-97FB-F30C99D71315}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +14,9 @@ Global {1ED86459-A2B3-43E6-A024-5C692500442B}.Debug|Any CPU.Build.0 = Debug|Any CPU {1ED86459-A2B3-43E6-A024-5C692500442B}.Release|Any CPU.ActiveCfg = Release|Any CPU {1ED86459-A2B3-43E6-A024-5C692500442B}.Release|Any CPU.Build.0 = Release|Any CPU + {15778276-08FB-4736-97FB-F30C99D71315}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15778276-08FB-4736-97FB-F30C99D71315}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15778276-08FB-4736-97FB-F30C99D71315}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15778276-08FB-4736-97FB-F30C99D71315}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/BookStoreAPI/BookStoreAPI.csproj b/BookStoreAPI/BookStoreAPI.csproj index eb558b6..6702e74 100644 --- a/BookStoreAPI/BookStoreAPI.csproj +++ b/BookStoreAPI/BookStoreAPI.csproj @@ -20,6 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/BookStoreAPI/Program.cs b/BookStoreAPI/Program.cs index 727676b..891ff8b 100644 --- a/BookStoreAPI/Program.cs +++ b/BookStoreAPI/Program.cs @@ -72,4 +72,8 @@ await context.Response.WriteAsJsonAsync(new app.MapControllers(); -app.Run(); \ No newline at end of file +app.Run(); + +public partial class Program +{ +} \ No newline at end of file