using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Dapper; using FluentAssertions; using Microsoft.Data.SqlClient; using Microsoft.IdentityModel.Tokens; using Microsoft.Extensions.DependencyInjection; using SIGCM2.Api.Controllers; using SIGCM2.Application.Abstractions.Security; using SIGCM2.Domain.Entities; using SIGCM2.TestSupport; using Xunit; namespace SIGCM2.Api.Tests.Audit; /// UDT-010 Batch 10 — AuditController integration tests. [Collection("ApiIntegration")] public sealed class AuditControllerTests : IClassFixture { private const string ConnectionString = "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; private readonly TestWebAppFactory _factory; public AuditControllerTests(TestWebAppFactory factory) { _factory = factory; } private async Task<(HttpClient client, int adminId)> AuthedAdminClientAsync() { await using var conn = new SqlConnection(ConnectionString); await conn.OpenAsync(); await conn.ExecuteAsync("DELETE FROM dbo.AuditEvent;"); var adminId = await conn.QuerySingleAsync("SELECT Id FROM dbo.Usuario WHERE Username = 'admin'"); var client = _factory.CreateClient(); var jwt = _factory.Services.GetRequiredService(); var token = jwt.GenerateAccessToken(new Usuario( id: adminId, username: "admin", passwordHash: "x", nombre: "Admin", apellido: "Sys", email: null, rol: "admin", permisosJson: """{"grant":[],"deny":[]}""", activo: true)); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return (client, adminId); } [Fact] public async Task GetEvents_WithoutPermission_Returns403() { var client = _factory.CreateClient(); var jwt = _factory.Services.GetRequiredService(); // Use a role without administracion:auditoria:ver (cajero only has ventas:contado:*) var operadorToken = jwt.GenerateAccessToken(new Usuario( id: 9999, username: "opx", passwordHash: "x", nombre: "X", apellido: "Y", email: null, rol: "cajero", permisosJson: """{"grant":[],"deny":[]}""", activo: true)); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", operadorToken); var response = await client.GetAsync("/api/v1/audit/events"); response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } [Fact] public async Task GetEvents_WithoutAuth_Returns401() { var client = _factory.CreateClient(); var response = await client.GetAsync("/api/v1/audit/events"); response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] public async Task GetEvents_AuthenticatedAdmin_ReturnsAuditEvents() { var (client, adminId) = await AuthedAdminClientAsync(); // Seed 3 events directly await using var conn = new SqlConnection(ConnectionString); await conn.OpenAsync(); for (var i = 0; i < 3; i++) { await conn.ExecuteAsync(""" INSERT INTO dbo.AuditEvent (OccurredAt, ActorUserId, Action, TargetType, TargetId) VALUES (@O, @A, @Ac, 'Usuario', @T); """, new { O = DateTime.UtcNow.AddSeconds(-i), A = adminId, Ac = $"test.seed{i}", T = i.ToString(), }); } var response = await client.GetAsync("/api/v1/audit/events?targetType=Usuario"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await response.Content.ReadFromJsonAsync(); body.Should().NotBeNull(); body!.Items.Should().HaveCount(3); body.Items.Should().OnlyContain(e => e.TargetType == "Usuario"); } [Fact] public async Task GetEvents_InvalidLimit_Returns400() { var (client, _) = await AuthedAdminClientAsync(); var response = await client.GetAsync("/api/v1/audit/events?limit=0"); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var response2 = await client.GetAsync("/api/v1/audit/events?limit=101"); response2.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task GetEvents_FromGreaterThanTo_Returns400() { var (client, _) = await AuthedAdminClientAsync(); var response = await client.GetAsync( "/api/v1/audit/events?from=2026-05-01T00:00:00Z&to=2026-04-01T00:00:00Z"); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } }