cricinfo-cli-go 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +63 -0
- package/CONTRIBUTORS.md +75 -0
- package/LICENSE +21 -0
- package/Makefile +131 -0
- package/README.md +130 -0
- package/bin/cricinfo.js +44 -0
- package/cmd/cricinfo/main.go +15 -0
- package/go.mod +10 -0
- package/go.sum +10 -0
- package/internal/app/app.go +11 -0
- package/internal/app/app_test.go +122 -0
- package/internal/buildinfo/buildinfo.go +16 -0
- package/internal/cli/analysis.go +262 -0
- package/internal/cli/analysis_test.go +175 -0
- package/internal/cli/competitions.go +154 -0
- package/internal/cli/competitions_test.go +165 -0
- package/internal/cli/leagues.go +297 -0
- package/internal/cli/leagues_test.go +194 -0
- package/internal/cli/matches.go +403 -0
- package/internal/cli/matches_test.go +413 -0
- package/internal/cli/players.go +263 -0
- package/internal/cli/players_test.go +384 -0
- package/internal/cli/root.go +141 -0
- package/internal/cli/search.go +119 -0
- package/internal/cli/teams.go +214 -0
- package/internal/cli/teams_test.go +192 -0
- package/internal/cricinfo/analysis.go +1401 -0
- package/internal/cricinfo/analysis_phase15_test.go +267 -0
- package/internal/cricinfo/client.go +471 -0
- package/internal/cricinfo/client_test.go +280 -0
- package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
- package/internal/cricinfo/competitions.go +405 -0
- package/internal/cricinfo/competitions_phase13_test.go +234 -0
- package/internal/cricinfo/coverage_ledger.go +122 -0
- package/internal/cricinfo/coverage_ledger_test.go +253 -0
- package/internal/cricinfo/decode.go +115 -0
- package/internal/cricinfo/decode_test.go +100 -0
- package/internal/cricinfo/entity_index.go +618 -0
- package/internal/cricinfo/entity_index_test.go +175 -0
- package/internal/cricinfo/fixture_matrix.go +243 -0
- package/internal/cricinfo/fixture_matrix_test.go +49 -0
- package/internal/cricinfo/fixtures_test.go +264 -0
- package/internal/cricinfo/historical_hydration.go +1641 -0
- package/internal/cricinfo/historical_phase14_test.go +542 -0
- package/internal/cricinfo/leagues.go +1210 -0
- package/internal/cricinfo/leagues_phase12_test.go +324 -0
- package/internal/cricinfo/live_leagues_test.go +169 -0
- package/internal/cricinfo/live_matches_test.go +203 -0
- package/internal/cricinfo/live_matrix_test.go +118 -0
- package/internal/cricinfo/live_players_test.go +122 -0
- package/internal/cricinfo/live_search_test.go +86 -0
- package/internal/cricinfo/live_smoke_test.go +213 -0
- package/internal/cricinfo/live_teams_test.go +104 -0
- package/internal/cricinfo/matches.go +1508 -0
- package/internal/cricinfo/matches_phase7_test.go +207 -0
- package/internal/cricinfo/matches_phase9_test.go +253 -0
- package/internal/cricinfo/normalize_entities.go +1727 -0
- package/internal/cricinfo/normalize_leagues.go +346 -0
- package/internal/cricinfo/players.go +1332 -0
- package/internal/cricinfo/players_phase10_test.go +174 -0
- package/internal/cricinfo/players_phase11_test.go +373 -0
- package/internal/cricinfo/render_contract.go +1088 -0
- package/internal/cricinfo/render_phase4_test.go +633 -0
- package/internal/cricinfo/renderer.go +1689 -0
- package/internal/cricinfo/resolver.go +813 -0
- package/internal/cricinfo/resolver_test.go +244 -0
- package/internal/cricinfo/teams.go +603 -0
- package/internal/cricinfo/teams_phase8_test.go +231 -0
- package/internal/cricinfo/testdata/fixtures/README.md +43 -0
- package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
- package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
- package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
- package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
- package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
- package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
- package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
- package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
- package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
- package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
- package/internal/cricinfo/types.go +54 -0
- package/package.json +51 -0
- package/scripts/postinstall.js +153 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
package cricinfo
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
"time"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func TestLiveMatchesTraversalFromEvents(t *testing.T) {
|
|
12
|
+
t.Parallel()
|
|
13
|
+
requireLiveMatrix(t)
|
|
14
|
+
|
|
15
|
+
service, err := NewMatchService(MatchServiceConfig{})
|
|
16
|
+
if err != nil {
|
|
17
|
+
t.Fatalf("NewMatchService error: %v", err)
|
|
18
|
+
}
|
|
19
|
+
defer func() {
|
|
20
|
+
_ = service.Close()
|
|
21
|
+
}()
|
|
22
|
+
|
|
23
|
+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
24
|
+
defer cancel()
|
|
25
|
+
|
|
26
|
+
result, err := service.List(ctx, MatchListOptions{Limit: 5})
|
|
27
|
+
if err != nil {
|
|
28
|
+
t.Fatalf("MatchService.List error: %v", err)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if result.Status == ResultStatusError {
|
|
32
|
+
if result.Error != nil && result.Error.StatusCode == 503 {
|
|
33
|
+
t.Skipf("skipping live traversal after persistent 503: %s", result.Message)
|
|
34
|
+
}
|
|
35
|
+
t.Fatalf("unexpected error result: %+v", result)
|
|
36
|
+
}
|
|
37
|
+
if !strings.Contains(result.RequestedRef, "/events") {
|
|
38
|
+
t.Fatalf("expected list traversal to begin from /events, requestedRef=%q", result.RequestedRef)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if len(result.Items) == 0 {
|
|
42
|
+
if hasLive503Warning(result.Warnings) {
|
|
43
|
+
t.Skipf("skipping after transient 503 warnings: %v", result.Warnings)
|
|
44
|
+
}
|
|
45
|
+
t.Fatalf("expected at least one match from /events traversal")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
first, ok := result.Items[0].(Match)
|
|
49
|
+
if !ok {
|
|
50
|
+
t.Fatalf("expected first result item to be Match, got %T", result.Items[0])
|
|
51
|
+
}
|
|
52
|
+
if first.LeagueID == "" || first.EventID == "" || first.CompetitionID == "" {
|
|
53
|
+
t.Fatalf("expected match IDs for drill-down use, got %+v", first)
|
|
54
|
+
}
|
|
55
|
+
if len(first.Teams) == 0 {
|
|
56
|
+
t.Fatalf("expected match teams in live traversal output")
|
|
57
|
+
}
|
|
58
|
+
if strings.TrimSpace(first.Date) == "" {
|
|
59
|
+
t.Fatalf("expected match date in live traversal output")
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func TestLiveMatchDrillDownRoutes(t *testing.T) {
|
|
64
|
+
t.Parallel()
|
|
65
|
+
requireLiveMatrix(t)
|
|
66
|
+
|
|
67
|
+
client, err := NewClient(Config{
|
|
68
|
+
Timeout: 12 * time.Second,
|
|
69
|
+
MaxRetries: 3,
|
|
70
|
+
})
|
|
71
|
+
if err != nil {
|
|
72
|
+
t.Fatalf("NewClient error: %v", err)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
routes := []struct {
|
|
76
|
+
name string
|
|
77
|
+
ref string
|
|
78
|
+
}{
|
|
79
|
+
{name: "details", ref: "/leagues/19138/events/1529474/competitions/1529474/details"},
|
|
80
|
+
{name: "plays", ref: "/leagues/19138/events/1529474/competitions/1529474/plays"},
|
|
81
|
+
{name: "matchcards", ref: "/leagues/11132/events/1527966/competitions/1527966/matchcards"},
|
|
82
|
+
{name: "situation", ref: "/leagues/19138/events/1529474/competitions/1529474/situation"},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for _, tc := range routes {
|
|
86
|
+
tc := tc
|
|
87
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
88
|
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
89
|
+
defer cancel()
|
|
90
|
+
|
|
91
|
+
resolved, err := client.ResolveRefChain(ctx, tc.ref)
|
|
92
|
+
if err != nil {
|
|
93
|
+
if isLive503(err) {
|
|
94
|
+
t.Skipf("skipping %s after transient 503: %v", tc.name, err)
|
|
95
|
+
}
|
|
96
|
+
t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
switch tc.name {
|
|
100
|
+
case "details", "plays":
|
|
101
|
+
page, err := DecodePage[Ref](resolved.Body)
|
|
102
|
+
if err != nil {
|
|
103
|
+
t.Fatalf("DecodePage %s error: %v", tc.name, err)
|
|
104
|
+
}
|
|
105
|
+
if len(page.Items) == 0 {
|
|
106
|
+
t.Fatalf("expected at least one %s item ref", tc.name)
|
|
107
|
+
}
|
|
108
|
+
case "matchcards":
|
|
109
|
+
var payload map[string]any
|
|
110
|
+
if err := json.Unmarshal(resolved.Body, &payload); err != nil {
|
|
111
|
+
t.Fatalf("unmarshal matchcards payload: %v", err)
|
|
112
|
+
}
|
|
113
|
+
items := mapSliceField(payload, "items")
|
|
114
|
+
if len(items) == 0 {
|
|
115
|
+
t.Fatalf("expected matchcards items")
|
|
116
|
+
}
|
|
117
|
+
headlines := map[string]bool{}
|
|
118
|
+
for _, item := range items {
|
|
119
|
+
headline := strings.ToLower(strings.TrimSpace(stringField(item, "headline")))
|
|
120
|
+
if headline != "" {
|
|
121
|
+
headlines[headline] = true
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if !headlines["batting"] || !headlines["bowling"] {
|
|
125
|
+
t.Fatalf("expected matchcards to include batting and bowling sections, got %v", headlines)
|
|
126
|
+
}
|
|
127
|
+
case "situation":
|
|
128
|
+
var payload map[string]any
|
|
129
|
+
if err := json.Unmarshal(resolved.Body, &payload); err != nil {
|
|
130
|
+
t.Fatalf("unmarshal situation payload: %v", err)
|
|
131
|
+
}
|
|
132
|
+
if len(payload) == 0 {
|
|
133
|
+
t.Fatalf("expected non-empty situation payload")
|
|
134
|
+
}
|
|
135
|
+
if _, ok := payload["$ref"]; !ok {
|
|
136
|
+
t.Fatalf("expected situation payload to include $ref")
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func TestLiveMatchInningsDepthRoutes(t *testing.T) {
|
|
144
|
+
t.Parallel()
|
|
145
|
+
requireLiveMatrix(t)
|
|
146
|
+
|
|
147
|
+
client, err := NewClient(Config{
|
|
148
|
+
Timeout: 12 * time.Second,
|
|
149
|
+
MaxRetries: 3,
|
|
150
|
+
})
|
|
151
|
+
if err != nil {
|
|
152
|
+
t.Fatalf("NewClient error: %v", err)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
routes := []struct {
|
|
156
|
+
name string
|
|
157
|
+
ref string
|
|
158
|
+
keys []string
|
|
159
|
+
}{
|
|
160
|
+
{
|
|
161
|
+
name: "linescores",
|
|
162
|
+
ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores",
|
|
163
|
+
keys: []string{"items", "count"},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "period-statistics",
|
|
167
|
+
ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores/1/1/statistics/0",
|
|
168
|
+
keys: []string{"splits", "team"},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "partnerships",
|
|
172
|
+
ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores/1/1/partnerships",
|
|
173
|
+
keys: []string{"items", "count"},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "fow",
|
|
177
|
+
ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores/1/1/fow",
|
|
178
|
+
keys: []string{"items", "count"},
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for _, tc := range routes {
|
|
183
|
+
tc := tc
|
|
184
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
185
|
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
186
|
+
defer cancel()
|
|
187
|
+
|
|
188
|
+
resolved, err := client.ResolveRefChain(ctx, tc.ref)
|
|
189
|
+
if err != nil {
|
|
190
|
+
if isLive503(err) {
|
|
191
|
+
t.Skipf("skipping %s after transient 503: %v", tc.name, err)
|
|
192
|
+
}
|
|
193
|
+
t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
var payload map[string]any
|
|
197
|
+
if err := json.Unmarshal(resolved.Body, &payload); err != nil {
|
|
198
|
+
t.Fatalf("unmarshal %s payload: %v", tc.name, err)
|
|
199
|
+
}
|
|
200
|
+
requireAnyKey(t, payload, tc.keys...)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package cricinfo
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
9
|
+
"testing"
|
|
10
|
+
"time"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const (
|
|
14
|
+
liveMatrixEnv = "CRICINFO_LIVE_MATRIX"
|
|
15
|
+
liveMatrixFamiliesEnv = "CRICINFO_LIVE_FAMILIES"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func TestLiveFixtureMatrixByFamily(t *testing.T) {
|
|
19
|
+
t.Parallel()
|
|
20
|
+
requireLiveMatrix(t)
|
|
21
|
+
|
|
22
|
+
selected, err := ParseFixtureFamilies(os.Getenv(liveMatrixFamiliesEnv))
|
|
23
|
+
if err != nil {
|
|
24
|
+
t.Fatalf("ParseFixtureFamilies error: %v", err)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
matrix := FixtureMatrix()
|
|
28
|
+
matrix = FilterFixtureMatrixByFamily(matrix, selected)
|
|
29
|
+
matrix = LiveProbeMatrix(matrix)
|
|
30
|
+
|
|
31
|
+
if len(matrix) == 0 {
|
|
32
|
+
t.Fatalf("no live probes selected from fixture matrix")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
requiredFamilies := map[FixtureFamily]struct{}{}
|
|
36
|
+
for _, spec := range matrix {
|
|
37
|
+
requiredFamilies[spec.Family] = struct{}{}
|
|
38
|
+
}
|
|
39
|
+
for family := range selected {
|
|
40
|
+
if _, ok := requiredFamilies[family]; !ok {
|
|
41
|
+
t.Fatalf("selected family %q has no live probe in matrix", family)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
client, err := NewClient(Config{
|
|
46
|
+
Timeout: 12 * time.Second,
|
|
47
|
+
MaxRetries: 3,
|
|
48
|
+
})
|
|
49
|
+
if err != nil {
|
|
50
|
+
t.Fatalf("NewClient error: %v", err)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for _, spec := range matrix {
|
|
54
|
+
spec := spec
|
|
55
|
+
t.Run(fmt.Sprintf("%s/%s", spec.Family, spec.Name), func(t *testing.T) {
|
|
56
|
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
57
|
+
defer cancel()
|
|
58
|
+
|
|
59
|
+
resolved, err := client.ResolveRefChain(ctx, spec.Ref)
|
|
60
|
+
if err != nil {
|
|
61
|
+
if isLive503(err) {
|
|
62
|
+
t.Skipf("skipping after persistent 503 for %s: %v", spec.Ref, err)
|
|
63
|
+
}
|
|
64
|
+
t.Fatalf("ResolveRefChain(%q) error: %v", spec.Ref, err)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var payload map[string]any
|
|
68
|
+
if err := json.Unmarshal(resolved.Body, &payload); err != nil {
|
|
69
|
+
t.Fatalf("unmarshal %q: %v", spec.Ref, err)
|
|
70
|
+
}
|
|
71
|
+
if len(payload) == 0 {
|
|
72
|
+
t.Fatalf("empty payload from %q", spec.Ref)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
validateLiveFamilyPayload(t, spec.Family, payload)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func requireLiveMatrix(t *testing.T) {
|
|
81
|
+
t.Helper()
|
|
82
|
+
if os.Getenv(liveMatrixEnv) != "1" {
|
|
83
|
+
t.Skip("set CRICINFO_LIVE_MATRIX=1 to run live fixture matrix tests")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func isLive503(err error) bool {
|
|
88
|
+
var statusErr *HTTPStatusError
|
|
89
|
+
if errors.As(err, &statusErr) {
|
|
90
|
+
return statusErr.StatusCode == 503
|
|
91
|
+
}
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func validateLiveFamilyPayload(t *testing.T, family FixtureFamily, payload map[string]any) {
|
|
96
|
+
t.Helper()
|
|
97
|
+
|
|
98
|
+
switch family {
|
|
99
|
+
case FixtureFamilyRootDiscovery:
|
|
100
|
+
requireAnyKey(t, payload, "events", "leagues", "items")
|
|
101
|
+
case FixtureFamilyMatchesCompetition:
|
|
102
|
+
requireAnyKey(t, payload, "competitors", "status", "date")
|
|
103
|
+
case FixtureFamilyDetailsPlays:
|
|
104
|
+
requireAnyKey(t, payload, "items", "count")
|
|
105
|
+
case FixtureFamilyTeamCompetitor:
|
|
106
|
+
requireAnyKey(t, payload, "id", "team", "record")
|
|
107
|
+
case FixtureFamilyInningsDepth:
|
|
108
|
+
requireAnyKey(t, payload, "period", "runs", "wickets")
|
|
109
|
+
case FixtureFamilyPlayers:
|
|
110
|
+
requireAnyKey(t, payload, "id", "displayName", "fullName")
|
|
111
|
+
case FixtureFamilyLeagueSeason:
|
|
112
|
+
requireAnyKey(t, payload, "children", "items", "entries")
|
|
113
|
+
case FixtureFamilyAuxCompetitionMeta:
|
|
114
|
+
requireAnyKey(t, payload, "items", "count", "entries")
|
|
115
|
+
default:
|
|
116
|
+
t.Fatalf("unsupported family %q", family)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
package cricinfo
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"testing"
|
|
7
|
+
"time"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestLivePlayerRoutes(t *testing.T) {
|
|
11
|
+
t.Parallel()
|
|
12
|
+
requireLiveMatrix(t)
|
|
13
|
+
|
|
14
|
+
client, err := NewClient(Config{
|
|
15
|
+
Timeout: 12 * time.Second,
|
|
16
|
+
MaxRetries: 3,
|
|
17
|
+
})
|
|
18
|
+
if err != nil {
|
|
19
|
+
t.Fatalf("NewClient error: %v", err)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
routes := []struct {
|
|
23
|
+
name string
|
|
24
|
+
ref string
|
|
25
|
+
keys []string
|
|
26
|
+
}{
|
|
27
|
+
{
|
|
28
|
+
name: "athlete-profile",
|
|
29
|
+
ref: "/athletes/1361257",
|
|
30
|
+
keys: []string{"id", "displayName", "styles", "team"},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "athlete-news",
|
|
34
|
+
ref: "/athletes/253802/news",
|
|
35
|
+
keys: []string{"items", "count"},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "athlete-statistics",
|
|
39
|
+
ref: "/athletes/1361257/statistics",
|
|
40
|
+
keys: []string{"splits", "athlete"},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for _, tc := range routes {
|
|
45
|
+
tc := tc
|
|
46
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
47
|
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
48
|
+
defer cancel()
|
|
49
|
+
|
|
50
|
+
resolved, err := client.ResolveRefChain(ctx, tc.ref)
|
|
51
|
+
if err != nil {
|
|
52
|
+
if isLive503(err) {
|
|
53
|
+
t.Skipf("skipping %s after transient 503: %v", tc.name, err)
|
|
54
|
+
}
|
|
55
|
+
t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var payload map[string]any
|
|
59
|
+
if err := json.Unmarshal(resolved.Body, &payload); err != nil {
|
|
60
|
+
t.Fatalf("unmarshal %s payload: %v", tc.name, err)
|
|
61
|
+
}
|
|
62
|
+
requireAnyKey(t, payload, tc.keys...)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func TestLivePlayerMatchContextRoutes(t *testing.T) {
|
|
68
|
+
t.Parallel()
|
|
69
|
+
requireLiveMatrix(t)
|
|
70
|
+
|
|
71
|
+
client, err := NewClient(Config{
|
|
72
|
+
Timeout: 12 * time.Second,
|
|
73
|
+
MaxRetries: 3,
|
|
74
|
+
})
|
|
75
|
+
if err != nil {
|
|
76
|
+
t.Fatalf("NewClient error: %v", err)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
routes := []struct {
|
|
80
|
+
name string
|
|
81
|
+
ref string
|
|
82
|
+
keys []string
|
|
83
|
+
}{
|
|
84
|
+
{
|
|
85
|
+
name: "roster-player-statistics",
|
|
86
|
+
ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/roster/1361257/statistics/0",
|
|
87
|
+
keys: []string{"splits", "athlete", "competition"},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "roster-player-linescores",
|
|
91
|
+
ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/roster/1361257/linescores",
|
|
92
|
+
keys: []string{"items", "count"},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "roster-player-linescore-statistics",
|
|
96
|
+
ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/roster/1361257/linescores/1/1/statistics/0",
|
|
97
|
+
keys: []string{"splits", "athlete", "competition"},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for _, tc := range routes {
|
|
102
|
+
tc := tc
|
|
103
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
104
|
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
105
|
+
defer cancel()
|
|
106
|
+
|
|
107
|
+
resolved, err := client.ResolveRefChain(ctx, tc.ref)
|
|
108
|
+
if err != nil {
|
|
109
|
+
if isLive503(err) {
|
|
110
|
+
t.Skipf("skipping %s after transient 503: %v", tc.name, err)
|
|
111
|
+
}
|
|
112
|
+
t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
var payload map[string]any
|
|
116
|
+
if err := json.Unmarshal(resolved.Body, &payload); err != nil {
|
|
117
|
+
t.Fatalf("unmarshal %s payload: %v", tc.name, err)
|
|
118
|
+
}
|
|
119
|
+
requireAnyKey(t, payload, tc.keys...)
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package cricinfo
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
"time"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestLiveSearchDiscoveryAcrossEntityFamilies(t *testing.T) {
|
|
11
|
+
t.Parallel()
|
|
12
|
+
requireLiveMatrix(t)
|
|
13
|
+
|
|
14
|
+
resolver, err := NewResolver(ResolverConfig{
|
|
15
|
+
EventSeedTTL: time.Minute,
|
|
16
|
+
MaxEventSeed: 24,
|
|
17
|
+
IndexPath: t.TempDir() + "/live-search-index.json",
|
|
18
|
+
})
|
|
19
|
+
if err != nil {
|
|
20
|
+
t.Fatalf("NewResolver error: %v", err)
|
|
21
|
+
}
|
|
22
|
+
defer func() {
|
|
23
|
+
_ = resolver.Close()
|
|
24
|
+
}()
|
|
25
|
+
|
|
26
|
+
tests := []struct {
|
|
27
|
+
name string
|
|
28
|
+
kind EntityKind
|
|
29
|
+
query string
|
|
30
|
+
opts ResolveOptions
|
|
31
|
+
id string
|
|
32
|
+
}{
|
|
33
|
+
{name: "player", kind: EntityPlayer, query: "1361257", opts: ResolveOptions{Limit: 5}, id: "1361257"},
|
|
34
|
+
{name: "team", kind: EntityTeam, query: "789643", opts: ResolveOptions{Limit: 5}, id: "789643"},
|
|
35
|
+
{name: "league", kind: EntityLeague, query: "19138", opts: ResolveOptions{Limit: 5}, id: "19138"},
|
|
36
|
+
{name: "match", kind: EntityMatch, query: "1529474", opts: ResolveOptions{Limit: 5, LeagueID: "19138"}, id: "1529474"},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for _, tc := range tests {
|
|
40
|
+
tc := tc
|
|
41
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
42
|
+
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
|
43
|
+
defer cancel()
|
|
44
|
+
|
|
45
|
+
result, err := resolver.Search(ctx, tc.kind, tc.query, tc.opts)
|
|
46
|
+
if err != nil {
|
|
47
|
+
if isLiveSearchTransient503(err.Error()) {
|
|
48
|
+
t.Skipf("skipping %s search after transient 503: %v", tc.name, err)
|
|
49
|
+
}
|
|
50
|
+
t.Fatalf("Search error for %s: %v", tc.name, err)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if len(result.Entities) == 0 {
|
|
54
|
+
if hasLive503Warning(result.Warnings) {
|
|
55
|
+
t.Skipf("skipping %s search after transient 503 warnings: %v", tc.name, result.Warnings)
|
|
56
|
+
}
|
|
57
|
+
t.Fatalf("expected at least one %s result for query %q", tc.name, tc.query)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
found := false
|
|
61
|
+
for _, entity := range result.Entities {
|
|
62
|
+
if entity.ID == tc.id {
|
|
63
|
+
found = true
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if !found {
|
|
68
|
+
t.Fatalf("expected %s result set to contain id %s, got %+v", tc.name, tc.id, result.Entities)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func hasLive503Warning(warnings []string) bool {
|
|
75
|
+
for _, warning := range warnings {
|
|
76
|
+
if isLiveSearchTransient503(warning) {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func isLiveSearchTransient503(raw string) bool {
|
|
84
|
+
raw = strings.ToLower(strings.TrimSpace(raw))
|
|
85
|
+
return strings.Contains(raw, "status 503") || strings.Contains(raw, "503 service unavailable")
|
|
86
|
+
}
|