tests/Application/InventoryAccessServiceTests.cs

Unit tests for InventoryAccessService in the application layer. They exercise granting and revoking inventory access tied to connections, characters and interaction sessions, and validate proof/revision/epoch behavior under connection reuse and churn.

using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class InventoryAccessServiceTests
{
	[TestMethod]
	public void GrantIsBoundToConnectionCharacterAndCapabilities()
	{
		var service = new InventoryAccessService();
		var connection = ConnectionId.New();
		var character = CharacterId.New();
		var inventory = InventoryId.New();
		service.OpenConnection( connection );
		service.Grant( new InventoryGrant
		{
			ConnectionId = connection,
			CharacterId = character,
			InventoryId = inventory,
			Capabilities = InventoryCapability.View | InventoryCapability.Move,
			Kind = InventoryGrantKind.Character
		} );

		Assert.IsTrue( service.Has( connection, character, inventory, InventoryCapability.View ) );
		Assert.IsFalse( service.Has( connection, character, inventory, InventoryCapability.Drop ) );
		Assert.IsFalse( service.Has( ConnectionId.New(), character, inventory, InventoryCapability.View ) );
		Assert.IsFalse( service.Has( connection, CharacterId.New(), inventory, InventoryCapability.View ) );
	}

	[TestMethod]
	public void CharacterUnloadRevokesAllCharacterGrants()
	{
		var service = new InventoryAccessService();
		var connection = ConnectionId.New();
		var character = CharacterId.New();
		service.OpenConnection( connection );
		for ( var i = 0; i < 2; i++ )
		{
			service.Grant( new InventoryGrant
			{
				ConnectionId = connection,
				CharacterId = character,
				InventoryId = InventoryId.New(),
				Capabilities = InventoryCapability.View,
				Kind = InventoryGrantKind.Character
			} );
		}

		Assert.HasCount( 2, service.RevokeCharacter( connection, character ) );
	}

	[TestMethod]
	public void ConnectionReuseChangesEpochAndRejectsPreDisconnectProof()
	{
		var service = new InventoryAccessService();
		var connection = ConnectionId.New();
		var character = CharacterId.New();
		var inventory = InventoryId.New();
		service.OpenConnection( connection );
		service.Grant( CharacterGrant( connection, character, inventory ) );
		var stale = service.Prove( connection, character, inventory, InventoryCapability.View );
		Assert.IsNotNull( stale );

		Assert.HasCount( 1, service.RevokeConnection( connection ) );
		Assert.AreEqual( 0, service.RevisionEntryCount );
		Assert.AreEqual( 0, service.ConnectionEpochCount );

		Assert.ThrowsExactly<InvalidOperationException>( () =>
			service.Grant( CharacterGrant( connection, character, inventory ) ) );
		service.OpenConnection( connection );
		service.Grant( CharacterGrant( connection, character, inventory ) );
		var current = service.Prove( connection, character, inventory, InventoryCapability.View );
		Assert.IsNotNull( current );
		Assert.AreNotEqual( stale.ConnectionEpoch, current.ConnectionEpoch );
		Assert.AreNotEqual( stale.CapabilityRevision, current.CapabilityRevision );
		Assert.IsNotNull( service.ValidateProof( stale ) );
		Assert.IsNull( service.ValidateProof( current ) );
	}

	[TestMethod]
	public void CharacterChurnPrunesRevisionsWithoutWeakeningStaleProofRejection()
	{
		var service = new InventoryAccessService();
		var connection = ConnectionId.New();
		InventoryAccessProof? firstProof = null;
		service.OpenConnection( connection );
		for ( var index = 0; index < 10_000; index++ )
		{
			var character = CharacterId.New();
			var inventory = InventoryId.New();
			service.Grant( CharacterGrant( connection, character, inventory ) );
			var proof = service.Prove( connection, character, inventory, InventoryCapability.View );
			Assert.IsNotNull( proof );
			firstProof ??= proof;
			Assert.HasCount( 1, service.RevokeCharacter( connection, character ) );
			Assert.IsNotNull( service.ValidateProof( proof ) );
		}

		Assert.AreEqual( 0, service.RevisionEntryCount );
		Assert.AreEqual( 1, service.ConnectionEpochCount );
		Assert.IsNotNull( firstProof );
		Assert.IsNotNull( service.ValidateProof( firstProof ) );
		Assert.IsEmpty( service.RevokeConnection( connection ) );
		Assert.AreEqual( 0, service.ConnectionEpochCount );
	}

	[TestMethod]
	public void PartialRevocationRetainsOneRevisionAndInvalidatesPriorProofs()
	{
		var service = new InventoryAccessService();
		var connection = ConnectionId.New();
		var character = CharacterId.New();
		var inventory = InventoryId.New();
		var session = InteractionSessionId.New();
		service.OpenConnection( connection );
		service.Grant( CharacterGrant( connection, character, inventory ) );
		var beforeSession = service.Prove( connection, character, inventory, InventoryCapability.View );
		Assert.IsNotNull( beforeSession );
		service.Grant( new InventoryGrant
		{
			ConnectionId = connection,
			CharacterId = character,
			InventoryId = inventory,
			Capabilities = InventoryCapability.Move,
			Kind = InventoryGrantKind.InteractionSession,
			SessionId = session
		} );
		var duringSession = service.Prove( connection, character, inventory, InventoryCapability.View );
		Assert.IsNotNull( duringSession );
		Assert.IsNotNull( service.ValidateProof( beforeSession ) );

		Assert.HasCount( 1, service.RevokeSession( session ) );
		Assert.AreEqual( 1, service.RevisionEntryCount );
		Assert.IsNotNull( service.ValidateProof( duringSession ) );
		var afterSession = service.Prove( connection, character, inventory, InventoryCapability.View );
		Assert.IsNotNull( afterSession );
		Assert.IsNull( service.ValidateProof( afterSession ) );
	}

	private static InventoryGrant CharacterGrant(
		ConnectionId connection,
		CharacterId character,
		InventoryId inventory ) => new()
	{
		ConnectionId = connection,
		CharacterId = character,
		InventoryId = inventory,
		Capabilities = InventoryCapability.View,
		Kind = InventoryGrantKind.Character
	};
}