Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c0f1fd9
Create backend-integrationtests-agent.md for test guidelines
AntoMars14 Feb 5, 2026
745a297
Initial plan
Copilot Feb 5, 2026
0a340a4
Add SQLite persistence integration test
Copilot Feb 5, 2026
196f634
Add municipality persistence tests
Copilot Feb 5, 2026
561e30c
Merge pull request #139 from SPM-25-26/copilot/setup-integration-test…
AntoMars14 Feb 5, 2026
4b37ddc
Initial plan
Copilot Feb 5, 2026
aad96b7
Add WebApplicationFactory integration test
Copilot Feb 5, 2026
85a19cf
Harden reachability test guard
Copilot Feb 5, 2026
77e3153
Adjust test factory DB init
Copilot Feb 5, 2026
ae4d76a
Add municipality endpoint smoke tests
Copilot Feb 5, 2026
a88aba5
Convert municipality endpoint tests to API calls
Copilot Feb 5, 2026
da7e5a9
Use language param and JSON parse in API tests
Copilot Feb 5, 2026
91268a8
Refine API smoke test assertions
Copilot Feb 5, 2026
11220ca
Guard teardown disposals in API tests
Copilot Feb 5, 2026
e8385ba
Seed municipality API integration tests
Copilot Feb 5, 2026
e65219c
Seed municipality data for API routes
Copilot Feb 5, 2026
c0f00a1
Guard seeding in municipality API tests
Copilot Feb 5, 2026
74d979c
Harden municipality data seeding
Copilot Feb 5, 2026
d3e0dd3
Use AddRange for API seed data
Copilot Feb 5, 2026
4b05e9e
Make seeding lock explicit
Copilot Feb 5, 2026
35f9552
Simplify municipality seeding guard
Copilot Feb 5, 2026
b58ee3d
Rename seed lock field
Copilot Feb 5, 2026
ceb170f
Merge pull request #140 from SPM-25-26/copilot/add-api-integration-tests
AntoMars14 Feb 5, 2026
f653df8
Tests refactoring and tests fix.
AntoMars14 Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions .github/agents/backend-integrationtests-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
## Agent Profile — NAM Backend Integration Test Agent (NUnit)

You are the **NAM Backend Integration Test Agent** for repository **SPM-25-26/NAM**.

Your mission: add the **minimal necessary integration tests** focused on:
1) **API integration (HTTP-level)** for `nam.Server`
2) **Database integration** for persistence behavior

Scope is intentionally limited:
- ✅ API + DB integration
- ❌ No Qdrant integration tests
- ❌ No end-to-end (full stack) tests
- ❌ No tests requiring external networks or secrets

Repository contains `nam.sln` and backend projects:
- `nam.Server`
- `Infrastructure`
- `Domain`
- `DataInjection.Core`
- `DataInjection.SQL`
- `Datainjection.Qdrant` (OUT OF SCOPE for integration tests here)

Existing test project:
- `ServerTests/nam.ServerTests.csproj` (MUST be used)

---

## Key Requirement: Verify tests are not already present

Before adding new tests:
1) Search under `ServerTests/` for existing tests of the same feature.
2) Do NOT duplicate tests that already validate endpoint logic by calling endpoint methods directly.
- Example: `ServerTests/NamServer/Endpoints/Auth/*` already tests auth endpoint methods with an in-memory context.
3) Prefer adding missing coverage at the **HTTP boundary** (routing, middleware, serialization, status codes, DI wiring).

---

## Integration Test Types (Minimal Set)

### A) API Integration Tests (HTTP boundary) — REQUIRED
Goal: prove that the server can start and handle at least a small set of requests via HTTP.

Use:
- `Microsoft.AspNetCore.Mvc.Testing` (`WebApplicationFactory`)
- `HttpClient` against the in-memory TestServer

**Implementation Details:**
- Inherit from `WebApplicationFactory<Program>`.
- Override `ConfigureWebHost` to:
1. Remove the existing `DbContext` registration (SQL Server/Postgres).
2. Inject `DbContext` using `Microsoft.EntityFrameworkCore.Sqlite`.
3. Use connection string `DataSource=:memory:`.
4. **Crucial:** Keep the `SqliteConnection` object alive/open during the test lifetime, otherwise the in-memory DB is wiped between EF calls.

Test only a **minimal set**:
- A **health-style** or simplest reachable endpoint (200 OK)
- One representative endpoint that hits the DB (e.g., auth register if it is exposed via HTTP routes)

### B) Database Integration Tests — REQUIRED (minimal)
Goal: verify EF Core persistence behavior in a relational way.

Preferred approach (CI-friendly, deterministic):
- Use **SQLite in-memory** (`DataSource=:memory:`) and keep the connection open for test lifetime.
- Apply `EnsureCreated()` (or migrations if the app requires it and it is fast).

Fallback:
- EF Core InMemory is allowed only if SQLite cannot be wired without significant production changes.
(Note: EF InMemory is not relational and may miss constraints/translation issues.)

---

## Placement & Folder Structure

All tests live under:
- `ServerTests/`

Add these folders:
- `ServerTests/Integration/Api/` — HTTP-level tests (WebApplicationFactory)
- `ServerTests/Integration/Database/` — relational persistence tests
- `ServerTests/Integration/Shared/` — factory/fixtures (keep minimal)

---

## NUnit Standards

Prefer NUnit consistently for new tests:
- Use `[TestFixture]`, `[Test]`, `[SetUp]`, `[TearDown]`
- Use `Assert.That(...)` syntax
- Arrange / Act / Assert pattern

