tests/SboxSuperuser.Contracts.Tests/ModerationServiceTests.cs

Unit tests for the moderation service contract. Exercises banning, muting, warnings, storage interactions and permission checks using a test rig and asserts expected command statuses, store behavior and disconnect counts.

using SboxSuperuser.Core.Moderation;
using SboxSuperuser.Engine.Moderation;
using Xunit;

namespace SboxSuperuser.Contracts.Tests;

public sealed class ModerationServiceTests
{
	[Fact]
	public async Task BanCommitsBeforeInvokingDisconnect()
	{
		using var fixture = new ModerationRig();
		var release = new TaskCompletionSource();
		fixture.Rig.Store.BeforeCommit = (_, _) => release.Task;
		var command = fixture.Run( "ban", DispatcherRig.TargetId, "2h", "Test ban" );
		Assert.False( command.IsCompleted );
		Assert.Equal( 0, fixture.DisconnectRequests );
		release.SetResult();
		Assert.Equal( CommandStatus.Success, (await command).Status );
		Assert.True( fixture.RecordExistedBeforeDisconnect );
		Assert.Equal( 1, fixture.DisconnectRequests );
		Assert.Equal( CommandStatus.Denied, fixture.Moderation.CheckRestriction( DispatcherRig.TargetId, ModerationKind.Ban ).Status );
	}

	[Fact]
	public async Task StoreFailureDoesNotBanOrDisconnect()
	{
		using var fixture = new ModerationRig();
		fixture.Rig.Store.WriteOverride = StoreWriteStatus.Unavailable;
		Assert.Equal( CommandStatus.StorageUnavailable, (await fixture.Run( "ban", DispatcherRig.TargetId, "permanent", "Test" )).Status );
		Assert.Equal( 0, fixture.DisconnectRequests );
		Assert.Equal( CommandStatus.Success, fixture.Moderation.CheckRestriction( DispatcherRig.TargetId, ModerationKind.Ban ).Status );
		Assert.Empty( fixture.Rig.Store.Documents );
	}

	[Fact]
	public async Task RepeatedRevocationIsNoChangeAndKeepsTheOriginalHistory()
	{
		using var fixture = new ModerationRig();
		Assert.Equal( CommandStatus.Success, (await fixture.Run( "mute", DispatcherRig.TargetId, "permanent", "Test" )).Status );
		Assert.Equal( CommandStatus.Success, (await fixture.Run( "unmute", DispatcherRig.TargetId )).Status );
		var writes = fixture.Rig.Store.WriteCount;
		Assert.Equal( CommandStatus.NoChange, (await fixture.Run( "unmute", DispatcherRig.TargetId )).Status );
		Assert.Equal( writes, fixture.Rig.Store.WriteCount );
		var document = fixture.Rig.Store.Documents[ MemoryPolicyStore.Id( HostModerationService.Key ) ];
		var ledger = ModerationCodec.Read( document.PayloadJson, document.Revision );
		Assert.Equal( 2, ledger.Entries.Count );
		Assert.Equal( ledger.Entries[ 0 ].Id, ledger.Entries[ 1 ].Revokes );
	}

	[Fact]
	public async Task ExpiredMuteStopsEnforcementWithoutATimerOrDeletion()
	{
		using var fixture = new ModerationRig();
		Assert.Equal( CommandStatus.Success, (await fixture.Run( "mute", DispatcherRig.TargetId, "30m", "Test" )).Status );
		Assert.Equal( CommandStatus.Denied, fixture.Moderation.CheckRestriction( DispatcherRig.TargetId, ModerationKind.TextMute ).Status );
		fixture.Now = fixture.Now.AddMinutes( 30 );
		Assert.Equal( CommandStatus.Success, fixture.Moderation.CheckRestriction( DispatcherRig.TargetId, ModerationKind.TextMute ).Status );
		Assert.Equal( CommandStatus.NoChange, (await fixture.Run( "unmute", DispatcherRig.TargetId )).Status );
		Assert.Single( fixture.Rig.Store.Documents );
	}

	[Fact]
	public async Task OfflineWarningPersistsButOfflineKickDoesNotRun()
	{
		using var fixture = new ModerationRig();
		const string offline = "76561198000000004";
		Assert.Equal( CommandStatus.Success, (await fixture.Run( "warn", offline, "Offline warning" )).Status );
		var writes = fixture.Rig.Store.WriteCount;
		Assert.Equal( CommandStatus.InvalidArguments, (await fixture.Run( "kick", offline, "Offline kick" )).Status );
		Assert.Equal( writes, fixture.Rig.Store.WriteCount );
		Assert.Equal( 0, fixture.DisconnectRequests );
	}

	[Fact]
	public async Task EqualRankAndSelfPunishmentHaveNoPersistentEffect()
	{
		using var fixture = new ModerationRig();
		Assert.Equal( CommandStatus.Denied, (await fixture.Run( "ban", DispatcherRig.OwnerId, "permanent", "Self" )).Status );
		await fixture.Rig.Edit( PolicyChange.AddUserGroup( DispatcherRig.UserId, "admin" ) );
		await fixture.Rig.Edit( PolicyChange.AddUserGroup( DispatcherRig.TargetId, "admin" ) );
		var writes = fixture.Rig.Store.WriteCount;
		var result = await fixture.Rig.Submit( "ban", DispatcherRig.User, new[] { DispatcherRig.TargetId, "permanent", "Equal" } );
		Assert.Equal( CommandStatus.Denied, result.Status );
		Assert.Equal( writes, fixture.Rig.Store.WriteCount );
		Assert.Equal( 0, fixture.DisconnectRequests );
	}

	[Fact]
	public async Task LostHistoryFailsClosedInsteadOfResettingRestrictions()
	{
		using var fixture = new ModerationRig();
		await fixture.Run( "ban", DispatcherRig.TargetId, "permanent", "Test" );
		fixture.Rig.Store.Documents.Remove( MemoryPolicyStore.Id( HostModerationService.Key ) );
		fixture.Moderation.OnStorageChanged( HostModerationService.Key );
		Assert.Equal( StoreReadStatus.Corrupt, fixture.Moderation.Status );
		Assert.Equal( CommandStatus.StorageUnavailable, fixture.Moderation.CheckRestriction( DispatcherRig.TargetId, ModerationKind.Ban ).Status );
	}

	[Fact]
	public async Task UnauthorizedCallerCannotReadModerationReasons()
	{
		using var fixture = new ModerationRig();
		await fixture.Run( "warn", DispatcherRig.TargetId, "Private note" );
		var result = await fixture.Rig.Submit( "moderation.history", DispatcherRig.User, new[] { DispatcherRig.TargetId } );
		Assert.Equal( CommandStatus.Denied, result.Status );
		Assert.Empty( result.Data );
	}
}