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.
| Request | Expected 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.
| Request | Expected status | Expected body |
|---|
GET /api/libraries/2/books | 200 OK | Empty list (0 books) |
GET /api/libraries/1/books | 200 OK | List with 2 books |
GET /api/libraries/31232/books | 404 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.
| Request | Expected status | Notes |
|---|
POST /api/libraries/1/books {"name": "test book 1"} | 201 Created | Setup step |
DELETE /api/libraries/1 | 204 No Content | Library and its books are removed |
GET /api/libraries/1/books | 404 Not Found | Library no longer exists |
DELETE /api/libraries/1 | 404 Not Found | Idempotent: 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:
| Package | Version | Purpose |
|---|
Microsoft.AspNetCore.Mvc.Testing | 8.0.16 | WebApplicationFactory — boots the full ASP.NET Core host in-process |
Microsoft.EntityFrameworkCore.InMemory | 8.0.2 | In-memory EF Core provider (used internally by the host during setup) |
Microsoft.EntityFrameworkCore.Sqlite | 8.0.2 | SQLite provider for tests — replaces Npgsql in the DI container |
xunit | 2.9.3 | Test framework |
FluentAssertions | 5.0.0 | Fluent assertion syntax (.Should().BeEquivalentTo(...)) |
Newtonsoft.Json | 13.0.3 | JSON serialization/deserialization inside test methods |