Consumers now depend on IAuditService instead of the concrete class, so tests mock the interface (Mock<IAuditService>()) rather than subclassing AuditService via virtual methods.
340 lines
12 KiB
C#
340 lines
12 KiB
C#
using MicCheck.Api.Audit;
|
|
using MicCheck.Api.Data;
|
|
using MicCheck.Api.Organizations;
|
|
using MicCheck.Api.Tests.Unit.TestSupport;
|
|
using MicCheck.Api.Users;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
|
|
namespace MicCheck.Api.Tests.Unit.Organizations;
|
|
|
|
[TestFixture]
|
|
public class OrganizationServiceTests
|
|
{
|
|
private Mock<IMicCheckDbContext> _db = null!;
|
|
private List<Organization> _organizations = null!;
|
|
private List<OrganizationUser> _members = null!;
|
|
private List<User> _users = null!;
|
|
private OrganizationService _service = null!;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_db = new Mock<IMicCheckDbContext>();
|
|
_organizations = [];
|
|
_members = [];
|
|
var organizationsSet = MockDbSetFactory.Create(_organizations);
|
|
organizationsSet.Setup(m => m.Add(It.IsAny<Organization>())).Callback<Organization>(org =>
|
|
{
|
|
typeof(Organization).GetProperty(nameof(Organization.Id))!.SetValue(org, _organizations.Count + 1);
|
|
_organizations.Add(org);
|
|
// Mimics EF Core's cascade-insert and FK fixup of a loaded, mapped collection navigation on SaveChanges.
|
|
foreach (var member in org.Members)
|
|
{
|
|
typeof(OrganizationUser).GetProperty(nameof(OrganizationUser.OrganizationId))!.SetValue(member, org.Id);
|
|
_members.Add(member);
|
|
}
|
|
});
|
|
_db.Setup(c => c.Organizations).Returns(organizationsSet.Object);
|
|
_db.SetupDbSet(c => c.OrganizationUsers, _members);
|
|
_users = [];
|
|
_db.SetupDbSet(c => c.Users, _users);
|
|
|
|
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
|
|
var auditService = new Mock<IAuditService>();
|
|
auditService.Setup(a => a.LogAsync(
|
|
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
|
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),
|
|
It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
_service = new OrganizationService(_db.Object, auditService.Object);
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenCreatingAnOrganization_ThenItIsPersistedWithTheCreatorAsAdmin()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
|
|
Assert.That(org.Name, Is.EqualTo("My Org"));
|
|
Assert.That(_members.Single().UserId, Is.EqualTo(1));
|
|
Assert.That(_members.Single().Role, Is.EqualTo(OrganizationRole.Admin));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenCreatingAUsersFirstOrganization_ThenItIsMarkedAsPrimary()
|
|
{
|
|
var org = await _service.CreateAsync("First Org", creatorUserId: 1);
|
|
|
|
Assert.That(_members.Single(m => m.OrganizationId == org.Id).IsPrimary, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenCreatingASecondOrganizationForAUser_ThenItIsNotMarkedAsPrimary()
|
|
{
|
|
await _service.CreateAsync("First Org", creatorUserId: 1);
|
|
|
|
var second = await _service.CreateAsync("Second Org", creatorUserId: 1);
|
|
|
|
Assert.That(_members.Single(m => m.OrganizationId == second.Id).IsPrimary, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenListingOrganizationsForAUser_ThenOnlyThatUsersOrganizationsAreReturnedWithPrimaryFlag()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
await _service.CreateAsync("Other User Org", creatorUserId: 2);
|
|
|
|
var results = await _service.ListForUserAsync(1);
|
|
|
|
Assert.That(results, Has.Count.EqualTo(1));
|
|
Assert.That(results[0].Org.Id, Is.EqualTo(org.Id));
|
|
Assert.That(results[0].IsPrimary, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenFindingAnOrganizationByIdThatExists_ThenItIsReturned()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
|
|
var found = await _service.FindByIdAsync(org.Id);
|
|
|
|
Assert.That(found, Is.Not.Null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenFindingAnOrganizationByIdThatDoesNotExist_ThenNullIsReturned()
|
|
{
|
|
var found = await _service.FindByIdAsync(999);
|
|
|
|
Assert.That(found, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void WhenSettingPrimaryForAUserThatIsNotAMember_ThenKeyNotFoundExceptionIsThrown()
|
|
{
|
|
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.SetPrimaryAsync(999, 1));
|
|
}
|
|
|
|
[Test]
|
|
public void WhenUpdatingAnOrganizationThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
|
{
|
|
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.UpdateAsync(999, "renamed"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenUpdatingAnOrganization_ThenTheNameIsChanged()
|
|
{
|
|
var org = await _service.CreateAsync("Old Name", creatorUserId: 1);
|
|
|
|
var updated = await _service.UpdateAsync(org.Id, "New Name");
|
|
|
|
Assert.That(updated.Name, Is.EqualTo("New Name"));
|
|
}
|
|
|
|
[Test]
|
|
public void WhenDeletingAnOrganizationThatDoesNotExist_ThenNoExceptionIsThrown()
|
|
{
|
|
Assert.DoesNotThrowAsync(() => _service.DeleteAsync(999));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenDeletingAnOrganization_ThenItIsRemoved()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
|
|
await _service.DeleteAsync(org.Id);
|
|
|
|
Assert.That(_organizations.Any(o => o.Id == org.Id), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenListingMembers_ThenOnlyThatOrganizationsMembersAreReturned()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
await _service.CreateAsync("Other Org", creatorUserId: 2);
|
|
|
|
var members = await _service.ListMembersAsync(org.Id);
|
|
|
|
Assert.That(members, Has.Count.EqualTo(1));
|
|
Assert.That(members[0].UserId, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenInvitingAUserWhoIsNotYetAMember_ThenTheyAreAddedWithTheGivenRole()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
|
|
await _service.InviteUserAsync(org.Id, userId: 2, OrganizationRole.User);
|
|
|
|
Assert.That(_members.Single(m => m.UserId == 2).Role, Is.EqualTo(OrganizationRole.User));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenInvitingAUserWhoIsAlreadyAMember_ThenTheirRoleIsUpdatedWithoutDuplication()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
await _service.InviteUserAsync(org.Id, userId: 2, OrganizationRole.User);
|
|
|
|
await _service.InviteUserAsync(org.Id, userId: 2, OrganizationRole.Admin);
|
|
|
|
Assert.That(_members.Count(m => m.UserId == 2), Is.EqualTo(1));
|
|
Assert.That(_members.Single(m => m.UserId == 2).Role, Is.EqualTo(OrganizationRole.Admin));
|
|
}
|
|
|
|
[Test]
|
|
public void WhenRemovingAMemberThatDoesNotExist_ThenNoExceptionIsThrown()
|
|
{
|
|
Assert.DoesNotThrowAsync(() => _service.RemoveMemberAsync(1, 999));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenRemovingAMember_ThenTheyAreNoLongerListed()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
await _service.InviteUserAsync(org.Id, userId: 2, OrganizationRole.User);
|
|
|
|
await _service.RemoveMemberAsync(org.Id, 2);
|
|
|
|
Assert.That(_members.Any(m => m.UserId == 2), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void WhenGettingAnInviteTokenForAnOrganizationThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
|
{
|
|
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.GetOrCreateInviteTokenAsync(999));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenGettingAnInviteTokenForTheFirstTime_ThenANewTokenIsGeneratedAndPersisted()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
|
|
var token = await _service.GetOrCreateInviteTokenAsync(org.Id);
|
|
|
|
Assert.That(token, Is.Not.Null.And.Not.Empty);
|
|
Assert.That(_organizations.Single(o => o.Id == org.Id).InviteToken, Is.EqualTo(token));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenGettingAnInviteTokenASecondTime_ThenTheSameTokenIsReturned()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
var first = await _service.GetOrCreateInviteTokenAsync(org.Id);
|
|
|
|
var second = await _service.GetOrCreateInviteTokenAsync(org.Id);
|
|
|
|
Assert.That(second, Is.EqualTo(first));
|
|
}
|
|
|
|
[Test]
|
|
public void WhenRegeneratingAnInviteTokenForAnOrganizationThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
|
{
|
|
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.RegenerateInviteTokenAsync(999));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenRegeneratingAnInviteToken_ThenANewTokenReplacesTheOld()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
var first = await _service.GetOrCreateInviteTokenAsync(org.Id);
|
|
|
|
var regenerated = await _service.RegenerateInviteTokenAsync(org.Id);
|
|
|
|
Assert.That(regenerated, Is.Not.EqualTo(first));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenFindingAnOrganizationByAValidInviteToken_ThenItIsReturned()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
var token = await _service.GetOrCreateInviteTokenAsync(org.Id);
|
|
|
|
var found = await _service.FindByInviteTokenAsync(token);
|
|
|
|
Assert.That(found, Is.Not.Null);
|
|
Assert.That(found!.Id, Is.EqualTo(org.Id));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenFindingAnOrganizationByAnInvalidInviteToken_ThenNullIsReturned()
|
|
{
|
|
var found = await _service.FindByInviteTokenAsync("not-a-real-token");
|
|
|
|
Assert.That(found, Is.Null);
|
|
}
|
|
|
|
private User AddUser(int id, string email, bool isActive = true) => new()
|
|
{
|
|
Id = id,
|
|
Email = email,
|
|
PasswordHash = "hashed",
|
|
FirstName = "Test",
|
|
LastName = "User",
|
|
IsActive = isActive,
|
|
CreatedAt = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
[Test]
|
|
public async Task WhenInvitingByEmailForAUserThatDoesNotExist_ThenTheResultIndicatesFailure()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
|
|
var results = await _service.InviteUsersByEmailAsync(org.Id, [new InviteByEmailEntry("nobody@example.com", "User")]);
|
|
|
|
Assert.That(results[0].Success, Is.False);
|
|
Assert.That(results[0].Error, Is.Not.Null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenInvitingByEmailWithAnInvalidRole_ThenTheResultIndicatesFailure()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
_users.Add(AddUser(2, "bob@example.com"));
|
|
|
|
var results = await _service.InviteUsersByEmailAsync(org.Id, [new InviteByEmailEntry("bob@example.com", "NotARole")]);
|
|
|
|
Assert.That(results[0].Success, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenInvitingByEmailForAnInactiveUser_ThenTheResultIndicatesFailure()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
_users.Add(AddUser(2, "bob@example.com", isActive: false));
|
|
|
|
var results = await _service.InviteUsersByEmailAsync(org.Id, [new InviteByEmailEntry("bob@example.com", "User")]);
|
|
|
|
Assert.That(results[0].Success, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenInvitingByEmailForAValidActiveUser_ThenTheyAreAddedAsAMemberAndSuccessIsReturned()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
_users.Add(AddUser(2, "bob@example.com"));
|
|
|
|
var results = await _service.InviteUsersByEmailAsync(org.Id, [new InviteByEmailEntry(" BOB@Example.com ", "Admin")]);
|
|
|
|
Assert.That(results[0].Success, Is.True);
|
|
Assert.That(_members.Single(m => m.UserId == 2).Role, Is.EqualTo(OrganizationRole.Admin));
|
|
}
|
|
|
|
[Test]
|
|
public void WhenAcceptingAnInviteWithAnInvalidToken_ThenKeyNotFoundExceptionIsThrown()
|
|
{
|
|
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.AcceptInviteAsync("not-a-real-token", 2));
|
|
}
|
|
|
|
[Test]
|
|
public async Task WhenAcceptingAValidInvite_ThenTheUserIsAddedAsAMemberWithUserRole()
|
|
{
|
|
var org = await _service.CreateAsync("My Org", creatorUserId: 1);
|
|
var token = await _service.GetOrCreateInviteTokenAsync(org.Id);
|
|
|
|
await _service.AcceptInviteAsync(token, userId: 2);
|
|
|
|
Assert.That(_members.Single(m => m.UserId == 2).Role, Is.EqualTo(OrganizationRole.User));
|
|
}
|
|
}
|