33using RuriLib . Models . Jobs ;
44using RuriLib . Models . Jobs . StartConditions ;
55using System ;
6+ using System . Collections . Generic ;
67using System . IO ;
78using System . Reflection ;
89using System . Runtime . CompilerServices ;
@@ -159,6 +160,54 @@ public async Task Start_AfterCompletedRun_RestartsFromBeginning()
159160 Assert . Equal ( 0 , job . Skip ) ;
160161 }
161162
163+ [ Fact ]
164+ public async Task GetHitsSnapshot_DuringConcurrentWrites_DoesNotThrow ( )
165+ {
166+ var job = CreateJob ( ) ;
167+ var hits = job . Hits ;
168+ var hitsLock = typeof ( MultiRunJob )
169+ . GetField ( "hitsLock" , BindingFlags . Instance | BindingFlags . NonPublic ) !
170+ . GetValue ( job ) ! ;
171+ using var cts = new CancellationTokenSource ( ) ;
172+
173+ var writer = Task . Run ( async ( ) =>
174+ {
175+ var index = 0 ;
176+
177+ while ( ! cts . Token . IsCancellationRequested )
178+ {
179+ lock ( hitsLock )
180+ {
181+ hits . Add ( CreateHit ( index ++ ) ) ;
182+ }
183+
184+ await Task . Yield ( ) ;
185+ }
186+ } , TestCancellationToken ) ;
187+
188+ for ( var i = 0 ; i < 200 ; i ++ )
189+ {
190+ var exception = Record . Exception ( ( ) => job . GetHitsSnapshot ( ) ) ;
191+ Assert . Null ( exception ) ;
192+ await Task . Yield ( ) ;
193+ }
194+
195+ await cts . CancelAsync ( ) ;
196+ await writer . WaitAsync ( TestCancellationToken ) ;
197+ }
198+
199+ [ Fact ]
200+ public void FindHit_ReturnsMatchingHit ( )
201+ {
202+ var job = CreateJob ( ) ;
203+ var hit = CreateHit ( 1 ) ;
204+ job . Hits . Add ( hit ) ;
205+
206+ var found = job . FindHit ( hit . Id ) ;
207+
208+ Assert . Same ( hit , found ) ;
209+ }
210+
162211 private static MultiRunJob CreateJob ( )
163212 => new ( CreateSettingsService ( ) , CreatePluginRepository ( ) ) ;
164213
@@ -179,6 +228,27 @@ private static async Task WaitUntilIdleAsync(MultiRunJob job)
179228 Assert . Equal ( JobStatus . Idle , job . Status ) ;
180229 }
181230
231+ private static global ::RuriLib . Models . Hits . Hit CreateHit ( int index )
232+ {
233+ const string wordlistTypeName = "default" ;
234+
235+ return new global ::RuriLib . Models . Hits . Hit
236+ {
237+ Data = new DataLine (
238+ $ "user{ index } :pass{ index } ",
239+ new global ::RuriLib . Models . Environment . WordlistType { Name = wordlistTypeName } ) ,
240+ CapturedData = new Dictionary < string , object > { [ "token" ] = $ "abc{ index } " } ,
241+ Date = DateTime . UtcNow ,
242+ Type = "SUCCESS" ,
243+ Config = new Config
244+ {
245+ Id = $ "cfg-{ index } ",
246+ Metadata = new ConfigMetadata { Name = "Config" , Category = "Cat" }
247+ } ,
248+ DataPool = new TestDataPool ( [ $ "user{ index } :pass{ index } "] , wordlistTypeName )
249+ } ;
250+ }
251+
182252 private sealed class TestDataPool : DataPool
183253 {
184254 public TestDataPool ( )
0 commit comments