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.
Files changed (121) hide show
  1. package/AGENTS.md +63 -0
  2. package/CONTRIBUTORS.md +75 -0
  3. package/LICENSE +21 -0
  4. package/Makefile +131 -0
  5. package/README.md +130 -0
  6. package/bin/cricinfo.js +44 -0
  7. package/cmd/cricinfo/main.go +15 -0
  8. package/go.mod +10 -0
  9. package/go.sum +10 -0
  10. package/internal/app/app.go +11 -0
  11. package/internal/app/app_test.go +122 -0
  12. package/internal/buildinfo/buildinfo.go +16 -0
  13. package/internal/cli/analysis.go +262 -0
  14. package/internal/cli/analysis_test.go +175 -0
  15. package/internal/cli/competitions.go +154 -0
  16. package/internal/cli/competitions_test.go +165 -0
  17. package/internal/cli/leagues.go +297 -0
  18. package/internal/cli/leagues_test.go +194 -0
  19. package/internal/cli/matches.go +403 -0
  20. package/internal/cli/matches_test.go +413 -0
  21. package/internal/cli/players.go +263 -0
  22. package/internal/cli/players_test.go +384 -0
  23. package/internal/cli/root.go +141 -0
  24. package/internal/cli/search.go +119 -0
  25. package/internal/cli/teams.go +214 -0
  26. package/internal/cli/teams_test.go +192 -0
  27. package/internal/cricinfo/analysis.go +1401 -0
  28. package/internal/cricinfo/analysis_phase15_test.go +267 -0
  29. package/internal/cricinfo/client.go +471 -0
  30. package/internal/cricinfo/client_test.go +280 -0
  31. package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
  32. package/internal/cricinfo/competitions.go +405 -0
  33. package/internal/cricinfo/competitions_phase13_test.go +234 -0
  34. package/internal/cricinfo/coverage_ledger.go +122 -0
  35. package/internal/cricinfo/coverage_ledger_test.go +253 -0
  36. package/internal/cricinfo/decode.go +115 -0
  37. package/internal/cricinfo/decode_test.go +100 -0
  38. package/internal/cricinfo/entity_index.go +618 -0
  39. package/internal/cricinfo/entity_index_test.go +175 -0
  40. package/internal/cricinfo/fixture_matrix.go +243 -0
  41. package/internal/cricinfo/fixture_matrix_test.go +49 -0
  42. package/internal/cricinfo/fixtures_test.go +264 -0
  43. package/internal/cricinfo/historical_hydration.go +1641 -0
  44. package/internal/cricinfo/historical_phase14_test.go +542 -0
  45. package/internal/cricinfo/leagues.go +1210 -0
  46. package/internal/cricinfo/leagues_phase12_test.go +324 -0
  47. package/internal/cricinfo/live_leagues_test.go +169 -0
  48. package/internal/cricinfo/live_matches_test.go +203 -0
  49. package/internal/cricinfo/live_matrix_test.go +118 -0
  50. package/internal/cricinfo/live_players_test.go +122 -0
  51. package/internal/cricinfo/live_search_test.go +86 -0
  52. package/internal/cricinfo/live_smoke_test.go +213 -0
  53. package/internal/cricinfo/live_teams_test.go +104 -0
  54. package/internal/cricinfo/matches.go +1508 -0
  55. package/internal/cricinfo/matches_phase7_test.go +207 -0
  56. package/internal/cricinfo/matches_phase9_test.go +253 -0
  57. package/internal/cricinfo/normalize_entities.go +1727 -0
  58. package/internal/cricinfo/normalize_leagues.go +346 -0
  59. package/internal/cricinfo/players.go +1332 -0
  60. package/internal/cricinfo/players_phase10_test.go +174 -0
  61. package/internal/cricinfo/players_phase11_test.go +373 -0
  62. package/internal/cricinfo/render_contract.go +1088 -0
  63. package/internal/cricinfo/render_phase4_test.go +633 -0
  64. package/internal/cricinfo/renderer.go +1689 -0
  65. package/internal/cricinfo/resolver.go +813 -0
  66. package/internal/cricinfo/resolver_test.go +244 -0
  67. package/internal/cricinfo/teams.go +603 -0
  68. package/internal/cricinfo/teams_phase8_test.go +231 -0
  69. package/internal/cricinfo/testdata/fixtures/README.md +43 -0
  70. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
  71. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
  72. package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
  73. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
  74. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
  75. package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
  76. package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
  77. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
  78. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
  79. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
  80. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
  81. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
  82. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
  83. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
  84. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
  85. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
  86. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
  87. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
  88. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
  89. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
  90. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
  91. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
  92. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
  93. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
  94. package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
  95. package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
  96. package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
  97. package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
  98. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
  99. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
  100. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
  101. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
  102. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
  103. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
  104. package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
  105. package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
  106. package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
  107. package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
  108. package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
  109. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
  110. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
  111. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
  112. package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
  113. package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
  114. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
  115. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
  116. package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
  117. package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
  118. package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
  119. package/internal/cricinfo/types.go +54 -0
  120. package/package.json +51 -0
  121. package/scripts/postinstall.js +153 -0
