using Microsoft.VisualStudio.TestTools.UnitTesting; using MarketData.Cache; using MarketData.MarketDataModel; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Moq; using MarketData.Utils; using System.Reflection; namespace MarketDataUnitTests; [TestClass] public class GBPriceCacheTests { private GBPriceCache cache = default!; private Mock pricingMock = default!; private readonly string symbol = "AAPL"; private readonly DateTime today = DateTime.Today; // ------------------------ // Helper to fully reset singleton // ------------------------ private void ResetCacheSingleton() { if (null != cache) cache.Dispose(); FieldInfo field = typeof(GBPriceCache) .GetField("priceCacheInstance", BindingFlags.Static | BindingFlags.NonPublic); if (null != field) field.SetValue(null, null); cache = GBPriceCache.GetInstance(); cache.Clear(); } [TestInitialize] public void Setup() { ResetCacheSingleton(); pricingMock = new Mock(); cache.PricingDataAccess = pricingMock.Object; } [TestCleanup] public void Cleanup() { cache.Dispose(); } // ------------------------ // Singleton Behavior // ------------------------ [TestMethod] public void GetInstance_ReturnsSingleton() { GBPriceCache instance1 = GBPriceCache.GetInstance(); GBPriceCache instance2 = GBPriceCache.GetInstance(); Assert.AreSame(instance1, instance2); } // ------------------------ // Basic GetPrice // ------------------------ [TestMethod] public void GetPrice_ReturnsNull_WhenNoData() { Price nullPrice = default; pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(nullPrice); Price result = cache.GetPrice(symbol, today); Assert.IsNull(result); } [TestMethod] public void GetPrice_CachesResult_WhenDataExists() { Price price = new Price { Symbol = symbol, Date = today }; pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(price); Price first = cache.GetPrice(symbol, today); Price second = cache.GetPrice(symbol, today); Assert.AreSame(first, second); Assert.AreEqual(symbol, first.Symbol); } // ------------------------ // ContainsPrice // ------------------------ [TestMethod] public void ContainsPrice_BehavesCorrectly() { Price price = new Price { Symbol = symbol, Date = today }; pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(price); Assert.IsFalse(cache.ContainsPrice(symbol, today)); cache.GetPrice(symbol, today); Assert.IsTrue(cache.ContainsPrice(symbol, today)); } // ------------------------ // GetPriceOrLatestAvailable // ------------------------ [TestMethod] public void GetPriceOrLatestAvailable_ReturnsLatestPrice() { DateTime earlier = today.AddDays(-1); Price latestPrice = new Price { Symbol = symbol, Date = today }; pricingMock.Setup(x => x.GetLatestDateOnOrBefore(symbol, earlier)).Returns(today); pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(latestPrice); Price result = cache.GetPriceOrLatestAvailable(symbol, earlier); Assert.IsNotNull(result); Assert.AreEqual(today, result.Date); } // ------------------------ // GetPrices (DateTime overload) // ------------------------ [TestMethod] public void GetPrices_DateTimeOverload_ReturnsList() { DateGenerator generator = new DateGenerator(); DateTime later = generator.GetPrevBusinessDay(DateTime.Today); DateTime earlier = later; for (int i = 0; i < 2; i++) earlier = generator.GetPrevBusinessDay(earlier.AddDays(-1)); List allDates = generator.GenerateHistoricalDates(later, 1000); List allPrices = allDates.Select(d => new Price { Symbol = symbol, Date = d }).ToList(); pricingMock.Setup(x => x.GetPrices(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((string s, DateTime max, DateTime min) => { List matched = allPrices.Where(p => p.Date >= min && p.Date <= max).ToList(); return new Prices(matched); }); int dayCount = generator.GenerateHistoricalDates(later, 100) .Where(d => d <= later && d >= earlier) .Count(); Prices prices = cache.GetPrices(symbol, earlier, later); Assert.IsNotNull(prices); Assert.AreEqual(dayCount, prices.Count); Assert.IsTrue(prices.SequenceEqual(prices.OrderByDescending(p => p.Date))); } // ------------------------ // GetPrices (startDate + dayCount) // ------------------------ [TestMethod] public void GetPrices_StartDateDayCount_Works() { int dayCount = 5; List allPrices = new List(); for (int i = 0; i < 100; i++) allPrices.Add(new Price { Symbol = symbol, Date = today.AddDays(-i) }); pricingMock.Setup(x => x.GetPrices(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((string s, DateTime max, DateTime min) => { List matched = allPrices.Where(p => p.Date >= min && p.Date <= max).ToList(); return new Prices(matched); }); Prices prices = cache.GetPrices(symbol, today, dayCount); Assert.AreEqual(dayCount, prices.Count); Assert.IsTrue(prices.SequenceEqual(prices.OrderByDescending(p => p.Date))); } // ------------------------ // Clear // ------------------------ [TestMethod] public void Clear_RemovesAllData() { Price price = new Price { Symbol = symbol, Date = today }; pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(price); cache.GetPrice(symbol, today); cache.Clear(); Assert.IsFalse(cache.ContainsPrice(symbol, today)); } // ------------------------ // ClearCacheOnOrBefore // ------------------------ [TestMethod] public void ClearCacheOnOrBefore_FiltersOldPrices() { Price oldPrice = new Price { Symbol = symbol, Date = today.AddDays(-5) }; Price recentPrice = new Price { Symbol = symbol, Date = today }; pricingMock.Setup(x => x.GetPrice(symbol, oldPrice.Date)).Returns(oldPrice); pricingMock.Setup(x => x.GetPrice(symbol, recentPrice.Date)).Returns(recentPrice); cache.GetPrice(symbol, oldPrice.Date); cache.GetPrice(symbol, recentPrice.Date); cache.ClearCacheOnOrBefore(today.AddDays(-1)); Assert.IsFalse(cache.ContainsPrice(symbol, oldPrice.Date)); Assert.IsTrue(cache.ContainsPrice(symbol, recentPrice.Date)); } // ------------------------ // Concurrency Tests // ------------------------ [TestMethod] public void Concurrent_ReadWrite_DoesNotThrow() { Price price = new Price { Symbol = symbol, Date = today }; pricingMock.Setup(x => x.GetPrice(symbol, It.IsAny())).Returns(price); Parallel.For(0, 100, i => { cache.GetPrice(symbol, today.AddDays(-i)); cache.ContainsPrice(symbol, today); }); Assert.IsTrue(true); } [TestMethod] public void Concurrent_MultipleSymbols_DoesNotThrow() { List symbols = new List { "AAPL", "MSFT", "GOOG" }; Price price = new Price { Symbol = "TEST", Date = today }; pricingMock.Setup(x => x.GetPrice(It.IsAny(), It.IsAny())).Returns(price); Parallel.ForEach(symbols, s => { for (int i = 0; i < 50; i++) { cache.GetPrice(s, today.AddDays(-i)); } }); Assert.IsTrue(true); } // ------------------------ // Dispose // ------------------------ [TestMethod] public void Dispose_CanBeCalledMultipleTimes() { cache.Dispose(); cache.Dispose(); Assert.IsTrue(true); } }