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,633 @@
|
|
|
1
|
+
package cricinfo
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestNormalizeCoreEntitiesFromFixtures(t *testing.T) {
|
|
13
|
+
t.Parallel()
|
|
14
|
+
|
|
15
|
+
competition := mustReadFixtureFile(t, "matches-competitions/competition.json")
|
|
16
|
+
match, err := NormalizeMatch(competition)
|
|
17
|
+
if err != nil {
|
|
18
|
+
t.Fatalf("NormalizeMatch error: %v", err)
|
|
19
|
+
}
|
|
20
|
+
assertJSONHasKeys(t, match, "id", "leagueId", "eventId", "competitionId", "teams")
|
|
21
|
+
if len(match.Teams) != 2 {
|
|
22
|
+
t.Fatalf("expected 2 teams, got %d", len(match.Teams))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
athlete := mustReadFixtureFile(t, "players/athlete-1361257.json")
|
|
26
|
+
player, err := NormalizePlayer(athlete)
|
|
27
|
+
if err != nil {
|
|
28
|
+
t.Fatalf("NormalizePlayer error: %v", err)
|
|
29
|
+
}
|
|
30
|
+
assertJSONHasKeys(t, player, "id", "displayName", "team", "styles", "majorTeams")
|
|
31
|
+
|
|
32
|
+
competitor := mustReadFixtureFile(t, "team-competitor/competitor-789643.json")
|
|
33
|
+
team, err := NormalizeTeam(competitor)
|
|
34
|
+
if err != nil {
|
|
35
|
+
t.Fatalf("NormalizeTeam error: %v", err)
|
|
36
|
+
}
|
|
37
|
+
assertJSONHasKeys(t, team, "id", "homeAway", "rosterRef")
|
|
38
|
+
|
|
39
|
+
rosterBody := mustReadFixtureFile(t, "team-competitor/roster-1147772.json")
|
|
40
|
+
rosterEntries, err := NormalizeTeamRosterEntries(rosterBody, Team{ID: "1147772"}, TeamScopeMatch, "1475396")
|
|
41
|
+
if err != nil {
|
|
42
|
+
t.Fatalf("NormalizeTeamRosterEntries array error: %v", err)
|
|
43
|
+
}
|
|
44
|
+
if len(rosterEntries) == 0 {
|
|
45
|
+
t.Fatalf("expected roster entries from array-shaped roster fixture")
|
|
46
|
+
}
|
|
47
|
+
assertJSONHasKeys(t, rosterEntries[0], "playerId", "playerRef", "linescoresRef")
|
|
48
|
+
|
|
49
|
+
rosterObjectBody := mustReadFixtureFile(t, "team-competitor/roster-1147772-object.json")
|
|
50
|
+
rosterObjectEntries, err := NormalizeTeamRosterEntries(rosterObjectBody, Team{ID: "1147772"}, TeamScopeMatch, "1475396")
|
|
51
|
+
if err != nil {
|
|
52
|
+
t.Fatalf("NormalizeTeamRosterEntries object error: %v", err)
|
|
53
|
+
}
|
|
54
|
+
if len(rosterObjectEntries) == 0 {
|
|
55
|
+
t.Fatalf("expected roster entries from object-shaped roster fixture")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
root := mustReadFixtureFile(t, "root-discovery/root.json")
|
|
59
|
+
league, err := NormalizeLeague(root)
|
|
60
|
+
if err != nil {
|
|
61
|
+
t.Fatalf("NormalizeLeague error: %v", err)
|
|
62
|
+
}
|
|
63
|
+
assertJSONHasKeys(t, league, "id", "name", "slug")
|
|
64
|
+
|
|
65
|
+
seasonsBody := mustReadFixtureFile(t, "leagues-seasons-standings/seasons.json")
|
|
66
|
+
seasons, err := NormalizeSeasonList(seasonsBody)
|
|
67
|
+
if err != nil {
|
|
68
|
+
t.Fatalf("NormalizeSeasonList error: %v", err)
|
|
69
|
+
}
|
|
70
|
+
if len(seasons) == 0 {
|
|
71
|
+
t.Fatal("expected seasons")
|
|
72
|
+
}
|
|
73
|
+
assertJSONHasKeys(t, seasons[0], "id", "leagueId", "year")
|
|
74
|
+
|
|
75
|
+
standingsBody := mustReadFixtureFile(t, "leagues-seasons-standings/standings.json")
|
|
76
|
+
groups, err := NormalizeStandingsGroups(standingsBody)
|
|
77
|
+
if err != nil {
|
|
78
|
+
t.Fatalf("NormalizeStandingsGroups error: %v", err)
|
|
79
|
+
}
|
|
80
|
+
if len(groups) == 0 {
|
|
81
|
+
t.Fatal("expected standings groups")
|
|
82
|
+
}
|
|
83
|
+
assertJSONHasKeys(t, groups[0], "id", "seasonId")
|
|
84
|
+
|
|
85
|
+
inningsBody := mustReadFixtureFile(t, "innings-fow-partnerships/innings-1-2.json")
|
|
86
|
+
innings, err := NormalizeInnings(inningsBody)
|
|
87
|
+
if err != nil {
|
|
88
|
+
t.Fatalf("NormalizeInnings error: %v", err)
|
|
89
|
+
}
|
|
90
|
+
assertJSONHasKeys(t, innings, "id", "period", "runs", "wickets", "partnershipsRef", "fallOfWicketRef")
|
|
91
|
+
|
|
92
|
+
detailBody := mustReadFixtureFile(t, "details-plays/detail-110.json")
|
|
93
|
+
delivery, err := NormalizeDeliveryEvent(detailBody)
|
|
94
|
+
if err != nil {
|
|
95
|
+
t.Fatalf("NormalizeDeliveryEvent error: %v", err)
|
|
96
|
+
}
|
|
97
|
+
assertJSONHasKeys(t, delivery, "id", "period", "overNumber", "ballNumber", "scoreValue", "batsmanRef", "bowlerRef", "dismissal", "playType", "bbbTimestamp", "xCoordinate", "yCoordinate")
|
|
98
|
+
|
|
99
|
+
matchcardsBody := mustReadFixtureFile(t, "matches-competitions/matchcards-1527966.json")
|
|
100
|
+
scorecard, err := NormalizeMatchScorecard(matchcardsBody, *match)
|
|
101
|
+
if err != nil {
|
|
102
|
+
t.Fatalf("NormalizeMatchScorecard error: %v", err)
|
|
103
|
+
}
|
|
104
|
+
assertJSONHasKeys(t, scorecard, "matchId", "battingCards", "bowlingCards", "partnershipCards")
|
|
105
|
+
if len(scorecard.BattingCards) == 0 {
|
|
106
|
+
t.Fatalf("expected batting cards in scorecard fixture")
|
|
107
|
+
}
|
|
108
|
+
if len(scorecard.BowlingCards) == 0 {
|
|
109
|
+
t.Fatalf("expected bowling cards in scorecard fixture")
|
|
110
|
+
}
|
|
111
|
+
if len(scorecard.PartnershipCards) == 0 {
|
|
112
|
+
t.Fatalf("expected partnerships cards in scorecard fixture")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
situationBody := mustReadFixtureFile(t, "matches-competitions/situation-1529474.json")
|
|
116
|
+
situation, err := NormalizeMatchSituation(situationBody, *match)
|
|
117
|
+
if err != nil {
|
|
118
|
+
t.Fatalf("NormalizeMatchSituation error: %v", err)
|
|
119
|
+
}
|
|
120
|
+
assertJSONHasKeys(t, situation, "matchId", "oddsRef")
|
|
121
|
+
if !isSparseSituation(situation) {
|
|
122
|
+
t.Fatalf("expected sparse situation fixture to normalize as empty data")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
statsBody := mustReadFixtureFile(t, "players/athlete-1361257-statistics.json")
|
|
126
|
+
playerStats, err := NormalizePlayerStatistics(statsBody)
|
|
127
|
+
if err != nil {
|
|
128
|
+
t.Fatalf("NormalizePlayerStatistics error: %v", err)
|
|
129
|
+
}
|
|
130
|
+
if len(playerStats.Categories) == 0 {
|
|
131
|
+
t.Fatal("expected stat categories")
|
|
132
|
+
}
|
|
133
|
+
assertJSONHasKeys(t, playerStats, "name", "categories")
|
|
134
|
+
assertJSONHasKeys(t, playerStats.Categories[0], "name", "displayName", "stats")
|
|
135
|
+
|
|
136
|
+
scoreBody := mustReadFixtureFile(t, "team-competitor/scores-789643.json")
|
|
137
|
+
score, err := NormalizeTeamScore(scoreBody, Team{ID: "789643"}, TeamScopeMatch, "1529474")
|
|
138
|
+
if err != nil {
|
|
139
|
+
t.Fatalf("NormalizeTeamScore error: %v", err)
|
|
140
|
+
}
|
|
141
|
+
assertJSONHasKeys(t, score, "teamId", "displayValue", "value")
|
|
142
|
+
|
|
143
|
+
leadersBody := mustReadFixtureFile(t, "team-competitor/leaders-789643.json")
|
|
144
|
+
teamLeaders, err := NormalizeTeamLeaders(leadersBody, Team{ID: "789643"}, TeamScopeMatch, "1529474")
|
|
145
|
+
if err != nil {
|
|
146
|
+
t.Fatalf("NormalizeTeamLeaders error: %v", err)
|
|
147
|
+
}
|
|
148
|
+
assertJSONHasKeys(t, teamLeaders, "teamId", "matchId", "categories")
|
|
149
|
+
if len(teamLeaders.Categories) == 0 {
|
|
150
|
+
t.Fatalf("expected team leader categories")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
recordsBody := mustReadFixtureFile(t, "team-competitor/records-789643.json")
|
|
154
|
+
recordCategories, err := NormalizeTeamRecordCategories(recordsBody)
|
|
155
|
+
if err != nil {
|
|
156
|
+
t.Fatalf("NormalizeTeamRecordCategories error: %v", err)
|
|
157
|
+
}
|
|
158
|
+
if len(recordCategories) == 0 {
|
|
159
|
+
t.Fatalf("expected team record categories")
|
|
160
|
+
}
|
|
161
|
+
assertJSONHasKeys(t, recordCategories[0], "name", "displayName", "stats")
|
|
162
|
+
|
|
163
|
+
partnershipBody := mustReadFixtureFile(t, "innings-fow-partnerships/partnerships.json")
|
|
164
|
+
partnerships, err := NormalizePartnerships(partnershipBody)
|
|
165
|
+
if err != nil {
|
|
166
|
+
t.Fatalf("NormalizePartnerships error: %v", err)
|
|
167
|
+
}
|
|
168
|
+
if len(partnerships) == 0 {
|
|
169
|
+
t.Fatal("expected partnerships")
|
|
170
|
+
}
|
|
171
|
+
assertJSONHasKeys(t, partnerships[0], "id", "inningsId", "order")
|
|
172
|
+
|
|
173
|
+
fowBody := mustReadFixtureFile(t, "innings-fow-partnerships/fow.json")
|
|
174
|
+
wickets, err := NormalizeFallOfWickets(fowBody)
|
|
175
|
+
if err != nil {
|
|
176
|
+
t.Fatalf("NormalizeFallOfWickets error: %v", err)
|
|
177
|
+
}
|
|
178
|
+
if len(wickets) == 0 {
|
|
179
|
+
t.Fatal("expected wicket entries")
|
|
180
|
+
}
|
|
181
|
+
assertJSONHasKeys(t, wickets[0], "id", "inningsId", "wicketNumber")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func TestNormalizeExtensionsPreserveLongTailFields(t *testing.T) {
|
|
185
|
+
t.Parallel()
|
|
186
|
+
|
|
187
|
+
competition := mustReadFixtureFile(t, "matches-competitions/competition.json")
|
|
188
|
+
match, err := NormalizeMatch(competition)
|
|
189
|
+
if err != nil {
|
|
190
|
+
t.Fatalf("NormalizeMatch error: %v", err)
|
|
191
|
+
}
|
|
192
|
+
if _, ok := match.Extensions["notes"]; !ok {
|
|
193
|
+
t.Fatalf("expected match extensions to preserve notes, got keys: %v", mapKeys(match.Extensions))
|
|
194
|
+
}
|
|
195
|
+
if _, ok := match.Extensions["class"]; !ok {
|
|
196
|
+
t.Fatalf("expected match extensions to preserve class")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
athlete := mustReadFixtureFile(t, "players/athlete-1361257.json")
|
|
200
|
+
player, err := NormalizePlayer(athlete)
|
|
201
|
+
if err != nil {
|
|
202
|
+
t.Fatalf("NormalizePlayer error: %v", err)
|
|
203
|
+
}
|
|
204
|
+
if _, ok := player.Extensions["links"]; !ok {
|
|
205
|
+
t.Fatalf("expected player extensions to preserve links, got keys: %v", mapKeys(player.Extensions))
|
|
206
|
+
}
|
|
207
|
+
if _, ok := player.Extensions["headshot"]; !ok {
|
|
208
|
+
t.Fatalf("expected player extensions to preserve headshot")
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
detailBody := mustReadFixtureFile(t, "details-plays/detail-110.json")
|
|
212
|
+
delivery, err := NormalizeDeliveryEvent(detailBody)
|
|
213
|
+
if err != nil {
|
|
214
|
+
t.Fatalf("NormalizeDeliveryEvent error: %v", err)
|
|
215
|
+
}
|
|
216
|
+
if _, ok := delivery.Extensions["athletesInvolved"]; !ok {
|
|
217
|
+
t.Fatalf("expected delivery extensions to preserve athletesInvolved, got keys: %v", mapKeys(delivery.Extensions))
|
|
218
|
+
}
|
|
219
|
+
if _, ok := delivery.Extensions["innings"]; !ok {
|
|
220
|
+
t.Fatalf("expected delivery extensions to preserve innings")
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
func TestRenderScorecardFixtureShowsBattingBowlingPartnershipSections(t *testing.T) {
|
|
225
|
+
t.Parallel()
|
|
226
|
+
|
|
227
|
+
competitionBody := mustReadFixtureFile(t, "matches-competitions/competition.json")
|
|
228
|
+
match, err := NormalizeMatch(competitionBody)
|
|
229
|
+
if err != nil {
|
|
230
|
+
t.Fatalf("NormalizeMatch error: %v", err)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
matchcardsBody := mustReadFixtureFile(t, "matches-competitions/matchcards-1527966.json")
|
|
234
|
+
scorecard, err := NormalizeMatchScorecard(matchcardsBody, *match)
|
|
235
|
+
if err != nil {
|
|
236
|
+
t.Fatalf("NormalizeMatchScorecard error: %v", err)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
var buf bytes.Buffer
|
|
240
|
+
if err := Render(&buf, NewDataResult(EntityMatchScorecard, scorecard), RenderOptions{Format: "text"}); err != nil {
|
|
241
|
+
t.Fatalf("Render scorecard text error: %v", err)
|
|
242
|
+
}
|
|
243
|
+
text := buf.String()
|
|
244
|
+
if !strings.Contains(text, "Batting") {
|
|
245
|
+
t.Fatalf("expected Batting section in scorecard output, got %q", text)
|
|
246
|
+
}
|
|
247
|
+
if !strings.Contains(text, "Bowling") {
|
|
248
|
+
t.Fatalf("expected Bowling section in scorecard output, got %q", text)
|
|
249
|
+
}
|
|
250
|
+
if !strings.Contains(text, "Partnerships") {
|
|
251
|
+
t.Fatalf("expected Partnerships section in scorecard output, got %q", text)
|
|
252
|
+
}
|
|
253
|
+
if !strings.Contains(text, "Suman Shrestha") {
|
|
254
|
+
t.Fatalf("expected batting player names in scorecard output, got %q", text)
|
|
255
|
+
}
|
|
256
|
+
if !strings.Contains(text, "KS Airee") {
|
|
257
|
+
t.Fatalf("expected bowling player names in scorecard output, got %q", text)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
func TestRenderDeliveryJSONPreservesAdvancedFields(t *testing.T) {
|
|
262
|
+
t.Parallel()
|
|
263
|
+
|
|
264
|
+
detailBody := mustReadFixtureFile(t, "details-plays/detail-110.json")
|
|
265
|
+
delivery, err := NormalizeDeliveryEvent(detailBody)
|
|
266
|
+
if err != nil {
|
|
267
|
+
t.Fatalf("NormalizeDeliveryEvent error: %v", err)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
result := NewDataResult(EntityDeliveryEvent, delivery)
|
|
271
|
+
var buf bytes.Buffer
|
|
272
|
+
if err := Render(&buf, result, RenderOptions{Format: "json"}); err != nil {
|
|
273
|
+
t.Fatalf("Render delivery json error: %v", err)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
payload := decodeJSONMap(t, buf.Bytes())
|
|
277
|
+
data, ok := payload["data"].(map[string]any)
|
|
278
|
+
if !ok {
|
|
279
|
+
t.Fatalf("expected data object in delivery json output")
|
|
280
|
+
}
|
|
281
|
+
assertMapHasKey(t, data, "dismissal")
|
|
282
|
+
assertMapHasKey(t, data, "playType")
|
|
283
|
+
assertMapHasKey(t, data, "bbbTimestamp")
|
|
284
|
+
assertMapHasKey(t, data, "xCoordinate")
|
|
285
|
+
assertMapHasKey(t, data, "yCoordinate")
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
func TestRenderTeamLeadersFixtureShowsBattingAndBowlingSections(t *testing.T) {
|
|
289
|
+
t.Parallel()
|
|
290
|
+
|
|
291
|
+
leadersBody := mustReadFixtureFile(t, "team-competitor/leaders-789643.json")
|
|
292
|
+
leaders, err := NormalizeTeamLeaders(leadersBody, Team{ID: "789643"}, TeamScopeMatch, "1529474")
|
|
293
|
+
if err != nil {
|
|
294
|
+
t.Fatalf("NormalizeTeamLeaders error: %v", err)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var buf bytes.Buffer
|
|
298
|
+
if err := Render(&buf, NewDataResult(EntityTeamLeaders, leaders), RenderOptions{Format: "text"}); err != nil {
|
|
299
|
+
t.Fatalf("Render team leaders text error: %v", err)
|
|
300
|
+
}
|
|
301
|
+
text := buf.String()
|
|
302
|
+
if !strings.Contains(text, "Batting Leaders") {
|
|
303
|
+
t.Fatalf("expected Batting Leaders section in team leaders output, got %q", text)
|
|
304
|
+
}
|
|
305
|
+
if !strings.Contains(text, "Bowling Leaders") {
|
|
306
|
+
t.Fatalf("expected Bowling Leaders section in team leaders output, got %q", text)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
func TestRenderTextSanitizesTransportWarningsAndAnalysisKeys(t *testing.T) {
|
|
311
|
+
t.Parallel()
|
|
312
|
+
|
|
313
|
+
view := AnalysisView{
|
|
314
|
+
Command: "bowling",
|
|
315
|
+
Metric: "economy",
|
|
316
|
+
Rows: []AnalysisRow{
|
|
317
|
+
{
|
|
318
|
+
Rank: 1,
|
|
319
|
+
Key: "player=http://core.espnuk.org/v2/sports/cricket/athletes/253802",
|
|
320
|
+
Value: 6.25,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
result := NewPartialResult(EntityAnalysisBowl, view, "detail http://core.espnuk.org/v2/sports/cricket/leagues/11132/events/1527689/competitions/1527689/details/1: <p>backend error</p>")
|
|
325
|
+
|
|
326
|
+
var buf bytes.Buffer
|
|
327
|
+
if err := Render(&buf, result, RenderOptions{Format: "text"}); err != nil {
|
|
328
|
+
t.Fatalf("Render analysis text error: %v", err)
|
|
329
|
+
}
|
|
330
|
+
text := buf.String()
|
|
331
|
+
if strings.Contains(text, "http://") || strings.Contains(text, "/v2/sports/cricket") {
|
|
332
|
+
t.Fatalf("expected transport urls to be removed from text output, got %q", text)
|
|
333
|
+
}
|
|
334
|
+
if strings.Contains(text, "<p>") || strings.Contains(text, "</p>") {
|
|
335
|
+
t.Fatalf("expected html tags to be stripped from text output, got %q", text)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func TestRenderTeamRosterSummaryOmitsRawPlayerRef(t *testing.T) {
|
|
340
|
+
t.Parallel()
|
|
341
|
+
|
|
342
|
+
items := []any{
|
|
343
|
+
TeamRosterEntry{
|
|
344
|
+
PlayerID: "253802",
|
|
345
|
+
PlayerRef: "http://core.espnuk.org/v2/sports/cricket/athletes/253802",
|
|
346
|
+
DisplayName: "Virat Kohli",
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
result := NewListResult(EntityTeamRoster, items)
|
|
350
|
+
|
|
351
|
+
var buf bytes.Buffer
|
|
352
|
+
if err := Render(&buf, result, RenderOptions{Format: "text"}); err != nil {
|
|
353
|
+
t.Fatalf("Render team roster text error: %v", err)
|
|
354
|
+
}
|
|
355
|
+
text := buf.String()
|
|
356
|
+
if strings.Contains(text, "/athletes/") || strings.Contains(text, "http://") {
|
|
357
|
+
t.Fatalf("expected roster summary to omit raw player refs, got %q", text)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func TestNormalizeEventAndCompetitionMatchDecoding(t *testing.T) {
|
|
362
|
+
t.Parallel()
|
|
363
|
+
|
|
364
|
+
eventBody := mustReadFixtureFile(t, "matches-competitions/event-1529474.json")
|
|
365
|
+
eventMatches, err := NormalizeMatchesFromEvent(eventBody)
|
|
366
|
+
if err != nil {
|
|
367
|
+
t.Fatalf("NormalizeMatchesFromEvent error: %v", err)
|
|
368
|
+
}
|
|
369
|
+
if len(eventMatches) == 0 {
|
|
370
|
+
t.Fatalf("expected at least one normalized match from event fixture")
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
eventMatch := eventMatches[0]
|
|
374
|
+
assertJSONHasKeys(t, eventMatch, "id", "leagueId", "eventId", "competitionId", "teams", "date", "venueSummary", "statusRef")
|
|
375
|
+
if len(eventMatch.Teams) != 2 {
|
|
376
|
+
t.Fatalf("expected 2 teams in event-normalized match, got %d", len(eventMatch.Teams))
|
|
377
|
+
}
|
|
378
|
+
if strings.TrimSpace(eventMatch.VenueSummary) == "" {
|
|
379
|
+
t.Fatalf("expected venue summary to be populated from event fixture")
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
competitionBody := mustReadFixtureFile(t, "matches-competitions/competition.json")
|
|
383
|
+
competitionMatch, err := NormalizeMatch(competitionBody)
|
|
384
|
+
if err != nil {
|
|
385
|
+
t.Fatalf("NormalizeMatch competition error: %v", err)
|
|
386
|
+
}
|
|
387
|
+
assertJSONHasKeys(t, competitionMatch, "id", "leagueId", "eventId", "competitionId", "teams", "date", "venueSummary", "statusRef")
|
|
388
|
+
if competitionMatch.CompetitionID != "1529474" {
|
|
389
|
+
t.Fatalf("expected competition id 1529474, got %q", competitionMatch.CompetitionID)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
func TestRenderTextGoldenSnapshots(t *testing.T) {
|
|
394
|
+
t.Parallel()
|
|
395
|
+
|
|
396
|
+
competition := mustReadFixtureFile(t, "matches-competitions/competition.json")
|
|
397
|
+
match, err := NormalizeMatch(competition)
|
|
398
|
+
if err != nil {
|
|
399
|
+
t.Fatalf("NormalizeMatch error: %v", err)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
tests := []struct {
|
|
403
|
+
name string
|
|
404
|
+
file string
|
|
405
|
+
result NormalizedResult
|
|
406
|
+
}{
|
|
407
|
+
{
|
|
408
|
+
name: "match-list",
|
|
409
|
+
file: "match-list.golden",
|
|
410
|
+
result: NewListResult(EntityMatch, []any{match}),
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "match-empty",
|
|
414
|
+
file: "match-empty.golden",
|
|
415
|
+
result: NewListResult(EntityMatch, nil),
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "match-partial",
|
|
419
|
+
file: "match-partial.golden",
|
|
420
|
+
result: NewPartialListResult(EntityMatch, []any{match}, "plays endpoint returned pointer-only payload"),
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
for _, tc := range tests {
|
|
425
|
+
tc := tc
|
|
426
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
427
|
+
t.Parallel()
|
|
428
|
+
|
|
429
|
+
var buf bytes.Buffer
|
|
430
|
+
if err := Render(&buf, tc.result, RenderOptions{Format: "text"}); err != nil {
|
|
431
|
+
t.Fatalf("Render text error: %v", err)
|
|
432
|
+
}
|
|
433
|
+
assertGolden(t, tc.file, buf.String())
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
func TestRenderJSONStructureAndAllFieldsToggle(t *testing.T) {
|
|
439
|
+
t.Parallel()
|
|
440
|
+
|
|
441
|
+
competition := mustReadFixtureFile(t, "matches-competitions/competition.json")
|
|
442
|
+
match, err := NormalizeMatch(competition)
|
|
443
|
+
if err != nil {
|
|
444
|
+
t.Fatalf("NormalizeMatch error: %v", err)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
result := NewDataResult(EntityMatch, match)
|
|
448
|
+
|
|
449
|
+
var compact bytes.Buffer
|
|
450
|
+
if err := Render(&compact, result, RenderOptions{Format: "json"}); err != nil {
|
|
451
|
+
t.Fatalf("Render json error: %v", err)
|
|
452
|
+
}
|
|
453
|
+
compactMap := decodeJSONMap(t, compact.Bytes())
|
|
454
|
+
assertMapHasKey(t, compactMap, "kind")
|
|
455
|
+
assertMapHasKey(t, compactMap, "status")
|
|
456
|
+
assertMapHasKey(t, compactMap, "data")
|
|
457
|
+
|
|
458
|
+
dataMap, ok := compactMap["data"].(map[string]any)
|
|
459
|
+
if !ok {
|
|
460
|
+
t.Fatalf("expected data object in json output")
|
|
461
|
+
}
|
|
462
|
+
if _, ok := dataMap["extensions"]; ok {
|
|
463
|
+
t.Fatalf("expected extensions to be omitted without --all-fields")
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
var allFields bytes.Buffer
|
|
467
|
+
if err := Render(&allFields, result, RenderOptions{Format: "json", AllFields: true}); err != nil {
|
|
468
|
+
t.Fatalf("Render json all-fields error: %v", err)
|
|
469
|
+
}
|
|
470
|
+
allFieldsMap := decodeJSONMap(t, allFields.Bytes())
|
|
471
|
+
allDataMap := allFieldsMap["data"].(map[string]any)
|
|
472
|
+
extensions, ok := allDataMap["extensions"].(map[string]any)
|
|
473
|
+
if !ok {
|
|
474
|
+
t.Fatalf("expected extensions with --all-fields")
|
|
475
|
+
}
|
|
476
|
+
if _, ok := extensions["notes"]; !ok {
|
|
477
|
+
t.Fatalf("expected notes to survive in extensions")
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
func TestRenderJSONLBehavior(t *testing.T) {
|
|
482
|
+
t.Parallel()
|
|
483
|
+
|
|
484
|
+
seasonsBody := mustReadFixtureFile(t, "leagues-seasons-standings/seasons.json")
|
|
485
|
+
seasons, err := NormalizeSeasonList(seasonsBody)
|
|
486
|
+
if err != nil {
|
|
487
|
+
t.Fatalf("NormalizeSeasonList error: %v", err)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
items := make([]any, 0, len(seasons))
|
|
491
|
+
for i := 0; i < 2 && i < len(seasons); i++ {
|
|
492
|
+
items = append(items, seasons[i])
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
result := NewListResult(EntitySeason, items)
|
|
496
|
+
var buf bytes.Buffer
|
|
497
|
+
if err := Render(&buf, result, RenderOptions{Format: "jsonl"}); err != nil {
|
|
498
|
+
t.Fatalf("Render jsonl error: %v", err)
|
|
499
|
+
}
|
|
500
|
+
lines := splitNonEmptyLines(buf.String())
|
|
501
|
+
if len(lines) != len(items) {
|
|
502
|
+
t.Fatalf("expected %d jsonl lines, got %d", len(items), len(lines))
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
partial := NewPartialListResult(EntitySeason, items, "season endpoint timed out on page 2")
|
|
506
|
+
buf.Reset()
|
|
507
|
+
if err := Render(&buf, partial, RenderOptions{Format: "jsonl"}); err != nil {
|
|
508
|
+
t.Fatalf("Render jsonl partial error: %v", err)
|
|
509
|
+
}
|
|
510
|
+
partialLines := splitNonEmptyLines(buf.String())
|
|
511
|
+
if len(partialLines) != len(items)+1 {
|
|
512
|
+
t.Fatalf("expected meta + %d items for partial jsonl, got %d", len(items), len(partialLines))
|
|
513
|
+
}
|
|
514
|
+
if !strings.Contains(partialLines[0], "_meta") {
|
|
515
|
+
t.Fatalf("expected first jsonl line to be metadata, got %q", partialLines[0])
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
empty := NewListResult(EntitySeason, nil)
|
|
519
|
+
buf.Reset()
|
|
520
|
+
if err := Render(&buf, empty, RenderOptions{Format: "jsonl"}); err != nil {
|
|
521
|
+
t.Fatalf("Render jsonl empty error: %v", err)
|
|
522
|
+
}
|
|
523
|
+
if strings.TrimSpace(buf.String()) != "" {
|
|
524
|
+
t.Fatalf("expected empty jsonl output for empty list, got %q", buf.String())
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
single := NewDataResult(EntitySeason, seasons[0])
|
|
528
|
+
if err := Render(&buf, single, RenderOptions{Format: "jsonl"}); err == nil {
|
|
529
|
+
t.Fatal("expected jsonl render error for single-entity result")
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
func TestTransportErrorMessaging(t *testing.T) {
|
|
534
|
+
t.Parallel()
|
|
535
|
+
|
|
536
|
+
err := &HTTPStatusError{URL: "http://example.com/events", StatusCode: 503}
|
|
537
|
+
result := NewTransportErrorResult(EntityMatch, "/events", err)
|
|
538
|
+
if result.Status != ResultStatusError {
|
|
539
|
+
t.Fatalf("expected error status, got %s", result.Status)
|
|
540
|
+
}
|
|
541
|
+
if !strings.Contains(result.Message, "status 503") {
|
|
542
|
+
t.Fatalf("expected status code in error message, got %q", result.Message)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
var buf bytes.Buffer
|
|
546
|
+
if err := Render(&buf, result, RenderOptions{Format: "text"}); err != nil {
|
|
547
|
+
t.Fatalf("Render text error result: %v", err)
|
|
548
|
+
}
|
|
549
|
+
output := buf.String()
|
|
550
|
+
if !strings.Contains(output, "Status: 503") {
|
|
551
|
+
t.Fatalf("expected text renderer to include status code, got %q", output)
|
|
552
|
+
}
|
|
553
|
+
if !strings.Contains(output, "Requested: /events") {
|
|
554
|
+
t.Fatalf("expected text renderer to include requested ref, got %q", output)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
func mustReadFixtureFile(t *testing.T, fixturePath string) []byte {
|
|
559
|
+
t.Helper()
|
|
560
|
+
path := filepath.Join("testdata", "fixtures", fixturePath)
|
|
561
|
+
data, err := os.ReadFile(path)
|
|
562
|
+
if err != nil {
|
|
563
|
+
t.Fatalf("read fixture %q: %v", path, err)
|
|
564
|
+
}
|
|
565
|
+
return data
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
func assertJSONHasKeys(t *testing.T, value any, keys ...string) {
|
|
569
|
+
t.Helper()
|
|
570
|
+
blob, err := json.Marshal(value)
|
|
571
|
+
if err != nil {
|
|
572
|
+
t.Fatalf("marshal value: %v", err)
|
|
573
|
+
}
|
|
574
|
+
mapped := decodeJSONMap(t, blob)
|
|
575
|
+
for _, key := range keys {
|
|
576
|
+
assertMapHasKey(t, mapped, key)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
func assertMapHasKey(t *testing.T, mapped map[string]any, key string) {
|
|
581
|
+
t.Helper()
|
|
582
|
+
if _, ok := mapped[key]; !ok {
|
|
583
|
+
t.Fatalf("expected json key %q, got keys: %v", key, mapKeys(mapped))
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
func decodeJSONMap(t *testing.T, blob []byte) map[string]any {
|
|
588
|
+
t.Helper()
|
|
589
|
+
var mapped map[string]any
|
|
590
|
+
if err := json.Unmarshal(blob, &mapped); err != nil {
|
|
591
|
+
t.Fatalf("decode json map: %v", err)
|
|
592
|
+
}
|
|
593
|
+
return mapped
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
func splitNonEmptyLines(text string) []string {
|
|
597
|
+
lines := strings.Split(strings.TrimSpace(text), "\n")
|
|
598
|
+
if len(lines) == 1 && lines[0] == "" {
|
|
599
|
+
return nil
|
|
600
|
+
}
|
|
601
|
+
out := make([]string, 0, len(lines))
|
|
602
|
+
for _, line := range lines {
|
|
603
|
+
line = strings.TrimSpace(line)
|
|
604
|
+
if line == "" {
|
|
605
|
+
continue
|
|
606
|
+
}
|
|
607
|
+
out = append(out, line)
|
|
608
|
+
}
|
|
609
|
+
return out
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
func assertGolden(t *testing.T, goldenFile string, actual string) {
|
|
613
|
+
t.Helper()
|
|
614
|
+
goldenPath := filepath.Join("testdata", "golden", goldenFile)
|
|
615
|
+
|
|
616
|
+
if os.Getenv("UPDATE_GOLDEN") == "1" {
|
|
617
|
+
if err := os.MkdirAll(filepath.Dir(goldenPath), 0o755); err != nil {
|
|
618
|
+
t.Fatalf("mkdir golden dir: %v", err)
|
|
619
|
+
}
|
|
620
|
+
if err := os.WriteFile(goldenPath, []byte(actual), 0o644); err != nil {
|
|
621
|
+
t.Fatalf("write golden file: %v", err)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
expectedBytes, err := os.ReadFile(goldenPath)
|
|
626
|
+
if err != nil {
|
|
627
|
+
t.Fatalf("read golden file %q: %v", goldenPath, err)
|
|
628
|
+
}
|
|
629
|
+
expected := string(expectedBytes)
|
|
630
|
+
if actual != expected {
|
|
631
|
+
t.Fatalf("golden mismatch for %s\n--- expected ---\n%s\n--- actual ---\n%s", goldenFile, expected, actual)
|
|
632
|
+
}
|
|
633
|
+
}
|