Compare commits

...

4 Commits

Author SHA1 Message Date
313f39b0ce docs(comments): add some comments 2026-05-24 12:16:13 +02:00
85b0c53c0a docs(comments): cleanup comments (#3) 2026-05-24 12:02:50 +02:00
366d80a9ee test(dillydallying): testing the ci workflow (#2) 2026-05-23 22:12:46 +02:00
60f52f9851 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
2026-05-23 21:43:30 +02:00
24 changed files with 165 additions and 173 deletions

38
.github/workflows/ci.yml vendored Normal file
View File

@@ -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 ./...

16
.golangci.yml Normal file
View File

@@ -0,0 +1,16 @@
version: "2"
run:
timeout: 5m
linters:
default: none
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
exclusions:
paths:
- terminal-shop-source

View File

@@ -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()

View File

@@ -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)

View File

@@ -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),
})

View File

@@ -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))

View File

@@ -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

View File

@@ -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))

View File

@@ -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()

View File

@@ -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(`<!DOCTYPE html><html><body style="font-family:monospace;text-align:center;padding:4rem"><h2>Card added.</h2><p>You can close this tab.</p></body></html>`))
_, _ = w.Write([]byte(`<!DOCTYPE html><html><body style="font-family:monospace;text-align:center;padding:4rem"><h2>Card added.</h2><p>You can close this tab.</p></body></html>`))
})
r.Handle("/metrics", middleware.MetricsHandler())

View File

@@ -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(&registerResp); 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)
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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{}
}

View File

@@ -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

View File

@@ -16,6 +16,7 @@ import (
var slackClient = &http.Client{Timeout: 5 * time.Second}
// dollars converts cents to dollars
func dollars(cents int) string {
return fmt.Sprintf("$%.2f", float64(cents)/100)
}
@@ -32,7 +33,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 +67,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"`

View File

@@ -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 {

View File

@@ -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()

View File

@@ -248,4 +248,3 @@ func (m Model) displayStyle(k models.DisplayKind) lipgloss.Style {
}
return m.theme.TextHighlight()
}

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
}
}