Note: Repository currently contains a mix of NUnit and MSTest attributes in some files; DO NOT add more MSTest-based tests.

---

## Determinism Rules (Hard)

Tests must not require:
- real external DB instances
- docker
- real secrets
- network calls to external services

All integration tests must:
- run with `dotnet test` in CI
- be stable and isolated (unique DB per test or clean state)

---

## Definition of Done

- Add **at least 1** HTTP-level API integration test using `WebApplicationFactory`
- Add **at least 1** DB integration test verifying a real persistence behavior (SQLite in-memory preferred)
- No Qdrant integration or E2E tests
- All tests pass locally and in CI:
- `dotnet restore`
- `dotnet build`
- `dotnet test`
- Clear test naming and minimal helper infrastructure
36 changes: 36 additions & 0 deletions ServerTests/Integration/Api/BasicReachabilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using NUnit.Framework;
using nam.ServerTests.Integration.Shared;

namespace nam.ServerTests.Integration.Api
{
[TestFixture]
public sealed class BasicReachabilityTests
{
private NamTestFactory? _factory;
private HttpClient? _client;

[SetUp]
public void Setup()
{
_factory = new NamTestFactory();
_client = _factory.CreateClient();
}

[TearDown]
public void TearDown()
{
_client?.Dispose();
_factory?.Dispose();
}

[Test]
public async Task Health_endpoint_is_reachable_async()
{
var client = _client ?? throw new System.InvalidOperationException("HTTP client was not initialized.");

var response = await client.GetAsync("/health");

NUnit.Framework.Assert.That(response.StatusCode, Is.EqualTo(System.Net.HttpStatusCode.OK));
}
}
}
147 changes: 147 additions & 0 deletions ServerTests/Integration/Database/PersistenceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using Domain.Entities;
using Domain.Entities.MunicipalityEntities;
using Infrastructure;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using NUnit.Framework;

namespace nam.ServerTests.Integration.Database
{
[TestFixture]
public sealed class PersistenceTests
{

private DbContextOptions<ApplicationDbContext> GetOptions(SqliteConnection connection)
{
return new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.Options;
}

[Test]
public async Task User_is_persisted_across_contexts_async()
{

await using var connection = new SqliteConnection("DataSource=:memory:");
await connection.OpenAsync();

var options = GetOptions(connection);


await using (var context = new ApplicationDbContext(options))
{
await context.Database.EnsureCreatedAsync();

context.Users.Add(new User
{
Email = "persisted@example.com",
PasswordHash = "hash",
IsEmailVerified = true
});

await context.SaveChangesAsync();
}


await using (var context = new ApplicationDbContext(options))
{
var user = await context.Users.SingleOrDefaultAsync(u => u.Email == "persisted@example.com");

Assert.That(user, Is.Not.Null, "Expected user to be persisted across contexts.");
Assert.That(user!.Email, Is.EqualTo("persisted@example.com"));
}
}

[Test]
public async Task Map_marker_is_persisted_across_contexts_async()
{
await using var connection = new SqliteConnection("DataSource=:memory:");
await connection.OpenAsync();

var options = GetOptions(connection);

await using (var context = new ApplicationDbContext(options))
{
await context.Database.EnsureCreatedAsync();

context.MapMarkers.Add(new MapMarker
{
Name = "Marker One",
ImagePath = "marker.png",
Typology = "Test"
});

await context.SaveChangesAsync();
}

await using (var context = new ApplicationDbContext(options))
{
var marker = await context.MapMarkers.SingleOrDefaultAsync(m => m.Name == "Marker One");

Assert.That(marker, Is.Not.Null, "Expected map marker to be persisted across contexts.");
Assert.That(marker!.ImagePath, Is.EqualTo("marker.png"));
}
}

[Test]
public async Task Map_data_is_persisted_across_contexts_async()
{
await using var connection = new SqliteConnection("DataSource=:memory:");
await connection.OpenAsync();

var options = GetOptions(connection);

await using (var context = new ApplicationDbContext(options))
{
await context.Database.EnsureCreatedAsync();

context.MapData.Add(new MapData
{
Name = "Test Map",
CenterLatitude = 45.123,
CenterLongitude = 9.456
});

await context.SaveChangesAsync();
}

await using (var context = new ApplicationDbContext(options))
{
var mapData = await context.MapData.SingleOrDefaultAsync(m => m.Name == "Test Map");

Assert.That(mapData, Is.Not.Null, "Expected map data to be persisted across contexts.");
Assert.That(mapData!.CenterLatitude, Is.EqualTo(45.123));
}
}

[Test]
public async Task Municipality_card_is_persisted_across_contexts_async()
{
await using var connection = new SqliteConnection("DataSource=:memory:");
await connection.OpenAsync();

var options = GetOptions(connection);

await using (var context = new ApplicationDbContext(options))
{
await context.Database.EnsureCreatedAsync();

context.MunicipalityCards.Add(new MunicipalityCard
{
LegalName = "Test Municipality",
ImagePath = "municipality.png"
});

await context.SaveChangesAsync();
}

await using (var context = new ApplicationDbContext(options))
{
var card = await context.MunicipalityCards.SingleOrDefaultAsync(m => m.LegalName == "Test Municipality");

Assert.That(card, Is.Not.Null, "Expected municipality card to be persisted across contexts.");
Assert.That(card!.ImagePath, Is.EqualTo("municipality.png"));
}
}
}
}
Loading
Loading