From eec44074e83247952b83f28d5ac498b122fe3ae8 Mon Sep 17 00:00:00 2001 From: David Ficociello Date: Wed, 15 Apr 2026 06:47:06 -0400 Subject: [PATCH] fix: clean up 15 golangci-lint violations and data race in events test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings pulse to a green lint and race-test state. The fixes are all pre-existing issues that surfaced the first time CI ran against main. errcheck: - internal/torznab/types.go — MarshalXML now propagates EncodeToken errors via early-return. writeElement's internal EncodeToken calls are explicitly unchecked with `_ =` since the outer MarshalXML will surface a broken encoder at its next checked call. - internal/api/v1/torznab.go — 6 handler-side Write/Encode/Copy calls now use `_ =` since there's nothing meaningful to do with a response-side error after WriteHeader has fired. - internal/core/indexer/tester.go — io.Copy drain in defer uses `_ =`. - internal/core/health/checker.go — same drain pattern. - pkg/sdk/client_test.go — sqlDB.Exec cleanup loop uses `_, _ =`. ineffassign: - internal/core/indexer/tester.go — dropped the dead trailing-slash branch above capsURL. The next line overwrote the value anyway, so nothing actually changes at runtime. unused: - internal/core/indexer/prowlarr.go — removed the inline `result` type that used to be populated by the per-file loop but has been unreferenced since fetchAllViaZip / fetchIndividual replaced it. - internal/api/v1/download_clients.go — removed toDLClientBody, a no-op stub with a comment explaining it's handled inline. data race: - internal/events/bus_test.go — TestBusPublishSetsTimestamp used a shared `var received Event` that the handler goroutine wrote to and the main goroutine read without synchronization (bus_test.go:43 vs bus.go:87). Replaced with a buffered channel + select, which is race-free and also drops the brittle 50ms sleep. Verified with `go test -race ./...` and `golangci-lint run ./...` — both clean. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/api/v1/download_clients.go | 5 ---- internal/api/v1/torznab.go | 12 +++++----- internal/core/health/checker.go | 2 +- internal/core/indexer/prowlarr.go | 7 ------ internal/core/indexer/tester.go | 8 ++----- internal/events/bus_test.go | 14 +++++++---- internal/torznab/types.go | 36 +++++++++++++++++++---------- pkg/sdk/client_test.go | 2 +- 8 files changed, 43 insertions(+), 43 deletions(-) diff --git a/internal/api/v1/download_clients.go b/internal/api/v1/download_clients.go index 7b5c759..aaf30a0 100644 --- a/internal/api/v1/download_clients.go +++ b/internal/api/v1/download_clients.go @@ -78,11 +78,6 @@ type dlClientTestInput struct { } } -func toDLClientBody(row interface{ GetID() string }) dlClientBody { - // This is handled inline since the generated type doesn't have methods - return dlClientBody{} -} - // RegisterDownloadClientRoutes registers download client management endpoints. func RegisterDownloadClientRoutes(api huma.API, svc *downloadclient.Service) { // POST /api/v1/download-clients — create diff --git a/internal/api/v1/torznab.go b/internal/api/v1/torznab.go index a34145d..ca613db 100644 --- a/internal/api/v1/torznab.go +++ b/internal/api/v1/torznab.go @@ -114,8 +114,8 @@ func (h *TorznabHandler) handleCaps(w http.ResponseWriter, runner *scraper.Runne w.Header().Set("Content-Type", "application/xml; charset=utf-8") w.WriteHeader(http.StatusOK) - w.Write([]byte(xml.Header)) - xml.NewEncoder(w).Encode(caps) + _, _ = w.Write([]byte(xml.Header)) + _ = xml.NewEncoder(w).Encode(caps) } // handleSearch executes a search and returns Torznab XML results. @@ -211,8 +211,8 @@ func (h *TorznabHandler) writeResults(w http.ResponseWriter, r *http.Request, id w.Header().Set("Content-Type", "application/xml; charset=utf-8") w.WriteHeader(http.StatusOK) - w.Write([]byte(xml.Header)) - xml.NewEncoder(w).Encode(feed) + _, _ = w.Write([]byte(xml.Header)) + _ = xml.NewEncoder(w).Encode(feed) } // xmlError writes a Torznab error response. @@ -304,7 +304,7 @@ func (h *TorznabHandler) HandleDownload(w http.ResponseWriter, r *http.Request) w.Header().Set("Content-Disposition", cd) } w.WriteHeader(proxyResp.StatusCode) - io.Copy(w, proxyResp.Body) + _, _ = io.Copy(w, proxyResp.Body) } // TestSearchResult is the JSON response from the test-search endpoint. @@ -371,7 +371,7 @@ func (h *TorznabHandler) HandleTestSearch(w http.ResponseWriter, r *http.Request func writeJSON(w http.ResponseWriter, v interface{}) { w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(v) + _ = json.NewEncoder(w).Encode(v) } func since(start time.Time) string { diff --git a/internal/core/health/checker.go b/internal/core/health/checker.go index fc47f43..5e66f03 100644 --- a/internal/core/health/checker.go +++ b/internal/core/health/checker.go @@ -82,7 +82,7 @@ func (c *Checker) checkOne(ctx context.Context, svc db.Service) { return } defer func() { - io.Copy(io.Discard, resp.Body) + _, _ = io.Copy(io.Discard, resp.Body) resp.Body.Close() }() diff --git a/internal/core/indexer/prowlarr.go b/internal/core/indexer/prowlarr.go index bcdac6b..65ad096 100644 --- a/internal/core/indexer/prowlarr.go +++ b/internal/core/indexer/prowlarr.go @@ -119,13 +119,6 @@ func (pc *ProwlarrCatalog) Refresh(ctx context.Context) error { pc.logger.Info("prowlarr: found definition files", "count", len(files)) - // Step 2: Fetch and parse each YAML file. - // Use a semaphore to limit concurrent requests. - type result struct { - entry CatalogEntry - ok bool - } - // Fetch all definitions via the zipball to avoid per-file rate limits. pc.logger.Info("prowlarr: downloading zipball...") entries, errCount, err := pc.fetchAllViaZip(ctx, files) diff --git a/internal/core/indexer/tester.go b/internal/core/indexer/tester.go index 2ae6ebd..bbaf632 100644 --- a/internal/core/indexer/tester.go +++ b/internal/core/indexer/tester.go @@ -33,11 +33,7 @@ func TestIndexer(ctx context.Context, kind, url, apiKey string) TestResult { func testTorznab(ctx context.Context, client *http.Client, baseURL, apiKey string, start time.Time) TestResult { // Torznab/Newznab caps endpoint: /api?t=caps&apikey=... - capsURL := baseURL - if capsURL != "" && capsURL[len(capsURL)-1] != '/' { - capsURL += "/" - } - capsURL = baseURL + "?t=caps" + capsURL := baseURL + "?t=caps" if apiKey != "" { capsURL += "&apikey=" + apiKey } @@ -105,7 +101,7 @@ func testGenericHTTP(ctx context.Context, client *http.Client, url, apiKey strin return TestResult{Success: false, Message: fmt.Sprintf("Connection failed: %v", err), Duration: since(start)} } defer func() { - io.Copy(io.Discard, resp.Body) + _, _ = io.Copy(io.Discard, resp.Body) resp.Body.Close() }() diff --git a/internal/events/bus_test.go b/internal/events/bus_test.go index 67c3338..e3dbc37 100644 --- a/internal/events/bus_test.go +++ b/internal/events/bus_test.go @@ -31,17 +31,21 @@ func TestBusPublishSubscribe(t *testing.T) { func TestBusPublishSetsTimestamp(t *testing.T) { bus := New(slog.Default()) - var received Event + received := make(chan Event, 1) bus.Subscribe(func(_ context.Context, e Event) { - received = e + received <- e }) bus.Publish(context.Background(), Event{Type: TypeConfigUpdated}) - time.Sleep(50 * time.Millisecond) - if received.Timestamp.IsZero() { - t.Error("expected non-zero timestamp") + select { + case e := <-received: + if e.Timestamp.IsZero() { + t.Error("expected non-zero timestamp") + } + case <-time.After(1 * time.Second): + t.Fatal("timed out waiting for event") } } diff --git a/internal/torznab/types.go b/internal/torznab/types.go index 4502414..f867f6f 100644 --- a/internal/torznab/types.go +++ b/internal/torznab/types.go @@ -88,7 +88,9 @@ type Enclosure struct { // MarshalXML produces the Torznab-compatible XML for an Item. func (item Item) MarshalXML(e *xml.Encoder, start xml.StartElement) error { start.Name = xml.Name{Local: "item"} - e.EncodeToken(start) + if err := e.EncodeToken(start); err != nil { + return err + } writeElement(e, "title", item.Title) writeElement(e, "guid", item.GUID) @@ -103,39 +105,49 @@ func (item Item) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if encType == "" { encType = "application/x-bittorrent" } - e.EncodeToken(xml.StartElement{ + if err := e.EncodeToken(xml.StartElement{ Name: xml.Name{Local: "enclosure"}, Attr: []xml.Attr{ {Name: xml.Name{Local: "url"}, Value: item.Enclosure.URL}, {Name: xml.Name{Local: "length"}, Value: strconv.FormatInt(item.Enclosure.Length, 10)}, {Name: xml.Name{Local: "type"}, Value: encType}, }, - }) - e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "enclosure"}}) + }); err != nil { + return err + } + if err := e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "enclosure"}}); err != nil { + return err + } } // Torznab attributes torznabNS := "http://torznab.com/schemas/2015/feed" for name, value := range item.Attrs { - e.EncodeToken(xml.StartElement{ + if err := e.EncodeToken(xml.StartElement{ Name: xml.Name{Space: torznabNS, Local: "attr"}, Attr: []xml.Attr{ {Name: xml.Name{Local: "name"}, Value: name}, {Name: xml.Name{Local: "value"}, Value: value}, }, - }) - e.EncodeToken(xml.EndElement{Name: xml.Name{Space: torznabNS, Local: "attr"}}) + }); err != nil { + return err + } + if err := e.EncodeToken(xml.EndElement{Name: xml.Name{Space: torznabNS, Local: "attr"}}); err != nil { + return err + } } - e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "item"}}) - return nil + return e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "item"}}) } func writeElement(e *xml.Encoder, name, value string) { start := xml.StartElement{Name: xml.Name{Local: name}} - e.EncodeToken(start) - e.EncodeToken(xml.CharData(value)) - e.EncodeToken(xml.EndElement{Name: xml.Name{Local: name}}) + // The outer MarshalXML returns any encoder error via its final + // EncodeToken call, so these helpers ignore errors by design — + // a broken encoder will surface at the next checked call. + _ = e.EncodeToken(start) + _ = e.EncodeToken(xml.CharData(value)) + _ = e.EncodeToken(xml.EndElement{Name: xml.Name{Local: name}}) } // ── Builders ──────────────────────────────────────────────────────────────── diff --git a/pkg/sdk/client_test.go b/pkg/sdk/client_test.go index e68e030..334f26d 100644 --- a/pkg/sdk/client_test.go +++ b/pkg/sdk/client_test.go @@ -45,7 +45,7 @@ func setupTestServer(t *testing.T) *httptest.Server { // Clean all tables for test isolation. for _, table := range []string{"indexer_tags", "service_tags", "tags", "config_subscriptions", "config_entries", "indexer_assignments", "indexers", "service_capabilities", "services", "filter_presets", "download_clients"} { - sqlDB.Exec("DELETE FROM " + table) + _, _ = sqlDB.Exec("DELETE FROM " + table) } logger := slog.Default()