tests/SboxSuperuser.Contracts.Tests/AuthorityTests.cs

Unit tests for the host policy and authority logic. Exercises HostPolicyService, TargetProtection, PolicyEdits, persistence semantics, owner recovery, and permission explanation behavior using an in-memory policy store.

File AccessNetworking
using SboxSuperuser.Core.Permissions;
using SboxSuperuser.Engine.Authority;
using Xunit;

namespace SboxSuperuser.Contracts.Tests;

public sealed class AuthorityTests
{
	private const string Owner = "76561198000000001";
	private const string Other = "76561198000000002";
	private const string Offline = "76561198000000003";
	private static readonly ActorIdentity OwnerActor = new( ActorKind.Player, Owner );
	private static readonly ActorIdentity OtherActor = new( ActorKind.Player, Other );
	private static readonly ActorIdentity LocalConsole = new( ActorKind.DedicatedConsole, "dedicated-console" );
	private static readonly CancellationToken None = CancellationToken.None;

	private static HostPolicyService Service( MemoryPolicyStore store, IEnumerable<string> owners = null, Func<bool> available = null )
		=> new( store, owners ?? new[] { Owner }, StoreReadStatus.Found, available ?? (() => true), _ => { } );

	private static Task<CommandResult> Apply( HostPolicyService service, ActorIdentity actor, PolicyChange change )
		=> service.ApplyAsync( () => actor, change, CommandOrigin.Console, Guid.NewGuid(), None );

	private static string Payload( string state ) => new PolicyTransaction( state, new[]
	{
		new AuditIntent( Guid.NewGuid(), Guid.NewGuid(), ActorKind.DedicatedConsole, "dedicated-console",
			CommandOrigin.Console, "test.policy", DateTimeOffset.UtcNow )
	} ).CreateWriteRequest( HostPolicyService.PolicyKey, 0 ).Document.PayloadJson;

	[Theory]
	[InlineData( 10, 0, true )]
	[InlineData( 10, 10, false )]
	[InlineData( 10, 50, false )]
	[InlineData( 50, 10, true )]
	[InlineData( 50, 50, false )]
	[InlineData( 0, 0, false )]
	public void PunitiveProtectionUsesStrictOfflineRankComparison( int actorLevel, int targetLevel, bool expected )
	{
		var policy = new PolicySnapshot( 1, new[]
		{
			new GroupPolicy( "player" ), new GroupPolicy( "actor", protectionLevel: actorLevel ),
			new GroupPolicy( "target", protectionLevel: targetLevel )
		}, new[] { new UserPolicy( Owner, new[] { "actor" } ), new UserPolicy( Offline, new[] { "target" } ) } );
		var result = TargetProtection.Check( policy.Graph, new OwnerSet( Array.Empty<string>() ), Owner, false, Offline );
		Assert.Equal( expected, result.IsAllowed );
		Assert.Equal( actorLevel, result.ActorLevel );
		Assert.Equal( targetLevel, result.TargetLevel );
	}

	[Fact]
	public void ProtectionUsesHighestInheritedLevelAndRefusesSelfEvenForOwner()
	{
		var defaults = PolicySnapshot.CreateDefault();
		var graph = new PolicySnapshot( 1, defaults.Groups, new[] { new UserPolicy( Other, new[] { "admin", "moderator" } ) } ).Graph;
		var owners = new OwnerSet( new[] { Owner } );
		Assert.Equal( 50, TargetProtection.Level( graph, owners, Other ) );
		Assert.Equal( 100, TargetProtection.Level( graph, owners, Owner ) );
		Assert.Equal( 0, TargetProtection.Level( graph, owners, Offline ) );
		Assert.False( TargetProtection.Check( graph, owners, Owner, false, Owner ).IsAllowed );
		Assert.False( TargetProtection.Check( graph, owners, Other, false, Owner ).IsAllowed );
		Assert.True( TargetProtection.Check( graph, owners, "dedicated-console", true, Owner ).IsAllowed );
	}

