package version import ( "bytes" "errors" "io" "net/http" "strconv" "sync/atomic" "testing" "time" ) type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } type signalReadCloser struct { rc io.ReadCloser onClose func() } func (s *signalReadCloser) Read(p []byte) (int, error) { return s.rc.Read(p) } func (s *signalReadCloser) Close() error { if s.onClose != nil { s.onClose() s.onClose = nil } return s.rc.Close() } func httpResp(status int, body string) *http.Response { return &http.Response{ StatusCode: status, Status: strconv.Itoa(status), Header: make(http.Header), Body: io.NopCloser(bytes.NewBufferString(body)), } } func TestChecker_Check_ErrorsAndSuccess(t *testing.T) { origVersion := Version t.Cleanup(func() { Version = origVersion }) t.Run("client error", func(t *testing.T) { c := &Checker{ client: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { return nil, errors.New("boom") }), }, } c.check() if c.lastCheck != (time.Time{}) { t.Fatalf("expected lastCheck untouched on error, got %v", c.lastCheck) } }) t.Run("non-200", func(t *testing.T) { c := &Checker{ client: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { return httpResp(http.StatusTooManyRequests, "{}"), nil }), }, } c.check() if c.latestVersion != "" || c.releaseURL != "" || c.hasUpdate { t.Fatalf("expected no state update on non-200, got latest=%q url=%q hasUpdate=%v", c.latestVersion, c.releaseURL, c.hasUpdate) } }) t.Run("bad json", func(t *testing.T) { c := &Checker{ client: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { return httpResp(http.StatusOK, "{"), nil }), }, } c.check() if c.latestVersion != "" || c.releaseURL != "" || c.hasUpdate { t.Fatalf("expected no state update on bad json, got latest=%q url=%q hasUpdate=%v", c.latestVersion, c.releaseURL, c.hasUpdate) } }) t.Run("success no update", func(t *testing.T) { Version = "v1.2.3" c := &Checker{ client: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { if req.Header.Get("Accept") == "" || req.Header.Get("User-Agent") == "" { t.Fatalf("expected headers set, got Accept=%q UA=%q", req.Header.Get("Accept"), req.Header.Get("User-Agent")) } return httpResp(http.StatusOK, `{"tag_name":"v1.2.3","html_url":"https://example.com/release"}`), nil }), }, } c.check() if c.latestVersion != "v1.2.3" || c.releaseURL != "https://example.com/release" { t.Fatalf("unexpected state: latest=%q url=%q", c.latestVersion, c.releaseURL) } if c.hasUpdate { t.Fatalf("expected hasUpdate=false when versions equal") } if c.lastCheck.IsZero() { t.Fatalf("expected lastCheck set") } }) t.Run("success has update", func(t *testing.T) { Version = "v1.0.0" c := &Checker{ client: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { return httpResp(http.StatusOK, `{"tag_name":"v2.0.0","html_url":"https://example.com/release2"}`), nil }), }, } c.check() if !c.hasUpdate || c.latestVersion != "v2.0.0" || c.releaseURL != "https://example.com/release2" { t.Fatalf("unexpected state: hasUpdate=%v latest=%q url=%q", c.hasUpdate, c.latestVersion, c.releaseURL) } }) } func TestStartChecker_RunsCheckOnce(t *testing.T) { origVersion := Version origClient := checker.client t.Cleanup(func() { Version = origVersion checker.client = origClient }) Version = "v1.0.0" var calls int32 done := make(chan struct{}) checker.client = &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { atomic.AddInt32(&calls, 1) resp := httpResp(http.StatusOK, `{"tag_name":"v2.0.0","html_url":"https://example.com/release"}`) resp.Body = &signalReadCloser{ rc: resp.Body, onClose: func() { select { case <-done: default: close(done) } }, } return resp, nil }), } StartChecker() select { case <-done: case <-time.After(1 * time.Second): t.Fatalf("expected StartChecker to run check at least once") } if atomic.LoadInt32(&calls) == 0 { t.Fatalf("expected StartChecker to issue at least one HTTP call") } checker.mu.RLock() lastCheck := checker.lastCheck checker.mu.RUnlock() if lastCheck.IsZero() { t.Fatalf("expected lastCheck to be set") } }