44using System . Runtime . InteropServices ;
55using OnePassword . Common ;
66using OnePassword . Documents ;
7+ using OnePassword . Items ;
8+ using OnePassword . Templates ;
79using OnePassword . Vaults ;
810
911namespace OnePassword ;
@@ -211,6 +213,74 @@ public void UpdateExtractsCurrentPlatformExecutablePayload()
211213 } ) ;
212214 }
213215
216+ [ Test ]
217+ public void AcceptChangesOnItemClearsNestedUrlChanges ( )
218+ {
219+ var item = CreateTrackedItem ( "item-id" , "Original Title" ) ;
220+ item . Urls . Add ( new Url { Href = "https://initial.example.com" } ) ;
221+
222+ AcceptChanges ( item ) ;
223+
224+ Assert . That ( IsTrackedChanged ( item ) , Is . False ) ;
225+
226+ item . Urls [ 0 ] . Href = "https://updated.example.com" ;
227+ Assert . That ( IsTrackedChanged ( item ) , Is . True ) ;
228+
229+ AcceptChanges ( item ) ;
230+
231+ Assert . That ( IsTrackedChanged ( item ) , Is . False ) ;
232+ }
233+
234+ [ Test ]
235+ public void AcceptChangesOnFieldClearsTypeChanges ( )
236+ {
237+ var field = new Field ( "Environment" , FieldType . String , "Production" ) ;
238+
239+ AcceptChanges ( field ) ;
240+
241+ Assert . That ( IsTrackedChanged ( field ) , Is . False ) ;
242+
243+ field . Type = FieldType . Concealed ;
244+ Assert . That ( IsTrackedChanged ( field ) , Is . True ) ;
245+
246+ AcceptChanges ( field ) ;
247+
248+ Assert . That ( IsTrackedChanged ( field ) , Is . False ) ;
249+ }
250+
251+ [ Test ]
252+ public void CreateItemFailureKeepsTemplateDirty ( )
253+ {
254+ using var fakeCli = new FakeCli ( errorOutput : "[ERROR] create failed" ) ;
255+ var manager = fakeCli . CreateManager ( ) ;
256+ var template = new Template
257+ {
258+ Title = "Original Title"
259+ } ;
260+
261+ AcceptChanges ( template ) ;
262+
263+ template . Title = "Updated Title" ;
264+
265+ Assert . Throws < InvalidOperationException > ( ( ) => manager . CreateItem ( template , "vault-id" ) ) ;
266+ Assert . That ( IsTrackedChanged ( template ) , Is . True ) ;
267+ }
268+
269+ [ Test ]
270+ public void EditItemFailureKeepsItemDirty ( )
271+ {
272+ using var fakeCli = new FakeCli ( errorOutput : "[ERROR] edit failed" ) ;
273+ var manager = fakeCli . CreateManager ( ) ;
274+ var item = CreateTrackedItem ( "item-id" , "Original Title" ) ;
275+
276+ AcceptChanges ( item ) ;
277+
278+ item . Title = "Updated Title" ;
279+
280+ Assert . Throws < InvalidOperationException > ( ( ) => manager . EditItem ( item , "vault-id" ) ) ;
281+ Assert . That ( IsTrackedChanged ( item ) , Is . True ) ;
282+ }
283+
214284 [ Test ]
215285 public void ShareItemWithoutEmailsOmitsEmailsFlag ( )
216286 {
@@ -345,16 +415,18 @@ private sealed class FakeCli : IDisposable
345415 {
346416 private readonly string _argumentsPath ;
347417 private readonly string _directoryPath ;
418+ private readonly string _errorOutputPath ;
348419 private readonly string _nextOutputPath ;
349420 private readonly string _updateMessagePath ;
350421 private readonly string _updatePayloadPath ;
351422 private readonly string _updatedVersionOutputPath ;
352423 private readonly string _versionOutputPath ;
353424
354- public FakeCli ( string versionOutput = "2.32.1\n " , string nextOutput = "{}" , string ? updateVersionOutput = null )
425+ public FakeCli ( string versionOutput = "2.32.1\n " , string nextOutput = "{}" , string ? updateVersionOutput = null , string ? errorOutput = null )
355426 {
356427 _directoryPath = Path . Combine ( Path . GetTempPath ( ) , Path . GetRandomFileName ( ) ) ;
357428 _argumentsPath = Path . Combine ( _directoryPath , "last-arguments.txt" ) ;
429+ _errorOutputPath = Path . Combine ( _directoryPath , "error-output.txt" ) ;
358430 _nextOutputPath = Path . Combine ( _directoryPath , "next-output.txt" ) ;
359431 _updateMessagePath = Path . Combine ( _directoryPath , "update-output.txt" ) ;
360432 _updatePayloadPath = Path . Combine ( _directoryPath , "update-payload.zip" ) ;
@@ -364,6 +436,8 @@ public FakeCli(string versionOutput = "2.32.1\n", string nextOutput = "{}", stri
364436 Directory . CreateDirectory ( _directoryPath ) ;
365437 File . WriteAllText ( _nextOutputPath , nextOutput ) ;
366438 File . WriteAllText ( _versionOutputPath , versionOutput ) ;
439+ if ( errorOutput is not null )
440+ File . WriteAllText ( _errorOutputPath , errorOutput ) ;
367441 if ( updateVersionOutput is not null )
368442 {
369443 File . WriteAllText ( _updateMessagePath , $ "Version { updateVersionOutput . Trim ( ) } is now available.") ;
@@ -435,6 +509,10 @@ @echo off
435509 type "%~dp0VERSION_OUTPUT_PLACEHOLDER"
436510 exit /b 0
437511 )
512+ if exist "%~dp0error-output.txt" (
513+ type "%~dp0error-output.txt" 1>&2
514+ exit /b 0
515+ )
438516 type "%~dp0next-output.txt"
439517 """ . Replace ( "VERSION_OUTPUT_PLACEHOLDER" , versionOutputFileName )
440518 : """
@@ -454,13 +532,65 @@ exit 0
454532 cat "$script_dir/VERSION_OUTPUT_PLACEHOLDER"
455533 exit 0
456534 fi
535+ if [ -f "$script_dir/error-output.txt" ]; then
536+ cat "$script_dir/error-output.txt" >&2
537+ exit 0
538+ fi
457539 cat "$script_dir/next-output.txt"
458540 """ . Replace ( "VERSION_OUTPUT_PLACEHOLDER" , versionOutputFileName ) ;
459541 }
460542
461543 private static string PackagedExecutableName => RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ? "op.exe" : "op" ;
462544 }
463545
546+ private static Item CreateTrackedItem ( string itemId , string title )
547+ {
548+ var item = new Item
549+ {
550+ Title = title
551+ } ;
552+ SetNonPublicProperty ( item , nameof ( Item . Id ) , itemId ) ;
553+ return item ;
554+ }
555+
556+ private static void SetNonPublicProperty ( object target , string propertyName , object ? value )
557+ {
558+ var property = target . GetType ( ) . GetProperty ( propertyName , BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic )
559+ ?? throw new InvalidOperationException ( $ "Could not find property '{ propertyName } ' on type { target . GetType ( ) . Name } .") ;
560+ property . SetValue ( target , value ) ;
561+ }
562+
563+ private static bool IsTrackedChanged ( object tracked )
564+ {
565+ var interfaceType = GetTrackedInterface ( tracked . GetType ( ) ) ;
566+ var changedProperty = interfaceType . GetProperty ( "Changed" , BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic )
567+ ?? throw new InvalidOperationException ( "Could not find ITracked.Changed." ) ;
568+ var interfaceMap = tracked . GetType ( ) . GetInterfaceMap ( interfaceType ) ;
569+ var methodIndex = Array . IndexOf ( interfaceMap . InterfaceMethods , changedProperty . GetMethod ) ;
570+ return methodIndex >= 0
571+ ? ( bool ) ( interfaceMap . TargetMethods [ methodIndex ] . Invoke ( tracked , null ) ?? false )
572+ : throw new InvalidOperationException ( "Could not resolve the ITracked.Changed implementation." ) ;
573+ }
574+
575+ private static void AcceptChanges ( object tracked )
576+ {
577+ var interfaceType = GetTrackedInterface ( tracked . GetType ( ) ) ;
578+ var acceptChangesMethod = interfaceType . GetMethod ( "AcceptChanges" , BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic )
579+ ?? throw new InvalidOperationException ( "Could not find ITracked.AcceptChanges." ) ;
580+ var interfaceMap = tracked . GetType ( ) . GetInterfaceMap ( interfaceType ) ;
581+ var methodIndex = Array . IndexOf ( interfaceMap . InterfaceMethods , acceptChangesMethod ) ;
582+ if ( methodIndex < 0 )
583+ throw new InvalidOperationException ( "Could not resolve the ITracked.AcceptChanges implementation." ) ;
584+
585+ interfaceMap . TargetMethods [ methodIndex ] . Invoke ( tracked , null ) ;
586+ }
587+
588+ private static Type GetTrackedInterface ( Type type )
589+ {
590+ return type . GetInterfaces ( ) . FirstOrDefault ( x => x . FullName == "OnePassword.Common.ITracked" )
591+ ?? throw new InvalidOperationException ( $ "Type { type . Name } does not implement OnePassword.Common.ITracked.") ;
592+ }
593+
464594 private sealed class TestDocument ( string id ) : IDocument
465595 {
466596 public string Id { get ; } = id ;
0 commit comments