	[Fact]
	public async Task OrdinaryAdminCannotEditPolicyEvenWithExplicitSecurityPermission()
	{
		var store = new MemoryPolicyStore();
		var service = Service( store );
		Assert.Equal( CommandStatus.Success, (await Apply( service, OwnerActor, PolicyChange.AddUserGroup( Other, "admin" ) )).Status );
		Assert.Equal( CommandStatus.Success, (await Apply( service, OwnerActor, PolicyChange.SetUserPermission( Other, "admin.security.manage", PermissionEffect.Allow ) )).Status );
		Assert.True( service.Explain( OtherActor, "admin.security.manage" ).IsAllowed );
		Assert.Equal( CommandStatus.Denied, (await Apply( service, OtherActor, PolicyChange.CreateGroup( "escalation" ) )).Status );
		Assert.Equal( CommandStatus.Denied, (await Apply( service, null, PolicyChange.CreateGroup( "nullroot" ) )).Status );
		Assert.Equal( CommandStatus.Denied, (await Apply( service, new ActorIdentity( ActorKind.InternalService, "test.service" ), PolicyChange.CreateGroup( "servicegroup" ) )).Status );
		Assert.Equal( 2, store.WriteCount );
		Assert.False( store.Documents.ContainsKey( "security/owners" ) );
	}

	[Fact]
	public async Task OwnExplainWorksButAnotherSubjectRequiresItsSeparatePermission()
	{
		var service = Service( new MemoryPolicyStore() );
		Assert.NotNull( await service.ExplainForAsync( () => OtherActor, Other, "admin.help" ) );
		Assert.Null( await service.ExplainForAsync( () => OtherActor, Owner, "admin.help" ) );
		var owner = await service.ExplainForAsync( () => OwnerActor, Owner, "admin.players.ban" );
		Assert.True( owner.IsAllowed );
		Assert.True( owner.OwnerBypass );
		Assert.False( service.Explain( OwnerActor, "unknown.permission" ).IsAllowed );
		Assert.False( service.Explain( OwnerActor, "unknown.permission" ).OwnerBypass );
		await Apply( service, OwnerActor, PolicyChange.SetUserPermission( Other, "admin.permissions.explain", PermissionEffect.Allow ) );
		Assert.NotNull( await service.ExplainForAsync( () => OtherActor, Owner, "admin.help" ) );
	}

	[Fact]
	public async Task PolicyActivationAndSuccessWaitForPersistence()
	{
		var store = new MemoryPolicyStore();
		var started = new TaskCompletionSource();
		var resume = new TaskCompletionSource();
		store.BeforeCommit = async (_, _) => { started.TrySetResult(); await resume.Task; };
		var service = Service( store );
		var operation = Apply( service, OwnerActor, PolicyChange.SetUserPermission( Other, "admin.players.kick", PermissionEffect.Allow ) );
		await started.Task;
		Assert.False( operation.IsCompleted );
		Assert.Equal( 0, service.PolicyRevision );
		Assert.False( service.Explain( OtherActor, "admin.players.kick" ).IsAllowed );
		resume.SetResult();
		Assert.Equal( CommandStatus.Success, (await operation).Status );
		Assert.Equal( 1, service.PolicyRevision );
		Assert.True( service.Explain( OtherActor, "admin.players.kick" ).IsAllowed );
		Assert.Contains( "Unknown", store.Documents[ "security/policy" ].PayloadJson );
		Assert.Contains( "security.policy.change", store.Documents[ "security/policy" ].PayloadJson );
	}

	[Fact]
	public async Task FailedPersistenceDoesNotPublishCandidatePolicy()
	{
		var store = new MemoryPolicyStore { WriteOverride = StoreWriteStatus.Unavailable };
		var service = Service( store );
		var result = await Apply( service, OwnerActor, PolicyChange.SetUserPermission( Other, "admin.players.kick", PermissionEffect.Allow ) );
		Assert.Equal( CommandStatus.StorageUnavailable, result.Status );
		Assert.Equal( StoreWriteStatus.Unavailable, service.LastWriteStatus );
		Assert.Equal( 0, service.PolicyRevision );
		Assert.False( service.Explain( OtherActor, "admin.players.kick" ).IsAllowed );
		Assert.Empty( store.Documents );
	}

