summaryrefslogtreecommitdiff
path: root/tests/FundLab.Web.Tests/BoundaryTests.fs
blob: 9059cef7014de0e5884ef524f6872aba676deb28 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
namespace FundLab.Web.Tests

open Xunit
open FundLab.Web

module BoundaryTests =

    let someText (value: string) : obj = box { case = "Some"; fields = [| value |] }

    let sampleInstrument code name fundType : Instrument =
        { code = code; name = name; fundType = fundType }

    let sampleObservation navDate nav dailyReturn : NavObservation =
        { navDate = navDate; nav = nav; dailyReturn = dailyReturn }

    [<Fact>]
    let ``decode search converts raw JSON array into F# list with optional fund type`` () =
        let raw =
            {
                instruments =
                    [|
                        { code = "000001"; name = "华夏成长混合"; fundType = someText "混合型" }
                        { code = "110011"; name = "易方达优质精选混合"; fundType = null }
                    |]
            }

        let decoded = Api.decodeSearch raw

        Assert.Equal(2, decoded.Length)
        Assert.Equal("000001", decoded[0].code)
        Assert.Equal("华夏成长混合", decoded[0].name)
        Assert.Equal(Some "混合型", decoded[0].fundType)
        Assert.Equal(None, decoded[1].fundType)

    [<Fact>]
    let ``decode nav tolerates missing daily return at the boundary`` () =
        let raw =
            {
                observations =
                    [|
                        { navDate = "2026-09-01"; nav = "1.2345"; dailyReturn = someText "0.45" }
                        { navDate = "2026-09-02"; nav = "1.2400"; dailyReturn = null }
                    |]
            }

        let decoded = Api.decodeNav raw

        Assert.Equal(2, decoded.Length)
        Assert.Equal(Some "0.45", decoded[0].dailyReturn)
        Assert.Equal(None, decoded[1].dailyReturn)
        Assert.Equal("2026-09-02", decoded[1].navDate)

    [<Fact>]
    let ``stale search completion does not overwrite newer results`` () =
        let sentinel = [ sampleInstrument "999999" "旧结果" None ]
        let model = { init () with searchSeq = 2; searchInFlight = true; searchResults = sentinel }

        let staleModel, _ = update (SearchCompleted(1, [ sampleInstrument "000001" "新结果" None ])) model

        Assert.Equal<Instrument>(sentinel, staleModel.searchResults)
        Assert.True(staleModel.searchInFlight)

        let freshModel, _ = update (SearchCompleted(2, [ sampleInstrument "000001" "新结果" None ])) model

        Assert.Equal(1, freshModel.searchResults.Length)
        Assert.Equal("000001", freshModel.searchResults[0].code)
        Assert.False(freshModel.searchInFlight)

    [<Fact>]
    let ``stale nav failure does not overwrite newer in-flight state`` () =
        let selected = sampleInstrument "000001" "华夏成长混合" (Some "混合型")
        let model = { init () with navSeq = 3; navInFlight = true; selected = Some selected }

        let staleModel, _ = update (NavFailed(2, "过期失败")) model

        Assert.True(staleModel.navInFlight)
        Assert.Equal(None, staleModel.error)

        let freshModel, _ = update (NavFailed(3, "同步失败")) model

        Assert.False(freshModel.navInFlight)
        Assert.Equal(Some "同步失败", freshModel.error)

    [<Fact>]
    let ``changing token clears private results selection and loading state`` () =
        let model =
            { init () with
                token = "old-token"
                searchResults = [ sampleInstrument "000001" "华夏成长混合" (Some "混合型") ]
                selected = Some(sampleInstrument "110011" "易方达优质精选混合" None)
                observations = [ sampleObservation "2026-09-01" "1.2345" (Some "0.45") ]
                searchInFlight = true
                navInFlight = true
                error = Some "旧错误" }

        let updated, _ = update (TokenChanged "new-token") model

        Assert.Equal("new-token", updated.token)
        Assert.Empty(updated.searchResults)
        Assert.Equal(None, updated.selected)
        Assert.Empty(updated.observations)
        Assert.False(updated.searchInFlight)
        Assert.False(updated.navInFlight)
        Assert.Equal(None, updated.error)

    [<Fact>]
    let ``token change invalidates in flight search and nav responses`` () =
        let model = { init () with searchSeq = 1; navSeq = 1 }

        let cleared, _ = update (TokenChanged "new-token") model

        Assert.Equal(2, cleared.searchSeq)
        Assert.Equal(2, cleared.navSeq)

        let staleSearchModel, _ =
            update (SearchCompleted(1, [ sampleInstrument "000001" "过期结果" (Some "混合型") ])) cleared

        Assert.Empty(staleSearchModel.searchResults)

        let staleSearchFailureModel, _ = update (SearchFailed(1, "过期搜索失败")) cleared

        Assert.Equal(None, staleSearchFailureModel.error)

        let staleNavModel, _ =
            update (NavCompleted(1, [ sampleObservation "2026-09-01" "1.2345" (Some "0.45") ])) cleared

        Assert.Empty(staleNavModel.observations)

        let staleNavFailureModel, _ = update (NavFailed(1, "过期净值失败")) cleared

        Assert.Equal(None, staleNavFailureModel.error)

        let freshNavModel, _ =
            update (NavCompleted(2, [ sampleObservation "2026-09-01" "1.2345" None ])) cleared

        Assert.Equal(1, freshNavModel.observations.Length)

    [<Fact>]
    let ``search failure clears loading and next attempt recovers`` () =
        let model = { init () with token = "qa-token"; query = "华夏"; searchSeq = 1; searchInFlight = true }

        let failedModel, _ = update (SearchFailed(1, "collector exited with code 1")) model

        Assert.False(failedModel.searchInFlight)
        Assert.Equal(Some "collector exited with code 1", failedModel.error)

        let retryModel, _ = update SearchRequested failedModel

        Assert.True(retryModel.searchInFlight)
        Assert.Equal(None, retryModel.error)
        Assert.Equal(2, retryModel.searchSeq)