test: add finance handler, service, repository tests
All checks were successful
CI / ci (push) Successful in 36s
All checks were successful
CI / ci (push) Successful in 36s
This commit is contained in:
232
internal/handler/finance_test.go
Normal file
232
internal/handler/finance_test.go
Normal file
@@ -0,0 +1,232 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user