diff --git a/.github/agents/backend-integrationtests-agent.md b/.github/agents/backend-integrationtests-agent.md new file mode 100644 index 0000000..d82d07b --- /dev/null +++ b/.github/agents/backend-integrationtests-agent.md @@ -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`. +- 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 diff --git a/ServerTests/Integration/Api/BasicReachabilityTests.cs b/ServerTests/Integration/Api/BasicReachabilityTests.cs new file mode 100644 index 0000000..0aba582 --- /dev/null +++ b/ServerTests/Integration/Api/BasicReachabilityTests.cs @@ -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)); + } + } +} diff --git a/ServerTests/Integration/Database/PersistenceTests.cs b/ServerTests/Integration/Database/PersistenceTests.cs new file mode 100644 index 0000000..6dbd2f3 --- /dev/null +++ b/ServerTests/Integration/Database/PersistenceTests.cs @@ -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 GetOptions(SqliteConnection connection) + { + return new DbContextOptionsBuilder() + .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")); + } + } + } +} \ No newline at end of file diff --git a/ServerTests/Integration/Shared/NamTestFactory.cs b/ServerTests/Integration/Shared/NamTestFactory.cs new file mode 100644 index 0000000..923375a --- /dev/null +++ b/ServerTests/Integration/Shared/NamTestFactory.cs @@ -0,0 +1,103 @@ +using Infrastructure; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using System.Data.Common; +using System.Linq; +using System.Text.Json.Serialization; + +namespace nam.ServerTests.Integration.Shared +{ + public class NamTestFactory : WebApplicationFactory + { + private SqliteConnection _connection; + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + + builder.UseEnvironment("Development"); + + builder.ConfigureServices(services => + { + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = TestAuthHandler.AuthenticationScheme; + options.DefaultChallengeScheme = TestAuthHandler.AuthenticationScheme; + }) + .AddScheme( + TestAuthHandler.AuthenticationScheme, options => { }); + + services.AddControllers().AddJsonOptions(options => + { + options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; + }); + + var contextType = typeof(ApplicationDbContext); + var servicesToRemove = services.Where(d => + d.ServiceType == contextType || + (d.ServiceType.IsGenericType && d.ServiceType.GetGenericArguments().Contains(contextType)) + ).ToList(); + + foreach (var descriptor in servicesToRemove) + { + services.Remove(descriptor); + } + + var dbConnectionDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbConnection)); + if (dbConnectionDescriptor != null) + { + services.Remove(dbConnectionDescriptor); + } + + + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + services.AddDbContext((container, options) => + { + options.UseSqlite(_connection); + + }); + + services.Configure(options => + { + options.Registrations.Clear(); + }); + + }); + } + + protected override IHost CreateHost(IHostBuilder builder) + { + var host = base.CreateHost(builder); + + using (var scope = host.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + try + { + db.Database.EnsureCreated(); + } + catch + { + + } + } + + return host; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _connection?.Close(); + _connection?.Dispose(); + } + } +} \ No newline at end of file diff --git a/ServerTests/Integration/Shared/TestAuthHandler.cs b/ServerTests/Integration/Shared/TestAuthHandler.cs new file mode 100644 index 0000000..548f1fd --- /dev/null +++ b/ServerTests/Integration/Shared/TestAuthHandler.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace nam.ServerTests.Integration.Shared +{ + public class TestAuthHandler : AuthenticationHandler + { + public const string AuthenticationScheme = "Test"; + + public TestAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + protected override Task HandleAuthenticateAsync() + { + // Creiamo un utente finto con dei claim di base + var claims = new[] + { + new Claim(ClaimTypes.Name, "TestUser"), + new Claim(ClaimTypes.NameIdentifier, "test-user-id"), + }; + var identity = new ClaimsIdentity(claims, AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, AuthenticationScheme); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + } +} \ No newline at end of file diff --git a/ServerTests/NamServer/Endpoints/Auth/AuthLoginTests.cs b/ServerTests/NamServer/Endpoints/Auth/AuthLoginTests.cs index 110bc9e..4a697bc 100644 --- a/ServerTests/NamServer/Endpoints/Auth/AuthLoginTests.cs +++ b/ServerTests/NamServer/Endpoints/Auth/AuthLoginTests.cs @@ -1,93 +1,80 @@ using nam.Server.DTOs; using nam.Server.Services.Implemented.Auth; using nam.ServerTests.NamServer.Endpoints.Auth.mock; +using NUnit.Framework; +using Assert = NUnit.Framework.Assert; namespace nam.ServerTests.NamServer.Endpoints.Auth { - [TestClass] + [TestFixture] public sealed class AuthLoginTests { private AuthServiceTestBuilder _builder = null!; private AuthService _authService = null!; - [TestInitialize] + [SetUp] public void Setup() { _builder = new AuthServiceTestBuilder(); _authService = _builder.Build(); } - [TestCleanup] + [TearDown] public void Cleanup() { _builder.Dispose(); } - [TestMethod] + [Test] public async Task Login_ReturnsToken_WhenCredentialsAreValid() { - // Arrange var email = "user@example.com"; var passwordPlain = "$Password1"; - // Insert the user into the in-memory DB via the builder await _builder.SeedUserAsync(email, passwordPlain, isVerified: true); var credentials = new LoginCredentialsDto(email, passwordPlain); - // Act - Call the service directly (pure unit test of the service) var token = await _authService.GenerateTokenAsync(credentials); - // Assert - Assert.IsNotNull(token, "Il token non dovrebbe essere null"); - Assert.AreEqual("fake-jwt-token-generated", token); + Assert.That(token, Is.Not.Null); + Assert.That(token, Is.EqualTo("fake-jwt-token-generated")); } - [TestMethod] + [Test] public async Task Login_ReturnsNull_WhenCredentialsAreInvalid() { - // Arrange var email = "user@example.com"; await _builder.SeedUserAsync(email, "PasswordCorretta", isVerified: true); var credentials = new LoginCredentialsDto(email, "PasswordSbagliata!"); - // Act var token = await _authService.GenerateTokenAsync(credentials); - // Assert - Assert.IsNull(token, "Il token dovrebbe essere null se la password è errata"); + Assert.That(token, Is.Null); } - [TestMethod] + [Test] public async Task Login_ReturnsNull_WhenEmailNotVerified() { - // Arrange var email = "notverified@example.com"; - // Creiamo l'utente ma con email NON verificata await _builder.SeedUserAsync(email, "Password123", isVerified: false); var credentials = new LoginCredentialsDto(email, "Password123"); - // Act var token = await _authService.GenerateTokenAsync(credentials); - // Assert - Assert.IsNull(token, "Il token dovrebbe essere null se l'email non è verificata"); + Assert.That(token, Is.Null); } - [TestMethod] + [Test] public async Task Login_ReturnsNull_WhenUserDoesNotExist() { - // Arrange - // Nessun utente nel DB var credentials = new LoginCredentialsDto("ghost@example.com", "Password123"); - // Act var token = await _authService.GenerateTokenAsync(credentials); - // Assert - Assert.IsNull(token); + Assert.That(token, Is.Null); } } } \ No newline at end of file diff --git a/ServerTests/NamServer/Endpoints/Auth/MSTestSettings.cs b/ServerTests/NamServer/Endpoints/Auth/MSTestSettings.cs deleted file mode 100644 index aaf278c..0000000 --- a/ServerTests/NamServer/Endpoints/Auth/MSTestSettings.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/ServerTests/NamServer/Endpoints/Auth/RegistrationTests.cs b/ServerTests/NamServer/Endpoints/Auth/RegistrationTests.cs index 378b657..8c31084 100644 --- a/ServerTests/NamServer/Endpoints/Auth/RegistrationTests.cs +++ b/ServerTests/NamServer/Endpoints/Auth/RegistrationTests.cs @@ -6,24 +6,24 @@ using nam.Server.Services.Interfaces.Auth; using nam.Server.Validators; using nam.ServerTests.NamServer.Endpoints.Auth.mock; +using NUnit.Framework; using Serilog; +using Assert = NUnit.Framework.Assert; namespace nam.ServerTests.NamServer.Endpoints.Auth { - [TestClass] + [TestFixture] public sealed class RegistrationTests { private AuthServiceTestBuilder _builder = null!; private IAuthService _authService = null!; - [TestInitialize] + [SetUp] public void Setup() { - // 1. Use the Builder to configure in-memory DB and fake dependencies _builder = new AuthServiceTestBuilder(); _authService = _builder.Build(); - // Configure Serilog var logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console() @@ -32,17 +32,16 @@ public void Setup() AuthEndpoints.ConfigureLogger(logger); } - [TestCleanup] + [TearDown] public void Cleanup() { _builder.Dispose(); Log.CloseAndFlush(); } - [TestMethod] + [Test] public async Task RegisterUser_ReturnsOk_WhenRegistrationIsSuccessfulAsync() { - // Arrange RegisterUserValidator validator = new(); RegisterUserDto registrationData = new() { @@ -51,31 +50,23 @@ public async Task RegisterUser_ReturnsOk_WhenRegistrationIsSuccessfulAsync() ConfirmPassword = "ValidPassword123!" }; - // Act - // Assume that the endpoint now accepts IAuthService var result = await AuthEndpoints.RegisterUser(registrationData, _authService, validator); - // Assert - Assert.IsInstanceOfType(result, typeof(Ok)); + Assert.That(result, Is.InstanceOf>()); - // Verify in the DB (via the builder's context) var userInDb = await _builder.Context.Users.FirstOrDefaultAsync(u => u.Email == registrationData.Email); - Assert.IsNotNull(userInDb, "User should exist in the database"); - Assert.AreEqual("validmail@gmail.com", userInDb.Email); - - // Optional verification: password hashed? - Assert.AreNotEqual("ValidPassword123!", userInDb.PasswordHash); + Assert.That(userInDb, Is.Not.Null); + Assert.That(userInDb!.Email, Is.EqualTo("validmail@gmail.com")); + Assert.That(userInDb.PasswordHash, Is.Not.EqualTo("ValidPassword123!")); } - [TestMethod] + [Test] public async Task RegisterUser_ReturnsConflict_WhenEmailAlreadyExists() { - // Arrange RegisterUserValidator validator = new(); var existingEmail = "existing@example.com"; - // Seed of the DB using the builder's helper method await _builder.SeedUserAsync(existingEmail, "SomePassword123!"); RegisterUserDto registrationData = new() @@ -85,38 +76,30 @@ public async Task RegisterUser_ReturnsConflict_WhenEmailAlreadyExists() ConfirmPassword = "ValidPassword123!" }; - // Act var result = await AuthEndpoints.RegisterUser(registrationData, _authService, validator); - // Assert - // Assume that AuthService returns false if exists, and the endpoint returns Conflict - Assert.IsInstanceOfType(result, typeof(Conflict)); + Assert.That(result, Is.InstanceOf>()); } - [TestMethod] + [Test] public async Task RegisterUser_ReturnsValidationProblem_WhenPasswordsDoNotMatch() { - // Arrange RegisterUserValidator validator = new(); RegisterUserDto registrationData = new() { Email = "newuser@example.com", Password = "ValidPasswordA123!", - ConfirmPassword = "ValidPasswordB123!" // Mismatch + ConfirmPassword = "ValidPasswordB123!" }; - // Act - // The validator triggers before calling the service, so _authService will not actually be invoked var result = await AuthEndpoints.RegisterUser(registrationData, _authService, validator); - // Assert - Assert.IsInstanceOfType(result, typeof(ValidationProblem)); + Assert.That(result, Is.InstanceOf()); } - [TestMethod] + [Test] public async Task RegisterUser_ReturnsValidationProblem_WhenEmailIsInvalid() { - // Arrange RegisterUserValidator validator = new(); RegisterUserDto registrationData = new() { @@ -125,30 +108,25 @@ public async Task RegisterUser_ReturnsValidationProblem_WhenEmailIsInvalid() ConfirmPassword = "ValidPassword123!" }; - // Act var result = await AuthEndpoints.RegisterUser(registrationData, _authService, validator); - // Assert - Assert.IsInstanceOfType(result, typeof(ValidationProblem)); + Assert.That(result, Is.InstanceOf()); } - [TestMethod] + [Test] public async Task RegisterUser_ReturnsValidationProblem_WhenPasswordIsTooWeak() { - // Arrange RegisterUserValidator validator = new(); RegisterUserDto registrationData = new() { Email = "valid@example.com", - Password = "123", // Too short/simple + Password = "123", ConfirmPassword = "123" }; - // Act var result = await AuthEndpoints.RegisterUser(registrationData, _authService, validator); - // Assert - Assert.IsInstanceOfType(result, typeof(ValidationProblem)); + Assert.That(result, Is.InstanceOf()); } } } \ No newline at end of file diff --git a/ServerTests/NamServer/Endpoints/Auth/ResetPasswordTest.cs b/ServerTests/NamServer/Endpoints/Auth/ResetPasswordTest.cs index c740bb5..3f1a19a 100644 --- a/ServerTests/NamServer/Endpoints/Auth/ResetPasswordTest.cs +++ b/ServerTests/NamServer/Endpoints/Auth/ResetPasswordTest.cs @@ -1,100 +1,238 @@ +using Domain.Entities; +using Domain.Entities.Auth; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; -using nam.Server.ApiResponse; using nam.Server.DTOs; using nam.Server.Endpoints.Auth; using nam.Server.Services.Interfaces.Auth; -using nam.Server.Validators; using nam.ServerTests.NamServer.Endpoints.Auth.mock; -using Serilog; +using NSubstitute; +using NUnit.Framework; +using Assert = NUnit.Framework.Assert; namespace nam.ServerTests.NamServer.Endpoints.Auth { - [TestClass] - public sealed class ResetPasswordTest + [TestFixture] + public sealed class PasswordResetTests { - private AuthServiceTestBuilder _builder = null!; private IAuthService _authService = null!; + private const string StaticAuthCode = "123456"; - [TestInitialize] + [SetUp] public void Setup() { _builder = new AuthServiceTestBuilder(); _authService = _builder.Build(); - - var logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.Console() - .CreateLogger(); - - AuthEndpoints.ConfigureLogger(logger); } - [TestCleanup] + [TearDown] public void Cleanup() { _builder.Dispose(); - Log.CloseAndFlush(); } - [TestMethod] - public async Task RegisterUser_ReturnsOk_WhenRegistrationIsSuccessfulAsync() + private PasswordResetResponseDto GetDtoFromAnonymousResult(object result) + { + + Assert.That(result.GetType().Name, Does.StartWith("Ok"), "Il risultato non un Ok Result"); + + + var valueProp = result.GetType().GetProperty("Value"); + var anonymousValue = valueProp?.GetValue(result); + Assert.That(anonymousValue, Is.Not.Null, "Il valore della risposta nullo"); + + + var dataProp = anonymousValue!.GetType().GetProperty("data"); + Assert.That(dataProp, Is.Not.Null, "La risposta non contiene la propriet 'data'"); + + return (PasswordResetResponseDto)dataProp!.GetValue(anonymousValue)!; + } + + [Test] + public async Task RequestPasswordReset_EmailNotExists_ReturnsNotFound() + { + var request = new PasswordResetRequestDto { Email = "nonexistent@example.com" }; + + var result = await AuthEndpoints.RequestPasswordReset(request, _authService); + + + Assert.That(result, Is.InstanceOf()); + var problem = (ProblemHttpResult)result; + + Assert.That(problem.StatusCode, Is.EqualTo(404)); + Assert.That(problem.ProblemDetails.Detail, Is.EqualTo("The email not found")); + + var codesCount = await _builder.Context.ResetPasswordAuth.CountAsync(); + Assert.That(codesCount, Is.EqualTo(0)); + } + + [Test] + public async Task RequestPasswordReset_EmailExists_CodeIsCreatedAndSaved() { - RegisterUserValidator validator = new(); - RegisterUserDto registrationData = new() + const string testEmail = "test@example.com"; + Guid testUserId = Guid.NewGuid(); + + _builder.Context.Users.Add(new User { - Email = "validmail@gmail.com", - Password = "ValidPassword123!", - ConfirmPassword = "ValidPassword123!" - }; + Id = testUserId, + Email = testEmail, + PasswordHash = "dummyhash", + IsEmailVerified = true + }); + await _builder.Context.SaveChangesAsync(); + + var request = new PasswordResetRequestDto { Email = testEmail }; + var beforeRequest = DateTime.UtcNow; + + var result = await AuthEndpoints.RequestPasswordReset(request, _authService); - var result = await AuthEndpoints.RegisterUser(registrationData, _authService, validator); + + if (result is ProblemHttpResult problem) Assert.Fail($"Server Error: {problem.ProblemDetails.Detail}"); - Assert.IsInstanceOfType(result, typeof(Ok)); + + var responseDto = GetDtoFromAnonymousResult(result); + Assert.That(responseDto.Success, Is.True); - var userInDb = await _builder.Context.Users.FirstOrDefaultAsync(u => u.Email == registrationData.Email); + + var savedCode = await _builder.Context.ResetPasswordAuth + .FirstOrDefaultAsync(c => c.UserId == testUserId.ToString()); - Assert.IsNotNull(userInDb, "User should exist in the database"); - Assert.AreEqual("validmail@gmail.com", userInDb.Email); + Assert.That(savedCode, Is.Not.Null); + Assert.That(savedCode!.AuthCode, Is.EqualTo(StaticAuthCode)); + Assert.That(savedCode.ExpiresAt, Is.GreaterThan(beforeRequest.AddMinutes(14))); + Assert.That(savedCode.UserId, Is.EqualTo(testUserId.ToString())); - Assert.AreNotEqual("ValidPassword123!", userInDb.PasswordHash); + await _builder.EmailService.Received(1).SendEmailAsync(testEmail, Arg.Any(), Arg.Any()); } - [TestMethod] - public async Task RegisterUser_ReturnsConflict_WhenEmailAlreadyExists() + [Test] + public async Task RequestPasswordReset_ExistingCodeIsOverwritten() { - RegisterUserValidator validator = new(); - var existingEmail = "existing@example.com"; + const string testEmail = "overwrite@example.com"; + Guid testUserId = Guid.NewGuid(); - await _builder.SeedUserAsync(existingEmail, "SomePassword123!"); + _builder.Context.Users.Add(new User + { + Id = testUserId, + Email = testEmail, + PasswordHash = "dummyhash", + IsEmailVerified = true + }); + + var oldAuthCode = "999999"; + var oldExpiration = DateTime.UtcNow.AddMinutes(5); + + _builder.Context.ResetPasswordAuth.Add(new PasswordResetCode + { + UserId = testUserId.ToString(), + AuthCode = oldAuthCode, + ExpiresAt = oldExpiration, + CreatedAt = DateTime.UtcNow.AddMinutes(-10) + }); + await _builder.Context.SaveChangesAsync(); + + var request = new PasswordResetRequestDto { Email = testEmail }; + var beforeSecondRequest = DateTime.UtcNow; + + var result = await AuthEndpoints.RequestPasswordReset(request, _authService); + + + var responseDto = GetDtoFromAnonymousResult(result); + Assert.That(responseDto.Success, Is.True); + + var codesCount = await _builder.Context.ResetPasswordAuth.CountAsync(); + Assert.That(codesCount, Is.EqualTo(1)); + + var updatedCode = await _builder.Context.ResetPasswordAuth.SingleAsync(); + Assert.That(updatedCode.AuthCode, Is.EqualTo(StaticAuthCode)); + Assert.That(updatedCode.ExpiresAt, Is.GreaterThan(beforeSecondRequest.AddMinutes(14))); + } - RegisterUserDto registrationData = new() + [Test] + public async Task ResetPassword_ExpiredCode_ReturnsBadRequest() + { + const string testEmail = "expired@example.com"; + Guid testUserId = Guid.NewGuid(); + + _builder.Context.Users.Add(new User + { + Id = testUserId, + Email = testEmail, + PasswordHash = "dummyhash", + IsEmailVerified = true + }); + + var expiredTime = DateTime.UtcNow.AddMinutes(-5); + _builder.Context.ResetPasswordAuth.Add(new PasswordResetCode { - Email = existingEmail, - Password = "ValidPassword123!", - ConfirmPassword = "ValidPassword123!" + UserId = testUserId.ToString(), + AuthCode = StaticAuthCode, + CreatedAt = expiredTime.AddMinutes(-15), + ExpiresAt = expiredTime + }); + await _builder.Context.SaveChangesAsync(); + + var request = new PasswordResetConfirmDto + { + AuthCode = StaticAuthCode, + NewPassword = "mock_password_1", + ConfirmPassword = "mock_password_1", }; - var result = await AuthEndpoints.RegisterUser(registrationData, _authService, validator); + var result = await AuthEndpoints.ResetPassword(request, _authService); + + + Assert.That(result, Is.InstanceOf()); + var problem = (ProblemHttpResult)result; - Assert.IsInstanceOfType(result, typeof(Conflict)); + Assert.That(problem.StatusCode, Is.EqualTo(400)); + Assert.That(problem.ProblemDetails.Detail.ToLower(), Does.Contain("expired")); } - [TestMethod] - public async Task RegisterUser_ReturnsValidationProblem_WhenPasswordsDoNotMatch() + [Test] + public async Task ResetPassword_IncorrectCode_ReturnsBadRequest() { - RegisterUserValidator validator = new(); - RegisterUserDto registrationData = new() + const string testEmail = "wrongcode@example.com"; + Guid testUserId = Guid.NewGuid(); + + _builder.Context.Users.Add(new User + { + Id = testUserId, + Email = testEmail, + PasswordHash = "dummyhash", + IsEmailVerified = true + }); + + var correctCode = "555555"; + var validTime = DateTime.UtcNow.AddMinutes(5); + _builder.Context.ResetPasswordAuth.Add(new PasswordResetCode + { + UserId = testUserId.ToString(), + AuthCode = correctCode, + ExpiresAt = validTime, + CreatedAt = DateTime.UtcNow + }); + await _builder.Context.SaveChangesAsync(); + + var request = new PasswordResetConfirmDto { - Email = "newuser@example.com", - Password = "ValidPasswordA123! ", - ConfirmPassword = "ValidPasswordB123!" + AuthCode = "999999", + NewPassword = "new", + ConfirmPassword = "new" }; - var result = await AuthEndpoints.RegisterUser(registrationData, _authService, validator); + var result = await AuthEndpoints.ResetPassword(request, _authService); + + // L'endpoint usa TypedResults.Problem(400) + Assert.That(result, Is.InstanceOf()); + var problem = (ProblemHttpResult)result; + + Assert.That(problem.StatusCode, Is.EqualTo(400)); + Assert.That(problem.ProblemDetails.Detail.ToLower(), Does.Match(".*(invalid|not found).*")); - Assert.IsInstanceOfType(result, typeof(ValidationProblem)); + var codeExists = await _builder.Context.ResetPasswordAuth.AnyAsync(); + Assert.That(codeExists, Is.True); } } } \ No newline at end of file diff --git a/ServerTests/NamServer/Endpoints/Auth/TokenLogoutTest.cs b/ServerTests/NamServer/Endpoints/Auth/TokenLogoutTest.cs index 94638e8..c3a9268 100644 --- a/ServerTests/NamServer/Endpoints/Auth/TokenLogoutTest.cs +++ b/ServerTests/NamServer/Endpoints/Auth/TokenLogoutTest.cs @@ -4,39 +4,38 @@ using nam.Server.Endpoints.Auth; using nam.Server.Services.Interfaces.Auth; using nam.ServerTests.NamServer.Endpoints.Auth.mock; +using NUnit.Framework; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; +using Assert = NUnit.Framework.Assert; namespace nam.ServerTests.NamServer.Endpoints.Auth { - [TestClass] + [TestFixture] public sealed class TokenLogoutTest { private AuthServiceTestBuilder _builder = null!; private IAuthService _authService = null!; - [TestInitialize] + [SetUp] public void Setup() { - // Initialize the builder and the service (which uses the in-memory DB) _builder = new AuthServiceTestBuilder(); _authService = _builder.Build(); } - [TestCleanup] + [TearDown] public void Cleanup() { _builder.Dispose(); } - [TestMethod] + [Test] public async Task Logout_ReturnsOk_AndRevokesToken_WhenUserIsAuthenticated() { - // Arrange: HttpContext with authenticated user and claim jti/exp var httpContext = new DefaultHttpContext(); var jti = Guid.NewGuid().ToString(); - // Exp claims are in Unix seconds var exp = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds().ToString(); var claims = new List @@ -49,43 +48,30 @@ public async Task Logout_ReturnsOk_AndRevokesToken_WhenUserIsAuthenticated() var principal = new ClaimsPrincipal(identity); httpContext.User = principal; - // Act - // Assume that the endpoint now accepts IAuthService var result = await AuthEndpoints.LogoutAsync(httpContext, CancellationToken.None, _authService); - // Assert - Verify Response - // Verify that it is NOT an error - Assert.IsNotInstanceOfType(result, typeof(UnauthorizedHttpResult)); - Assert.IsNotInstanceOfType(result, typeof(BadRequest)); + Assert.That(result, Is.Not.InstanceOf()); + Assert.That(result, Is.Not.InstanceOf>()); - // Generic check on the OK result var okResult = result as dynamic; - Assert.IsNotNull(okResult); + Assert.That(okResult, Is.Not.Null); - var value = okResult.Value; - Assert.IsNotNull(value); + Assert.That(okResult.Value, Is.Not.Null); + Assert.That(okResult.Value.Message, Is.EqualTo("Logout done, token revokated.")); - Assert.IsNotNull(okResult.Value); - Assert.AreEqual("Logout done, token revokated.", okResult.Value.Message); - - // Assert - Verify Database side effect - // Verify that AuthService has written to the RevokedTokens table var isRevokedInDb = await _builder.Context.RevokedTokens.AnyAsync(t => t.Jti == jti); - Assert.IsTrue(isRevokedInDb, "Il token (jti) dovrebbe essere presente nella tabella RevokedTokens del DB."); + Assert.That(isRevokedInDb, Is.True); } - [TestMethod] + [Test] public async Task Logout_ReturnsUnauthorized_WhenUserIsNotAuthenticated() { - // Arrange: HttpContext user not authenticated var httpContext = new DefaultHttpContext(); - httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); // No AuthType = Not Authenticated + httpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); - // Act var result = await AuthEndpoints.LogoutAsync(httpContext, CancellationToken.None, _authService); - // Assert - Assert.IsInstanceOfType(result, typeof(ProblemHttpResult)); + Assert.That(result, Is.InstanceOf()); } } } \ No newline at end of file diff --git a/ServerTests/NamServer/Endpoints/Auth/mock/AuthServiceTestBuilder.cs b/ServerTests/NamServer/Endpoints/Auth/mock/AuthServiceTestBuilder.cs index d79a06d..f461b95 100644 --- a/ServerTests/NamServer/Endpoints/Auth/mock/AuthServiceTestBuilder.cs +++ b/ServerTests/NamServer/Endpoints/Auth/mock/AuthServiceTestBuilder.cs @@ -1,38 +1,71 @@ using Domain.Entities; using Infrastructure; using Infrastructure.Repositories.Interfaces; -using Infrastructure.Repositories.Interfaces.MunicipalityEntities; using Infrastructure.UnitOfWork; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using nam.Server.Services.Implemented.Auth; using nam.Server.Services.Interfaces.Auth; +using NSubstitute; using System.Linq.Expressions; -using System.Security.Claims; namespace nam.ServerTests.NamServer.Endpoints.Auth.mock { public class AuthServiceTestBuilder : IDisposable { + private readonly SqliteConnection _connection; public ApplicationDbContext Context { get; } - - public FakeEmailService EmailService { get; } = new(); - public FakeCodeService CodeService { get; } = new(); + public IEmailService EmailService { get; } + public ICodeService CodeService { get; } + public ITokenGeneration TokenService { get; } + public IUnitOfWork UnitOfWork { get; } public AuthServiceTestBuilder() { + + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"AuthDb_{Guid.NewGuid()}") + .UseSqlite(_connection) .Options; Context = new ApplicationDbContext(options); + Context.Database.EnsureCreated(); + + + EmailService = Substitute.For(); + CodeService = Substitute.For(); + TokenService = Substitute.For(); + + + CodeService.GenerateAuthCode().Returns("123456"); + CodeService.TimeToLiveMinutes.Returns(15); + EmailService.SendEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + TokenService.GenerateTokenAsync(Arg.Any(), Arg.Any()) + .Returns("fake-jwt-token-generated"); + + + var fakePrincipal = new System.Security.Claims.ClaimsPrincipal( + new System.Security.Claims.ClaimsIdentity(new[] { + new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Email, "valid@example.com") + })); + TokenService.ValidateEmailVerificationToken(Arg.Is("valid-token")).Returns(fakePrincipal); + + + UnitOfWork = Substitute.For(); + + + var userRepository = new TestUserRepository(Context); + UnitOfWork.Users.Returns(userRepository); + + UnitOfWork.CompleteAsync().Returns(async x => await Context.SaveChangesAsync()); } public AuthService Build() { - var unitOfWork = new FakeUnitOfWork(Context); - var tokenGen = new FakeTokenGeneration(); - var myConfiguration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -41,10 +74,10 @@ public AuthService Build() .Build(); return new AuthService( - unitOfWork, - tokenGen, + UnitOfWork, + TokenService, myConfiguration, - Context, + Context, EmailService, CodeService ); @@ -65,165 +98,52 @@ public async Task SeedUserAsync(string email, string clearPassword, bool isVerif public void Dispose() { - Context.Database.EnsureDeleted(); Context.Dispose(); + _connection.Close(); + _connection.Dispose(); } } - public class FakeUnitOfWork : IUnitOfWork - { - private readonly ApplicationDbContext _context; - - public FakeUnitOfWork(ApplicationDbContext context) - { - _context = context; - Users = new FakeUserRepository(_context); - ArtCulture = null!; - Article = null!; - MunicipalityCard = null!; - Nature = null!; - Organization = null!; - PublicEvent = null!; - EntertainmentLeisure = null!; - } - - public IUserRepository Users { get; } - - public IArtCultureRepository ArtCulture { get; } - - public IArticleRepository Article { get; } - - public IMunicipalityCardRepository MunicipalityCard { get; } - - public INatureRepository Nature { get; } - - public IOrganizationRepository Organization { get; } - - public IPublicEventRepository PublicEvent { get; } - - public IEntertainmentLeisureRepository EntertainmentLeisure { get; } - - public IUserRepository Questionaires => throw new NotImplementedException(); - - public IRouteRepository Route => throw new NotImplementedException(); - - public IServiceRepository Service => throw new NotImplementedException(); - - public IShoppingRepository Shopping => throw new NotImplementedException(); - - public ISleepRepository Sleep => throw new NotImplementedException(); - public IEatAndDrinkRepository EatAndDrink => throw new NotImplementedException(); - public IMapDataRepository MapData => throw new NotImplementedException(); - public Task CompleteAsync() - { - return _context.SaveChangesAsync(); - } - } - - public class FakeUserRepository : IUserRepository + // Repository Concreto per i test: evita problemi con Mock asincroni + public class TestUserRepository : IUserRepository { private readonly ApplicationDbContext _context; private readonly DbSet _dbSet; - public FakeUserRepository(ApplicationDbContext context) + public TestUserRepository(ApplicationDbContext context) { _context = context; _dbSet = context.Set(); } - public async Task GetAsync(Guid id, CancellationToken cancellationToken = default) - { - return await _dbSet.FindAsync(new object[] { id }, cancellationToken); - } + // Metodi usati da AuthService + public async Task GetByEmailAsync(string email, CancellationToken ct = default) + => await _dbSet.FirstOrDefaultAsync(u => u.Email == email, ct); - public async Task> GetAllAsync(CancellationToken cancellationToken = default) - { - return await _dbSet.ToListAsync(cancellationToken); - } + public async Task EmailExistsAsync(string email, CancellationToken ct = default) + => await _dbSet.AnyAsync(u => u.Email == email, ct); - public IEnumerable Find(Expression> predicate) + public async Task AddAsync(User user, CancellationToken ct = default) { - return _dbSet.Where(predicate).ToList(); + await _dbSet.AddAsync(user, ct); + return await _context.SaveChangesAsync(ct) > 0; } - public void Add(User entity) - { - _dbSet.Add(entity); - } - - public void Remove(User entity) - { - _dbSet.Remove(entity); - } - - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return _context.SaveChangesAsync(cancellationToken); - } - - public Task GetByEmailAsync(string email, CancellationToken cancellationToken = default) - { - return _dbSet.FirstOrDefaultAsync(u => u.Email == email, cancellationToken); - } - - public Task EmailExistsAsync(string email, CancellationToken cancellationToken = default) - { - return _dbSet.AnyAsync(u => u.Email == email, cancellationToken); - } - - public async Task AddAsync(User user, CancellationToken cancellationToken = default) - { - await _dbSet.AddAsync(user, cancellationToken); - var changes = await _context.SaveChangesAsync(cancellationToken); - return changes > 0; - } - - public async Task UpdateAsync(User user, CancellationToken cancellationToken = default) + public async Task UpdateAsync(User user, CancellationToken ct = default) { _dbSet.Update(user); - var changes = await _context.SaveChangesAsync(cancellationToken); - return changes >= 0; + return await _context.SaveChangesAsync(ct) >= 0; } - public Task UpdateQuestionaireByEmailAsync(Questionaire questionaire, string email, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - } - - public class FakeTokenGeneration : ITokenGeneration - { - public Task GenerateTokenAsync(string userId, string email) - { - return Task.FromResult("fake-jwt-token-generated"); - } - - public ClaimsPrincipal? ValidateEmailVerificationToken(string token) - { - if (token == "valid-token") - { - var claims = new List { new Claim(ClaimTypes.Email, "user@example.com") }; - var identity = new ClaimsIdentity(claims, "TestAuth"); - return new ClaimsPrincipal(identity); - } - return null; - } - } - public class FakeEmailService : IEmailService - { - public List<(string to, string subject, string body)> SentEmails { get; } = new(); + public async Task GetAsync(Guid id, CancellationToken ct = default) => await _dbSet.FindAsync(new object[] { id }, ct); + public async Task> GetAllAsync(CancellationToken ct = default) => await _dbSet.ToListAsync(ct); + public IEnumerable Find(Expression> predicate) => _dbSet.Where(predicate).ToList(); + public void Add(User entity) => _dbSet.Add(entity); + public void Remove(User entity) => _dbSet.Remove(entity); + public Task SaveChangesAsync(CancellationToken ct = default) => _context.SaveChangesAsync(ct); - public Task SendEmailAsync(string to, string subject, string body) - { - SentEmails.Add((to, subject, body)); - return Task.CompletedTask; - } - } - - public class FakeCodeService : ICodeService - { - public int TimeToLiveMinutes => 15; - public string GenerateAuthCode() => "123456"; + public Task UpdateQuestionaireByEmailAsync(Questionaire questionaire, string email, CancellationToken ct = default) + => throw new NotImplementedException("Not needed for Auth tests"); } } \ No newline at end of file diff --git a/ServerTests/NamServer/Endpoints/MunicipalityEntities/MunicipalityEntityEndpointsSmokeTests.cs b/ServerTests/NamServer/Endpoints/MunicipalityEntities/MunicipalityEntityEndpointsSmokeTests.cs new file mode 100644 index 0000000..8c6019b --- /dev/null +++ b/ServerTests/NamServer/Endpoints/MunicipalityEntities/MunicipalityEntityEndpointsSmokeTests.cs @@ -0,0 +1,280 @@ +using Domain.Entities.MunicipalityEntities; +using Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using nam.ServerTests.Integration.Shared; +using NUnit.Framework; +using NUnitAssert = NUnit.Framework.Assert; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json; + +namespace nam.ServerTests.NamServer.Endpoints.MunicipalityEntities +{ + [TestFixture] + public class MunicipalityEntityEndpointsSmokeTests + { + private const string MunicipalityName = "TestTown"; + private static readonly object s_seedLock = new(); + private NamTestFactory? _factory; + private HttpClient? _client; + + [OneTimeSetUp] + public void Setup() + { + _factory = new NamTestFactory(); + SeedMunicipalityData(); + _client = _factory.CreateClient(); + _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Test"); + } + + [OneTimeTearDown] + public void TearDown() + { + _client?.Dispose(); + _factory?.Dispose(); + } + + [TestCase("/api/art-culture/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/article/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/eat-and-drink/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/entertainment-leisure/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/nature/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/organizations/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/public-event/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/routes/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/services/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/shopping/card-list?municipality=TestTown&language=it", "entityName")] + [TestCase("/api/organizations/municipalities?search=TestTown&language=it", "legalName")] + public async Task Get_CardList_Returns_Data(string url, string expectedField) + { + var client = _client ?? throw new InvalidOperationException("HTTP client was not initialized."); + var response = await client.GetAsync(url); + + NUnitAssert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + var content = await response.Content.ReadAsStringAsync(); + using var document = JsonDocument.Parse(content); + + + NUnitAssert.That(document.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Array)); + + + NUnitAssert.That(document.RootElement.GetArrayLength(), Is.GreaterThan(0), "L'API ha restituito un array vuoto, il Seed non ha funzionato o il filtro fallisce."); + + NUnitAssert.That(content, Does.Contain(expectedField)); + } + + private void SeedMunicipalityData() + { + var factory = _factory ?? throw new InvalidOperationException("Factory was not initialized."); + using var scope = factory.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + lock (s_seedLock) + { + if (context.MunicipalityCards.Any(card => + card.LegalName != null && card.LegalName.Contains(MunicipalityName))) + { + return; + } + + var municipalityData = CreateMunicipalityData(); + + var artCultureDetail = new ArtCultureNatureDetail + { + Identifier = Guid.NewGuid(), + OfficialName = "Art Culture", + MunicipalityData = municipalityData + }; + var artCultureCard = new ArtCultureNatureCard + { + EntityId = Guid.NewGuid(), + EntityName = "Art Culture", + BadgeText = "Badge", + ImagePath = "image.png", + Detail = artCultureDetail + }; + + var articleDetail = new ArticleDetail + { + Identifier = Guid.NewGuid(), + Title = "Article Title", + Script = "Script", + ImagePath = "image.png", + UpdatedAt = DateTime.UtcNow, + MunicipalityData = municipalityData + }; + var articleCard = new ArticleCard + { + EntityId = Guid.NewGuid(), + EntityName = "Article", + BadgeText = "Badge", + ImagePath = "image.png", + Detail = articleDetail + }; + + var eatAndDrinkDetail = new EatAndDrinkDetail + { + Identifier = Guid.NewGuid(), + OfficialName = "Eat & Drink", + MunicipalityData = municipalityData + }; + var eatAndDrinkCard = new EatAndDrinkCard + { + EntityId = Guid.NewGuid(), + EntityName = "Eat & Drink", + BadgeText = "Badge", + ImagePath = "image.png", + Detail = eatAndDrinkDetail + }; + + var entertainmentDetail = new EntertainmentLeisureDetail + { + Identifier = Guid.NewGuid(), + OfficialName = "Entertainment", + MunicipalityData = municipalityData + }; + var entertainmentCard = new EntertainmentLeisureCard + { + EntityId = Guid.NewGuid(), + EntityName = "Entertainment", + BadgeText = "Badge", + ImagePath = "image.png", + Detail = entertainmentDetail + }; + + var natureDetail = new ArtCultureNatureDetail + { + Identifier = Guid.NewGuid(), + OfficialName = "Nature", + MunicipalityData = municipalityData + }; + var natureCard = new Nature + { + EntityId = Guid.NewGuid(), + EntityName = "Nature", + BadgeText = "Badge", + ImagePath = "image.png", + Detail = natureDetail + }; + + var organizationDetail = new OrganizationMobileDetail + { + TaxCode = "ORG001", + LegalName = "Organization", + MunicipalityData = municipalityData + }; + var organizationCard = new OrganizationCard + { + TaxCode = "ORG001", + EntityName = "Organization", + Detail = organizationDetail + }; + + var publicEventDetail = new PublicEventMobileDetail + { + Identifier = Guid.NewGuid(), + Title = "Public Event", + MunicipalityData = municipalityData + }; + + var routeDetail = new RouteDetail + { + Identifier = Guid.NewGuid(), + Name = "Route", + MunicipalityData = municipalityData + }; + var routeCard = new RouteCard + { + EntityId = routeDetail.Identifier, + EntityName = "Route", + BadgeText = "Badge", + ImagePath = "image.png", + Detail = routeDetail + }; + var publicEventCard = new PublicEventCard + { + EntityId = publicEventDetail.Identifier, + EntityName = "Public Event", + BadgeText = "Badge", + ImagePath = "image.png", + Address = "Address", + Date = "2026-02-05", + Detail = publicEventDetail + }; + + var serviceDetail = new ServiceDetail + { + Identifier = Guid.NewGuid(), + Name = "Service", + MunicipalityData = municipalityData + }; + var serviceCard = new ServiceCard + { + EntityId = serviceDetail.Identifier, + EntityName = "Service", + BadgeText = "Badge", + ImagePath = "image.png", + Detail = serviceDetail + }; + + var shoppingDetail = new ShoppingCardDetail + { + Identifier = Guid.NewGuid(), + OfficialName = "Shopping", + MunicipalityData = municipalityData + }; + var shoppingCard = new ShoppingCard + { + EntityId = shoppingDetail.Identifier, + EntityName = "Shopping", + BadgeText = "Badge", + ImagePath = "image.png", + Detail = shoppingDetail + }; + + var municipalityCard = new MunicipalityCard + { + LegalName = $"{MunicipalityName} Municipality", + ImagePath = "image.png" + }; + + context.AddRange( + municipalityData, + artCultureDetail, + artCultureCard, + natureDetail, + natureCard, + articleDetail, + articleCard, + eatAndDrinkDetail, + eatAndDrinkCard, + entertainmentDetail, + entertainmentCard, + organizationDetail, + organizationCard, + publicEventDetail, + publicEventCard, + routeDetail, + routeCard, + serviceDetail, + serviceCard, + shoppingDetail, + shoppingCard, + municipalityCard); + + context.SaveChanges(); + } + } + + private MunicipalityForLocalStorageSetting CreateMunicipalityData() + { + return new MunicipalityForLocalStorageSetting + { + Name = MunicipalityName, + LogoPath = "logo.png" + }; + } + } +} \ No newline at end of file diff --git a/ServerTests/nam.ServerTests.csproj b/ServerTests/nam.ServerTests.csproj index 84bc4d8..42b6882 100644 --- a/ServerTests/nam.ServerTests.csproj +++ b/ServerTests/nam.ServerTests.csproj @@ -1,21 +1,23 @@ - + net9.0 latest enable enable + true - - - - - + + + + + + @@ -27,11 +29,8 @@ - + - - - diff --git a/nam.Server/Program.cs b/nam.Server/Program.cs index 0b09294..a9e9eb2 100644 --- a/nam.Server/Program.cs +++ b/nam.Server/Program.cs @@ -95,3 +95,7 @@ app.ReccomandationMap(); app.MapChatbot(); app.Run(); + +public partial class Program +{ +} diff --git a/nam.client/package-lock.json b/nam.client/package-lock.json index 7cbed6e..681bf29 100644 --- a/nam.client/package-lock.json +++ b/nam.client/package-lock.json @@ -61,6 +61,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1624,6 +1625,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1667,6 +1669,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2456,6 +2459,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.5", @@ -3181,6 +3185,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3201,6 +3206,7 @@ "version": "19.2.6", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3286,6 +3292,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -3531,6 +3538,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3802,6 +3810,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4543,6 +4552,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7523,6 +7533,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7531,6 +7542,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8582,6 +8594,7 @@ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -8633,6 +8646,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -8802,6 +8816,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9095,6 +9110,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -9217,6 +9233,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -9513,6 +9530,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -9557,6 +9575,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -9836,6 +9855,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }