package handler import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/daniil/homelab-api/internal/middleware" ) // helper to create request with user_id in context func financeReq(method, path string, body interface{}, userID int64) (*http.Request, *httptest.ResponseRecorder) { var buf bytes.Buffer if body != nil { json.NewEncoder(&buf).Encode(body) } req := httptest.NewRequest(method, path, &buf) req.Header.Set("Content-Type", "application/json") ctx := context.WithValue(req.Context(), middleware.UserIDKey, userID) req = req.WithContext(ctx) rr := httptest.NewRecorder() return req, rr } func financeReqWithParam(method, path, paramName, paramVal string, body interface{}, userID int64) (*http.Request, *httptest.ResponseRecorder) { req, rr := financeReq(method, path, body, userID) rctx := chi.NewRouteContext() rctx.URLParams.Add(paramName, paramVal) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) return req, rr } // --- Owner-only access tests (403 for non-owner) --- func TestFinance_OwnerOnly_ListCategories(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("GET", "/finance/categories", nil, 999) h.ListCategories(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } func TestFinance_OwnerOnly_CreateCategory(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("POST", "/finance/categories", map[string]string{"name": "Test", "type": "expense"}, 42) h.CreateCategory(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } func TestFinance_OwnerOnly_UpdateCategory(t *testing.T) { h := &FinanceHandler{} req, rr := financeReqWithParam("PUT", "/finance/categories/1", "id", "1", map[string]string{"name": "X"}, 42) h.UpdateCategory(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } func TestFinance_OwnerOnly_DeleteCategory(t *testing.T) { h := &FinanceHandler{} req, rr := financeReqWithParam("DELETE", "/finance/categories/1", "id", "1", nil, 42) h.DeleteCategory(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } func TestFinance_OwnerOnly_ListTransactions(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("GET", "/finance/transactions", nil, 42) h.ListTransactions(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } func TestFinance_OwnerOnly_CreateTransaction(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("POST", "/finance/transactions", map[string]interface{}{"amount": 100, "type": "expense"}, 42) h.CreateTransaction(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } func TestFinance_OwnerOnly_UpdateTransaction(t *testing.T) { h := &FinanceHandler{} req, rr := financeReqWithParam("PUT", "/finance/transactions/1", "id", "1", map[string]interface{}{"amount": 50}, 42) h.UpdateTransaction(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } func TestFinance_OwnerOnly_DeleteTransaction(t *testing.T) { h := &FinanceHandler{} req, rr := financeReqWithParam("DELETE", "/finance/transactions/1", "id", "1", nil, 42) h.DeleteTransaction(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } func TestFinance_OwnerOnly_Summary(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("GET", "/finance/summary", nil, 42) h.Summary(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } func TestFinance_OwnerOnly_Analytics(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("GET", "/finance/analytics", nil, 42) h.Analytics(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("expected 403 for non-owner, got %d", rr.Code) } } // --- Validation tests (owner but bad input) --- func TestFinance_CreateCategory_InvalidJSON(t *testing.T) { h := &FinanceHandler{} req := httptest.NewRequest("POST", "/finance/categories", bytes.NewBufferString("{bad")) req.Header.Set("Content-Type", "application/json") ctx := context.WithValue(req.Context(), middleware.UserIDKey, int64(1)) req = req.WithContext(ctx) rr := httptest.NewRecorder() h.CreateCategory(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", rr.Code) } } func TestFinance_CreateCategory_MissingFields(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("POST", "/finance/categories", map[string]string{"name": "Test"}, 1) h.CreateCategory(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400 for missing type, got %d", rr.Code) } } func TestFinance_CreateCategory_InvalidType(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("POST", "/finance/categories", map[string]string{"name": "Test", "type": "invalid"}, 1) h.CreateCategory(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid type, got %d", rr.Code) } } func TestFinance_CreateTransaction_InvalidJSON(t *testing.T) { h := &FinanceHandler{} req := httptest.NewRequest("POST", "/finance/transactions", bytes.NewBufferString("{bad")) req.Header.Set("Content-Type", "application/json") ctx := context.WithValue(req.Context(), middleware.UserIDKey, int64(1)) req = req.WithContext(ctx) rr := httptest.NewRecorder() h.CreateTransaction(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", rr.Code) } } func TestFinance_CreateTransaction_ZeroAmount(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("POST", "/finance/transactions", map[string]interface{}{ "amount": 0, "type": "expense", "category_id": 1, "date": "2026-03-01", }, 1) h.CreateTransaction(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400 for zero amount, got %d", rr.Code) } } func TestFinance_CreateTransaction_InvalidType(t *testing.T) { h := &FinanceHandler{} req, rr := financeReq("POST", "/finance/transactions", map[string]interface{}{ "amount": 100, "type": "invalid", "category_id": 1, "date": "2026-03-01", }, 1) h.CreateTransaction(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid type, got %d", rr.Code) } } func TestFinance_UpdateCategory_InvalidID(t *testing.T) { h := &FinanceHandler{} req, rr := financeReqWithParam("PUT", "/finance/categories/abc", "id", "abc", map[string]string{"name": "X"}, 1) h.UpdateCategory(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid id, got %d", rr.Code) } } func TestFinance_DeleteCategory_InvalidID(t *testing.T) { h := &FinanceHandler{} req, rr := financeReqWithParam("DELETE", "/finance/categories/abc", "id", "abc", nil, 1) h.DeleteCategory(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid id, got %d", rr.Code) } } func TestFinance_UpdateTransaction_InvalidID(t *testing.T) { h := &FinanceHandler{} req, rr := financeReqWithParam("PUT", "/finance/transactions/abc", "id", "abc", map[string]interface{}{"amount": 50}, 1) h.UpdateTransaction(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid id, got %d", rr.Code) } } func TestFinance_DeleteTransaction_InvalidID(t *testing.T) { h := &FinanceHandler{} req, rr := financeReqWithParam("DELETE", "/finance/transactions/abc", "id", "abc", nil, 1) h.DeleteTransaction(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid id, got %d", rr.Code) } }