summaryrefslogtreecommitdiff
path: root/tests/FundLab.Api.Tests/PersistenceTests.fs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/FundLab.Api.Tests/PersistenceTests.fs')
-rw-r--r--tests/FundLab.Api.Tests/PersistenceTests.fs144
1 files changed, 143 insertions, 1 deletions
diff --git a/tests/FundLab.Api.Tests/PersistenceTests.fs b/tests/FundLab.Api.Tests/PersistenceTests.fs
index f319839..5157a9c 100644
--- a/tests/FundLab.Api.Tests/PersistenceTests.fs
+++ b/tests/FundLab.Api.Tests/PersistenceTests.fs
@@ -203,7 +203,15 @@ module PersistenceTestHelpers =
use services = ServiceCollection().AddGiraffe().BuildServiceProvider()
context.RequestServices <- services
context.Request.Method <- method
- context.Request.Path <- PathString(path)
+
+ let queryIndex = path.IndexOf('?')
+
+ if queryIndex >= 0 then
+ context.Request.Path <- PathString(path.Substring(0, queryIndex))
+ context.Request.QueryString <- QueryString(path.Substring(queryIndex))
+ else
+ context.Request.Path <- PathString(path)
+
context.Request.Body <-
new MemoryStream(Encoding.UTF8.GetBytes(body))
:> Stream
@@ -319,6 +327,72 @@ type PersistenceTests(fixture: PostgresFixture) =
value
[<Fact>]
+ member _.``market data survives repository restart with source provenance``() =
+ let code = Random.Shared.Next(0, 1000000).ToString("D6")
+ let collectedAt = DateTimeOffset(2026, 9, 21, 10, 0, 0, TimeSpan.Zero)
+
+ let searchPayload =
+ {
+ Source = "akshare"
+ SourceRevision = "akshare-test/eastmoney"
+ CollectedAt = collectedAt
+ Instruments =
+ [
+ {
+ Code = code
+ Name = "测试基金"
+ FundType = None
+ }
+ ]
+ }
+
+ let navPayload =
+ {
+ Source = "akshare"
+ SourceRevision = "akshare-test/eastmoney"
+ CollectedAt = collectedAt
+ Code = code
+ Observations =
+ [
+ {
+ NavDate = DateOnly(2026, 9, 18)
+ PublishedAt = None
+ Nav = 1.23456789m
+ AccumulatedNav = Some 2.34567890m
+ DailyReturn = Some -0.12m
+ }
+ ]
+ }
+
+ let firstRepository = repository ()
+ firstRepository.UpsertInstruments(searchPayload, "search-hash")
+ firstRepository.UpsertNavObservations(navPayload, "nav-hash")
+
+ let restarted = FundRepository(fixture.ConnectionString)
+
+ match restarted.GetInstrument(code) with
+ | Some instrument ->
+ Assert.Equal(code, instrument.Code)
+ Assert.Equal("测试基金", instrument.Name)
+ Assert.Equal(None, instrument.FundType)
+ Assert.Equal("akshare-test/eastmoney", instrument.SourceRevision)
+ Assert.Equal("search-hash", instrument.SourcePayloadHash)
+ | None ->
+ Assert.Fail("instrument was not readable after repository restart")
+
+ match restarted.GetNav(code, Some(DateOnly(2026, 9, 1)), Some(DateOnly(2026, 9, 30))) with
+ | [ observation ] ->
+ Assert.Equal(code, observation.Code)
+ Assert.Equal(DateOnly(2026, 9, 18), observation.NavDate)
+ Assert.Equal(None, observation.PublishedAt)
+ Assert.Equal(1.23456789m, observation.Nav)
+ Assert.Equal(Some 2.34567890m, observation.AccumulatedNav)
+ Assert.Equal(Some -0.12m, observation.DailyReturn)
+ Assert.Equal("nav-hash", observation.SourcePayloadHash)
+ | observations ->
+ Assert.Fail(sprintf "expected one NAV observation, got %d" observations.Length)
+
+ [<Fact>]
member _.``created fund remains readable after repository restart with exact values``() =
let command =
{
@@ -584,6 +658,74 @@ type PersistenceTests(fixture: PostgresFixture) =
Assert.Contains("\"initialCash\":\"42.10\"", readBody)
[<Fact>]
+ member _.``market data API searches exact codes and returns persisted NAV dates``() =
+ let code = "000001"
+ let collectedAt = DateTimeOffset(2026, 9, 21, 10, 0, 0, TimeSpan.Zero)
+
+ let searchPayload =
+ {
+ Source = "akshare"
+ SourceRevision = "akshare-test/eastmoney"
+ CollectedAt = collectedAt
+ Instruments =
+ [
+ {
+ Code = code
+ Name = "华夏成长混合"
+ FundType = Some "混合型-灵活"
+ }
+ ]
+ }
+
+ let observation =
+ {
+ Code = code
+ NavDate = DateOnly(2026, 9, 18)
+ PublishedAt = None
+ Nav = 1.23456789m
+ AccumulatedNav = None
+ DailyReturn = Some -0.12m
+ Source = "akshare"
+ SourceRevision = "akshare-test/eastmoney"
+ SourceCollectedAt = collectedAt
+ SourcePayloadHash = "nav-hash"
+ FirstSeenAt = collectedAt
+ LastSeenAt = collectedAt
+ }
+
+ let marketData =
+ { new IMarketDataService with
+ member _.Search _ = Ok searchPayload
+ member _.RefreshNav _ = Ok [ observation ]
+ member _.GetNav(_, _, _) = Ok [ observation ] }
+
+ let app = App.createApplicationWithMarketData (repository ()) marketData
+
+ let searchStatus, searchBody =
+ PersistenceTestHelpers.invoke
+ app
+ "GET"
+ "/api/instruments/search?q=000001"
+ [ "Authorization", "Bearer test-token" ]
+ ""
+
+ Assert.Equal(200, searchStatus)
+ Assert.Contains("\"code\":\"000001\"", searchBody)
+ Assert.Contains("华夏成长混合", searchBody)
+
+ let navStatus, navBody =
+ PersistenceTestHelpers.invoke
+ app
+ "POST"
+ "/api/instruments/000001/nav/refresh"
+ [ "Authorization", "Bearer test-token" ]
+ ""
+
+ Assert.Equal(200, navStatus)
+ Assert.Contains("\"navDate\":\"2026-09-18\"", navBody)
+ Assert.Contains("\"source\":\"akshare\"", navBody)
+
+ [<Fact>]
member _.``fund API rejects non-object JSON bodies``() =
let app = App.createApplication (repository ())