Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/marchena96/Paradigma-lab1/llms.txt

Use this file to discover all available pages before exploring further.

The test project (LibraryService.Integration.Test) contains three [Fact] tests that exercise the full HTTP stack using WebApplicationFactory<Program> with a SQLite in-memory database — no live PostgreSQL connection required. All requests go through real ASP.NET Core middleware, routing, controllers, and EF Core, but the backing store is an isolated in-memory SQLite instance that is created fresh for every test class instantiation.

Test infrastructure

IClassFixture and WebApplicationFactory

The test class implements IClassFixture<WebApplicationFactory<Program>>, which tells xUnit to create one shared WebApplicationFactory<Program> instance per test class and inject it into the constructor. The factory spins up the full ASP.NET Core pipeline — including Startup.cs — just as it runs in production, then exposes an in-process HttpClient that talks to that test host.

Replacing LibraryContext with SQLite in-memory

Inside the constructor, the real LibraryContext (backed by Npgsql/PostgreSQL) is swapped out for a SQLite in-memory context using services.RemoveAll and services.AddSingleton:
public IntegrationTests(WebApplicationFactory<Program> factory)
{
    _factory = factory;
    context = new LibraryContext(new DbContextOptionsBuilder<LibraryContext>()
                .UseSqlite("DataSource=:memory:")
                .EnableSensitiveDataLogging()
                .Options);
    Client = _factory.WithWebHostBuilder(builder =>
        builder.UseStartup<Startup>()
        .ConfigureServices(services =>
        {
            services.RemoveAll(typeof(LibraryContext));
            services.AddSingleton(context);

            context.Database.OpenConnection();
            context.Database.EnsureCreated();

            context.SaveChanges();

            // Clear local context cache
            foreach (var entity in context.ChangeTracker.Entries().ToList())
            {
                entity.State = EntityState.Detached;
            }
        })
    ).CreateClient();
}
Key points:
  • DataSource=:memory: tells SQLite to keep the database entirely in RAM. The connection must be kept open manually (OpenConnection()) for the lifetime of the test, otherwise SQLite drops the in-memory database the moment the connection closes.
  • EnsureCreated() creates the schema directly from the EF Core model (no migrations), which is safe here because the SQLite database starts empty each time.
  • After seeding, the ChangeTracker is cleared so that EF does not hold stale entity instances that could interfere with subsequent requests.

Startup.cs guard: skipping Migrate() for SQLite

Startup.cs normally calls db.Database.Migrate() at startup to apply pending PostgreSQL migrations. Running Migrate() on a SQLite provider would fail because the migration history table format is incompatible with the in-memory schema created by EnsureCreated(). A provider guard skips auto-migration for SQLite:
// Only run migrations for non-SQLite providers (Npgsql in production)
if (!(db.Database.ProviderName?.Contains("Sqlite") ?? false)
    && db.Database.GetPendingMigrations().Any())
{
    db.Database.Migrate();
}

Idempotency guard for JWT Bearer registration

WebApplicationFactory calls ConfigureServices twice — once when Program builds the host, and again when the test’s WithWebHostBuilder lambda runs. Without a guard, ASP.NET Core throws "Scheme already exists: Bearer" on the second registration. A check at the top of ConfigureServices prevents duplicate JWT Bearer setup:
// If JwtSettings is already registered, DI has already been configured — skip.
if (services.Any(s => s.ServiceType == typeof(JwtSettings)))
    return;

Seed helpers

Two private async helpers insert baseline data directly through the shared LibraryContext so that tests start from a known state:
private async Task SeedLibrary()
{
    var libraries = new List<Library>
    {
        new Library { Name = "Library Name 1", Location = "Location 1" },
        new Library { Name = "Library Name 2", Location = "Location 2" },
        new Library { Name = "Library Name 3", Location = "Location 3" },
        new Library { Name = "Library Name 4", Location = "Location 4" }
    };

    await context.Libraries.AddRangeAsync(libraries);
    await context.SaveChangesAsync();
}

private async Task SeedBook(string bookName, int libraryId)
{
    var bookForm = new BookForm
    {
        Name = bookName
    };
    var response1 = await Client.PostAsync($"/api/libraries/{libraryId}/books",
        new StringContent(JsonConvert.SerializeObject(bookForm), Encoding.UTF8, "application/json"));
}
SeedLibrary() inserts four libraries with sequential IDs (1–4) by writing directly to the context. SeedBook() goes through the HTTP client rather than the context, which means it exercises the real POST /api/libraries/{libraryId}/books endpoint, verifying the full add-book path as a side effect of setup.

Test cases

