chore: remove prealloc linter

This commit is contained in:
Frédéric Guillot
2026-01-07 20:21:05 -08:00
parent df28028b24
commit 56a11d1d45
7 changed files with 278 additions and 280 deletions
-1
View File
@@ -10,7 +10,6 @@ linters:
- loggercheck - loggercheck
- misspell - misspell
- perfsprint - perfsprint
- prealloc
- sqlclosecheck - sqlclosecheck
- staticcheck - staticcheck
- whitespace - whitespace
+11 -11
View File
@@ -20,17 +20,17 @@ type entriesResponse struct {
// EntryImportRequest represents a manually imported entry for a feed. // EntryImportRequest represents a manually imported entry for a feed.
type EntryImportRequest struct { type EntryImportRequest struct {
URL string `json:"url"` URL string `json:"url"`
Title string `json:"title"` Title string `json:"title"`
Content string `json:"content"` Content string `json:"content"`
Author string `json:"author"` Author string `json:"author"`
CommentsURL string `json:"comments_url"` CommentsURL string `json:"comments_url"`
PublishedAt int64 `json:"published_at"` PublishedAt int64 `json:"published_at"`
Status string `json:"status"` Status string `json:"status"`
Starred bool `json:"starred"` Starred bool `json:"starred"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
ExternalID string `json:"external_id"` ExternalID string `json:"external_id"`
} }
type feedCreationResponse struct { type feedCreationResponse struct {
FeedID int64 `json:"feed_id"` FeedID int64 `json:"feed_id"`
+1 -1
View File
@@ -279,7 +279,7 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
if csvItemIDs != "" { if csvItemIDs != "" {
var itemIDs []int64 var itemIDs []int64
for _, strItemID := range strings.Split(csvItemIDs, ",") { for strItemID := range strings.SplitSeq(csvItemIDs, ",") {
strItemID = strings.TrimSpace(strItemID) strItemID = strings.TrimSpace(strItemID)
itemID, _ := strconv.ParseInt(strItemID, 10, 64) itemID, _ := strconv.ParseInt(strItemID, 10, 64)
itemIDs = append(itemIDs, itemID) itemIDs = append(itemIDs, itemID)
+237 -237
View File
@@ -4,257 +4,257 @@
package readeck package readeck
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
) )
func TestCreateBookmark(t *testing.T) { func TestCreateBookmark(t *testing.T) {
entryURL := "https://example.com/article" entryURL := "https://example.com/article"
entryTitle := "Example Title" entryTitle := "Example Title"
entryContent := "<p>Some HTML content</p>" entryContent := "<p>Some HTML content</p>"
labels := "tag1,tag2" labels := "tag1,tag2"
tests := []struct { tests := []struct {
name string name string
onlyURL bool onlyURL bool
baseURL string baseURL string
apiKey string apiKey string
labels string labels string
entryURL string entryURL string
entryTitle string entryTitle string
entryContent string entryContent string
serverResponse func(w http.ResponseWriter, r *http.Request) serverResponse func(w http.ResponseWriter, r *http.Request)
wantErr bool wantErr bool
errContains string errContains string
}{ }{
{ {
name: "successful bookmark creation with only URL", name: "successful bookmark creation with only URL",
onlyURL: true, onlyURL: true,
labels: labels, labels: labels,
entryURL: entryURL, entryURL: entryURL,
entryTitle: entryTitle, entryTitle: entryTitle,
entryContent: entryContent, entryContent: entryContent,
serverResponse: func(w http.ResponseWriter, r *http.Request) { serverResponse: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
} }
if r.URL.Path != "/api/bookmarks/" { if r.URL.Path != "/api/bookmarks/" {
t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path) t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
} }
if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") { if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
t.Errorf("expected Authorization Bearer header, got %q", got) t.Errorf("expected Authorization Bearer header, got %q", got)
} }
if ct := r.Header.Get("Content-Type"); ct != "application/json" { if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("expected Content-Type application/json, got %s", ct) t.Errorf("expected Content-Type application/json, got %s", ct)
} }
body, _ := io.ReadAll(r.Body) body, _ := io.ReadAll(r.Body)
var payload map[string]any var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil { if err := json.Unmarshal(body, &payload); err != nil {
t.Fatalf("failed to parse JSON body: %v", err) t.Fatalf("failed to parse JSON body: %v", err)
} }
if u := payload["url"]; u != entryURL { if u := payload["url"]; u != entryURL {
t.Errorf("expected url %s, got %v", entryURL, u) t.Errorf("expected url %s, got %v", entryURL, u)
} }
if title := payload["title"]; title != entryTitle { if title := payload["title"]; title != entryTitle {
t.Errorf("expected title %s, got %v", entryTitle, title) t.Errorf("expected title %s, got %v", entryTitle, title)
} }
// Labels should be split into an array // Labels should be split into an array
if raw := payload["labels"]; raw == nil { if raw := payload["labels"]; raw == nil {
t.Errorf("expected labels to be set") t.Errorf("expected labels to be set")
} else if arr, ok := raw.([]any); ok { } else if arr, ok := raw.([]any); ok {
if len(arr) != 2 || arr[0] != "tag1" || arr[1] != "tag2" { if len(arr) != 2 || arr[0] != "tag1" || arr[1] != "tag2" {
t.Errorf("unexpected labels: %#v", arr) t.Errorf("unexpected labels: %#v", arr)
} }
} else { } else {
t.Errorf("labels should be an array, got %T", raw) t.Errorf("labels should be an array, got %T", raw)
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
}, },
}, },
{ {
name: "successful bookmark creation with content (multipart)", name: "successful bookmark creation with content (multipart)",
onlyURL: false, onlyURL: false,
labels: labels, labels: labels,
entryURL: entryURL, entryURL: entryURL,
entryTitle: entryTitle, entryTitle: entryTitle,
entryContent: entryContent, entryContent: entryContent,
serverResponse: func(w http.ResponseWriter, r *http.Request) { serverResponse: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
} }
if r.URL.Path != "/api/bookmarks/" { if r.URL.Path != "/api/bookmarks/" {
t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path) t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
} }
if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") { if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
t.Errorf("expected Authorization Bearer header, got %q", got) t.Errorf("expected Authorization Bearer header, got %q", got)
} }
ct := r.Header.Get("Content-Type") ct := r.Header.Get("Content-Type")
if !strings.HasPrefix(ct, "multipart/form-data;") { if !strings.HasPrefix(ct, "multipart/form-data;") {
t.Errorf("expected multipart/form-data, got %s", ct) t.Errorf("expected multipart/form-data, got %s", ct)
} }
boundaryIdx := strings.Index(ct, "boundary=") boundaryIdx := strings.Index(ct, "boundary=")
if boundaryIdx == -1 { if boundaryIdx == -1 {
t.Fatalf("missing multipart boundary in Content-Type: %s", ct) t.Fatalf("missing multipart boundary in Content-Type: %s", ct)
} }
boundary := ct[boundaryIdx+len("boundary="):] boundary := ct[boundaryIdx+len("boundary="):]
mr := multipart.NewReader(r.Body, boundary) mr := multipart.NewReader(r.Body, boundary)
seenLabels := []string{} seenLabels := []string{}
var seenURL, seenTitle, seenFeature string var seenURL, seenTitle, seenFeature string
var resourceHeader map[string]any var resourceHeader map[string]any
var resourceBody string var resourceBody string
for { for {
part, err := mr.NextPart() part, err := mr.NextPart()
if err == io.EOF { if err == io.EOF {
break break
} }
if err != nil { if err != nil {
t.Fatalf("reading multipart: %v", err) t.Fatalf("reading multipart: %v", err)
} }
name := part.FormName() name := part.FormName()
data, _ := io.ReadAll(part) data, _ := io.ReadAll(part)
switch name { switch name {
case "url": case "url":
seenURL = string(data) seenURL = string(data)
case "title": case "title":
seenTitle = string(data) seenTitle = string(data)
case "feature_find_main": case "feature_find_main":
seenFeature = string(data) seenFeature = string(data)
case "labels": case "labels":
seenLabels = append(seenLabels, string(data)) seenLabels = append(seenLabels, string(data))
case "resource": case "resource":
// First line is JSON header, then newline, then content // First line is JSON header, then newline, then content
all := string(data) all := string(data)
idx := strings.IndexByte(all, '\n') idx := strings.IndexByte(all, '\n')
if idx == -1 { if idx == -1 {
t.Fatalf("resource content missing header separator") t.Fatalf("resource content missing header separator")
} }
headerJSON := all[:idx] headerJSON := all[:idx]
resourceBody = all[idx+1:] resourceBody = all[idx+1:]
if err := json.Unmarshal([]byte(headerJSON), &resourceHeader); err != nil { if err := json.Unmarshal([]byte(headerJSON), &resourceHeader); err != nil {
t.Fatalf("invalid resource header JSON: %v", err) t.Fatalf("invalid resource header JSON: %v", err)
} }
} }
} }
if seenURL != entryURL { if seenURL != entryURL {
t.Errorf("expected url %s, got %s", entryURL, seenURL) t.Errorf("expected url %s, got %s", entryURL, seenURL)
} }
if seenTitle != entryTitle { if seenTitle != entryTitle {
t.Errorf("expected title %s, got %s", entryTitle, seenTitle) t.Errorf("expected title %s, got %s", entryTitle, seenTitle)
} }
if seenFeature != "false" { if seenFeature != "false" {
t.Errorf("expected feature_find_main to be 'false', got %s", seenFeature) t.Errorf("expected feature_find_main to be 'false', got %s", seenFeature)
} }
if len(seenLabels) != 2 || seenLabels[0] != "tag1" || seenLabels[1] != "tag2" { if len(seenLabels) != 2 || seenLabels[0] != "tag1" || seenLabels[1] != "tag2" {
t.Errorf("unexpected labels: %#v", seenLabels) t.Errorf("unexpected labels: %#v", seenLabels)
} }
if resourceHeader == nil { if resourceHeader == nil {
t.Fatalf("missing resource header") t.Fatalf("missing resource header")
} }
if hURL, _ := resourceHeader["url"].(string); hURL != entryURL { if hURL, _ := resourceHeader["url"].(string); hURL != entryURL {
t.Errorf("expected resource header url %s, got %v", entryURL, hURL) t.Errorf("expected resource header url %s, got %v", entryURL, hURL)
} }
if headers, ok := resourceHeader["headers"].(map[string]any); ok { if headers, ok := resourceHeader["headers"].(map[string]any); ok {
if ct, _ := headers["content-type"].(string); ct != "text/html; charset=utf-8" { if ct, _ := headers["content-type"].(string); ct != "text/html; charset=utf-8" {
t.Errorf("expected resource header content-type text/html; charset=utf-8, got %v", ct) t.Errorf("expected resource header content-type text/html; charset=utf-8, got %v", ct)
} }
} else { } else {
t.Errorf("missing resource header 'headers' field") t.Errorf("missing resource header 'headers' field")
} }
if resourceBody != entryContent { if resourceBody != entryContent {
t.Errorf("expected resource body %q, got %q", entryContent, resourceBody) t.Errorf("expected resource body %q, got %q", entryContent, resourceBody)
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
}, },
}, },
{ {
name: "error when server returns 400", name: "error when server returns 400",
onlyURL: true, onlyURL: true,
labels: labels, labels: labels,
entryURL: entryURL, entryURL: entryURL,
entryTitle: entryTitle, entryTitle: entryTitle,
entryContent: entryContent, entryContent: entryContent,
serverResponse: func(w http.ResponseWriter, r *http.Request) { serverResponse: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
}, },
wantErr: true, wantErr: true,
errContains: "unable to create bookmark", errContains: "unable to create bookmark",
}, },
{ {
name: "error when missing baseURL or apiKey", name: "error when missing baseURL or apiKey",
onlyURL: true, onlyURL: true,
baseURL: "", baseURL: "",
apiKey: "", apiKey: "",
labels: labels, labels: labels,
entryURL: entryURL, entryURL: entryURL,
entryTitle: entryTitle, entryTitle: entryTitle,
entryContent: entryContent, entryContent: entryContent,
serverResponse: nil, serverResponse: nil,
wantErr: true, wantErr: true,
errContains: "missing base URL or API key", errContains: "missing base URL or API key",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var serverURL string var serverURL string
if tt.serverResponse != nil { if tt.serverResponse != nil {
srv := httptest.NewServer(http.HandlerFunc(tt.serverResponse)) srv := httptest.NewServer(http.HandlerFunc(tt.serverResponse))
defer srv.Close() defer srv.Close()
serverURL = srv.URL serverURL = srv.URL
} }
baseURL := tt.baseURL baseURL := tt.baseURL
if baseURL == "" { if baseURL == "" {
baseURL = serverURL baseURL = serverURL
} }
apiKey := tt.apiKey apiKey := tt.apiKey
if apiKey == "" { if apiKey == "" {
apiKey = "test-api-key" apiKey = "test-api-key"
} }
client := NewClient(baseURL, apiKey, tt.labels, tt.onlyURL) client := NewClient(baseURL, apiKey, tt.labels, tt.onlyURL)
err := client.CreateBookmark(tt.entryURL, tt.entryTitle, tt.entryContent) err := client.CreateBookmark(tt.entryURL, tt.entryTitle, tt.entryContent)
if tt.wantErr { if tt.wantErr {
if err == nil { if err == nil {
t.Fatalf("expected error, got none") t.Fatalf("expected error, got none")
} }
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Fatalf("expected error containing %q, got %q", tt.errContains, err.Error()) t.Fatalf("expected error containing %q, got %q", tt.errContains, err.Error())
} }
} else if err != nil { } else if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
}) })
} }
} }
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
baseURL := "https://readeck.example.com" baseURL := "https://readeck.example.com"
apiKey := "key" apiKey := "key"
labels := "tag1,tag2" labels := "tag1,tag2"
onlyURL := true onlyURL := true
c := NewClient(baseURL, apiKey, labels, onlyURL) c := NewClient(baseURL, apiKey, labels, onlyURL)
if c.baseURL != baseURL { if c.baseURL != baseURL {
t.Errorf("expected baseURL %s, got %s", baseURL, c.baseURL) t.Errorf("expected baseURL %s, got %s", baseURL, c.baseURL)
} }
if c.apiKey != apiKey { if c.apiKey != apiKey {
t.Errorf("expected apiKey %s, got %s", apiKey, c.apiKey) t.Errorf("expected apiKey %s, got %s", apiKey, c.apiKey)
} }
if c.labels != labels { if c.labels != labels {
t.Errorf("expected labels %s, got %s", labels, c.labels) t.Errorf("expected labels %s, got %s", labels, c.labels)
} }
if c.onlyURL != onlyURL { if c.onlyURL != onlyURL {
t.Errorf("expected onlyURL %v, got %v", onlyURL, c.onlyURL) t.Errorf("expected onlyURL %v, got %v", onlyURL, c.onlyURL)
} }
} }
+28 -28
View File
@@ -237,55 +237,55 @@ func (s *Storage) entryExists(tx *sql.Tx, entry *model.Entry) (bool, error) {
} }
func (s *Storage) getEntryIDByHash(tx *sql.Tx, feedID int64, entryHash string) (int64, error) { func (s *Storage) getEntryIDByHash(tx *sql.Tx, feedID int64, entryHash string) (int64, error) {
var entryID int64 var entryID int64
err := tx.QueryRow( err := tx.QueryRow(
`SELECT id FROM entries WHERE feed_id=$1 AND hash=$2 LIMIT 1`, `SELECT id FROM entries WHERE feed_id=$1 AND hash=$2 LIMIT 1`,
feedID, feedID,
entryHash, entryHash,
).Scan(&entryID) ).Scan(&entryID)
if err != nil { if err != nil {
return 0, fmt.Errorf(`store: unable to fetch entry ID: %v`, err) return 0, fmt.Errorf(`store: unable to fetch entry ID: %v`, err)
} }
return entryID, nil return entryID, nil
} }
// InsertEntryForFeed inserts a single entry into a feed, optionally updating if it already exists. // InsertEntryForFeed inserts a single entry into a feed, optionally updating if it already exists.
// Returns true if a new entry was created, false if an existing one was reused. // Returns true if a new entry was created, false if an existing one was reused.
func (s *Storage) InsertEntryForFeed(userID, feedID int64, entry *model.Entry) (bool, error) { func (s *Storage) InsertEntryForFeed(userID, feedID int64, entry *model.Entry) (bool, error) {
entry.UserID = userID entry.UserID = userID
entry.FeedID = feedID entry.FeedID = feedID
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
return false, fmt.Errorf("store: unable to start transaction: %v", err) return false, fmt.Errorf("store: unable to start transaction: %v", err)
} }
defer tx.Rollback() defer tx.Rollback()
exists, err := s.entryExists(tx, entry) exists, err := s.entryExists(tx, entry)
if err != nil { if err != nil {
return false, err return false, err
} }
if exists { if exists {
entryID, err := s.getEntryIDByHash(tx, entry.FeedID, entry.Hash) entryID, err := s.getEntryIDByHash(tx, entry.FeedID, entry.Hash)
if err != nil { if err != nil {
return false, err return false, err
} }
entry.ID = entryID entry.ID = entryID
} else { } else {
if err := s.createEntry(tx, entry); err != nil { if err := s.createEntry(tx, entry); err != nil {
return false, err return false, err
} }
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return false, err return false, err
} }
return !exists, nil return !exists, nil
} }
func (s *Storage) IsNewEntry(feedID int64, entryHash string) bool { func (s *Storage) IsNewEntry(feedID int64, entryHash string) bool {
+1 -1
View File
@@ -10,8 +10,8 @@ import (
"math" "math"
"net/mail" "net/mail"
"net/url" "net/url"
"strconv"
"slices" "slices"
"strconv"
"strings" "strings"
"time" "time"
-1
View File
@@ -91,4 +91,3 @@ func (h *handler) showStarredCategoryEntryPage(w http.ResponseWriter, r *http.Re
html.OK(w, r, view.Render("entry")) html.OK(w, r, view.Render("entry"))
} }