返回 DeepSeek-Reasonix
upgrade_test.go
根目录 / internal / cli / upgrade_test.go
1 package cli
2
3 import (
4 "archive/tar"
5 "bytes"
6 "compress/gzip"
7 "crypto/sha256"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "io"
13 "net/http"
14 "net/http/httptest"
15 "os"
16 "strings"
17 "testing"
18
19 "reasonix/internal/config"
20 )
21
22 func TestNormalizeVersion(t *testing.T) {
23 tests := []struct {
24 in string
25 want string
26 wantOK bool
27 }{
28 {"dev", "", false},
29 {"", "", false},
30 {" ", "", false},
31 {"abc", "", false},
32 {"v1.2.3", "v1.2.3", true},
33 {"1.2.3", "v1.2.3", true},
34 {"v1.2.3-rc1", "v1.2.3-rc1", true},
35 {" v0.10.0 ", "v0.10.0", true},
36 }
37 for _, tt := range tests {
38 got, ok := normalizeVersion(tt.in)
39 if ok != tt.wantOK || got != tt.want {
40 t.Errorf("normalizeVersion(%q) = (%q, %v), want (%q, %v)", tt.in, got, ok, tt.want, tt.wantOK)
41 }
42 }
43 }
44
45 func TestVerifyChecksum(t *testing.T) {
46 content := []byte("hello world")
47 sum := sha256.Sum256(content)
48 hash := hex.EncodeToString(sum[:])
49
50 t.Run("match", func(t *testing.T) {
51 checksumFile := []byte(fmt.Sprintf("%s reasonix-linux-amd64.tar.gz\n", hash))
52 if err := verifyChecksum(content, "reasonix-linux-amd64.tar.gz", checksumFile); err != nil {
53 t.Errorf("unexpected error: %v", err)
54 }
55 })
56
57 t.Run("mismatch", func(t *testing.T) {
58 checksumFile := []byte(fmt.Sprintf("%s reasonix-linux-amd64.tar.gz\n", "0000000000000000000000000000000000000000000000000000000000000000"))
59 if err := verifyChecksum(content, "reasonix-linux-amd64.tar.gz", checksumFile); err == nil {
60 t.Error("expected checksum mismatch error")
61 }
62 })
63
64 t.Run("not found", func(t *testing.T) {
65 checksumFile := []byte(fmt.Sprintf("%s reasonix-darwin-arm64.tar.gz\n", hash))
66 if err := verifyChecksum(content, "reasonix-linux-amd64.tar.gz", checksumFile); err == nil {
67 t.Error("expected not-found error")
68 }
69 })
70 }
71
72 func TestUpgradeSuccessMessageIncludesCurrentAndLatestVersions(t *testing.T) {
73 cur := "v1.10.0"
74 latest := "v1.11.0"
75
76 got := upgradeSuccessMessage(cur, latest)
77 if !strings.Contains(got, cur) {
78 t.Fatalf("success message %q does not include current version %q", got, cur)
79 }
80 if !strings.Contains(got, latest) {
81 t.Fatalf("success message %q does not include latest version %q", got, latest)
82 }
83 if strings.Index(got, cur) > strings.Index(got, latest) {
84 t.Fatalf("success message %q should report current version before latest version", got)
85 }
86 if strings.Contains(got, "%!") {
87 t.Fatalf("success message %q contains a missing fmt argument marker", got)
88 }
89 }
90
91 func TestExtractFromTarGz(t *testing.T) {
92 // Build a .tar.gz in memory containing a "reasonix" entry.
93 var buf bytes.Buffer
94 gw := gzip.NewWriter(&buf)
95 tw := tar.NewWriter(gw)
96
97 body := []byte("fake binary content")
98 if err := tw.WriteHeader(&tar.Header{
99 Name: "reasonix",
100 Mode: 0o755,
101 Size: int64(len(body)),
102 }); err != nil {
103 t.Fatal(err)
104 }
105 if _, err := tw.Write(body); err != nil {
106 t.Fatal(err)
107 }
108 if err := tw.Close(); err != nil {
109 t.Fatal(err)
110 }
111 if err := gw.Close(); err != nil {
112 t.Fatal(err)
113 }
114
115 got, err := extractFromTarGz(buf.Bytes(), "reasonix")
116 if err != nil {
117 t.Fatalf("extractFromTarGz: %v", err)
118 }
119 if !bytes.Equal(got, body) {
120 t.Errorf("extracted body = %q, want %q", got, body)
121 }
122 }
123
124 func TestExtractFromTarGz_Nested(t *testing.T) {
125 // Archives from goreleaser have the binary at the root with its name.
126 var buf bytes.Buffer
127 gw := gzip.NewWriter(&buf)
128 tw := tar.NewWriter(gw)
129
130 body := []byte("nested binary")
131 if err := tw.WriteHeader(&tar.Header{
132 Name: "reasonix-linux-amd64/reasonix",
133 Mode: 0o755,
134 Size: int64(len(body)),
135 }); err != nil {
136 t.Fatal(err)
137 }
138 if _, err := tw.Write(body); err != nil {
139 t.Fatal(err)
140 }
141 if err := tw.Close(); err != nil {
142 t.Fatal(err)
143 }
144 if err := gw.Close(); err != nil {
145 t.Fatal(err)
146 }
147
148 got, err := extractFromTarGz(buf.Bytes(), "reasonix")
149 if err != nil {
150 t.Fatalf("extractFromTarGz: %v", err)
151 }
152 if !bytes.Equal(got, body) {
153 t.Errorf("extracted body = %q, want %q", got, body)
154 }
155 }
156
157 func TestExtractFromTarGz_NotFound(t *testing.T) {
158 var buf bytes.Buffer
159 gw := gzip.NewWriter(&buf)
160 tw := tar.NewWriter(gw)
161 if err := tw.WriteHeader(&tar.Header{
162 Name: "other-file.txt",
163 Mode: 0o644,
164 Size: 3,
165 }); err != nil {
166 t.Fatal(err)
167 }
168 tw.Write([]byte("foo"))
169 tw.Close()
170 gw.Close()
171
172 _, err := extractFromTarGz(buf.Bytes(), "reasonix")
173 if err == nil {
174 t.Error("expected error for missing binary")
175 }
176 }
177
178 func TestIsCLITag(t *testing.T) {
179 tests := []struct {
180 tag string
181 want bool
182 }{
183 {"v1.6.0", true},
184 {"v0.1.0", true},
185 {"v2.0.0-rc.1", true},
186 {"desktop-v1.5.0", false},
187 {"npm-v1.4.0", false},
188 {"", false},
189 {"v", false},
190 }
191 for _, tt := range tests {
192 if got := isCLITag(tt.tag); got != tt.want {
193 t.Errorf("isCLITag(%q) = %v, want %v", tt.tag, got, tt.want)
194 }
195 }
196 }
197
198 func TestHumanSize(t *testing.T) {
199 tests := []struct {
200 bytes int64
201 want string
202 }{
203 {500, "500 B"},
204 {2048, "2.0 KiB"},
205 {19_000_000, "18.1 MiB"},
206 }
207 for _, tt := range tests {
208 if got := humanSize(tt.bytes); got != tt.want {
209 t.Errorf("humanSize(%d) = %q, want %q", tt.bytes, got, tt.want)
210 }
211 }
212 }
213
214 func completeCLIRelease(tag string, prerelease bool) ghRelease {
215 assets := make([]ghAsset, 0, len(requiredCLIAssets))
216 for _, name := range requiredCLIAssets {
217 assets = append(assets, ghAsset{
218 Name: name,
219 BrowserDownloadURL: fmt.Sprintf("https://github.com/esengine/DeepSeek-Reasonix/releases/download/%s/%s", tag, name),
220 Size: 42,
221 })
222 }
223 return ghRelease{TagName: tag, Prerelease: prerelease, Assets: assets}
224 }
225
226 func TestPickCLIRelease(t *testing.T) {
227 pick := func(rels []ghRelease, channel cliReleaseChannel) string {
228 if r := pickCLIRelease(rels, channel); r != nil {
229 return r.TagName
230 }
231 return ""
232 }
233
234 // Stable skips foreign namespaces and every prerelease, even when a Preview
235 // was published more recently than the latest Stable release.
236 mixed := []ghRelease{
237 completeCLIRelease("v1.18.0-preview.1", true),
238 {TagName: "desktop-v1.18.0"},
239 {TagName: "npm-v1.18.0"},
240 completeCLIRelease("v1.6.0", false),
241 }
242 if got := pick(mixed, cliReleaseStable); got != "v1.6.0" {
243 t.Errorf("stable channel: got %q, want v1.6.0", got)
244 }
245
246 prereleases := []ghRelease{
247 completeCLIRelease("v1.18.0-preview.2", true),
248 completeCLIRelease("v1.19.0-rc.1", true),
249 completeCLIRelease("v1.18.0-preview.12", true),
250 }
251 if got := pick(prereleases, cliReleaseStable); got != "" {
252 t.Errorf("official release selection accepted prerelease %q", got)
253 }
254
255 incomplete := completeCLIRelease("v1.7.0", false)
256 incomplete.Assets = incomplete.Assets[:len(incomplete.Assets)-1]
257 if got := pick([]ghRelease{incomplete, completeCLIRelease("v1.6.0", false)}, cliReleaseStable); got != "v1.6.0" {
258 t.Errorf("incomplete newest Stable release: got %q, want v1.6.0", got)
259 }
260
261 insecure := completeCLIRelease("v1.7.0", false)
262 insecure.Assets[0].BrowserDownloadURL = "http://example.invalid/reasonix.tar.gz"
263 if got := pick([]ghRelease{insecure, completeCLIRelease("v1.6.0", false)}, cliReleaseStable); got != "v1.6.0" {
264 t.Errorf("release with insecure asset URL: got %q, want v1.6.0", got)
265 }
266
267 spoofed := completeCLIRelease("v1.7.0", false)
268 spoofed.Assets[0].BrowserDownloadURL = "https://github.com@evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.7.0/reasonix-darwin-amd64.tar.gz"
269 if got := pick([]ghRelease{spoofed, completeCLIRelease("v1.6.0", false)}, cliReleaseStable); got != "v1.6.0" {
270 t.Errorf("release with spoofed asset host: got %q, want v1.6.0", got)
271 }
272
273 wrongTag := completeCLIRelease("v1.7.0", false)
274 wrongTag.Assets[0].BrowserDownloadURL = "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.6.0/reasonix-darwin-amd64.tar.gz"
275 if got := pick([]ghRelease{wrongTag, completeCLIRelease("v1.6.0", false)}, cliReleaseStable); got != "v1.6.0" {
276 t.Errorf("release with cross-tag asset URL: got %q, want v1.6.0", got)
277 }
278
279 empty := completeCLIRelease("v1.7.0", false)
280 empty.Assets[0].Size = 0
281 if got := pick([]ghRelease{empty, completeCLIRelease("v1.6.0", false)}, cliReleaseStable); got != "v1.6.0" {
282 t.Errorf("release with zero-byte asset: got %q, want v1.6.0", got)
283 }
284
285 duplicate := completeCLIRelease("v1.7.0", false)
286 duplicate.Assets = append(duplicate.Assets, duplicate.Assets[0])
287 if got := pick([]ghRelease{duplicate, completeCLIRelease("v1.6.0", false)}, cliReleaseStable); got != "v1.6.0" {
288 t.Errorf("release with duplicate required asset: got %q, want v1.6.0", got)
289 }
290
291 if got := pick([]ghRelease{{TagName: "desktop-v1.0.0"}}, cliReleaseStable); got != "" {
292 t.Errorf("no CLI release should return nil, got %q", got)
293 }
294 }
295
296 func TestFindCLIPlatformAssetRequiresExactArchiveName(t *testing.T) {
297 release := completeCLIRelease("v1.18.0", false)
298 release.Assets = append([]ghAsset{{
299 Name: "reasonix-linux-amd64.signature",
300 BrowserDownloadURL: "https://example.invalid/signature",
301 }}, release.Assets...)
302
303 asset := findCLIPlatformAsset(&release, "linux", "amd64")
304 if asset == nil || asset.Name != "reasonix-linux-amd64.tar.gz" {
305 t.Fatalf("Linux asset = %+v, want exact tar.gz archive", asset)
306 }
307 if got := cliPlatformAssetName("windows", "arm64"); got != "reasonix-windows-arm64.zip" {
308 t.Fatalf("Windows asset name = %q, want reasonix-windows-arm64.zip", got)
309 }
310
311 expectedURL := asset.BrowserDownloadURL
312 release.Assets = append([]ghAsset{{
313 Name: "reasonix-linux-amd64.tar.gz",
314 BrowserDownloadURL: "https://evil.invalid/reasonix-linux-amd64.tar.gz",
315 }}, release.Assets...)
316 asset = findCLIPlatformAsset(&release, "linux", "amd64")
317 if asset == nil || asset.BrowserDownloadURL != expectedURL {
318 t.Fatalf("platform selection accepted an unsafe duplicate: %+v", asset)
319 }
320
321 for i := range release.Assets {
322 if release.Assets[i].Name == "reasonix-linux-amd64.tar.gz" &&
323 release.Assets[i].BrowserDownloadURL == expectedURL {
324 release.Assets[i].Size = 0
325 }
326 }
327 if asset := findCLIPlatformAsset(&release, "linux", "amd64"); asset != nil {
328 t.Fatalf("platform selection accepted a zero-byte archive: %+v", asset)
329 }
330 }
331
332 func TestValidateCLIUpgradeRedirect(t *testing.T) {
333 tests := []struct {
334 name string
335 target string
336 wantError bool
337 }{
338 {name: "GitHub HTTPS asset redirect", target: "https://release-assets.githubusercontent.com/file"},
339 {name: "GitHub redirect", target: "https://github.com/file"},
340 {name: "HTTPS downgrade", target: "http://release-assets.githubusercontent.com/file", wantError: true},
341 {name: "userinfo", target: "https://user@release-assets.githubusercontent.com/file", wantError: true},
342 {name: "missing hostname", target: "https:///file", wantError: true},
343 {name: "untrusted HTTPS host", target: "https://example.invalid/file", wantError: true},
344 {name: "githubusercontent suffix spoof", target: "https://release-assets.githubusercontent.com.evil.invalid/file", wantError: true},
345 {name: "explicit port", target: "https://release-assets.githubusercontent.com:443/file", wantError: true},
346 }
347 for _, tt := range tests {
348 t.Run(tt.name, func(t *testing.T) {
349 req, err := http.NewRequest(http.MethodGet, tt.target, nil)
350 if err != nil {
351 t.Fatal(err)
352 }
353 err = validateCLIUpgradeRedirect(req, nil)
354 if (err != nil) != tt.wantError {
355 t.Fatalf("validateCLIUpgradeRedirect(%q) error = %v, wantError=%v", tt.target, err, tt.wantError)
356 }
357 })
358 }
359 t.Run("redirect limit", func(t *testing.T) {
360 req, err := http.NewRequest(http.MethodGet, "https://release-assets.githubusercontent.com/file", nil)
361 if err != nil {
362 t.Fatal(err)
363 }
364 if err := validateCLIUpgradeRedirect(req, make([]*http.Request, 10)); err == nil {
365 t.Fatal("validateCLIUpgradeRedirect accepted more than 10 redirects")
366 }
367 })
368 }
369
370 func TestCLIReleaseChannelContract(t *testing.T) {
371 if !strings.HasSuffix(ghAPIReleases, "?per_page=100") {
372 t.Fatalf("CLI release query must retain enough history to skip archived prereleases: %q", ghAPIReleases)
373 }
374
375 for _, tc := range []struct {
376 value string
377 want cliReleaseChannel
378 ok bool
379 }{
380 {"", cliReleaseStable, true},
381 {"stable", cliReleaseStable, true},
382 {"PREVIEW", cliReleaseStable, true},
383 {"canary", cliReleaseStable, true},
384 {"next", cliReleaseStable, true},
385 {"rc", "", false},
386 } {
387 got, err := parseCLIReleaseChannel(tc.value)
388 if (err == nil) != tc.ok || got != tc.want {
389 t.Errorf("parseCLIReleaseChannel(%q) = (%q, %v), want (%q, ok=%v)", tc.value, got, err, tc.want, tc.ok)
390 }
391 }
392
393 for _, tc := range []struct {
394 version string
395 channel cliReleaseChannel
396 want bool
397 }{
398 {"v1.17.21", cliReleaseStable, true},
399 {"v1.18.0-preview.1", cliReleaseStable, false},
400 {"v1.18.0-rc.1", cliReleaseStable, false},
401 } {
402 if got := versionBelongsToCLIChannel(tc.version, tc.channel); got != tc.want {
403 t.Errorf("versionBelongsToCLIChannel(%q, %q) = %v, want %v", tc.version, tc.channel, got, tc.want)
404 }
405 }
406 }
407
408 func TestParseAndResolveCLIUpgradeChannel(t *testing.T) {
409 for _, tc := range []struct {
410 name string
411 args []string
412 configured string
413 want cliReleaseChannel
414 wantSave bool
415 wantCheck bool
416 wantForce bool
417 }{
418 {name: "fresh default", configured: "", want: cliReleaseStable},
419 {name: "saved preview migrates", configured: "preview", want: cliReleaseStable},
420 {name: "legacy preview positional", args: []string{"preview"}, configured: "stable", want: cliReleaseStable},
421 {name: "stable positional", args: []string{"--check", "stable"}, configured: "preview", want: cliReleaseStable, wantCheck: true},
422 {name: "legacy flags after positional", args: []string{"preview", "--force"}, configured: "stable", want: cliReleaseStable, wantForce: true},
423 {name: "legacy one off override", args: []string{"--channel", "preview"}, configured: "stable", want: cliReleaseStable},
424 {name: "mixed legacy aliases", args: []string{"preview", "--channel=stable"}, configured: "stable", want: cliReleaseStable},
425 } {
426 t.Run(tc.name, func(t *testing.T) {
427 syntax, err := parseCLIUpgradeSyntax(tc.args)
428 if err != nil {
429 t.Fatal(err)
430 }
431 got, save, err := resolveCLIUpgradeChannel(syntax, tc.configured)
432 if err != nil {
433 t.Fatal(err)
434 }
435 if got != tc.want || save != tc.wantSave || syntax.checkOnly != tc.wantCheck || syntax.force != tc.wantForce {
436 t.Fatalf("resolved = (%q, save=%v, check=%v, force=%v), want (%q, save=%v, check=%v, force=%v)",
437 got, save, syntax.checkOnly, syntax.force, tc.want, tc.wantSave, tc.wantCheck, tc.wantForce)
438 }
439 })
440 }
441 }
442
443 func TestParseCLIUpgradeChannelRejectsAmbiguousArguments(t *testing.T) {
444 for _, args := range [][]string{
445 {"stable", "preview"},
446 {"--channel", "rc"},
447 {"--channel"},
448 {"--channel="},
449 } {
450 if _, err := parseCLIUpgradeSyntax(args); err == nil {
451 t.Errorf("parseCLIUpgradeSyntax(%q) unexpectedly succeeded", args)
452 }
453 }
454 }
455
456 func TestUpgradeCommandRejectsMalformedConfigWithoutPanicking(t *testing.T) {
457 oldLoad := loadCLIUpgradeConfig
458 loadCLIUpgradeConfig = func() (*config.Config, error) {
459 return nil, errors.New("malformed TOML")
460 }
461 t.Cleanup(func() { loadCLIUpgradeConfig = oldLoad })
462
463 if code := upgradeCommand([]string{"--channel", "stable"}, "v1.17.0"); code != 1 {
464 t.Fatalf("upgradeCommand exit = %d, want 1", code)
465 }
466 }
467
468 func TestPersistCLIReleaseChannelRemovesLegacyConfig(t *testing.T) {
469 for _, legacy := range []string{"stable", "preview", "canary", "beta", "next"} {
470 t.Run(legacy, func(t *testing.T) {
471 home := t.TempDir()
472 t.Setenv("REASONIX_HOME", home)
473 if err := os.WriteFile(config.UserConfigPath(), []byte("[cli]\nupdate_channel = \""+legacy+"\"\n"), 0o600); err != nil {
474 t.Fatal(err)
475 }
476 if err := persistCLIReleaseChannel(cliReleaseStable); err != nil {
477 t.Fatalf("migrate legacy channel: %v", err)
478 }
479 cfg, err := config.LoadForEditReadOnlyStrict(config.UserConfigPath())
480 if err != nil {
481 t.Fatal(err)
482 }
483 if got := cfg.CLIUpdateChannel(); got != "stable" {
484 t.Fatalf("saved CLI channel = %q, want stable", got)
485 }
486 raw, err := os.ReadFile(config.UserConfigPath())
487 if err != nil {
488 t.Fatal(err)
489 }
490 if strings.Contains(string(raw), "[cli]") || strings.Contains(string(raw), "update_channel") {
491 t.Fatalf("saved config retained retired CLI channel:\n%s", raw)
492 }
493 })
494 }
495 }
496
497 func TestFetchCLIReleasePointer(t *testing.T) {
498 valid := completeCLIRelease("v1.18.0", false)
499 invalidMetadata := completeCLIRelease("v1.18.0-preview.1", true)
500 incomplete := completeCLIRelease("v1.18.0", false)
501 incomplete.Assets = incomplete.Assets[:len(incomplete.Assets)-1]
502 insecure := completeCLIRelease("v1.18.0", false)
503 insecure.Assets[0].BrowserDownloadURL = "http://example.invalid/reasonix.tar.gz"
504
505 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
506 if r.Header.Get("Accept") != "application/json" || r.Header.Get("User-Agent") != "reasonix-cli" {
507 t.Errorf("unexpected pointer request headers: Accept=%q User-Agent=%q", r.Header.Get("Accept"), r.Header.Get("User-Agent"))
508 }
509 var release ghRelease
510 switch r.URL.Path {
511 case "/valid":
512 release = valid
513 case "/incomplete":
514 release = incomplete
515 case "/insecure":
516 release = insecure
517 default:
518 release = invalidMetadata
519 }
520 if err := json.NewEncoder(w).Encode(release); err != nil {
521 t.Errorf("encode release pointer: %v", err)
522 }
523 }))
524 defer server.Close()
525
526 release, err := fetchCLIReleasePointer(server.Client(), server.URL+"/valid", cliReleaseStable)
527 if err != nil || release.TagName != "v1.18.0" {
528 t.Fatalf("valid official pointer = (%+v, %v)", release, err)
529 }
530 if _, err := fetchCLIReleasePointer(server.Client(), server.URL+"/invalid", cliReleaseStable); err == nil {
531 t.Fatal("prerelease pointer should fail closed")
532 }
533 if _, err := fetchCLIReleasePointer(server.Client(), server.URL+"/incomplete", cliReleaseStable); err == nil {
534 t.Fatal("pointer missing a required CLI asset should fall back")
535 }
536 if _, err := fetchCLIReleasePointer(server.Client(), server.URL+"/insecure", cliReleaseStable); err == nil {
537 t.Fatal("pointer with an insecure asset URL should fall back")
538 }
539 }
540
541 type upgradeRoundTripFunc func(*http.Request) (*http.Response, error)
542
543 func (fn upgradeRoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
544 return fn(request)
545 }
546
547 func TestFetchLatestReleaseFallsThroughIncompletePointerAndGitHubRelease(t *testing.T) {
548 incompletePointer := completeCLIRelease("v1.8.0", false)
549 incompletePointer.Assets = incompletePointer.Assets[:len(incompletePointer.Assets)-1]
550 incompleteGitHub := completeCLIRelease("v1.7.0", false)
551 incompleteGitHub.Assets = incompleteGitHub.Assets[:len(incompleteGitHub.Assets)-1]
552 completeGitHub := completeCLIRelease("v1.6.0", false)
553
554 client := &http.Client{Transport: upgradeRoundTripFunc(func(request *http.Request) (*http.Response, error) {
555 var payload any
556 switch request.URL.String() {
557 case cliGatewayBase + "/stable/latest.json":
558 payload = incompletePointer
559 case ghAPIReleases:
560 payload = []ghRelease{incompleteGitHub, completeGitHub}
561 default:
562 t.Fatalf("unexpected release request: %s", request.URL)
563 }
564 body, err := json.Marshal(payload)
565 if err != nil {
566 t.Fatal(err)
567 }
568 return &http.Response{
569 StatusCode: http.StatusOK,
570 Status: "200 OK",
571 Header: make(http.Header),
572 Body: io.NopCloser(bytes.NewReader(body)),
573 Request: request,
574 }, nil
575 })}
576
577 release, err := fetchLatestRelease(client, cliReleaseStable)
578 if err != nil {
579 t.Fatalf("fetchLatestRelease: %v", err)
580 }
581 if release.TagName != "v1.6.0" {
582 t.Fatalf("fallback release = %q, want v1.6.0", release.TagName)
583 }
584 }
585
586 func TestFetchBytesSizedRequiresExactReleaseAssetLength(t *testing.T) {
587 client := &http.Client{Transport: upgradeRoundTripFunc(func(request *http.Request) (*http.Response, error) {
588 return &http.Response{
589 StatusCode: http.StatusOK,
590 Status: "200 OK",
591 Header: make(http.Header),
592 Body: io.NopCloser(strings.NewReader(request.URL.Query().Get("body"))),
593 Request: request,
594 }, nil
595 })}
596
597 if data, err := fetchBytesSized(client, "https://example.invalid/archive?body=exact", 5); err != nil || string(data) != "exact" {
598 t.Fatalf("exact release asset = %q, %v", data, err)
599 }
600 if _, err := fetchBytesSized(client, "https://example.invalid/archive?body=short", 6); err == nil {
601 t.Fatal("fetchBytesSized accepted fewer bytes than the release declared")
602 }
603 if _, err := fetchBytesSized(client, "https://example.invalid/archive?body=longer", 5); err == nil {
604 t.Fatal("fetchBytesSized accepted more bytes than the release declared")
605 }
606 if _, err := fetchBytesSized(client, "https://example.invalid/archive?body=", 0); err == nil {
607 t.Fatal("fetchBytesSized accepted a zero expected size")
608 }
609 if _, err := fetchBytesSized(client, "https://example.invalid/archive?body=x", maxCLIReleaseAssetSize+1); err == nil {
610 t.Fatal("fetchBytesSized accepted a size above the release maximum")
611 }
612 }
613
613 lines GO