diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 03:25:00 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 03:25:00 +0800 |
| commit | 7dd0e3f42105569b6e6ec02aeeec9b5a8bbd9dd3 (patch) | |
| tree | d7374c1b4bb5308374c3386ac3768ac7209a6440 /tests/FundLab.Api.Tests | |
| parent | 8c01adf05f1428bf2ada49574b25529c064da884 (diff) | |
| download | fund-lab-7dd0e3f42105569b6e6ec02aeeec9b5a8bbd9dd3.tar.gz | |
Add real AKShare stock daily history probe slice (3d-19)
Diffstat (limited to 'tests/FundLab.Api.Tests')
| -rw-r--r-- | tests/FundLab.Api.Tests/BondQuoteProbeTests.fs | 6 | ||||
| -rw-r--r-- | tests/FundLab.Api.Tests/FundLab.Api.Tests.fsproj | 1 | ||||
| -rw-r--r-- | tests/FundLab.Api.Tests/NavDateProbeTests.fs | 10 | ||||
| -rw-r--r-- | tests/FundLab.Api.Tests/NavSeriesProbeTests.fs | 3 | ||||
| -rw-r--r-- | tests/FundLab.Api.Tests/StockDailyProbeTests.fs | 198 | ||||
| -rw-r--r-- | tests/FundLab.Api.Tests/StockQuoteProbeTests.fs | 6 |
6 files changed, 217 insertions, 7 deletions
diff --git a/tests/FundLab.Api.Tests/BondQuoteProbeTests.fs b/tests/FundLab.Api.Tests/BondQuoteProbeTests.fs index c9821dc..bed590a 100644 --- a/tests/FundLab.Api.Tests/BondQuoteProbeTests.fs +++ b/tests/FundLab.Api.Tests/BondQuoteProbeTests.fs @@ -24,7 +24,8 @@ module BondQuoteProbeSupport = member _.Search(_, _) = Ok "" member _.FetchNav(_, _) = Ok "" member _.FetchBondQuote(_, _) = fetch () - member _.FetchStockQuote(_, _) = Ok "" } + member _.FetchStockQuote(_, _) = Ok "" + member _.FetchStockDaily(_, _, _) = Ok "" } let stubProbe (getQuote: string * CancellationToken -> Result<BondQuote, MarketDataFailure>) = { new IBondQuoteProbe with @@ -40,7 +41,8 @@ type BondQuoteProbeTests(fixture: PostgresFixture) = { NavDates = NavDateProbeSupport.stubNavDateProbe NavSeries = NavDateProbeSupport.stubNavSeriesProbe BondQuotes = probe - StockQuotes = NavDateProbeSupport.stubStockQuoteProbe } + StockQuotes = NavDateProbeSupport.stubStockQuoteProbe + StockDaily = NavDateProbeSupport.stubStockDailyProbe } [<Fact>] member _.``probe parses name, price and maturity date``() = diff --git a/tests/FundLab.Api.Tests/FundLab.Api.Tests.fsproj b/tests/FundLab.Api.Tests/FundLab.Api.Tests.fsproj index 755cc1d..83c96bc 100644 --- a/tests/FundLab.Api.Tests/FundLab.Api.Tests.fsproj +++ b/tests/FundLab.Api.Tests/FundLab.Api.Tests.fsproj @@ -33,6 +33,7 @@ <Compile Include="NavSeriesProbeTests.fs" /> <Compile Include="BondQuoteProbeTests.fs" /> <Compile Include="StockQuoteProbeTests.fs" /> + <Compile Include="StockDailyProbeTests.fs" /> <Compile Include="Program.fs" /> </ItemGroup> </Project> diff --git a/tests/FundLab.Api.Tests/NavDateProbeTests.fs b/tests/FundLab.Api.Tests/NavDateProbeTests.fs index cc513d7..12ff84a 100644 --- a/tests/FundLab.Api.Tests/NavDateProbeTests.fs +++ b/tests/FundLab.Api.Tests/NavDateProbeTests.fs @@ -29,7 +29,8 @@ module NavDateProbeSupport = member _.Search(_, _) = Ok "" member _.FetchNav(_, _) = fetch () member _.FetchBondQuote(_, _) = Ok "" - member _.FetchStockQuote(_, _) = Ok "" } + member _.FetchStockQuote(_, _) = Ok "" + member _.FetchStockDaily(_, _, _) = Ok "" } let stubBondQuoteProbe = { new IBondQuoteProbe with @@ -39,6 +40,10 @@ module NavDateProbeSupport = { new IStockQuoteProbe with member _.GetQuote(_, _) = Error(InvalidMarketDataRequest "stock probe not configured for this test") } + let stubStockDailyProbe = + { new IStockDailyProbe with + member _.RecentDaily(_, _, _) = Error(InvalidMarketDataRequest "stock daily probe not configured for this test") } + let stubNavDateProbe = { new INavDateProbe with member _.RecentNavDates(_, _, _) = Ok [] } @@ -99,7 +104,8 @@ type NavDateProbeTests(fixture: PostgresFixture) = { NavDates = probe NavSeries = stubSeriesProbe BondQuotes = NavDateProbeSupport.stubBondQuoteProbe - StockQuotes = NavDateProbeSupport.stubStockQuoteProbe } + StockQuotes = NavDateProbeSupport.stubStockQuoteProbe + StockDaily = NavDateProbeSupport.stubStockDailyProbe } [<Fact>] member _.``probe parses, dedupes, sorts descending and truncates``() = diff --git a/tests/FundLab.Api.Tests/NavSeriesProbeTests.fs b/tests/FundLab.Api.Tests/NavSeriesProbeTests.fs index 81d1733..19a6a50 100644 --- a/tests/FundLab.Api.Tests/NavSeriesProbeTests.fs +++ b/tests/FundLab.Api.Tests/NavSeriesProbeTests.fs @@ -41,7 +41,8 @@ type NavSeriesProbeTests(fixture: PostgresFixture) = { NavDates = stubDateProbe NavSeries = probe BondQuotes = NavDateProbeSupport.stubBondQuoteProbe - StockQuotes = NavDateProbeSupport.stubStockQuoteProbe } + StockQuotes = NavDateProbeSupport.stubStockQuoteProbe + StockDaily = NavDateProbeSupport.stubStockDailyProbe } [<Fact>] member _.``series parses points descending, keeps accumulated nav, drops zero navs and truncates``() = diff --git a/tests/FundLab.Api.Tests/StockDailyProbeTests.fs b/tests/FundLab.Api.Tests/StockDailyProbeTests.fs new file mode 100644 index 0000000..d8f8ee4 --- /dev/null +++ b/tests/FundLab.Api.Tests/StockDailyProbeTests.fs @@ -0,0 +1,198 @@ +namespace FundLab.Api.Tests + +open System +open System.Threading +open Xunit +open FundLab.Api + +module StockDailyProbeSupport = + let payload (code: string) (observations: (string * string * string option * string option) list) = + let body = + observations + |> List.map (fun (date, close, volume, amount) -> + let optionalText = + function + | Some value -> sprintf "\"%s\"" value + | None -> "null" + + sprintf + "{\"date\":\"%s\",\"close\":\"%s\",\"volume\":%s,\"amount\":%s}" + date + close + (optionalText volume) + (optionalText amount)) + |> String.concat "," + + sprintf + "{\"schema_version\":\"fund-lab.akshare.v1\",\"operation\":\"stock-daily\",\"source\":\"akshare\",\"source_revision\":\"akshare-test/eastmoney\",\"collected_at\":\"2026-09-21T08:00:00Z\",\"instrument\":{\"code\":\"%s\"},\"observations\":[%s]}" + code + body + + let collectorReturning (fetch: unit -> Result<string, string>) = + { new IMarketDataCollector with + member _.Search(_, _) = Ok "" + member _.FetchNav(_, _) = Ok "" + member _.FetchBondQuote(_, _) = Ok "" + member _.FetchStockQuote(_, _) = Ok "" + member _.FetchStockDaily(_, _, _) = fetch () } + + let stubProbe (recent: string * int * CancellationToken -> Result<StockDailyObservation list, MarketDataFailure>) = + { new IStockDailyProbe with + member _.RecentDaily(code, days, token) = recent (code, days, token) } + +[<Collection("postgres")>] +type StockDailyProbeTests(fixture: PostgresFixture) = + let repository () = FundRepository(fixture.ConnectionString) + + let appWithDaily (probe: IStockDailyProbe) = + App.createApplicationWithProbes + (repository ()) + { NavDates = NavDateProbeSupport.stubNavDateProbe + NavSeries = NavDateProbeSupport.stubNavSeriesProbe + BondQuotes = NavDateProbeSupport.stubBondQuoteProbe + StockQuotes = NavDateProbeSupport.stubStockQuoteProbe + StockDaily = probe } + + [<Fact>] + member _.``probe decodes observations ascending and de-duplicates dates``() = + let json = + StockDailyProbeSupport.payload + "600519" + [ + "2026-09-21", "1252.57", Some "2501689.0", Some "3135910045.0" + "2026-09-17", "1266.98", Some "1755380.0", Some "2217338283.0" + "2026-09-18", "1257.12", Some "2489087.0", Some "3135849108.0" + "2026-09-17", "1266.98", Some "1755380.0", Some "2217338283.0" + ] + + let probe = + AkshareStockDailyProbe(StockDailyProbeSupport.collectorReturning (fun () -> Ok json)) :> IStockDailyProbe + + match probe.RecentDaily("600519", 5, CancellationToken.None) with + | Ok observations -> + Assert.Equal<DateOnly list>( + [ DateOnly(2026, 9, 17); DateOnly(2026, 9, 18); DateOnly(2026, 9, 21) ], + observations |> List.map (fun observation -> observation.BarDate) + ) + + Assert.Equal(3, List.length observations) + Assert.Equal<decimal option>(Some 3135910045.0m, (observations |> List.last).Amount) + | Error failure -> failwithf "expected daily observations, got %A" failure + + [<Fact>] + member _.``probe rejects a payload whose code disagrees with the request``() = + let json = StockDailyProbeSupport.payload "000001" [ "2026-09-21", "1252.57", None, None ] + + let probe = + AkshareStockDailyProbe(StockDailyProbeSupport.collectorReturning (fun () -> Ok json)) :> IStockDailyProbe + + match probe.RecentDaily("600519", 5, CancellationToken.None) with + | Error(InvalidMarketDataPayload message) -> Assert.Contains("does not match requested code", message) + | other -> failwithf "expected InvalidMarketDataPayload, got %A" other + + [<Fact>] + member _.``probe rejects a malformed stock code without touching the collector``() = + let probe = + AkshareStockDailyProbe(StockDailyProbeSupport.collectorReturning (fun () -> failwith "collector should not be called")) + :> IStockDailyProbe + + match probe.RecentDaily("abc", 5, CancellationToken.None) with + | Error(InvalidMarketDataRequest message) -> Assert.Contains("six digits", message) + | other -> failwithf "expected InvalidMarketDataRequest, got %A" other + + [<Fact>] + member _.``probe surfaces a no valid history collector failure``() = + let probe = + AkshareStockDailyProbe( + StockDailyProbeSupport.collectorReturning (fun () -> Error "AKShare collector failed: no valid daily history for 999999") + ) + :> IStockDailyProbe + + match probe.RecentDaily("999999", 5, CancellationToken.None) with + | Error(MarketDataCollectorUnavailable message) -> Assert.Contains("no valid daily history for 999999", message) + | other -> failwithf "expected MarketDataCollectorUnavailable, got %A" other + + [<Fact>] + member _.``probe maps malformed payloads to an invalid payload reason``() = + let probe = + AkshareStockDailyProbe(StockDailyProbeSupport.collectorReturning (fun () -> Ok "{not-json")) :> IStockDailyProbe + + match probe.RecentDaily("600519", 5, CancellationToken.None) with + | Error(InvalidMarketDataPayload _) -> () + | other -> failwithf "expected InvalidMarketDataPayload, got %A" other + + [<Fact>] + member _.``market stock-daily endpoint returns the probe observations``() = + let probe = + StockDailyProbeSupport.stubProbe (fun (code, days, _) -> + Assert.Equal("600519", code) + Assert.Equal(5, days) + + Ok + [ + { BarDate = DateOnly(2026, 9, 18); Close = 1257.12m; Volume = Some 2489087m; Amount = Some 3135849108m } + { BarDate = DateOnly(2026, 9, 21); Close = 1252.57m; Volume = Some 2501689m; Amount = Some 3135910045m } + ]) + + let status, body = + PersistenceTestHelpers.invoke + (appWithDaily probe) + "GET" + "/api/market/stock-daily?code=600519&days=5" + [ "Authorization", "Bearer test-token" ] + "" + + Assert.Equal(200, status) + Assert.Contains("\"code\":\"600519\"", body) + Assert.Contains("\"date\":\"2026-09-21\"", body) + Assert.Contains("1252.57", body) + + [<Fact>] + member _.``market stock-daily endpoint answers 503 when the probe fails``() = + let probe = + StockDailyProbeSupport.stubProbe (fun _ -> + Error(MarketDataCollectorUnavailable "AKShare collector failed: no valid daily history for 999999")) + + let status, body = + PersistenceTestHelpers.invoke + (appWithDaily probe) + "GET" + "/api/market/stock-daily?code=999999&days=5" + [ "Authorization", "Bearer test-token" ] + "" + + Assert.Equal(503, status) + Assert.Contains("MARKET_DATA_UNAVAILABLE", body) + + [<Fact>] + member _.``market stock-daily endpoint rejects a malformed code with 400``() = + let probe = + StockDailyProbeSupport.stubProbe (fun _ -> + Error(InvalidMarketDataRequest "stock code must contain exactly six digits")) + + let status, body = + PersistenceTestHelpers.invoke + (appWithDaily probe) + "GET" + "/api/market/stock-daily?code=abc&days=5" + [ "Authorization", "Bearer test-token" ] + "" + + Assert.Equal(400, status) + Assert.Contains("INVALID_MARKET_DATA_REQUEST", body) + + [<Fact>] + member _.``market stock-daily endpoint rejects an out-of-range days with 400``() = + let probe = + StockDailyProbeSupport.stubProbe (fun _ -> failwith "probe should not be called for invalid days") + + let status, body = + PersistenceTestHelpers.invoke + (appWithDaily probe) + "GET" + "/api/market/stock-daily?code=600519&days=999" + [ "Authorization", "Bearer test-token" ] + "" + + Assert.Equal(400, status) + Assert.Contains("days must be an integer between 1 and 30", body) diff --git a/tests/FundLab.Api.Tests/StockQuoteProbeTests.fs b/tests/FundLab.Api.Tests/StockQuoteProbeTests.fs index e29c666..2cad557 100644 --- a/tests/FundLab.Api.Tests/StockQuoteProbeTests.fs +++ b/tests/FundLab.Api.Tests/StockQuoteProbeTests.fs @@ -22,7 +22,8 @@ module StockQuoteProbeSupport = member _.Search(_, _) = Ok "" member _.FetchNav(_, _) = Ok "" member _.FetchBondQuote(_, _) = Ok "" - member _.FetchStockQuote(_, _) = fetch () } + member _.FetchStockQuote(_, _) = fetch () + member _.FetchStockDaily(_, _, _) = Ok "" } let stubProbe (getQuote: string * CancellationToken -> Result<StockQuote, MarketDataFailure>) = { new IStockQuoteProbe with @@ -38,7 +39,8 @@ type StockQuoteProbeTests(fixture: PostgresFixture) = { NavDates = NavDateProbeSupport.stubNavDateProbe NavSeries = NavDateProbeSupport.stubNavSeriesProbe BondQuotes = NavDateProbeSupport.stubBondQuoteProbe - StockQuotes = probe } + StockQuotes = probe + StockDaily = NavDateProbeSupport.stubStockDailyProbe } [<Fact>] member _.``probe parses name, price and currency``() = |
