using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using line_gestao_api.Data; using line_gestao_api.Dtos; using line_gestao_api.Models; using line_gestao_api.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Xunit; namespace line_gestao_api.Tests; public class SystemTenantIntegrationTests { private static readonly Guid TenantAId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); private static readonly Guid TenantBId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); private const string TenantAClientName = "CLIENTE-ALFA LTDA"; private const string TenantBClientName = "CLIENTE-BETA S/A"; [Fact] public async Task CommonUser_OnlySeesOwnTenantData() { using var factory = new ApiFactory(); var client = factory.CreateClient(); await SeedTenantsAndLinesAsync(factory.Services); await UpsertUserAsync(factory.Services, TenantAId, "tenanta.user@test.local", "TenantA123!", "cliente"); var token = await LoginAndGetTokenAsync(client, "tenanta.user@test.local", "TenantA123!", TenantAId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await client.GetAsync("/api/lines/clients"); response.EnsureSuccessStatusCode(); var clients = await response.Content.ReadFromJsonAsync>(); Assert.NotNull(clients); Assert.Contains(TenantAClientName, clients!); Assert.DoesNotContain(TenantBClientName, clients); } [Fact] public async Task CommonUser_CannotAccessSystemEndpoints() { using var factory = new ApiFactory(); var client = factory.CreateClient(); await SeedTenantsAndLinesAsync(factory.Services); await UpsertUserAsync(factory.Services, TenantAId, "tenanta.block@test.local", "TenantA123!", "cliente"); var token = await LoginAndGetTokenAsync(client, "tenanta.block@test.local", "TenantA123!", TenantAId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await client.GetAsync("/api/system/tenants?source=MobileLines.Cliente&active=true"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task SysAdmin_CanListClientTenants() { using var factory = new ApiFactory(); var client = factory.CreateClient(); await SeedTenantsAndLinesAsync(factory.Services); var token = await LoginAndGetTokenAsync( client, "admin.master@test.local", "AdminMaster123!", SystemTenantConstants.SystemTenantId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await client.GetAsync("/api/system/tenants?source=MobileLines.Cliente&active=true"); response.EnsureSuccessStatusCode(); var tenants = await response.Content.ReadFromJsonAsync>(); Assert.NotNull(tenants); Assert.Contains(tenants!, t => t.TenantId == TenantAId && t.NomeOficial == TenantAClientName); Assert.Contains(tenants, t => t.TenantId == TenantBId && t.NomeOficial == TenantBClientName); } [Fact] public async Task SysAdmin_CreatesTenantUser_AndNewUserSeesOnlyOwnTenant() { using var factory = new ApiFactory(); var client = factory.CreateClient(); await SeedTenantsAndLinesAsync(factory.Services); var adminToken = await LoginAndGetTokenAsync( client, "admin.master@test.local", "AdminMaster123!", SystemTenantConstants.SystemTenantId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var request = new CreateSystemTenantUserRequest { Name = "Usuário Cliente A", Email = "novo.clientea@test.local", Password = "ClienteA123!", Roles = new List { "cliente" } }; var createResponse = await client.PostAsJsonAsync($"/api/system/tenants/{TenantAId}/users", request); createResponse.EnsureSuccessStatusCode(); var created = await createResponse.Content.ReadFromJsonAsync(); Assert.NotNull(created); Assert.Equal(TenantAId, created!.TenantId); Assert.Equal("novo.clientea@test.local", created.Email); Assert.Contains("cliente", created.Roles); var userToken = await LoginAndGetTokenAsync(client, "novo.clientea@test.local", "ClienteA123!", TenantAId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", userToken); var visibleClientsResponse = await client.GetAsync("/api/lines/clients"); visibleClientsResponse.EnsureSuccessStatusCode(); var clients = await visibleClientsResponse.Content.ReadFromJsonAsync>(); Assert.NotNull(clients); Assert.Contains(TenantAClientName, clients!); Assert.DoesNotContain(TenantBClientName, clients); await using var scope = factory.Services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var systemAudit = await db.AuditLogs .IgnoreQueryFilters() .OrderByDescending(x => x.OccurredAtUtc) .FirstOrDefaultAsync(x => x.Action == SystemAuditActions.CreateTenantUser); Assert.NotNull(systemAudit); Assert.Equal(SystemTenantConstants.SystemTenantId, systemAudit!.ActorTenantId); Assert.Equal(TenantAId, systemAudit.TargetTenantId); Assert.DoesNotContain("ClienteA123!", systemAudit.MetadataJson); } private static async Task SeedTenantsAndLinesAsync(IServiceProvider services) { await using var scope = services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var tenantA = await db.Tenants.FirstOrDefaultAsync(t => t.Id == TenantAId); if (tenantA == null) { db.Tenants.Add(new Tenant { Id = TenantAId, NomeOficial = TenantAClientName, IsSystem = false, Ativo = true, SourceType = SystemTenantConstants.MobileLinesClienteSourceType, SourceKey = TenantAClientName, CreatedAt = DateTime.UtcNow }); } var tenantB = await db.Tenants.FirstOrDefaultAsync(t => t.Id == TenantBId); if (tenantB == null) { db.Tenants.Add(new Tenant { Id = TenantBId, NomeOficial = TenantBClientName, IsSystem = false, Ativo = true, SourceType = SystemTenantConstants.MobileLinesClienteSourceType, SourceKey = TenantBClientName, CreatedAt = DateTime.UtcNow }); } var currentLines = await db.MobileLines.IgnoreQueryFilters().ToListAsync(); if (currentLines.Count > 0) { db.MobileLines.RemoveRange(currentLines); } db.MobileLines.AddRange( new MobileLine { Id = Guid.NewGuid(), Item = 1, Linha = "5511999990001", Cliente = TenantAClientName, TenantId = TenantAId }, new MobileLine { Id = Guid.NewGuid(), Item = 2, Linha = "5511888880002", Cliente = TenantBClientName, TenantId = TenantBId }); await db.SaveChangesAsync(); } private static async Task UpsertUserAsync( IServiceProvider services, Guid tenantId, string email, string password, params string[] roles) { await using var scope = services.CreateAsyncScope(); var userManager = scope.ServiceProvider.GetRequiredService>(); var tenantProvider = scope.ServiceProvider.GetRequiredService(); var previousTenant = tenantProvider.ActorTenantId; tenantProvider.SetTenantId(tenantId); try { var normalizedEmail = userManager.NormalizeEmail(email); var user = await userManager.Users .IgnoreQueryFilters() .FirstOrDefaultAsync(u => u.TenantId == tenantId && u.NormalizedEmail == normalizedEmail); if (user == null) { user = new ApplicationUser { Name = email, Email = email, UserName = email, TenantId = tenantId, EmailConfirmed = true, IsActive = true, LockoutEnabled = true }; var createResult = await userManager.CreateAsync(user, password); Assert.True(createResult.Succeeded, string.Join("; ", createResult.Errors.Select(e => e.Description))); } else { var resetToken = await userManager.GeneratePasswordResetTokenAsync(user); var reset = await userManager.ResetPasswordAsync(user, resetToken, password); Assert.True(reset.Succeeded, string.Join("; ", reset.Errors.Select(e => e.Description))); } var existingRoles = await userManager.GetRolesAsync(user); if (existingRoles.Count > 0) { var removeRolesResult = await userManager.RemoveFromRolesAsync(user, existingRoles); Assert.True(removeRolesResult.Succeeded, string.Join("; ", removeRolesResult.Errors.Select(e => e.Description))); } var addRolesResult = await userManager.AddToRolesAsync(user, roles); Assert.True(addRolesResult.Succeeded, string.Join("; ", addRolesResult.Errors.Select(e => e.Description))); } finally { tenantProvider.SetTenantId(previousTenant); } } private static async Task LoginAndGetTokenAsync(HttpClient client, string email, string password, Guid tenantId) { var previousAuth = client.DefaultRequestHeaders.Authorization; client.DefaultRequestHeaders.Authorization = null; try { var response = await client.PostAsJsonAsync("/auth/login", new { email, password, tenantId }); response.EnsureSuccessStatusCode(); var auth = await response.Content.ReadFromJsonAsync(); Assert.NotNull(auth); Assert.False(string.IsNullOrWhiteSpace(auth!.Token)); return auth.Token; } finally { client.DefaultRequestHeaders.Authorization = previousAuth; } } private sealed class ApiFactory : WebApplicationFactory { private readonly string _databaseName = $"line-gestao-tests-{Guid.NewGuid()}"; protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder) { builder.UseEnvironment("Testing"); builder.ConfigureAppConfiguration((_, config) => { config.AddInMemoryCollection(new Dictionary { ["App:UseHttpsRedirection"] = "false", ["Seed:Enabled"] = "true", ["Seed:AdminMasterName"] = "Admin Master", ["Seed:AdminMasterEmail"] = "admin.master@test.local", ["Seed:AdminMasterPassword"] = "AdminMaster123!", ["Seed:ReapplyAdminCredentialsOnStartup"] = "true" }); }); builder.ConfigureServices(services => { var notificationHostedService = services .Where(d => d.ServiceType == typeof(IHostedService) && d.ImplementationType == typeof(VigenciaNotificationBackgroundService)) .ToList(); foreach (var descriptor in notificationHostedService) { services.Remove(descriptor); } services.RemoveAll(); services.RemoveAll>(); services.RemoveAll>(); services.AddDbContext(options => { options.UseInMemoryDatabase(_databaseName); }); }); } } }