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