using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Caching; using System.Text; using System.Threading; using System.Threading.Tasks; using KellermanSoftware.CompareNetObjects; using Newtonsoft.Json.Linq; using NUnit.Framework; using Projac.Connector; using Projac.Connector.Testing; namespace KickIt { //Events public class DonationCollectionStarted { public readonly Guid DonationCollectionId; public readonly Guid DonationId; public readonly string ReferralCode; public readonly DateTimeOffset StartDateTime; public DonationCollectionStarted(Guid donationCollectionId, Guid donationId, string referralCode, DateTimeOffset startDateTime) { DonationCollectionId = donationCollectionId; DonationId = donationId; ReferralCode = referralCode; StartDateTime = startDateTime; } } public class DonationToSuspendedCauseAttempted { public readonly Guid DonationCollectionId; public readonly Guid DonationId; public DonationToSuspendedCauseAttempted(Guid donationCollectionId, Guid donationId) { DonationCollectionId = donationCollectionId; DonationId = donationId; } } //Specs [TestFixture] public class DonationProjectionSpecs { [Test] public Task when_a_donation_collection_started() { return MemoryCacheProjection.For(new DonationProjection(MemoryAdditionalDataProvider.Empty)). Given( new DonationCollectionStarted( new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), new Guid("69AB3034-C7DD-4629-A1F8-97AF099C8556"), "BLA-BLA", DateTimeOffset.Parse("2015-12-15 11:14:01.088"))). Expect(new CacheItem( "69AB3034-C7DD-4629-A1F8-97AF099C8556".ToLower(), new DonationDataModel { DonationCollectionId = new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), DonationStartedTime = DateTimeOffset.Parse("2015-12-15 11:14:01.088"), ReferralCode = "BLA-BLA", AdditionalData = null, Invalid = false, Reason = null })); } [Test] public Task when_a_donation_to_a_suspended_cause_was_attempted() { return MemoryCacheProjection.For(new DonationProjection(MemoryAdditionalDataProvider.Empty)). Given( new DonationCollectionStarted( new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), new Guid("69AB3034-C7DD-4629-A1F8-97AF099C8556"), "BLA-BLA", DateTimeOffset.Parse("2015-12-15 11:14:01.088")), new DonationToSuspendedCauseAttempted( new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), new Guid("69AB3034-C7DD-4629-A1F8-97AF099C8556"))). Expect(new CacheItem( "69AB3034-C7DD-4629-A1F8-97AF099C8556".ToLower(), new DonationDataModel { DonationCollectionId = new Guid("438AEA16-54A2-4C76-B82E-97563C475D5E"), DonationStartedTime = DateTimeOffset.Parse("2015-12-15 11:14:01.088"), ReferralCode = "BLA-BLA", AdditionalData = null, Invalid = true, Reason = "Can't donate to a suspended cause" })); } } //Data Model public class DonationDataModel { public Guid DonationCollectionId { get; set; } public string ReferralCode { get; set; } public DateTimeOffset DonationStartedTime { get; set; } public string AdditionalData { get; set; } public bool Invalid { get; set; } public string Reason { get; set; } } //Projection public class DonationProjection : SyncConnectedProjection { public DonationProjection(IAdditionalDataProvider additionalDataProvider) { When( async (cache, message) => { var additionalData = await additionalDataProvider.GetAsync(message.DonationCollectionId); cache.Add( new CacheItem( message.DonationId.ToString(), new DonationDataModel { DonationCollectionId = message.DonationCollectionId, DonationStartedTime = message.StartDateTime, ReferralCode = message.ReferralCode, AdditionalData = additionalData, Invalid = false, Reason = null }), new CacheItemPolicy { AbsoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration }); }); WhenSync( (cache, message) => { var item = cache.GetCacheItem(message.DonationId.ToString()); if (item != null) { var model = (DonationDataModel)item.Value; model.Invalid = true; model.Reason = "Can't donate to a suspended cause"; } }); } } //Services public interface IAdditionalDataProvider { Task GetAsync(Guid donationId); } public class MemoryAdditionalDataProvider : IAdditionalDataProvider { public static readonly IAdditionalDataProvider Empty = new MemoryAdditionalDataProvider(new Dictionary()); private readonly IReadOnlyDictionary _store; public MemoryAdditionalDataProvider(IReadOnlyDictionary store) { if (store == null) throw new ArgumentNullException("store"); _store = store; } public Task GetAsync(Guid donationId) { string data; return Task.FromResult(_store.TryGetValue(donationId, out data) ? data : null); } } //Infrastructure public abstract class SyncConnectedProjection : ConnectedProjection { protected void WhenSync(Action handler) { When((connection, message) => { handler(connection, message); return Task.FromResult(null); }); } } public static class MemoryCacheProjection { public static ConnectedProjectionScenario For(ConnectedProjectionHandler[] handlers) { return new ConnectedProjectionScenario( ConcurrentResolve.WhenEqualToHandlerMessageType(handlers) ); } public static Task ExpectNone(this ConnectedProjectionScenario scenario) { return scenario. Verify(cache => { if (cache.GetCount() != 0) { return Task.FromResult( VerificationResult.Fail( string.Format("Expected no cache items, but found {0} cache item(s) ({1}).", cache.GetCount(), string.Join(",", cache.Select(pair => pair.Key))))); } return Task.FromResult(VerificationResult.Pass()); }). Assert(); } public static async Task Assert(this ConnectedProjectionTestSpecification specification) { if (specification == null) throw new ArgumentNullException("specification"); using (var cache = new MemoryCache(new Random().Next().ToString())) { await new ConnectedProjector(specification.Resolver). ProjectAsync(cache, specification.Messages); var result = await specification.Verification(cache, CancellationToken.None); if (result.Failed) { throw new AssertionException(result.Message); } } } public static Task Expect(this ConnectedProjectionScenario scenario, params CacheItem[] items) { if (items == null) throw new ArgumentNullException("items"); if (items.Length == 0) { return scenario.ExpectNone(); } return scenario. Verify(cache => { if (cache.GetCount() != items.Length) { if (cache.GetCount() == 0) { return Task.FromResult( VerificationResult.Fail( string.Format("Expected {0} cache item(s), but found 0 cache items.", items.Length))); } return Task.FromResult( VerificationResult.Fail( string.Format("Expected {0} cache item(s), but found {1} cache item(s) ({2}).", items.Length, cache.GetCount(), string.Join(",", cache.Select(pair => pair.Key))))); } if (!cache.Select(pair => cache.GetCacheItem(pair.Key)).SequenceEqual(items, new CacheItemEqualityComparer())) { var builder = new StringBuilder(); builder.AppendLine("Expected the following cache items:"); foreach (var expectedItem in items) { builder.AppendLine(expectedItem.Key + ": " + JToken.FromObject(expectedItem.Value).ToString()); } builder.AppendLine(); builder.AppendLine("But found the following cache items:"); foreach (var actualItem in cache) { builder.AppendLine(actualItem.Key + ": " + JToken.FromObject(actualItem.Value).ToString()); } return Task.FromResult(VerificationResult.Fail(builder.ToString())); } return Task.FromResult(VerificationResult.Pass()); }). Assert(); } } public class CacheItemEqualityComparer : IEqualityComparer { public bool Equals(CacheItem x, CacheItem y) { if (x == null && y == null) return true; if (x == null || y == null) return false; return x.Key.Equals(y.Key) && new CompareLogic().Compare(x.Value, y.Value).AreEqual; } public int GetHashCode(CacheItem obj) { throw new NotSupportedException(); } } }