test: add finance handler, service, repository tests
All checks were successful
CI / ci (push) Successful in 36s

This commit is contained in:
Cosmo
2026-03-01 05:12:07 +00:00
parent 23939ccc92
commit e782367ef0
4 changed files with 273 additions and 0 deletions

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

View File

@@ -0,0 +1,24 @@
package repository
import (
"testing"
)
func TestNewFinanceRepository(t *testing.T) {
repo := NewFinanceRepository(nil)
if repo == nil {
t.Error("expected non-nil FinanceRepository")
}
if repo.db != nil {
t.Error("expected nil db")
}
}
func TestFinanceCategoryErrors(t *testing.T) {
if ErrFinanceCategoryNotFound.Error() != "finance category not found" {
t.Errorf("unexpected error message: %s", ErrFinanceCategoryNotFound.Error())
}
if ErrFinanceTransactionNotFound.Error() != "finance transaction not found" {
t.Errorf("unexpected error message: %s", ErrFinanceTransactionNotFound.Error())
}
}

View File

@@ -0,0 +1,15 @@
package service
import (
"testing"
)
func TestNewFinanceService(t *testing.T) {
svc := NewFinanceService(nil)
if svc == nil {
t.Error("expected non-nil FinanceService")
}
if svc.repo != nil {
t.Error("expected nil repo")
}
}