@@ -0,0 +1,213 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "io"
7
+ "net/http"
8
+ "os"
9
+ "strings"
10
+ "sync/atomic"
11
+ "testing"
12
+ "time"
13
+ )
14
+
15
+ const liveSmokeEnv = "CRICINFO_LIVE_SMOKE"
16
+
17
+ func TestLiveSmokeValidatedRoutes(t *testing.T) {
18
+ t.Parallel()
19
+ requireLiveSmoke(t)
20
+
21
+ client, err := NewClient(Config{
22
+ Timeout: 10 * time.Second,
23
+ MaxRetries: 3,
24
+ })
25
+ if err != nil {
26
+ t.Fatalf("NewClient error: %v", err)
27
+ }
28
+
29
+ tests := []struct {
30
+ name string
31
+ ref string
32
+ validate func(t *testing.T, resolved *ResolvedDocument)
33
+ }{
34
+ {
35
+ name: "events",
36
+ ref: "/events",
37
+ validate: func(t *testing.T, resolved *ResolvedDocument) {
38
+ t.Helper()
39
+ page, err := DecodePage[Ref](resolved.Body)
40
+ if err != nil {
41
+ t.Fatalf("DecodePage error: %v", err)
42
+ }
43
+ if page.PageSize == 0 {
44
+ t.Fatalf("expected non-zero page size")
45
+ }
46
+ },
47
+ },
48
+ {
49
+ name: "athlete",
50
+ ref: "/athletes/1361257",
51
+ validate: func(t *testing.T, resolved *ResolvedDocument) {
52
+ t.Helper()
53
+ var payload map[string]any
54
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
55
+ t.Fatalf("unmarshal athlete: %v", err)
56
+ }
57
+ if payload["id"] == nil {
58
+ t.Fatalf("expected athlete id in payload")
59
+ }
60
+ },
61
+ },
62
+ {
63
+ name: "athlete statistics",
64
+ ref: "/athletes/1361257/statistics",
65
+ validate: func(t *testing.T, resolved *ResolvedDocument) {
66
+ t.Helper()
67
+ stats, err := DecodeStatsObject(resolved.Body)
68
+ if err != nil {
69
+ t.Fatalf("DecodeStatsObject error: %v", err)
70
+ }
71
+ if len(stats.Splits) == 0 {
72
+ t.Fatalf("expected non-empty stats splits")
73
+ }
74
+ },
75
+ },
76
+ {
77
+ name: "competition status",
78
+ ref: "/leagues/19138/events/1529474/competitions/1529474/status",
79
+ validate: func(t *testing.T, resolved *ResolvedDocument) {
80
+ t.Helper()
81
+ var payload map[string]any
82
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
83
+ t.Fatalf("unmarshal status payload: %v", err)
84
+ }
85
+ if payload["summary"] == nil && payload["longSummary"] == nil {
86
+ t.Fatalf("expected summary or longSummary in status payload")
87
+ }
88
+ },
89
+ },
90
+ {
91
+ name: "competition detail item",
92
+ ref: "/leagues/19138/events/1529474/competitions/1529474/details/110",
93
+ validate: func(t *testing.T, resolved *ResolvedDocument) {
94
+ t.Helper()
95
+ var payload map[string]any
96
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
97
+ t.Fatalf("unmarshal detail payload: %v", err)
98
+ }
99
+ if payload["text"] == nil && payload["over"] == nil {
100
+ t.Fatalf("expected detail payload fields in response")
101
+ }
102
+ },
103
+ },
104
+ {
105
+ name: "league standings",
106
+ ref: "/leagues/19138/standings",
107
+ validate: func(t *testing.T, resolved *ResolvedDocument) {
108
+ t.Helper()
109
+ if len(resolved.TraversedRef) == 0 {
110
+ t.Fatalf("expected at least one traversed ref")
111
+ }
112
+ if len(strings.TrimSpace(string(resolved.Body))) == 0 {
113
+ t.Fatalf("expected non-empty standings body")
114
+ }
115
+ },
116
+ },
117
+ }
118
+
119
+ for _, tc := range tests {
120
+ tc := tc
121
+ t.Run(tc.name, func(t *testing.T) {
122
+ ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
123
+ defer cancel()
124
+
125
+ resolved, err := client.ResolveRefChain(ctx, tc.ref)
126
+ if err != nil {
127
+ if isLive503(err) {
128
+ t.Skipf("skipping route after persistent 503: %s (%v)", tc.ref, err)
129
+ }
130
+ t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
131
+ }
132
+ if resolved.RequestedRef == "" || resolved.CanonicalRef == "" {
133
+ t.Fatalf("expected requested and canonical refs to be populated")
134
+ }
135
+ tc.validate(t, resolved)
136
+ })
137
+ }
138
+ }
139
+
140
+ func TestLiveSmokeTransient503Retry(t *testing.T) {
141
+ t.Parallel()
142
+ requireLiveSmoke(t)
143
+
144
+ transport := &transient503Transport{
145
+ base: http.DefaultTransport,
146
+ targetPath: "/v2/sports/cricket/events",
147
+ }
148
+
149
+ httpClient := &http.Client{Transport: transport}
150
+ client, err := NewClient(Config{
151
+ HTTPClient: httpClient,
152
+ Timeout: 10 * time.Second,
153
+ MaxRetries: 2,
154
+ RetryJitter: 0,
155
+ RandomFloat64: func() float64 {
156
+ return 0.5
157
+ },
158
+ })
159
+ if err != nil {
160
+ t.Fatalf("NewClient error: %v", err)
161
+ }
162
+
163
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
164
+ defer cancel()
165
+
166
+ doc, err := client.Fetch(ctx, "/events")
167
+ if err != nil {
168
+ t.Fatalf("Fetch /events with transient 503 injection failed: %v", err)
169
+ }
170
+
171
+ if !transport.injected.Load() {
172
+ t.Fatalf("expected synthetic transient 503 to be injected")
173
+ }
174
+ if transport.hits.Load() < 2 {
175
+ t.Fatalf("expected retry attempt after transient 503, got %d hits", transport.hits.Load())
176
+ }
177
+
178
+ if _, err := DecodePage[Ref](doc.Body); err != nil {
179
+ t.Fatalf("DecodePage for /events after retry failed: %v", err)
180
+ }
181
+ }
182
+
183
+ func requireLiveSmoke(t *testing.T) {
184
+ t.Helper()
185
+ if os.Getenv(liveSmokeEnv) != "1" {
186
+ t.Skip("set CRICINFO_LIVE_SMOKE=1 to run live smoke tests")
187
+ }
188
+ }
189
+
190
+ type transient503Transport struct {
191
+ base http.RoundTripper
192
+ targetPath string
193
+ injected atomic.Bool
194
+ hits atomic.Int32
195
+ }
196
+
197
+ func (t *transient503Transport) RoundTrip(req *http.Request) (*http.Response, error) {
198
+ if req.URL.Path == t.targetPath {
199
+ hit := t.hits.Add(1)
200
+ if hit == 1 {
201
+ t.injected.Store(true)
202
+ return &http.Response{
203
+ StatusCode: http.StatusServiceUnavailable,
204
+ Status: "503 Service Unavailable",
205
+ Header: make(http.Header),
206
+ Body: io.NopCloser(strings.NewReader(`{"error":"synthetic transient"}`)),
207
+ Request: req,
208
+ }, nil
209
+ }
210
+ }
211
+
212
+ return t.base.RoundTrip(req)
213
+ }
@@ -0,0 +1,104 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "strings"
7
+ "testing"
8
+ "time"
9
+ )
10
+
11
+ func TestLiveTeamCompetitorRoutes(t *testing.T) {
12
+ t.Parallel()
13
+ requireLiveMatrix(t)
14
+
15
+ client, err := NewClient(Config{
16
+ Timeout: 12 * time.Second,
17
+ MaxRetries: 3,
18
+ })
19
+ if err != nil {
20
+ t.Fatalf("NewClient error: %v", err)
21
+ }
22
+
23
+ routes := []struct {
24
+ name string
25
+ ref string
26
+ keys []string
27
+ }{
28
+ {name: "roster", ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/roster", keys: []string{"entries", "team", "competition"}},
29
+ {name: "leaders", ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/leaders", keys: []string{"categories", "name"}},
30
+ {name: "statistics", ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/statistics", keys: []string{"splits", "team"}},
31
+ {name: "records", ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/records", keys: []string{"items", "count"}},
32
+ {name: "scores", ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/scores", keys: []string{"displayValue", "value"}},
33
+ }
34
+
35
+ for _, tc := range routes {
36
+ tc := tc
37
+ t.Run(tc.name, func(t *testing.T) {
38
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
39
+ defer cancel()
40
+
41
+ resolved, err := client.ResolveRefChain(ctx, tc.ref)
42
+ if err != nil {
43
+ if isLive503(err) {
44
+ t.Skipf("skipping %s after transient 503: %v", tc.name, err)
45
+ }
46
+ t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
47
+ }
48
+
49
+ var payload map[string]any
50
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
51
+ t.Fatalf("unmarshal %s payload: %v", tc.name, err)
52
+ }
53
+
54
+ requireAnyKey(t, payload, tc.keys...)
55
+ })
56
+ }
57
+ }
58
+
59
+ func TestLiveTeamServiceMatchScopeByIDAndAlias(t *testing.T) {
60
+ t.Parallel()
61
+ requireLiveMatrix(t)
62
+
63
+ service, err := NewTeamService(TeamServiceConfig{})
64
+ if err != nil {
65
+ t.Fatalf("NewTeamService error: %v", err)
66
+ }
67
+ defer func() {
68
+ _ = service.Close()
69
+ }()
70
+
71
+ ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
72
+ defer cancel()
73
+
74
+ rosterResult, err := service.Roster(ctx, "789643", TeamLookupOptions{LeagueID: "19138", MatchQuery: "1529474"})
75
+ if err != nil {
76
+ t.Fatalf("TeamService.Roster id error: %v", err)
77
+ }
78
+ if rosterResult.Status == ResultStatusError {
79
+ if rosterResult.Error != nil && rosterResult.Error.StatusCode == 503 {
80
+ t.Skipf("skipping roster service route after persistent 503: %s", rosterResult.Message)
81
+ }
82
+ t.Fatalf("unexpected roster error result: %+v", rosterResult)
83
+ }
84
+ if len(rosterResult.Items) == 0 && hasLive503Warning(rosterResult.Warnings) {
85
+ t.Skipf("skipping roster service after 503 warnings: %v", rosterResult.Warnings)
86
+ }
87
+
88
+ leadersResult, err := service.Leaders(ctx, "Boost Region", TeamLookupOptions{LeagueID: "19138", MatchQuery: "1529474"})
89
+ if err != nil {
90
+ t.Fatalf("TeamService.Leaders alias error: %v", err)
91
+ }
92
+ if leadersResult.Status == ResultStatusError {
93
+ if leadersResult.Error != nil && leadersResult.Error.StatusCode == 503 {
94
+ t.Skipf("skipping leaders service route after persistent 503: %s", leadersResult.Message)
95
+ }
96
+ t.Fatalf("unexpected leaders error result: %+v", leadersResult)
97
+ }
98
+ if leadersResult.Kind != EntityTeamLeaders {
99
+ t.Fatalf("expected leaders kind %q, got %q", EntityTeamLeaders, leadersResult.Kind)
100
+ }
101
+ if leadersResult.Status == ResultStatusEmpty && !strings.Contains(strings.Join(leadersResult.Warnings, " "), "503") {
102
+ t.Fatalf("expected non-empty leaders result for alias lookup")
103
+ }
104
+ }