line-gestao-api/line-gestao-api.Tests/SystemTenantIntegrationTest...

336 lines
13 KiB
C#

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.Identity;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
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<List<string>>();
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<List<SystemTenantListItemDto>>();
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<string> { "cliente" }
};
var createResponse = await client.PostAsJsonAsync($"/api/system/tenants/{TenantAId}/users", request);
createResponse.EnsureSuccessStatusCode();
var created = await createResponse.Content.ReadFromJsonAsync<SystemTenantUserCreatedDto>();
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<List<string>>();
Assert.NotNull(clients);
Assert.Contains(TenantAClientName, clients!);
Assert.DoesNotContain(TenantBClientName, clients);
await using var scope = factory.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
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<AppDbContext>();
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<UserManager<ApplicationUser>>();
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
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<string> 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<AuthResponse>();
Assert.NotNull(auth);
Assert.False(string.IsNullOrWhiteSpace(auth!.Token));
return auth.Token;
}
finally
{
client.DefaultRequestHeaders.Authorization = previousAuth;
}
}
private sealed class ApiFactory : WebApplicationFactory<Program>
{
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<string, string?>
{
["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<AppDbContext>();
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(options =>
{
options.UseInMemoryDatabase(_databaseName);
});
});
}
}
}