1. TestAddBook_Ok_GetBook_NotFound

Verifies that POST /api/libraries/{libraryId}/books returns 201 Created for an existing library and 404 Not Found for a non-existent one. Setup: seeds 4 libraries.
RequestExpected status
POST /api/libraries/1/books {"name": "Test book 1"}201 Created
POST /api/libraries/100/books {"name": "Test book 2"}404 Not Found
[Fact]
public async Task TestAddBook_Ok_GetBook_NotFound()
{
    await SeedLibrary();

    var bookForm = new BookForm
    {
        Name = "Test book 1",
    };

    var response1 = await Client.PostAsync($"/api/libraries/1/books",
        new StringContent(JsonConvert.SerializeObject(bookForm), Encoding.UTF8, "application/json"));

    response1.StatusCode.Should().BeEquivalentTo(StatusCodes.Status201Created);

    bookForm = new BookForm
    {
        Name = "Test book 2",
    };

    var response2 = await Client.PostAsync($"/api/libraries/100/books",
        new StringContent(JsonConvert.SerializeObject(bookForm), Encoding.UTF8, "application/json"));

    response2.StatusCode.Should().BeEquivalentTo(StatusCodes.Status404NotFound);
}

2. TestGetBooks_Ok_NotFound

Verifies that GET /api/libraries/{libraryId}/books returns the correct book list for an existing library and 404 for a non-existent one. Setup: seeds 4 libraries, then seeds 2 books into library 1 via SeedBook.
RequestExpected statusExpected body
GET /api/libraries/2/books200 OKEmpty list (0 books)
GET /api/libraries/1/books200 OKList with 2 books
GET /api/libraries/31232/books404 Not Found
[Fact]
public async Task TestGetBooks_Ok_NotFound()
{
    await SeedLibrary();

    await SeedBook("test book 1", 1);
    await SeedBook("test book 2", 1);

    var response1 = await Client.GetAsync($"/api/libraries/2/books");
    response1.StatusCode.Should().BeEquivalentTo(StatusCodes.Status200OK);
    var books = JsonConvert.DeserializeObject<IEnumerable<Book>>(response1.Content.ReadAsStringAsync().Result).ToList();
    books.Count.Should().Be(0);

    var response2 = await Client.GetAsync($"/api/libraries/1/books");
    response2.StatusCode.Should().BeEquivalentTo(StatusCodes.Status200OK);
    var books2 = JsonConvert.DeserializeObject<IEnumerable<Book>>(response2.Content.ReadAsStringAsync().Result).ToList();
    books2.Count.Should().Be(2);

    var response3 = await Client.GetAsync($"/api/libraries/31232/books");
    response3.StatusCode.Should().BeEquivalentTo(StatusCodes.Status404NotFound);
}

3. TestDeleteLibrary

Verifies that DELETE /api/libraries/{libraryId} removes the library (and cascades to its books) and that subsequent requests against the deleted library return 404. Setup: seeds 4 libraries, then adds one book to library 1 via POST.
RequestExpected statusNotes
POST /api/libraries/1/books {"name": "test book 1"}201 CreatedSetup step
DELETE /api/libraries/1204 No ContentLibrary and its books are removed
GET /api/libraries/1/books404 Not FoundLibrary no longer exists
DELETE /api/libraries/1404 Not FoundIdempotent: deleting a non-existent library is 404
The cascade delete of books is enforced by the ON DELETE CASCADE foreign key defined in the InitialCreate migration — the controller only needs to delete the Library row.

HttpResponseExtensions helper

A small static extension class makes it easy to deserialize response bodies into typed objects within the tests:
public static class HttpResponseExtensions
{
    public static async Task<T> ReadBody<T>(this HttpResponseMessage response)
    {
        var content = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<T>(content);
    }
}
Usage example:
var books = await response.ReadBody<IEnumerable<Book>>();

Dependencies

All packages are declared in LibraryService.Integration.Test/LibraryService.Integration.Test.csproj:
PackageVersionPurpose
Microsoft.AspNetCore.Mvc.Testing8.0.16WebApplicationFactory — boots the full ASP.NET Core host in-process
Microsoft.EntityFrameworkCore.InMemory8.0.2In-memory EF Core provider (used internally by the host during setup)
Microsoft.EntityFrameworkCore.Sqlite8.0.2SQLite provider for tests — replaces Npgsql in the DI container
xunit2.9.3Test framework
FluentAssertions5.0.0Fluent assertion syntax (.Should().BeEquivalentTo(...))
Newtonsoft.Json13.0.3JSON serialization/deserialization inside test methods

Build docs developers (and LLMs) love