	[Fact]
	public async Task RevokedOwnerCannotExecuteAnAlreadyQueuedPolicyMutation()
	{
		var store = new MemoryPolicyStore();
		var started = new TaskCompletionSource();
		var resume = new TaskCompletionSource();
		store.BeforeCommit = async (_, _) => { started.TrySetResult(); await resume.Task; };
		var service = Service( store, new[] { Owner, Other } );
		var revoke = service.ChangeOwnerAsync( () => LocalConsole, Owner, false, Guid.NewGuid(), None );
		await started.Task;
		var queued = Apply( service, OwnerActor, PolicyChange.CreateGroup( "staleowner" ) );
		Assert.False( queued.IsCompleted );
		resume.SetResult();
		Assert.Equal( CommandStatus.Success, (await revoke).Status );
		Assert.Equal( CommandStatus.Denied, (await queued).Status );
		Assert.False( service.IsOwner( Owner ) );
		Assert.True( service.IsOwner( Other ) );
		Assert.Equal( 1, store.WriteCount );
	}

	[Fact]
	public async Task FinalOwnerCanOnlyBeClearedThroughExplicitLocalRecovery()
	{
		var store = new MemoryPolicyStore();
		var service = Service( store );
		Assert.Equal( CommandStatus.Denied, (await service.ChangeOwnerAsync( () => OwnerActor, Other, true, Guid.NewGuid(), None )).Status );
		Assert.Equal( CommandStatus.Denied, (await service.ChangeOwnerAsync( () => LocalConsole, Owner, false, Guid.NewGuid(), None )).Status );
		Assert.Empty( store.Documents );
		Assert.Equal( CommandStatus.Denied, (await service.RecoverOwnersAsync( () => OwnerActor, Array.Empty<string>(), Guid.NewGuid(), None )).Status );
		Assert.Equal( CommandStatus.Success, (await service.RecoverOwnersAsync( () => LocalConsole, Array.Empty<string>(), Guid.NewGuid(), None )).Status );
		Assert.False( service.IsOwner( Owner ) );
		var restarted = Service( store );
		await restarted.Initialization;
		Assert.False( restarted.IsOwner( Owner ) );
		Assert.Equal( "managed", restarted.OwnerSource );
		Assert.Equal( 0, restarted.OwnerCount );
		Assert.Equal( CommandStatus.Success, (await restarted.RecoverOwnersAsync( () => LocalConsole, new[] { Other }, Guid.NewGuid(), None )).Status );
		Assert.True( restarted.IsOwner( Other ) );
	}

	[Fact]
	public async Task CorruptManagedOwnersNeverFallBackToBootstrapAndCannotBeResetAsMissing()
	{
		var store = new MemoryPolicyStore();
		var service = Service( store );
		await service.ChangeOwnerAsync( () => LocalConsole, Other, true, Guid.NewGuid(), None );
		store.ReadOverrides[ "security/owners" ] = StoreReadStatus.Corrupt;
		await service.ReloadAsync( None );
		Assert.False( service.IsOwner( Owner ) );
		Assert.False( service.IsOwner( Other ) );
		Assert.Equal( StoreReadStatus.Corrupt, service.OwnerStatus );
		Assert.Equal( CommandStatus.StorageUnavailable, (await service.RecoverOwnersAsync( () => LocalConsole, new[] { Owner }, Guid.NewGuid(), None )).Status );
		store.ReadOverrides[ "security/owners" ] = StoreReadStatus.Missing;
		await service.ReloadAsync( None );
		Assert.Equal( StoreReadStatus.Corrupt, service.OwnerStatus );
		Assert.False( service.IsOwner( Owner ) );
		Assert.Equal( 1, store.WriteCount );
	}

