using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Xunit;
namespace TeamUp.IntegrationTests;
///
/// The org structure spine: divisions → products/services → teams. Owner-only writes, org-scoped
/// validation, and teams optionally attached to a product (nullable for pre-structure teams).
///
public sealed class OrgStructureTests(PostgresFixture postgres) : IClassFixture
{
private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId);
private sealed record AuthResponse(string Token, Guid MemberId);
private sealed record InviteResponse(Guid InvitationId, string Token);
private sealed record DivisionResponse(Guid Id, Guid OrganizationId, string Name);
private sealed record ProductResponse(Guid Id, Guid OrganizationId, Guid? DivisionId, string Name, string Kind);
private sealed record TeamResponse(Guid Id, Guid OrganizationId, string Name, Guid? ProductId);
[Fact]
public async Task Divisions_products_and_teams_form_the_spine()
{
await using var factory = new TeamUpWebFactory(postgres.ConnectionString);
using var anon = factory.CreateClient();
var owner = await PostOk(anon, "/api/identity/bootstrap", new
{
organizationName = "AliaSaaS",
ownerEmail = "owner@alia.test",
ownerDisplayName = "Owner",
ownerPassword = "Passw0rd!",
});
using var client = Authed(factory, owner.Token);
await client.PostAsJsonAsync("/api/orgboard/organizations", new { organizationId = owner.OrganizationId, name = "AliaSaaS" });
// Division → product under it; a service with no division; both listable.
var technical = await PostOk(client, "/api/orgboard/divisions",
new { organizationId = owner.OrganizationId, name = "Technical" });
var ipnops = await PostOk(client, "/api/orgboard/products",
new { organizationId = owner.OrganizationId, name = "IPNOPS", kind = "Product", divisionId = technical.Id });
Assert.Equal(technical.Id, ipnops.DivisionId);
var payroll = await PostOk(client, "/api/orgboard/products",
new { organizationId = owner.OrganizationId, name = "Payroll", kind = "Service" });
Assert.Null(payroll.DivisionId);
Assert.Equal("Service", payroll.Kind);
var divisions = await client.GetFromJsonAsync>(
$"/api/orgboard/divisions?organizationId={owner.OrganizationId}");
Assert.Contains(divisions!, d => d.Name == "Technical");
var products = await client.GetFromJsonAsync>(
$"/api/orgboard/products?organizationId={owner.OrganizationId}");
Assert.Equal(2, products!.Count);
// A team under the product; a team without one still works (backward compatible).
var coreTeam = await PostOk(client, "/api/orgboard/teams",
new { organizationId = owner.OrganizationId, name = "Core", productId = ipnops.Id });
Assert.Equal(ipnops.Id, coreTeam.ProductId);
var looseTeam = await PostOk(client, "/api/orgboard/teams",
new { organizationId = owner.OrganizationId, name = "Loose" });
Assert.Null(looseTeam.ProductId);
var teams = await client.GetFromJsonAsync>(
$"/api/orgboard/teams?organizationId={owner.OrganizationId}");
Assert.Contains(teams!, t => t.Id == coreTeam.Id && t.ProductId == ipnops.Id);
// Validation: a product can't attach to a foreign/unknown division; nor a team to an unknown product.
var badProduct = await client.PostAsJsonAsync("/api/orgboard/products",
new { organizationId = owner.OrganizationId, name = "X", kind = "Product", divisionId = Guid.NewGuid() });
Assert.Equal(HttpStatusCode.BadRequest, badProduct.StatusCode);
var badTeam = await client.PostAsJsonAsync("/api/orgboard/teams",
new { organizationId = owner.OrganizationId, name = "X", productId = Guid.NewGuid() });
Assert.Equal(HttpStatusCode.BadRequest, badTeam.StatusCode);
// A plain Member cannot create structure (owner capability).
var invite = await PostOk(client, "/api/identity/invitations", new
{
email = "dev@alia.test",
scopeType = "Organization",
scopeId = owner.OrganizationId,
role = "Member",
organizationId = owner.OrganizationId,
});
var member = await PostOk(anon, "/api/identity/invitations/accept",
new { token = invite.Token, displayName = "Dev", password = "Passw0rd!" });
using var memberClient = Authed(factory, member.Token);
var forbidden = await memberClient.PostAsJsonAsync("/api/orgboard/divisions",
new { organizationId = owner.OrganizationId, name = "Nope" });
Assert.Equal(HttpStatusCode.Forbidden, forbidden.StatusCode);
}
private static HttpClient Authed(TeamUpWebFactory factory, string token)
{
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
return client;
}
private static async Task PostOk(HttpClient client, string url, object body)
{
var response = await client.PostAsJsonAsync(url, body);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var value = await response.Content.ReadFromJsonAsync();
Assert.NotNull(value);
return value!;
}
}