summaryrefslogtreecommitdiff
path: root/tests/FundLab.Api.Tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests/FundLab.Api.Tests')
-rw-r--r--tests/FundLab.Api.Tests/BondQuoteProbeTests.fs6
-rw-r--r--tests/FundLab.Api.Tests/FundLab.Api.Tests.fsproj1
-rw-r--r--tests/FundLab.Api.Tests/NavDateProbeTests.fs10
-rw-r--r--tests/FundLab.Api.Tests/NavSeriesProbeTests.fs3
-rw-r--r--tests/FundLab.Api.Tests/StockDailyProbeTests.fs198
-rw-r--r--tests/FundLab.Api.Tests/StockQuoteProbeTests.fs6
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``() =