	[Fact]
	public async Task InvalidPersistedPolicyAndReadExceptionsReportDegradedAuthority()
	{
		var store = new MemoryPolicyStore();
		store.Documents[ "security/policy" ] = new VersionedStoreDocument( 1, 1, Payload( "{}" ) );
		var service = Service( store );
		await service.Initialization;
		Assert.Equal( StoreReadStatus.Corrupt, service.PolicyStatus );
		Assert.False( service.Explain( OwnerActor, "admin.help" ).IsAllowed );
		Assert.Equal( CommandStatus.StorageUnavailable, (await Apply( service, LocalConsole, PolicyChange.CreateGroup( "reset" ) )).Status );
		Assert.Equal( 0, store.WriteCount );
		store.ThrowOnRead = true;
		var unavailable = Service( store );
		await unavailable.Initialization;
		Assert.Equal( StoreReadStatus.Unavailable, unavailable.PolicyStatus );
		Assert.False( unavailable.IsUsable );
	}

	[Fact]
	public async Task RecoveryNotificationInvalidatesAuthorizationAgainstStoredRevision()
	{
		var store = new MemoryPolicyStore();
		var service = Service( store );
		await Apply( service, OwnerActor, PolicyChange.SetUserPermission( Other, "admin.players.kick", PermissionEffect.Allow ) );
		Assert.True( service.Explain( OtherActor, "admin.players.kick" ).IsAllowed );
		var replacement = PolicySnapshot.CreateDefault( 2 );
		store.Documents[ "security/policy" ] = new VersionedStoreDocument( 1, 2, Payload( PolicyCodec.WriteState( replacement.Graph ) ) );
		service.OnStorageChanged( HostPolicyService.PolicyKey );
		await service.ReloadAsync( None );
		Assert.Equal( 2, service.PolicyRevision );
		Assert.False( service.Explain( OtherActor, "admin.players.kick" ).IsAllowed );
	}

	[Fact]
	public void PolicyEditsRejectCyclesAndReferencedDeletionWithoutChangingTheOriginal()
	{
		var graph = PolicySnapshot.CreateDefault().Graph;
		Assert.Throws<ArgumentException>( () => PolicyEdits.Apply( graph, PolicyChange.SetParents( "player", new[] { "admin" } ).Edit ) );
		Assert.Throws<ArgumentException>( () => PolicyEdits.Apply( graph, PolicyChange.DeleteGroup( "player" ).Edit ) );
		Assert.Throws<ArgumentException>( () => PolicyEdits.Apply( graph, PolicyChange.DeleteGroup( "moderator" ).Edit ) );
		Assert.Throws<ArgumentException>( () => PolicyChange.AddUserGroup( Owner, "owner" ) );
		Assert.Empty( graph.Users );
		Assert.Equal( 0, graph.Revision );
		var changed = PolicyEdits.Apply( graph, PolicyChange.AddUserGroup( Offline, "admin" ).Edit );
		Assert.Equal( 1, changed.Revision );
		Assert.Equal( "admin", Assert.Single( changed.Users[ Offline ].Groups ) );
		Assert.Same( changed, PolicyEdits.Apply( changed, PolicyChange.AddUserGroup( Offline, "admin" ).Edit ) );
		var roundTrip = PolicyCodec.ReadPayload( Payload( PolicyCodec.WriteState( changed ) ), 1 );
		Assert.Equal( PolicyCodec.WriteState( changed ), PolicyCodec.WriteState( roundTrip ) );
	}

	[Fact]
	public async Task RetiredRuntimeCannotExplainTargetOrMutate()
	{
		var available = true;
		var store = new MemoryPolicyStore();
		var service = Service( store, available: () => available );
		Assert.True( service.IsOwner( Owner ) );
		available = false;
		Assert.False( service.IsOwner( Owner ) );
		Assert.False( service.Explain( OwnerActor, "admin.help" ).IsAllowed );
		Assert.False( service.CheckTarget( LocalConsole, Offline ).IsAllowed );
		Assert.Equal( CommandStatus.Unsupported, (await Apply( service, LocalConsole, PolicyChange.CreateGroup( "retired" ) )).Status );
		Assert.Empty( store.Documents );
	}
}