2025-08-17 19:55:33 -04:00
using Moq ;
namespace LibreMetaverse.RLV.Tests.Commands
{
public class AttachCommandTests : RestrictionsBase
{
#region @attachover @attachoverorreplace @attach : < folder1 / . . . / folderN > = force
[Theory]
[InlineData("attach", true)]
[InlineData("attachoverorreplace", true)]
[InlineData("attachover", false)]
public async Task AttachForce ( string command , bool replaceExistingAttachments )
{
var sampleTree = SampleInventoryTree . BuildInventoryTree ( ) ;
var sharedFolder = sampleTree . Root ;
2025-08-21 04:43:06 -04:00
var inventoryMap = new InventoryMap ( sharedFolder , [ ] ) ;
2025-08-17 19:55:33 -04:00
_queryCallbacks . Setup ( e = >
2025-08-21 04:43:06 -04:00
e . TryGetInventoryMapAsync ( default )
) . ReturnsAsync ( ( true , inventoryMap ) ) ;
2025-08-17 19:55:33 -04:00
_actionCallbacks . Setup ( e = >
e . AttachAsync ( It . IsAny < IReadOnlyList < AttachmentRequest > > ( ) , It . IsAny < CancellationToken > ( ) )
) . Returns ( Task . CompletedTask ) ;
// Attach everything in the Clothing/Hats folder
var expected = new HashSet < AttachmentRequest > ( )
{
new ( sampleTree . Root_Clothing_Hats_FancyHat_Chin . Id , RlvAttachmentPoint . Chin , replaceExistingAttachments ) ,
new ( sampleTree . Root_Clothing_Hats_PartyHat_Spine . Id , RlvAttachmentPoint . Spine , replaceExistingAttachments ) ,
} ;
// Act
await _rlv . ProcessMessage ( $"@{command}:Clothing/Hats=force" , _sender . Id , _sender . Name ) ;
// Assert
_actionCallbacks . Verify ( e = >
e . AttachAsync (
It . Is < IReadOnlyList < AttachmentRequest > > ( ids = >
ids ! = null & &
ids . Count = = expected . Count & &
expected . SetEquals ( ids )
) ,
It . IsAny < CancellationToken > ( )
) ,
Times . Once
) ;
_actionCallbacks . VerifyNoOtherCalls ( ) ;
}
[Theory]
[InlineData("attach", true)]
[InlineData("attachoverorreplace", true)]
[InlineData("attachover", false)]
public async Task AttachForce_WithClothing ( string command , bool replaceExistingAttachments )
{
var sampleTree = SampleInventoryTree . BuildInventoryTree ( ) ;
var sharedFolder = sampleTree . Root ;
2025-08-21 04:43:06 -04:00
var inventoryMap = new InventoryMap ( sharedFolder , [ ] ) ;
2025-08-17 19:55:33 -04:00
_queryCallbacks . Setup ( e = >
2025-08-21 04:43:06 -04:00
e . TryGetInventoryMapAsync ( default )
) . ReturnsAsync ( ( true , inventoryMap ) ) ;
2025-08-17 19:55:33 -04:00
_actionCallbacks . Setup ( e = >
e . AttachAsync ( It . IsAny < IReadOnlyList < AttachmentRequest > > ( ) , It . IsAny < CancellationToken > ( ) )
) . Returns ( Task . CompletedTask ) ;
// Attach everything in the Clothing folder. Make sure clothing types (RlvWearableType) are also included
var expected = new HashSet < AttachmentRequest > ( )
{
new ( sampleTree . Root_Clothing_BusinessPants_Pelvis . Id , RlvAttachmentPoint . Pelvis , replaceExistingAttachments ) ,
new ( sampleTree . Root_Clothing_HappyShirt . Id , RlvAttachmentPoint . Default , replaceExistingAttachments ) ,
new ( sampleTree . Root_Clothing_RetroPants . Id , RlvAttachmentPoint . Default , replaceExistingAttachments ) ,
} ;
// Act
await _rlv . ProcessMessage ( $"@{command}:Clothing=force" , _sender . Id , _sender . Name ) ;
// Assert
_actionCallbacks . Verify ( e = >
e . AttachAsync (
It . Is < IReadOnlyList < AttachmentRequest > > ( ids = >
ids ! = null & &
ids . Count = = expected . Count & &
expected . SetEquals ( ids )
) ,
It . IsAny < CancellationToken > ( )
) ,
Times . Once
) ;
_actionCallbacks . VerifyNoOtherCalls ( ) ;
}
[Theory]
[InlineData("attach")]
[InlineData("attachoverorreplace")]
[InlineData("attachover")]
public async Task AttachForce_AlreadyAttached ( string command )
{
var sampleTree = SampleInventoryTree . BuildInventoryTree ( ) ;
var sharedFolder = sampleTree . Root ;
sampleTree . Root_Clothing_BusinessPants_Pelvis . AttachedTo = RlvAttachmentPoint . Groin ;
sampleTree . Root_Clothing_HappyShirt . AttachedTo = RlvAttachmentPoint . Chest ;
sampleTree . Root_Clothing_RetroPants . WornOn = RlvWearableType . Pants ;
2025-08-21 04:43:06 -04:00
var inventoryMap = new InventoryMap ( sharedFolder , [ ] ) ;
2025-08-17 19:55:33 -04:00
_queryCallbacks . Setup ( e = >
2025-08-21 04:43:06 -04:00
e . TryGetInventoryMapAsync ( default )
) . ReturnsAsync ( ( true , inventoryMap ) ) ;
2025-08-17 19:55:33 -04:00
_actionCallbacks . Setup ( e = >
e . AttachAsync ( It . IsAny < IReadOnlyList < AttachmentRequest > > ( ) , It . IsAny < CancellationToken > ( ) )
) . Returns ( Task . CompletedTask ) ;
// Attach nothing because everything in this folder is already attached
var expected = new HashSet < AttachmentRequest > ( )
{
} ;
// Act
await _rlv . ProcessMessage ( $"@{command}:Clothing=force" , _sender . Id , _sender . Name ) ;
// Assert
_actionCallbacks . Verify ( e = >
e . AttachAsync (
It . Is < IReadOnlyList < AttachmentRequest > > ( ids = >
ids ! = null & &
ids . Count = = expected . Count & &
expected . SetEquals ( ids )
) ,
It . IsAny < CancellationToken > ( )
) ,
Times . Once
) ;
_actionCallbacks . VerifyNoOtherCalls ( ) ;
}
[Theory]
[InlineData("attach", true)]
[InlineData("attachoverorreplace", true)]
[InlineData("attachover", false)]
public async Task AttachForce_PositionFromFolderName ( string command , bool replaceExistingAttachments )
{
var sampleTree = SampleInventoryTree . BuildInventoryTree ( ) ;
var sharedFolder = sampleTree . Root ;
sampleTree . Clothing_Hats_Folder . Name = "Hats (spine)" ;
// Item name overrides folder name
sampleTree . Root_Clothing_Hats_FancyHat_Chin . Name = "Fancy Hat (skull)" ;
2025-08-21 04:43:06 -04:00
var inventoryMap = new InventoryMap ( sharedFolder , [ ] ) ;
2025-08-17 19:55:33 -04:00
_queryCallbacks . Setup ( e = >
2025-08-21 04:43:06 -04:00
e . TryGetInventoryMapAsync ( default )
) . ReturnsAsync ( ( true , inventoryMap ) ) ;
2025-08-17 19:55:33 -04:00
_actionCallbacks . Setup ( e = >
e . AttachAsync ( It . IsAny < IReadOnlyList < AttachmentRequest > > ( ) , It . IsAny < CancellationToken > ( ) )
) . Returns ( Task . CompletedTask ) ;
// Attach everything under the "Clothing/Hats (spine)" folder, attaching everything to the Spine point unless the item explicitly specifies a different attachment point such as "Fancy Hat (skull)".
var expected = new HashSet < AttachmentRequest > ( )
{
new ( sampleTree . Root_Clothing_Hats_FancyHat_Chin . Id , RlvAttachmentPoint . Skull , replaceExistingAttachments ) ,
new ( sampleTree . Root_Clothing_Hats_PartyHat_Spine . Id , RlvAttachmentPoint . Spine , replaceExistingAttachments ) ,
} ;
// Act
await _rlv . ProcessMessage ( $"@{command}:{sampleTree.Clothing_Folder.Name}/{sampleTree.Clothing_Hats_Folder.Name}=force" , _sender . Id , _sender . Name ) ;
// Assert
_actionCallbacks . Verify ( e = >
e . AttachAsync (
It . Is < IReadOnlyList < AttachmentRequest > > ( ids = >
ids ! = null & &
ids . Count = = expected . Count & &
expected . SetEquals ( ids )
) ,
It . IsAny < CancellationToken > ( )
) ,
Times . Once
) ;
_actionCallbacks . VerifyNoOtherCalls ( ) ;
}
[Theory]
[InlineData("attach")]
[InlineData("attachoverorreplace")]
[InlineData("attachover")]
public async Task AttachForce_FolderNameSpecifiesToAddInsteadOfReplace ( string command )
{
var sampleTree = SampleInventoryTree . BuildInventoryTree ( ) ;
var sharedFolder = sampleTree . Root ;
sampleTree . Clothing_Hats_Folder . Name = "+Hats" ;
2025-08-21 04:43:06 -04:00
var inventoryMap = new InventoryMap ( sharedFolder , [ ] ) ;
2025-08-17 19:55:33 -04:00
_queryCallbacks . Setup ( e = >
2025-08-21 04:43:06 -04:00
e . TryGetInventoryMapAsync ( default )
) . ReturnsAsync ( ( true , inventoryMap ) ) ;
2025-08-17 19:55:33 -04:00
_actionCallbacks . Setup ( e = >
e . AttachAsync ( It . IsAny < IReadOnlyList < AttachmentRequest > > ( ) , It . IsAny < CancellationToken > ( ) )
) . Returns ( Task . CompletedTask ) ;
// Attach everything inside of the Clothing/Hats folder, but force 'add to' logic due to the + prefix on the hats folder
var expected = new HashSet < AttachmentRequest > ( )
{
new ( sampleTree . Root_Clothing_Hats_FancyHat_Chin . Id , RlvAttachmentPoint . Chin , false ) ,
new ( sampleTree . Root_Clothing_Hats_PartyHat_Spine . Id , RlvAttachmentPoint . Spine , false ) ,
} ;
// Act
await _rlv . ProcessMessage ( $"@{command}:{sampleTree.Clothing_Folder.Name}/{sampleTree.Clothing_Hats_Folder.Name}=force" , _sender . Id , _sender . Name ) ;
// Assert
_actionCallbacks . Verify ( e = >
e . AttachAsync (
It . Is < IReadOnlyList < AttachmentRequest > > ( ids = >
ids ! = null & &
ids . Count = = expected . Count & &
expected . SetEquals ( ids )
) ,
It . IsAny < CancellationToken > ( )
) ,
Times . Once
) ;
_actionCallbacks . VerifyNoOtherCalls ( ) ;
}
[Theory]
[InlineData("attach", true)]
[InlineData("attachoverorreplace", true)]
[InlineData("attachover", false)]
public async Task AttachForce_AttachPrivateParentFolder ( string command , bool replaceExistingAttachments )
{
var sampleTree = SampleInventoryTree . BuildInventoryTree ( ) ;
var sharedFolder = sampleTree . Root ;
sampleTree . Clothing_Folder . Name = ".clothing" ;
2025-08-21 04:43:06 -04:00
var inventoryMap = new InventoryMap ( sharedFolder , [ ] ) ;
2025-08-17 19:55:33 -04:00
_queryCallbacks . Setup ( e = >
2025-08-21 04:43:06 -04:00
e . TryGetInventoryMapAsync ( default )
) . ReturnsAsync ( ( true , inventoryMap ) ) ;
2025-08-17 19:55:33 -04:00
_actionCallbacks . Setup ( e = >
e . AttachAsync ( It . IsAny < IReadOnlyList < AttachmentRequest > > ( ) , It . IsAny < CancellationToken > ( ) )
) . Returns ( Task . CompletedTask ) ;
// This is allowed even though we're targeting a private folder. Only private subfolders are ignored
var expected = new HashSet < AttachmentRequest > ( )
{
new ( sampleTree . Root_Clothing_Hats_FancyHat_Chin . Id , RlvAttachmentPoint . Chin , replaceExistingAttachments ) ,
new ( sampleTree . Root_Clothing_Hats_PartyHat_Spine . Id , RlvAttachmentPoint . Spine , replaceExistingAttachments ) ,
} ;
// Act
await _rlv . ProcessMessage ( $"@{command}:{sampleTree.Clothing_Folder.Name}/{sampleTree.Clothing_Hats_Folder.Name}=force" , _sender . Id , _sender . Name ) ;
// Assert
_actionCallbacks . Verify ( e = >
e . AttachAsync (
It . Is < IReadOnlyList < AttachmentRequest > > ( ids = >
ids ! = null & &
ids . Count = = expected . Count & &
expected . SetEquals ( ids )
) ,
It . IsAny < CancellationToken > ( )
) ,
Times . Once
) ;
_actionCallbacks . VerifyNoOtherCalls ( ) ;
}
#endregion
}
}