using System.Net; using System.Net.Http.Json; using System.Text.Json.Nodes; using Bsevita.Library.Api.Data.Generated; using Bsevita.Library.Api.Data.Generated.Entities; using Bsevita.Library.Models.Students; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; namespace Bsevita.Library.Api.Tests; public sealed class ApiIntegrationTests { [Fact] public async Task SwaggerUi_AndSwaggerJson_AreAvailableInDevelopment() { await using var factory = new ApiFactory(); var client = factory.CreateClient(); var ui = await client.GetAsync("/swagger"); var document = await client.GetFromJsonAsync("/swagger/v1/swagger.json"); Assert.Equal(HttpStatusCode.OK, ui.StatusCode); Assert.Equal("Bsevita.Library.Api", document?["info"]?["title"]?.GetValue()); Assert.NotNull(document?["paths"]?["/api/students"]); Assert.NotNull(document?["paths"]?["/api/books"]); Assert.NotNull(document?["paths"]?["/api/loans"]); Assert.NotNull(document?["paths"]?["/api/returns/verify/{bookNumber}"]); Assert.NotNull(document?["paths"]?["/api/reports/statistics"]); } [Fact] public async Task PostStudent_ReturnsCreatedStudent() { await using var factory = new ApiFactory(); var client = factory.CreateClient(); var response = await client.PostAsJsonAsync("/api/students", new SaveStudentRequest { CardNumber = "S-1", FirstName = "Ada", LastName = "Lovelace", ClassName = "4AHIT" }); var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.Equal("S-1", body?.CardNumber); Assert.Equal("Ada Lovelace", body?.FullName); } [Fact] public async Task InvalidBody_ReturnsValidationProblemDetails() { await using var factory = new ApiFactory(); var client = factory.CreateClient(); var response = await client.PostAsJsonAsync("/api/students", new { cardNumber = "" }); var problem = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Equal("application/problem+json", response.Content.Headers.ContentType?.MediaType); Assert.Equal(400, problem?["status"]?.GetValue()); Assert.NotNull(problem?["errors"]); } [Fact] public async Task MissingResource_ReturnsProblemDetails() { await using var factory = new ApiFactory(); var client = factory.CreateClient(); var response = await client.GetAsync($"/api/students/{Guid.NewGuid()}"); var problem = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); Assert.Equal("application/problem+json", response.Content.Headers.ContentType?.MediaType); Assert.Equal(404, problem?["status"]?.GetValue()); Assert.Equal("Nicht gefunden", problem?["title"]?.GetValue()); } [Fact] public async Task UnknownRoute_ReturnsNotFound() { await using var factory = new ApiFactory(); var client = factory.CreateClient(); var response = await client.GetAsync("/api/does-not-exist"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task Health_ReturnsOk() { await using var factory = new ApiFactory(); var client = factory.CreateClient(); var response = await client.GetAsync("/health"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task LoanConflict_ReturnsConflictProblemDetails() { await using var factory = new ApiFactory(); await factory.SeedBorrowedBookAsync(); var client = factory.CreateClient(); var response = await client.PostAsJsonAsync("/api/loans", new { cardNumber = "S-2", bookNumber = "B-1" }); var problem = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); Assert.Equal("application/problem+json", response.Content.Headers.ContentType?.MediaType); Assert.Equal(409, problem?["status"]?.GetValue()); } private sealed class ApiFactory : WebApplicationFactory { private readonly string _databaseName = $"Bsevita.Library.Api.Tests.{Guid.NewGuid():N}"; protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Development"); builder.UseSetting("Database:ValidateOnStartup", "false"); builder.ConfigureLogging(logging => logging.ClearProviders()); builder.ConfigureServices(services => { services.RemoveAll(); services.RemoveAll(); services.RemoveAll>(); services.RemoveAll>(); services.RemoveAll(); services.AddDbContext(options => options.UseInMemoryDatabase(_databaseName)); services.AddSingleton(new FixedTimeProvider(new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero))); }); } public async Task SeedBorrowedBookAsync() { using var scope = Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var now = new DateTimeOffset(2026, 6, 22, 8, 0, 0, TimeSpan.Zero); var firstStudent = new Student { StudentId = Guid.NewGuid(), CardNumber = "S-1", FirstName = "Ada", LastName = "Lovelace", ClassName = "4AHIT", IsActive = true, CreatedAt = now, UpdatedAt = now }; var secondStudent = new Student { StudentId = Guid.NewGuid(), CardNumber = "S-2", FirstName = "Grace", LastName = "Hopper", ClassName = "5BHIT", IsActive = true, CreatedAt = now, UpdatedAt = now }; var book = new Book { BookId = Guid.NewGuid(), BookNumber = "B-1", Title = "Borrowed", Author = "A", Subject = "S", IsActive = true, CreatedAt = now, UpdatedAt = now }; dbContext.AddRange(firstStudent, secondStudent, book, new Loan { LoanId = Guid.NewGuid(), StudentId = firstStudent.StudentId, BookId = book.BookId, LoanedAt = now, DueAt = now.AddDays(14) }); await dbContext.SaveChangesAsync(); } } }