using EntKube.Web.Data; using FluentAssertions; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace EntKube.Web.Tests; /// /// Groups provide a way to organize users within a tenant. A group belongs /// to exactly one tenant and can contain multiple users. This enables bulk /// permission assignment and logical grouping of team members. /// public class GroupEntityTests : IDisposable { private readonly SqliteConnection connection; private readonly ApplicationDbContext context; public GroupEntityTests() { connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); DbContextOptions options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; context = new ApplicationDbContext(options); context.Database.EnsureCreated(); } [Fact] public async Task Group_CanBeCreated_WithinTenant() { // A group lives within a tenant's boundary. It has a name and belongs // to exactly one tenant — you can't share groups across tenants. Tenant tenant = new() { Id = Guid.NewGuid(), Name = "Acme", Slug = "acme" }; context.Tenants.Add(tenant); Group group = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "Engineering" }; context.Groups.Add(group); await context.SaveChangesAsync(); Group? retrieved = await context.Groups .Include(g => g.Tenant) .FirstOrDefaultAsync(g => g.Name == "Engineering"); retrieved.Should().NotBeNull(); retrieved!.Tenant.Name.Should().Be("Acme"); } [Fact] public async Task Group_CanHaveMultipleMembers() { // Users are added to groups through a membership join entity. // Multiple users can belong to the same group. Tenant tenant = new() { Id = Guid.NewGuid(), Name = "Acme", Slug = "acme" }; context.Tenants.Add(tenant); Group group = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "Engineering" }; context.Groups.Add(group); ApplicationUser userA = new() { Id = Guid.NewGuid().ToString(), UserName = "alice@acme.com", NormalizedUserName = "ALICE@ACME.COM", Email = "alice@acme.com", NormalizedEmail = "ALICE@ACME.COM", EmailConfirmed = true, SecurityStamp = Guid.NewGuid().ToString() }; ApplicationUser userB = new() { Id = Guid.NewGuid().ToString(), UserName = "bob@acme.com", NormalizedUserName = "BOB@ACME.COM", Email = "bob@acme.com", NormalizedEmail = "BOB@ACME.COM", EmailConfirmed = true, SecurityStamp = Guid.NewGuid().ToString() }; context.Users.AddRange(userA, userB); context.GroupMemberships.AddRange( new GroupMembership { UserId = userA.Id, GroupId = group.Id }, new GroupMembership { UserId = userB.Id, GroupId = group.Id } ); await context.SaveChangesAsync(); List members = await context.GroupMemberships .Where(gm => gm.GroupId == group.Id) .ToListAsync(); members.Should().HaveCount(2); } [Fact] public async Task User_CanBelongToMultipleGroups() { // A user might be in "Engineering" and "On-Call" simultaneously. Tenant tenant = new() { Id = Guid.NewGuid(), Name = "Acme", Slug = "acme" }; context.Tenants.Add(tenant); Group groupA = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "Engineering" }; Group groupB = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "On-Call" }; context.Groups.AddRange(groupA, groupB); ApplicationUser user = new() { Id = Guid.NewGuid().ToString(), UserName = "alice@acme.com", NormalizedUserName = "ALICE@ACME.COM", Email = "alice@acme.com", NormalizedEmail = "ALICE@ACME.COM", EmailConfirmed = true, SecurityStamp = Guid.NewGuid().ToString() }; context.Users.Add(user); context.GroupMemberships.AddRange( new GroupMembership { UserId = user.Id, GroupId = groupA.Id }, new GroupMembership { UserId = user.Id, GroupId = groupB.Id } ); await context.SaveChangesAsync(); List memberships = await context.GroupMemberships .Where(gm => gm.UserId == user.Id) .ToListAsync(); memberships.Should().HaveCount(2); } public void Dispose() { context.Dispose(); connection.Dispose(); } }