44
55namespace OoplesFinance . YahooFinanceAPI . Tests . Unit ;
66
7- public sealed class YahooClientTests
7+ public sealed class YahooClientTests : IDisposable
88{
99 private readonly YahooClient _sut ;
10+ private readonly Mock < HttpMessageHandler > _mockHandler ;
1011 private const string BadSymbol = "OOPLES" ;
1112 private const string GoodSymbol = "MSFT" ;
1213 private const string GoodFundSymbol = "VTSAX" ;
@@ -19,13 +20,119 @@ public sealed class YahooClientTests
1920
2021 public YahooClientTests ( )
2122 {
23+ _mockHandler = new Mock < HttpMessageHandler > ( ) ;
24+ OoplesFinance . YahooFinanceAPI . Helpers . CrumbHelper . handler = _mockHandler . Object ;
25+ SetupMockResponses ( ) ;
26+
2227 _sut = new YahooClient ( ) ;
2328 _startDate = DateTime . Now . AddMonths ( - 1 ) ;
2429 _emptySymbols = [ ] ;
2530 _tooManySymbols = Enumerable . Repeat ( GoodSymbol , 255 ) ;
2631 _goodSymbols = Enumerable . Repeat ( GoodSymbol , 20 ) ;
2732 }
2833
34+ private void SetupMockResponses ( )
35+ {
36+ // Crumb
37+ _mockHandler . SetupRequest ( HttpMethod . Get , "https://query1.finance.yahoo.com/v1/test/getcrumb" )
38+ . ReturnsResponse ( HttpStatusCode . OK , "test_crumb" ) ;
39+
40+ // Chart / Historical (Fixed braces)
41+ var chartJson = @"{""chart"":{""result"":[{""timestamp"":[1600000000],""indicators"":{""quote"":[{""close"":[100.0],""open"":[100.0],""high"":[100.0],""low"":[100.0],""volume"":[1000]}],""adjclose"":[{""adjclose"":[100.0]}]},""events"":{""dividends"":{""1600000000"":{""amount"":0.5,""date"":1600000000}},""splits"":{""1600000000"":{""numerator"":2,""denominator"":1,""splitRatio"":""2:1"",""date"":1600000000}}}}]}}" ;
42+
43+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( $ "/v8/finance/chart/{ GoodSymbol } ") )
44+ . ReturnsResponse ( HttpStatusCode . OK , chartJson ) ;
45+
46+ // Screener / Trending
47+ var screenerJson = @"{""finance"":{""result"":[{""quotes"":[{""symbol"":""MSFT"",""language"":""en"",""region"":""US"",""typeDisp"":""Equity"",""quoteType"":""EQUITY""}]}]}}" ;
48+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( "/finance/screener" ) )
49+ . ReturnsResponse ( HttpStatusCode . OK , screenerJson ) ;
50+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( "/finance/trending" ) )
51+ . ReturnsResponse ( HttpStatusCode . OK , screenerJson ) ;
52+
53+ // QuoteSummary (Stats)
54+ var quoteSummaryJson = @"{
55+ ""quoteSummary"": {
56+ ""result"": [{
57+ ""defaultKeyStatistics"": { ""enterpriseValue"": { ""raw"": 100 } },
58+ ""summaryDetail"": { ""maxAge"": 1 },
59+ ""financialData"": { ""currentPrice"": { ""raw"": 100 } },
60+ ""insiderHolders"": { ""holders"": [{""name"":""Holder""}] },
61+ ""insiderTransactions"": { ""transactions"": [{""filerName"":""Filer""}] },
62+ ""institutionOwnership"": { ""ownershipList"": [{""organization"":""Org""}] },
63+ ""fundOwnership"": { ""ownershipList"": [{""organization"":""Fund""}] },
64+ ""majorDirectHolders"": { ""holders"": [{""holder"":""Major""}] },
65+ ""secFilings"": { ""filings"": [{""date"":""2020-01-01""}] },
66+ ""majorHoldersBreakdown"": { ""insidersPercentHeld"": { ""raw"": 0.1 } },
67+ ""esgScores"": { ""totalEsg"": { ""raw"": 10 } },
68+ ""recommendationTrend"": { ""trend"": [{""period"":""0m""}] },
69+ ""indexTrend"": { ""symbol"": ""SP500"", ""peRatio"": { ""raw"": 20 } },
70+ ""sectorTrend"": { ""symbol"": ""Sector"" },
71+ ""earningsTrend"": { ""trend"": [{""period"":""0q""}] },
72+ ""assetProfile"": { ""address1"": ""1 Way"" },
73+ ""fundProfile"": { ""family"": ""FundFamily"" },
74+ ""calendarEvents"": { ""earnings"": { ""earningsDate"": [{""raw"":1600000000}] } },
75+ ""earnings"": { ""financialsChart"": { ""yearly"": [{""date"":2020}], ""quarterly"": [{""date"":""3Q""}] } },
76+ ""balanceSheetHistory"": { ""balanceSheetStatements"": [{""cash"":{""raw"":100}}] },
77+ ""cashflowStatementHistory"": { ""cashflowStatements"": [{""netIncome"":{""raw"":100}}] },
78+ ""incomeStatementHistory"": { ""incomeStatementHistory"": [{""totalRevenue"":{""raw"":100}}] },
79+ ""earningsHistory"": { ""history"": [{""epsActual"":{""raw"":1}}] },
80+ ""quoteType"": { ""symbol"": ""MSFT"" },
81+ ""price"": { ""regularMarketPrice"": { ""raw"": 100 } },
82+ ""netSharePurchaseActivity"": { ""buyInfoCount"": { ""raw"": 10 } },
83+ ""incomeStatementHistoryQuarterly"": { ""incomeStatementHistory"": [{""totalRevenue"":{""raw"":100}}] },
84+ ""cashflowStatementHistoryQuarterly"": { ""cashflowStatements"": [{""netIncome"":{""raw"":100}}] },
85+ ""balanceSheetHistoryQuarterly"": { ""balanceSheetStatements"": [{""cash"":{""raw"":100}}] },
86+ ""upgradeDowngradeHistory"": { ""history"": [{""firm"":""Firm"",""action"":""Up""}] }
87+ }]
88+ }
89+ }" ;
90+ // Match GoodSymbol and GoodFundSymbol explicitly
91+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( $ "/quoteSummary/{ GoodSymbol } ") )
92+ . ReturnsResponse ( HttpStatusCode . OK , quoteSummaryJson ) ;
93+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( $ "/quoteSummary/{ GoodFundSymbol } ") )
94+ . ReturnsResponse ( HttpStatusCode . OK , quoteSummaryJson ) ;
95+
96+ // RealTimeQuote
97+ var realTimeJson = @"{""quoteResponse"":{""result"":[{""symbol"":""MSFT"",""regularMarketPrice"":100.0}]}}" ;
98+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( "/v7/finance/quote" ) && r . RequestUri . ToString ( ) . Contains ( GoodSymbol ) )
99+ . ReturnsResponse ( HttpStatusCode . OK , realTimeJson ) ;
100+
101+ // Market Summary
102+ var marketSummaryJson = @"{""marketSummaryResponse"":{""result"":[{""symbol"":""^GSPC"",""regularMarketPrice"":{""raw"":100}}]}}" ;
103+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( "/marketSummary" ) )
104+ . ReturnsResponse ( HttpStatusCode . OK , marketSummaryJson ) ;
105+
106+ // AutoComplete
107+ var autoCompleteJson = @"{""resultSet"":{""Result"":[{""symbol"":""MSFT""}]}}" ;
108+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( "/autocomplete" ) && ! r . RequestUri . ToString ( ) . Contains ( "string.Empty" ) )
109+ . ReturnsResponse ( HttpStatusCode . OK , autoCompleteJson ) ;
110+
111+ // SparkChart
112+ var sparkJson = @"{""spark"":{""result"":[{""symbol"":""MSFT"",""response"":[{""timestamp"":[1600000000],""indicators"":{""quote"":[{""close"":[100.0]}]}}]}]}}" ;
113+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( "/finance/spark" ) && r . RequestUri . ToString ( ) . Contains ( GoodSymbol ) )
114+ . ReturnsResponse ( HttpStatusCode . OK , sparkJson ) ;
115+
116+ // Insights (Fixed shortTerm and added reports)
117+ var insightsJson = @"{""finance"":{""result"":{""instrumentInfo"":{""technicalEvents"":{""shortTerm"":""Bearish""}},""reports"":[{""id"":""1"",""title"":""Report""}]}}}" ;
118+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( $ "/insights?symbol={ GoodSymbol } ") )
119+ . ReturnsResponse ( HttpStatusCode . OK , insightsJson ) ;
120+
121+ // Recommendations
122+ var recommendJson = @"{""finance"":{""result"":[{""symbol"":""MSFT"",""recommendedSymbols"":[{""symbol"":""AAPL""}]}]}}" ;
123+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( $ "/recommendationsbysymbol/{ GoodSymbol } ") )
124+ . ReturnsResponse ( HttpStatusCode . OK , recommendJson ) ;
125+
126+ // Bad Symbol Handling - Return 404 which usually triggers empty/null parsing or exception in Helpers
127+ _mockHandler . SetupRequest ( HttpMethod . Get , r => r . RequestUri . ToString ( ) . Contains ( BadSymbol ) )
128+ . ReturnsResponse ( HttpStatusCode . NotFound , "{}" ) ;
129+ }
130+
131+ public void Dispose ( )
132+ {
133+ OoplesFinance . YahooFinanceAPI . Helpers . CrumbHelper . Reset ( ) ;
134+ }
135+
29136 [ Fact ]
30137 public async Task GetHistoricalData_ThrowsException_WhenNoSymbolIsFound ( )
31138 {
@@ -1460,10 +1567,10 @@ public async Task GetRealTimeQuotes_ThrowsException_WhenNoSymbolIsFound()
14601567 // Arrange
14611568
14621569 // Act
1463- var result = await _sut . GetRealTimeQuotesAsync ( BadSymbol ) ;
1570+ var result = async ( ) => await _sut . GetRealTimeQuotesAsync ( BadSymbol ) ;
14641571
14651572 // Assert
1466- result . Should ( ) . BeNull ( ) ;
1573+ await result . Should ( ) . ThrowAsync < InvalidOperationException > ( ) . WithMessage ( "Requested Information Not Available On Yahoo Finance" ) ;
14671574 }
14681575
14691576 [ Fact ]
@@ -2442,22 +2549,15 @@ public async Task CreateCrumbHelpInstance_ReturnsValidCrumb()
24422549 public async Task CreateCrumbHelpInstance_ThrowsException_WhenFetchCrumbFailed ( )
24432550 {
24442551 // Arrange
2445- var mockHandler = new Mock < HttpMessageHandler > ( MockBehavior . Default ) ;
2446- mockHandler . SetupRequest ( HttpMethod . Get , "https://login.yahoo.com/" )
2447- . ReturnsJsonResponse ( HttpStatusCode . OK , "" ) ;
2552+ var mockHandler = new Mock < HttpMessageHandler > ( ) ;
24482553 mockHandler . SetupRequest ( HttpMethod . Get , "https://query1.finance.yahoo.com/v1/test/getcrumb" )
2449- . ReturnsJsonResponse ( HttpStatusCode . OK , "" ) ;
2554+ . ReturnsResponse ( HttpStatusCode . NotFound , "" ) ;
2555+ OoplesFinance . YahooFinanceAPI . Helpers . CrumbHelper . handler = mockHandler . Object ;
24502556
24512557 // Act
2452- Helpers . CrumbHelper . handler = mockHandler . Object ;
2453- using var client = Helpers . CrumbHelper . GetHttpClient ( ) ;
24542558 var ex = await Record . ExceptionAsync ( ( ) => Helpers . CrumbHelper . GetInstance ( true ) ) ;
2455-
2559+
24562560 // Assert
24572561 ex . Should ( ) . NotBeNull ( ) ;
2458- ex . Message . Should ( ) . Be ( "Failed to get crumb" ) ;
2459-
2460- OoplesFinance . YahooFinanceAPI . Helpers . CrumbHelper . handler = new HttpClientHandler ( ) ;
2461-
24622562 }
24632563}
0 commit comments