public void WhenUserLoginFailsTooFast_ThenLockUserAccount()
{
var seconds = TimeSpan.FromSeconds(1).Ticks;
var events = new EventStream();
// Here we use the test scheduler to simulate time passing by
// because we have a dependency on time because of the Buffer
// method.
var scheduler = new TestScheduler();
var observable = scheduler.CreateColdObservable(
// Two users attempt to log in, 4 times in a row
new Recorded<Notification<LoginFailure>>(10 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 })),
new Recorded<Notification<LoginFailure>>(10 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
new Recorded<Notification<LoginFailure>>(20 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 })),
new Recorded<Notification<LoginFailure>>(20 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
new Recorded<Notification<LoginFailure>>(30 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 })),
new Recorded<Notification<LoginFailure>>(30 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
new Recorded<Notification<LoginFailure>>(40 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 })),
new Recorded<Notification<LoginFailure>>(40 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
// User 2 attems one more time within the 1' window
new Recorded<Notification<LoginFailure>>(45 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
// User 1 pulls out the paper where he wrote his pwd ;), so he takes longer
new Recorded<Notification<LoginFailure>>(75 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 }))
);
// This subscription bridges the scheduler-driven
// observable with our event stream, causing us
// to publish events as they are "raised" by the
// test scheduler.
observable.Subscribe(failure => events.Push(failure));
var query = events.Of<LoginFailure>()
// Sliding windows 1' long, every 10''
.Buffer(TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(10), scheduler)
// From all failure values
.SelectMany(failures => failures
// Group the failures by user
.GroupBy(failure => failure.UserId)
// Only grab those failures with more than 5 in the 1' window
.Where(group => group.Count() >= 5)
// Return the user id that failed to log in
.Select(group => group.Key));
var blocked = new List<int>();
using (var subscription = query.Subscribe(userId => blocked.Add(userId)))
{
// Here we could advance the scheduler half way and test intermediate
// state if needed. We go all the way past the end of our login failures.
scheduler.AdvanceTo(100 * seconds);
}
// We should have only user # 2 in the list.
Assert.False(blocked.Contains(1));
Assert.True(blocked.Contains(2));
}