1+ using System . Collections ;
2+ using System . Collections . ObjectModel ;
13using System . Reactive ;
24using System . Reactive . Linq ;
35using System . Reactive . Subjects ;
810using ByteSync . Business . Inventories ;
911using ByteSync . Client . UnitTests . Helpers ;
1012using ByteSync . Common . Business . Actions ;
13+ using ByteSync . Common . Business . EndPoints ;
1114using ByteSync . Common . Business . Inventories ;
15+ using ByteSync . Common . Business . Misc ;
1216using ByteSync . Interfaces . Controls . Comparisons ;
1317using ByteSync . Interfaces . Dialogs ;
1418using ByteSync . Interfaces . Factories . ViewModels ;
1519using ByteSync . Interfaces . Services . Localizations ;
20+ using ByteSync . Interfaces . Services . Sessions ;
1621using ByteSync . Models . Comparisons . Result ;
22+ using ByteSync . Models . Inventories ;
1723using ByteSync . TestsCommon ;
1824using ByteSync . ViewModels . Sessions . Comparisons . Actions ;
1925using FluentAssertions ;
@@ -73,10 +79,10 @@ public void SetUp()
7379 . Returns ( mockActionEditViewModel . Object ) ;
7480 }
7581
76- private ComparisonItem CreateMockComparisonItem ( FileSystemTypes fileSystemType )
82+ private ComparisonItem CreateMockComparisonItem ( FileSystemTypes fileSystemType , string linkingKey = "test-file" )
7783 {
7884 // Create a real ComparisonItem instance since it cannot be mocked (no parameterless constructor)
79- var pathIdentity = new PathIdentity ( fileSystemType , "test-file" , "test-file" , "test-file" ) ;
85+ var pathIdentity = new PathIdentity ( fileSystemType , linkingKey , linkingKey , linkingKey ) ;
8086 var comparisonItem = new ComparisonItem ( pathIdentity ) ;
8187
8288 return comparisonItem ;
@@ -546,4 +552,155 @@ public void ValidationFailureSummary_AffectedItemsTooltip_ShouldGenerateCorrectT
546552 // When FileName is null, it should use LinkingKeyValue
547553 tooltip . Should ( ) . Be ( "file1.txt\n directory1" ) ;
548554 }
555+
556+ [ Test ]
557+ public void Save_WithValidAction_ShouldLogConsistencySuccess ( )
558+ {
559+ var comparisonItems = new List < ComparisonItem >
560+ {
561+ CreateMockComparisonItem ( FileSystemTypes . File , "file1" ) ,
562+ CreateMockComparisonItem ( FileSystemTypes . File , "file2" )
563+ } ;
564+
565+ var dataPartIndexer = BuildDataPartIndexer ( ) ;
566+ var actionEditViewModel = new AtomicActionEditViewModel ( FileSystemTypes . File , false , comparisonItems , dataPartIndexer ) ;
567+
568+ _mockActionEditViewModelFactory . Setup ( x => x . BuildAtomicActionEditViewModel (
569+ It . IsAny < FileSystemTypes > ( ) , It . IsAny < bool > ( ) , It . IsAny < AtomicAction > ( ) , It . IsAny < List < ComparisonItem > > ( ) ) )
570+ . Returns ( actionEditViewModel ) ;
571+
572+ var viewModel = new TargetedActionGlobalViewModel (
573+ comparisonItems ,
574+ _mockDialogService . Object ,
575+ _mockLocalizationService . Object ,
576+ _mockTargetedActionsService . Object ,
577+ _mockAtomicActionConsistencyChecker . Object ,
578+ _mockActionEditViewModelFactory . Object ,
579+ _mockLogger . Object ,
580+ _mockFailureReasonService . Object
581+ ) ;
582+
583+ ConfigureValidAction ( actionEditViewModel ) ;
584+
585+ var result = new AtomicActionConsistencyCheckCanAddResult ( comparisonItems ) ;
586+ _mockAtomicActionConsistencyChecker . Setup ( x => x . CheckCanAdd ( It . IsAny < AtomicAction > ( ) , comparisonItems ) )
587+ . Returns ( result ) ;
588+
589+ viewModel . SaveCommand . Execute ( Unit . Default ) . Subscribe ( ) ;
590+
591+ _mockLogger . Verify (
592+ x => x . Log (
593+ LogLevel . Information ,
594+ It . IsAny < EventId > ( ) ,
595+ It . Is < It . IsAnyType > ( ( v , t ) =>
596+ v . ToString ( ) ! . Contains ( "Targeted action created." )
597+ && v . ToString ( ) ! . Contains ( "Items=file1, file2" ) ) ,
598+ It . IsAny < Exception ? > ( ) ,
599+ It . IsAny < Func < It . IsAnyType , Exception ? , string > > ( ) ) ,
600+ Times . Once ) ;
601+ }
602+
603+ private static void ConfigureValidAction ( AtomicActionEditViewModel actionEditViewModel )
604+ {
605+ var actions = GetInternalEnumerable ( actionEditViewModel , "Actions" ) ;
606+ var sources = GetInternalEnumerable ( actionEditViewModel , "Sources" ) ;
607+ var destinations = GetInternalEnumerable ( actionEditViewModel , "Destinations" ) ;
608+
609+ var selectedAction = GetActionByOperator ( actions , ActionOperatorTypes . Copy ) ;
610+
611+ SetInternalProperty ( actionEditViewModel , "SelectedAction" , selectedAction ) ;
612+ SetInternalProperty ( actionEditViewModel , "SelectedSource" , FirstItem ( sources ) ) ;
613+ SetInternalProperty ( actionEditViewModel , "SelectedDestination" , FirstItem ( destinations ) ) ;
614+ }
615+
616+ private sealed class TestDataPartIndexer : IDataPartIndexer
617+ {
618+ private readonly ReadOnlyCollection < DataPart > _dataParts ;
619+
620+ public TestDataPartIndexer ( IReadOnlyCollection < DataPart > dataParts )
621+ {
622+ _dataParts = new ReadOnlyCollection < DataPart > ( dataParts . ToList ( ) ) ;
623+ }
624+
625+ public void BuildMap ( List < Inventory > inventories )
626+ {
627+ }
628+
629+ public ReadOnlyCollection < DataPart > GetAllDataParts ( )
630+ {
631+ return _dataParts ;
632+ }
633+
634+ public DataPart ? GetDataPart ( string ? dataPartName )
635+ {
636+ return _dataParts . FirstOrDefault ( dp => dp . Name == dataPartName ) ;
637+ }
638+
639+ public void Remap ( ICollection < SynchronizationRule > synchronizationRules )
640+ {
641+ }
642+ }
643+
644+ private static IDataPartIndexer BuildDataPartIndexer ( )
645+ {
646+ var endpoint = new ByteSyncEndpoint
647+ {
648+ ClientId = "c" ,
649+ ClientInstanceId = "ci" ,
650+ Version = "v" ,
651+ OSPlatform = OSPlatforms . Windows ,
652+ IpAddress = "127.0.0.1"
653+ } ;
654+
655+ var inventoryA = new Inventory { InventoryId = "INV_A" , Code = "A" , Endpoint = endpoint , MachineName = "M" } ;
656+ var inventoryB = new Inventory { InventoryId = "INV_B" , Code = "B" , Endpoint = endpoint , MachineName = "M" } ;
657+ var partA = new InventoryPart ( inventoryA , "c:\\ a" , FileSystemTypes . Directory ) { Code = "A1" } ;
658+ var partB = new InventoryPart ( inventoryB , "c:\\ b" , FileSystemTypes . Directory ) { Code = "B1" } ;
659+
660+ var dataParts = new List < DataPart >
661+ {
662+ new ( "A" , partA ) ,
663+ new ( "B" , partB )
664+ } ;
665+
666+ return new TestDataPartIndexer ( dataParts ) ;
667+ }
668+
669+ private static IEnumerable GetInternalEnumerable ( object target , string propertyName )
670+ {
671+ var property = target . GetType ( ) . GetProperty ( propertyName , BindingFlags . Instance | BindingFlags . NonPublic ) ;
672+
673+ return ( IEnumerable ) property ! . GetValue ( target ) ! ;
674+ }
675+
676+ private static object FirstItem ( IEnumerable items )
677+ {
678+ foreach ( var item in items )
679+ {
680+ return item ! ;
681+ }
682+
683+ throw new InvalidOperationException ( "Empty collection" ) ;
684+ }
685+
686+ private static void SetInternalProperty ( object target , string propertyName , object ? value )
687+ {
688+ var property = target . GetType ( ) . GetProperty ( propertyName , BindingFlags . Instance | BindingFlags . NonPublic ) ;
689+ property ! . SetValue ( target , value ) ;
690+ }
691+
692+ private static object GetActionByOperator ( IEnumerable actions , ActionOperatorTypes operatorType )
693+ {
694+ foreach ( var action in actions )
695+ {
696+ var property = action ! . GetType ( ) . GetProperty ( "ActionOperatorType" , BindingFlags . Instance | BindingFlags . Public ) ;
697+ var value = ( ActionOperatorTypes ) property ! . GetValue ( action ) ! ;
698+ if ( value == operatorType )
699+ {
700+ return action ;
701+ }
702+ }
703+
704+ throw new InvalidOperationException ( "Action not found" ) ;
705+ }
549706}
0 commit comments