From 60f52f985116c70c3d58a670e819fefd75a65a7b Mon Sep 17 00:00:00 2001 From: marcus06 Date: Sat, 23 May 2026 21:43:30 +0200 Subject: [PATCH] add ci (#1) * ci(checks): add pull request workflow and golangci-lint config * refactor(lint): resolve golangci-lint findings * style(tui): drop traling blank line for gofmt * fix(cards): handle crypto/rand.Read error in storeRedirect --- .github/workflows/ci.yml | 38 +++++++++++++++++++ .golangci.yml | 16 ++++++++ api/handlers/addresses_test.go | 10 ++--- api/handlers/auth_test.go | 24 ++++++------ api/handlers/cards.go | 14 +++++-- api/handlers/cart_test.go | 50 ++++++++++++------------ api/handlers/products_test.go | 8 ++-- api/handlers/slack.go | 8 ++-- api/main.go | 5 ++- api/routes/routes.go | 2 +- pkg/api/client.go | 38 +++++++++---------- pkg/auth/ssh_auth_test.go | 14 +++---- pkg/bring/booking.go | 2 +- pkg/bring/bring.go | 2 +- pkg/database/db.go | 2 +- pkg/database/db_test.go | 8 ++-- pkg/notify/slack.go | 4 +- pkg/shippo/shippo.go | 4 +- pkg/tui/account.go | 6 +-- pkg/tui/account_orders.go | 1 - pkg/tui/confirmation.go | 6 +-- pkg/tui/model.go | 69 ---------------------------------- pkg/utils/response.go | 4 +- pkg/validate/validate.go | 2 +- 24 files changed, 164 insertions(+), 173 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .golangci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6e52b1e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + pull_request: + branches: [master] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build + run: go build ./... + + - name: Vet + run: go vet ./... + + - name: Format check + run: | + unformatted=$(gofmt -l $(go list -f '{{.Dir}}' ./...)) + if [ -n "$unformatted" ]; then + echo "These files need gofmt:" + echo "$unformatted" + exit 1 + fi + + - name: Lint + uses: golangci/golangci-lint-action@v7 + with: + version: v2.4.0 + + - name: Test (race detector) + run: go test -race ./... diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..3d929d7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,16 @@ +version: "2" + +run: + timeout: 5m + +linters: + default: none + enable: + - errcheck + - govet + - ineffassign + - staticcheck + - unused + exclusions: + paths: + - terminal-shop-source \ No newline at end of file diff --git a/api/handlers/addresses_test.go b/api/handlers/addresses_test.go index 3094e01..c022c6d 100644 --- a/api/handlers/addresses_test.go +++ b/api/handlers/addresses_test.go @@ -13,7 +13,7 @@ import ( func setupAddressTestDB(t *testing.T) (string, models.User) { t.Helper() testDB := "test_addresses.db" - os.Remove(testDB) + _ = os.Remove(testDB) database.ResetForTesting() db, err := database.Connect(testDB) @@ -42,7 +42,7 @@ func setupAddressTestDB(t *testing.T) (string, models.User) { func TestGetAddressesEmpty(t *testing.T) { testDB, user := setupAddressTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewAddressHandler("", "", "") @@ -74,7 +74,7 @@ func TestGetAddressesEmpty(t *testing.T) { func TestGetAddressesIsolation(t *testing.T) { testDB, user1 := setupAddressTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -105,7 +105,7 @@ func TestGetAddressesIsolation(t *testing.T) { Addresses []models.Address `json:"addresses"` } `json:"data"` } - json.NewDecoder(w.Body).Decode(&resp) + _ = json.NewDecoder(w.Body).Decode(&resp) if len(resp.Data.Addresses) != 1 { t.Fatalf("expexted 1 address for user1, got %d", len(resp.Data.Addresses)) @@ -117,7 +117,7 @@ func TestGetAddressesIsolation(t *testing.T) { func TestGetAddressesWithData(t *testing.T) { testDB, user := setupAddressTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() diff --git a/api/handlers/auth_test.go b/api/handlers/auth_test.go index 64bd405..a587459 100644 --- a/api/handlers/auth_test.go +++ b/api/handlers/auth_test.go @@ -22,7 +22,7 @@ const testClientSecret = "test-client-secret" func setupAuthHandlerTestDB(t *testing.T) (string, *auth.JWTManager, *AuthHandler) { t.Helper() testDB := "test_auth.db" - os.Remove(testDB) + _ = os.Remove(testDB) database.ResetForTesting() db, err := database.Connect(testDB) @@ -62,7 +62,7 @@ func createAuthTestUser(t *testing.T, fingerprint string) models.User { func TestGetTokenSuccess(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() user := createAuthTestUser(t, "SHA256:testfingerprint123") @@ -108,7 +108,7 @@ func TestGetTokenSuccess(t *testing.T) { func TestGetTokenBadSecret(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() body, _ := json.Marshal(map[string]string{ @@ -128,7 +128,7 @@ func TestGetTokenBadSecret(t *testing.T) { func TestGetTokenMissingFingerprint(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() body, _ := json.Marshal(map[string]string{ @@ -147,7 +147,7 @@ func TestGetTokenMissingFingerprint(t *testing.T) { func TestGetTokenUnknownFingerprint(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() body, _ := json.Marshal(map[string]string{ @@ -167,7 +167,7 @@ func TestGetTokenUnknownFingerprint(t *testing.T) { func TestGetTokenInvalidJSON(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() req := httptest.NewRequest("POST", "/api/v1/auth/token", bytes.NewBufferString("{bad json")) @@ -185,7 +185,7 @@ func TestGetTokenInvalidJSON(t *testing.T) { func TestRegisterWithSSHKeySuccess(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() body, _ := json.Marshal(map[string]string{ @@ -207,7 +207,7 @@ func TestRegisterWithSSHKeySuccess(t *testing.T) { func TestRegisterWithSSHKeyDuplicate(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() createAuthTestUser(t, "SHA256:existingfp") @@ -229,7 +229,7 @@ func TestRegisterWithSSHKeyDuplicate(t *testing.T) { func TestRegisterWithSSHKeyMissingFields(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() body, _ := json.Marshal(map[string]string{ @@ -250,7 +250,7 @@ func TestRegisterWithSSHKeyMissingFields(t *testing.T) { func TestGetUserBySSHKeySuccess(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() createAuthTestUser(t, "SHA256:lookupfp") @@ -267,7 +267,7 @@ func TestGetUserBySSHKeySuccess(t *testing.T) { func TestGetUserBySSHKeyNotFound(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() req := httptest.NewRequest("GET", "/api/v1/auth/ssh?fingerprint=SHA256:nope", nil) @@ -282,7 +282,7 @@ func TestGetUserBySSHKeyNotFound(t *testing.T) { func TestGetUserBySSHKeyMissingParam(t *testing.T) { testDB, _, handler := setupAuthHandlerTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() req := httptest.NewRequest("GET", "/api/v1/auth/ssh", nil) diff --git a/api/handlers/cards.go b/api/handlers/cards.go index db18d84..8f05b08 100644 --- a/api/handlers/cards.go +++ b/api/handlers/cards.go @@ -30,14 +30,16 @@ var ( payRedirectsMu sync.Mutex ) -func storeRedirect(stripeURL string) string { +func storeRedirect(stripeURL string) (string, error) { b := make([]byte, 16) - rand.Read(b) + if _, err := rand.Read(b); err != nil { + return "", err + } token := hex.EncodeToString(b) payRedirectsMu.Lock() payRedirects[token] = stripeURL payRedirectsMu.Unlock() - return token + return token, nil } // PayRedirect resolves a short token and redirects to the Stripe checkout URL. @@ -166,7 +168,11 @@ func (h *CardHandler) CollectCard(w http.ResponseWriter, r *http.Request) { return } - token := storeRedirect(sess.URL) + token, err := storeRedirect(sess.URL) + if err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to generate redirect token", nil) + return + } utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "url": fmt.Sprintf("%s/pay/%s", h.appURL, token), }) diff --git a/api/handlers/cart_test.go b/api/handlers/cart_test.go index 7187803..819f42c 100644 --- a/api/handlers/cart_test.go +++ b/api/handlers/cart_test.go @@ -20,7 +20,7 @@ import ( func setupCartTestDB(t *testing.T) (string, models.User) { t.Helper() testDB := "test_cart.db" - os.Remove(testDB) + _ = os.Remove(testDB) database.ResetForTesting() db, err := database.Connect(testDB) @@ -62,7 +62,7 @@ func authRequest(method, url string, body []byte, userID uint) *http.Request { func TestGetCartEmpty(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -101,7 +101,7 @@ func TestGetCartEmpty(t *testing.T) { func TestSetItemAndGetCart(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -157,7 +157,7 @@ func TestSetItemAndGetCart(t *testing.T) { func TestSetItemUpdateQuantity(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -190,7 +190,7 @@ func TestSetItemUpdateQuantity(t *testing.T) { } `json:"cart"` } `json:"data"` } - json.NewDecoder(w.Body).Decode(&resp) + _ = json.NewDecoder(w.Body).Decode(&resp) if len(resp.Data.Cart.Items) != 1 { t.Fatalf("Expected 1 item after update, got %d", len(resp.Data.Cart.Items)) @@ -202,7 +202,7 @@ func TestSetItemUpdateQuantity(t *testing.T) { func TestSetItemRemove(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -232,7 +232,7 @@ func TestSetItemRemove(t *testing.T) { } `json:"cart"` } `json:"data"` } - json.NewDecoder(w.Body).Decode(&resp) + _ = json.NewDecoder(w.Body).Decode(&resp) if len(resp.Data.Cart.Items) != 0 { t.Errorf("Expected 0 items after removal, got %d", len(resp.Data.Cart.Items)) @@ -241,7 +241,7 @@ func TestSetItemRemove(t *testing.T) { func TestSetItemInvalidProduct(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -258,7 +258,7 @@ func TestSetItemInvalidProduct(t *testing.T) { func TestSetItemMissingCoffeeID(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -275,7 +275,7 @@ func TestSetItemMissingCoffeeID(t *testing.T) { func TestClearCart(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -310,7 +310,7 @@ func TestClearCart(t *testing.T) { } `json:"cart"` } `json:"data"` } - json.NewDecoder(w.Body).Decode(&resp) + _ = json.NewDecoder(w.Body).Decode(&resp) if len(resp.Data.Cart.Items) != 0 { t.Errorf("Expected 0 items after clear, got %d", len(resp.Data.Cart.Items)) @@ -319,7 +319,7 @@ func TestClearCart(t *testing.T) { func TestSetAddress(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -358,7 +358,7 @@ func TestSetAddress(t *testing.T) { } `json:"cart"` } `json:"data"` } - json.NewDecoder(w.Body).Decode(&resp) + _ = json.NewDecoder(w.Body).Decode(&resp) if resp.Data.Cart.AddressID == nil || *resp.Data.Cart.AddressID != addr.ID { t.Errorf("Expected address_id %d, got %v", addr.ID, resp.Data.Cart.AddressID) @@ -367,7 +367,7 @@ func TestSetAddress(t *testing.T) { func TestSetAddressWrongUser(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -402,7 +402,7 @@ func TestSetAddressWrongUser(t *testing.T) { func TestSetCard(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -440,7 +440,7 @@ func TestSetCard(t *testing.T) { } `json:"cart"` } `json:"data"` } - json.NewDecoder(w.Body).Decode(&resp) + _ = json.NewDecoder(w.Body).Decode(&resp) if resp.Data.Cart.CardID == nil || *resp.Data.Cart.CardID != card.ID { t.Errorf("Expected card_id %d, got %v", card.ID, resp.Data.Cart.CardID) @@ -449,7 +449,7 @@ func TestSetCard(t *testing.T) { func TestSetCardWrongUser(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -482,7 +482,7 @@ func TestSetCardWrongUser(t *testing.T) { func TestConvertCartMissingAddress(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -505,7 +505,7 @@ func TestConvertCartMissingAddress(t *testing.T) { func TestConvertCartMissingCard(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -536,7 +536,7 @@ func TestConvertCartMissingCard(t *testing.T) { func TestConvertCartEmpty(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -553,7 +553,7 @@ func TestConvertCartEmpty(t *testing.T) { func TestMultipleItems(t *testing.T) { testDB, user := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewCartHandler("") @@ -583,7 +583,7 @@ func TestMultipleItems(t *testing.T) { } `json:"cart"` } `json:"data"` } - json.NewDecoder(w.Body).Decode(&resp) + _ = json.NewDecoder(w.Body).Decode(&resp) if len(resp.Data.Cart.Items) != 3 { t.Errorf("Expected 3 items, got %d", len(resp.Data.Cart.Items)) @@ -595,7 +595,7 @@ func TestMultipleItems(t *testing.T) { func TestCartIsolation(t *testing.T) { testDB, user1 := setupCartTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -638,7 +638,7 @@ func TestCartIsolation(t *testing.T) { } `json:"cart"` } `json:"data"` } - json.NewDecoder(w.Body).Decode(&resp) + _ = json.NewDecoder(w.Body).Decode(&resp) if len(resp.Data.Cart.Items) != 1 { t.Fatalf("User 1 expected 1 item, got %d", len(resp.Data.Cart.Items)) @@ -655,7 +655,7 @@ func TestCartIsolation(t *testing.T) { w = httptest.NewRecorder() r.ServeHTTP(w, req) - json.NewDecoder(w.Body).Decode(&resp) + _ = json.NewDecoder(w.Body).Decode(&resp) if len(resp.Data.Cart.Items) != 1 { t.Fatalf("User 2 expected 1 item, got %d", len(resp.Data.Cart.Items)) diff --git a/api/handlers/products_test.go b/api/handlers/products_test.go index 759ee84..8f589a2 100644 --- a/api/handlers/products_test.go +++ b/api/handlers/products_test.go @@ -16,7 +16,7 @@ func setupTestDB(t *testing.T) string { testDB := "test_products.db" // Remove if exists - os.Remove(testDB) + _ = os.Remove(testDB) // Reset singleton database.ResetForTesting() @@ -39,7 +39,7 @@ func setupTestDB(t *testing.T) string { func TestGetProduct(t *testing.T) { testDB := setupTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewProductHandler() @@ -78,7 +78,7 @@ func TestGetProduct(t *testing.T) { func TestGetProductNotFound(t *testing.T) { testDB := setupTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() handler := NewProductHandler() @@ -98,7 +98,7 @@ func TestGetProductNotFound(t *testing.T) { func TestGetProductsEmptyDatabase(t *testing.T) { testDB := "test_empty.db" - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() // Create database but don't seed diff --git a/api/handlers/slack.go b/api/handlers/slack.go index 9ba937b..83e8aa7 100644 --- a/api/handlers/slack.go +++ b/api/handlers/slack.go @@ -133,7 +133,7 @@ func (h *SlackHandler) handleBuyLabel(orderIDStr string, p slackInteractivePaylo notify.SlackPostToOrderThread(orderID, fmt.Sprintf(":x: Label purchase failed: %v", err)) return } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var result struct { Data struct { @@ -201,7 +201,7 @@ func (h *SlackHandler) handleMarkStatus(orderIDStr string, p slackInteractivePay notify.SlackPostToOrderThread(orderID, fmt.Sprintf(":x: Mark %s failed: %v", status, err)) return } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= 300 { notify.SlackPostToOrderThread(orderID, fmt.Sprintf(":x: Mark %s failed (HTTP %d)", status, resp.StatusCode)) return @@ -245,7 +245,7 @@ func (h *SlackHandler) editClearActions(responseURL, status, actor string) { slog.Warn("slack mark_status: response_url edit failed", "error", err) return } - resp.Body.Close() + _ = resp.Body.Close() } func (h *SlackHandler) verifySignature(r *http.Request, body []byte) bool { @@ -264,7 +264,7 @@ func (h *SlackHandler) verifySignature(r *http.Request, body []byte) bool { } mac := hmac.New(sha256.New, []byte(h.signingSecret)) - fmt.Fprintf(mac, "v0:%d:%s", ts, body) + _, _ = fmt.Fprintf(mac, "v0:%d:%s", ts, body) expected := "v0=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(sigHeader)) diff --git a/api/main.go b/api/main.go index 6197c9a..731f5e5 100644 --- a/api/main.go +++ b/api/main.go @@ -40,7 +40,8 @@ func main() { if err != nil { log.Fatalf("Failed to connect to database: %v", err) } - defer database.Close() + + defer func() { _ = database.Close() }() // Run migrations if err := database.Migrate(db); err != nil { @@ -61,7 +62,7 @@ func main() { if err != nil { log.Printf("tracing init failed (non-fatal): %v", err) } else { - defer shutdown(context.Background()) + defer func() { _ = shutdown(context.Background()) }() } stripeclient.InitOTel() diff --git a/api/routes/routes.go b/api/routes/routes.go index 973b521..cad2fa4 100644 --- a/api/routes/routes.go +++ b/api/routes/routes.go @@ -59,7 +59,7 @@ func SetupRoutes( r.Get("/t/{id}", handlers.TrackingRedirect) r.Get("/card-added", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`

Card added.

You can close this tab.

`)) + _, _ = w.Write([]byte(`

Card added.

You can close this tab.

`)) }) r.Handle("/metrics", middleware.MetricsHandler()) diff --git a/pkg/api/client.go b/pkg/api/client.go index 7898ae0..9ccd4d9 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -80,7 +80,7 @@ func (c *Client) GetOrCreateToken(fingerprint, pubKeyStr, clientSecret string) ( if err != nil { return "", models.PublicUser{}, fmt.Errorf("token refresh request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return "", models.PublicUser{}, fmt.Errorf("token refresh returned status %d", resp.StatusCode) @@ -137,7 +137,7 @@ func (c *Client) GetProducts() ([]models.Coffee, error) { if err != nil { return nil, fmt.Errorf("failed to fetch products: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API returned status %d", resp.StatusCode) @@ -167,7 +167,7 @@ func (c *Client) Health() error { if err != nil { return fmt.Errorf("health check failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("API returned status %d", resp.StatusCode) @@ -217,7 +217,7 @@ func (c *Client) RegisterUser(username, sshPublicKey, sshKeyFingerprint string) if err != nil { return nil, fmt.Errorf("failed to register user: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var registerResp RegisterResponse if err := json.NewDecoder(resp.Body).Decode(®isterResp); err != nil { @@ -302,7 +302,7 @@ func (c *Client) GetCart() (*CartData, error) { if err != nil { return nil, fmt.Errorf("failed to fetch cart: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var cartResp CartResponse if err := json.NewDecoder(resp.Body).Decode(&cartResp); err != nil { @@ -342,7 +342,7 @@ func (c *Client) SetCartItem(coffeeID uint, quantity int) (*CartData, error) { if err != nil { return nil, fmt.Errorf("failed to set cart item: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var cartResp CartResponse if err := json.NewDecoder(resp.Body).Decode(&cartResp); err != nil { @@ -381,7 +381,7 @@ func (c *Client) SetCartAddress(addressID uint) error { if err != nil { return fmt.Errorf("failed to set cart address: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var result APIResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { @@ -420,7 +420,7 @@ func (c *Client) SetCartCard(cardID uint) error { if err != nil { return fmt.Errorf("failed to set cart card: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var result APIResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { @@ -450,7 +450,7 @@ func (c *Client) ClearCart() error { if err != nil { return fmt.Errorf("failed to clear cart: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var result APIResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { @@ -480,7 +480,7 @@ func (c *Client) ConvertCart() (*models.Order, error) { if err != nil { return nil, fmt.Errorf("failed to convert cart: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var convertResp ConvertCartResponse if err := json.NewDecoder(resp.Body).Decode(&convertResp); err != nil { @@ -510,7 +510,7 @@ func (c *Client) GetCards() ([]models.Card, error) { if err != nil { return nil, fmt.Errorf("failed to fetch cards: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var cardsResp CardsResponse if err := json.NewDecoder(resp.Body).Decode(&cardsResp); err != nil { @@ -553,7 +553,7 @@ func (c *Client) SaveCard(params SaveCardRequest) (*models.Card, error) { if err != nil { return nil, fmt.Errorf("failed to save card: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var cardResp CardResponse if err := json.NewDecoder(resp.Body).Decode(&cardResp); err != nil { @@ -590,7 +590,7 @@ func (c *Client) CollectCard() (string, error) { if err != nil { return "", fmt.Errorf("failed to collect card: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var collectResp CollectCardResponse if err := json.NewDecoder(resp.Body).Decode(&collectResp); err != nil { @@ -619,7 +619,7 @@ func (c *Client) DeleteCard(id uint) error { if err != nil { return fmt.Errorf("failed to delete card: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("delete card failed with status %d", resp.StatusCode) @@ -648,7 +648,7 @@ func (c *Client) GetOrders() ([]models.Order, error) { if err != nil { return nil, fmt.Errorf("failed to fetch orders: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var ordersResp OrderResponse if err := json.NewDecoder(resp.Body).Decode(&ordersResp); err != nil { @@ -686,7 +686,7 @@ func (c *Client) GetAddresses() ([]models.Address, error) { return nil, fmt.Errorf("failed to fetch addresses: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var addrResp AddressResponse if err := json.NewDecoder(resp.Body).Decode(&addrResp); err != nil { @@ -732,7 +732,7 @@ func (c *Client) CreateAddress(addrReq CreateAddressRequest) (*models.Address, e if err != nil { return nil, fmt.Errorf("failed to create address: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var result struct { Success bool `json:"success"` @@ -769,7 +769,7 @@ func (c *Client) DeleteAddress(id uint) error { if err != nil { return fmt.Errorf("failed to delete address: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("delete address failed with status %d", resp.StatusCode) @@ -804,7 +804,7 @@ func (c *Client) GetViewInit() (ViewInitData, error) { if err != nil { return ViewInitData{}, fmt.Errorf("view init request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return ViewInitData{}, fmt.Errorf("view init returned status: %d", resp.StatusCode) } diff --git a/pkg/auth/ssh_auth_test.go b/pkg/auth/ssh_auth_test.go index c889856..cf887dd 100644 --- a/pkg/auth/ssh_auth_test.go +++ b/pkg/auth/ssh_auth_test.go @@ -17,7 +17,7 @@ import ( func setupAuthTestDB(t *testing.T) string { t.Helper() testDB := "test_ssh_auth.db" - os.Remove(testDB) + _ = os.Remove(testDB) database.ResetForTesting() db, err := database.Connect(testDB) @@ -47,7 +47,7 @@ func newTestSSHKey(t *testing.T) gossh.PublicKey { func TestAuthenticateSSHKeyNewUser(t *testing.T) { testDB := setupAuthTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -72,7 +72,7 @@ func TestAuthenticateSSHKeyNewUser(t *testing.T) { func TestAuthenticateSSHKeyExistingUser(t *testing.T) { testDB := setupAuthTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -96,7 +96,7 @@ func TestAuthenticateSSHKeyExistingUser(t *testing.T) { func TestRegisterWithSSHKey(t *testing.T) { testDB := setupAuthTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -121,7 +121,7 @@ func TestRegisterWithSSHKey(t *testing.T) { func TestRegisterWithSSHKeyDuplicate(t *testing.T) { testDB := setupAuthTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -145,7 +145,7 @@ func TestRegisterWithSSHKeyDuplicate(t *testing.T) { func TestIsSSHKeyRegistered(t *testing.T) { testDB := setupAuthTestDB(t) - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer database.ResetForTesting() db := database.GetDB() @@ -164,7 +164,7 @@ func TestIsSSHKeyRegistered(t *testing.T) { } // After authentication (which auto-creates) - svc.AuthenticateSSHKey(key) + _, _, _ = svc.AuthenticateSSHKey(key) registered, err = svc.IsSSHKeyRegistered(key) if err != nil { diff --git a/pkg/bring/booking.go b/pkg/bring/booking.go index f7cdbba..9b89eb4 100644 --- a/pkg/bring/booking.go +++ b/pkg/bring/booking.go @@ -197,7 +197,7 @@ func (c *BookingClient) doJSON(ctx context.Context, url string, body any, out an if err != nil { return fmt.Errorf("request: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("read response: %w", err) diff --git a/pkg/bring/bring.go b/pkg/bring/bring.go index 5d8db7c..659c567 100644 --- a/pkg/bring/bring.go +++ b/pkg/bring/bring.go @@ -110,7 +110,7 @@ func (c *Client) ValidateAddress(ctx context.Context, addr Address) (*Address, e if err != nil { return nil, fmt.Errorf("bring request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) if err != nil { diff --git a/pkg/database/db.go b/pkg/database/db.go index c134e59..78fe60d 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -101,7 +101,7 @@ func Close() error { // WARNING: Only use this in tests! func ResetForTesting() { if db != nil { - Close() + _ = Close() db = nil once = sync.Once{} } diff --git a/pkg/database/db_test.go b/pkg/database/db_test.go index 94f511f..1fcf301 100644 --- a/pkg/database/db_test.go +++ b/pkg/database/db_test.go @@ -10,7 +10,7 @@ import ( func TestConnect(t *testing.T) { // Use a temporary test database testDB := "test_terminalshop.db" - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer ResetForTesting() db, err := Connect(testDB) @@ -35,7 +35,7 @@ func TestConnect(t *testing.T) { func TestMigrate(t *testing.T) { testDB := "test_migrate.db" - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer ResetForTesting() db, err := Connect(testDB) @@ -57,7 +57,7 @@ func TestMigrate(t *testing.T) { func TestSeed(t *testing.T) { testDB := "test_seed.db" - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer ResetForTesting() db, err := Connect(testDB) @@ -115,7 +115,7 @@ func TestSeed(t *testing.T) { func TestGetDB(t *testing.T) { testDB := "test_getdb.db" - defer os.Remove(testDB) + defer func() { _ = os.Remove(testDB) }() defer ResetForTesting() // Initialize connection diff --git a/pkg/notify/slack.go b/pkg/notify/slack.go index 2e9c364..65034d4 100644 --- a/pkg/notify/slack.go +++ b/pkg/notify/slack.go @@ -32,7 +32,7 @@ func postSlackMessage(token string, payload map[string]any) (string, bool) { slog.Warn("slack post failed", "error", err) return "", false } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var parsed struct { Ok bool `json:"ok"` @@ -66,7 +66,7 @@ func slackPinCall(token, endpoint, channel, ts, benignErr string) bool { slog.Warn("slack pin call failed", "endpoint", endpoint, "error", err) return false } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var parsed struct { Ok bool `json:"ok"` diff --git a/pkg/shippo/shippo.go b/pkg/shippo/shippo.go index b224cd2..2ae9139 100644 --- a/pkg/shippo/shippo.go +++ b/pkg/shippo/shippo.go @@ -270,7 +270,7 @@ func (c *Client) doJSON(ctx context.Context, method, path string, body any, out if err != nil { return fmt.Errorf("request: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("read response: %w", err) @@ -432,7 +432,7 @@ func (c *Client) ValidateAddress(ctx context.Context, addr Address) (*Address, e if err != nil { return nil, fmt.Errorf("shippo request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) if err != nil { diff --git a/pkg/tui/account.go b/pkg/tui/account.go index f34a691..76a13a1 100644 --- a/pkg/tui/account.go +++ b/pkg/tui/account.go @@ -324,7 +324,8 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) { switch selectedItem { case "order history", "active orders": if len(m.currentOrderList()) > 0 { - if m.account.orderViewState == 0 { + switch m.account.orderViewState { + case 0: m.account.orderViewState = 1 m.account.orderCursor = 0 m.account.detailViewport.GotoTop() @@ -334,8 +335,7 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) { {key: "esc", value: "back"}, {key: "q", value: "quit"}, } - } else if m.account.orderViewState == 1 { - // Save scroll position before entering detail view + case 1: m.account.orderYOffset = m.account.detailViewport.YOffset m.account.orderViewState = 2 m.account.detailViewport.GotoTop() diff --git a/pkg/tui/account_orders.go b/pkg/tui/account_orders.go index d14085b..44aaf7d 100644 --- a/pkg/tui/account_orders.go +++ b/pkg/tui/account_orders.go @@ -248,4 +248,3 @@ func (m Model) displayStyle(k models.DisplayKind) lipgloss.Style { } return m.theme.TextHighlight() } - diff --git a/pkg/tui/confirmation.go b/pkg/tui/confirmation.go index 42fcfe6..f6c0a8c 100644 --- a/pkg/tui/confirmation.go +++ b/pkg/tui/confirmation.go @@ -145,18 +145,18 @@ func (m Model) generateReviewContent() string { if m.ShippingInfo.Street2 != "" { view.WriteString(m.ShippingInfo.Street2 + "\n") } - view.WriteString(fmt.Sprintf("%s, %s %s, %s\n", m.ShippingInfo.City, m.ShippingInfo.State, m.ShippingInfo.Zip, m.ShippingInfo.Country)) + fmt.Fprintf(&view, "%s, %s %s, %s\n", m.ShippingInfo.City, m.ShippingInfo.State, m.ShippingInfo.Zip, m.ShippingInfo.Country) } view.WriteString("\n") if m.SelectedCard != nil { - view.WriteString(fmt.Sprintf("cc: **** **** **** %s\n", m.SelectedCard.Last4)) + fmt.Fprintf(&view, "cc: **** **** **** %s\n", m.SelectedCard.Last4) } view.WriteString("\n") subtotal := m.CalculateSubtotal() - view.WriteString(fmt.Sprintf("subtotal: %s\n", formatUSD(subtotal))) + fmt.Fprintf(&view, "subtotal: %s\n", formatUSD(subtotal)) view.WriteString(m.theme.TextAccent().Render(fmt.Sprintf("total: %s", formatUSD(subtotal))) + "\n") view.WriteString("\n") view.WriteString(m.theme.TextBrand().Render("press enter to confirm") + "\n") diff --git a/pkg/tui/model.go b/pkg/tui/model.go index 9ffda67..9427eb1 100644 --- a/pkg/tui/model.go +++ b/pkg/tui/model.go @@ -400,23 +400,6 @@ func (m Model) fetchCartCmd() tea.Msg { return CartFetchedMsg{Cart: cart, Err: err} } -// fetchProductsCmd fetches products from the API -func (m Model) fetchProductsCmd() tea.Msg { - if m.APIClient == nil { - log.Println("[TUI] APIClient is nil, using fallback products") - return ProductsMsg{Err: nil} // No API client, use fallback - } - - log.Printf("[TUI] Fetching products from API at %s", m.APIClient.BaseURL) - products, err := m.APIClient.GetProducts() - if err != nil { - log.Printf("[TUI] Failed to fetch products: %v", err) - } else { - log.Printf("[TUI] Loaded %d products from API", len(products)) - } - return ProductsMsg{Products: products, Err: err} -} - func (m Model) Init() tea.Cmd { return tea.Batch( m.authCmd, @@ -670,58 +653,6 @@ func (m Model) syncCartItemCmd(coffeeID uint, quantity int) (Model, tea.Cmd) { } } -// saveCardAndConvert tokenizes the card locally using the Stripe publishable -// key (raw card data never reaches the backend), saves the token, then converts the cart -func (m Model) saveCardAndConvert(form PaymentFormCompleteMsg) tea.Cmd { - return func() tea.Msg { - if m.APIClient == nil { - return CheckoutResultMsg{Err: fmt.Errorf("API client not available")} - } - - stripe.Key = m.StripePublicKey - tok, err := stripetoken.New(&stripe.TokenParams{ - Card: &stripe.CardParams{ - Name: stripe.String(form.CardName), - Number: stripe.String(form.CardNumber), - ExpMonth: stripe.String(form.ExpiryMonth), - ExpYear: stripe.String(form.ExpiryYear), - CVC: stripe.String(form.CVC), - AddressZip: stripe.String(form.BillingZip), - }, - }) - if err != nil { - if stripeErr, ok := err.(*stripe.Error); ok { - return CheckoutResultMsg{Err: fmt.Errorf("%s", stripeErr.Msg)} - } - return CheckoutResultMsg{Err: fmt.Errorf("failed to tokenize card: %w", err)} - } - card, err := m.APIClient.SaveCard(api.SaveCardRequest{Token: tok.ID}) - if err != nil { - return CheckoutResultMsg{Err: fmt.Errorf("failed to save card: %w", err)} - } - - if err := m.resolveCartAddress(); err != nil { - return CheckoutResultMsg{Err: err} - } - - // 3. Set the card on the cart - if err := m.APIClient.SetCartCard(card.ID); err != nil { - return CheckoutResultMsg{Err: fmt.Errorf("failed to set card: %w", err)} - } - - // 4. Convert the cart to an order - order, err := m.APIClient.ConvertCart() - if err != nil { - return CheckoutResultMsg{Err: err} - } - - return CheckoutResultMsg{ - OrderID: order.ID, - Total: order.Total, - } - } -} - func (m Model) saveCardOnlyCmd(form PaymentFormCompleteMsg) tea.Cmd { return func() tea.Msg { if m.APIClient == nil { diff --git a/pkg/utils/response.go b/pkg/utils/response.go index c10a775..04615e4 100644 --- a/pkg/utils/response.go +++ b/pkg/utils/response.go @@ -35,7 +35,7 @@ func RespondSuccess(w http.ResponseWriter, statusCode int, data interface{}) { Data: data, } - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) } // RespondError sends an error JSON response @@ -52,7 +52,7 @@ func RespondError(w http.ResponseWriter, statusCode int, code, message string, d }, } - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) } // HealthData represents health check data diff --git a/pkg/validate/validate.go b/pkg/validate/validate.go index f771fad..8bcff01 100644 --- a/pkg/validate/validate.go +++ b/pkg/validate/validate.go @@ -66,7 +66,7 @@ func CcnValidator(cardNumber string) error { func IsDigits(name string) ErrorHandler { return func(str string) error { for _, c := range str { - if !(c >= '0' && c <= '9') { + if c < '0' || c > '9' { return fmt.Errorf("[%s] expected only digits but got %s", name, str) } }