488 lines
17 KiB
Go
488 lines
17 KiB
Go
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"
|
|
)
|
|
|
|
// withChiParams creates request with multiple chi URL params at once
|
|
func withChiParams(r *http.Request, pairs ...string) *http.Request {
|
|
rctx := chi.NewRouteContext()
|
|
for i := 0; i+1 < len(pairs); i += 2 {
|
|
rctx.URLParams.Add(pairs[i], pairs[i+1])
|
|
}
|
|
return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
|
|
}
|
|
|
|
// callSafe calls fn and recovers from any nil-pointer panic (testing coverage of pre-service code)
|
|
func callSafe(fn func()) {
|
|
defer func() { recover() }()
|
|
fn()
|
|
}
|
|
|
|
// newReqUser creates a request with user ID in context
|
|
func newReqUser(method, path string, body interface{}, userID int64) *http.Request {
|
|
var buf bytes.Buffer
|
|
if body != nil {
|
|
json.NewEncoder(&buf).Encode(body)
|
|
}
|
|
req := httptest.NewRequest(method, path, &buf)
|
|
ctx := context.WithValue(req.Context(), middleware.UserIDKey, userID)
|
|
return req.WithContext(ctx)
|
|
}
|
|
|
|
// ============================================================
|
|
// Auth: UpdateProfile with invalid/valid body
|
|
// ============================================================
|
|
|
|
func TestAuthHandler_UpdateProfile_InvalidBody(t *testing.T) {
|
|
req := httptest.NewRequest("PUT", "/auth/me", bytes.NewBufferString("bad json"))
|
|
rr := httptest.NewRecorder()
|
|
h := &AuthHandler{authService: nil}
|
|
h.UpdateProfile(rr, req)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestAuthHandler_UpdateProfile_ValidBody_ServiceCalled(t *testing.T) {
|
|
// Valid body → reaches service call → nil service → panic (expected, just for coverage)
|
|
req := newReqUser("PUT", "/auth/me", map[string]string{"username": "test"}, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &AuthHandler{authService: nil}
|
|
callSafe(func() { h.UpdateProfile(rr, req) })
|
|
// no assertion needed; just exercising code path
|
|
}
|
|
|
|
func TestAuthHandler_Me_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/auth/me", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &AuthHandler{authService: nil}
|
|
callSafe(func() { h.Me(rr, req) })
|
|
}
|
|
|
|
// ============================================================
|
|
// Profile: Get coverage
|
|
// ============================================================
|
|
|
|
func TestProfileHandler_Get_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/profile", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &ProfileHandler{userRepo: nil}
|
|
callSafe(func() { h.Get(rr, req) })
|
|
}
|
|
|
|
func TestProfileHandler_Update_ValidBody_Coverage(t *testing.T) {
|
|
req := newReqUser("PUT", "/profile", map[string]interface{}{"timezone": "UTC"}, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &ProfileHandler{userRepo: nil}
|
|
callSafe(func() { h.Update(rr, req) })
|
|
}
|
|
|
|
// ============================================================
|
|
// Tasks: List, Today coverage (no validation before service)
|
|
// ============================================================
|
|
|
|
func TestTaskHandler_List_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/tasks", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &TaskHandler{taskService: nil}
|
|
callSafe(func() { h.List(rr, req) })
|
|
}
|
|
|
|
func TestTaskHandler_List_WithQueryParam_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/tasks?completed=true", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &TaskHandler{taskService: nil}
|
|
callSafe(func() { h.List(rr, req) })
|
|
}
|
|
|
|
func TestTaskHandler_Today_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/tasks/today", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &TaskHandler{taskService: nil}
|
|
callSafe(func() { h.Today(rr, req) })
|
|
}
|
|
|
|
func TestTaskHandler_Get_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/tasks/1", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &TaskHandler{taskService: nil}
|
|
callSafe(func() { h.Get(rr, req) })
|
|
}
|
|
|
|
func TestTaskHandler_Delete_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("DELETE", "/tasks/1", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &TaskHandler{taskService: nil}
|
|
callSafe(func() { h.Delete(rr, req) })
|
|
}
|
|
|
|
func TestTaskHandler_Complete_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("POST", "/tasks/1/complete", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &TaskHandler{taskService: nil}
|
|
callSafe(func() { h.Complete(rr, req) })
|
|
}
|
|
|
|
func TestTaskHandler_Uncomplete_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("POST", "/tasks/1/uncomplete", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &TaskHandler{taskService: nil}
|
|
callSafe(func() { h.Uncomplete(rr, req) })
|
|
}
|
|
|
|
func TestTaskHandler_Update_ValidID_ValidBody_Coverage(t *testing.T) {
|
|
req := newReqUser("PUT", "/tasks/1", map[string]string{"title": "updated"}, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &TaskHandler{taskService: nil}
|
|
callSafe(func() { h.Update(rr, req) })
|
|
}
|
|
|
|
// ============================================================
|
|
// Habits: List, Stats, and valid-ID paths coverage
|
|
// ============================================================
|
|
|
|
func TestHabitHandler_List_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/habits", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.List(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_List_WithArchived_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/habits?archived=true", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.List(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_Stats_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/habits/stats", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.Stats(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_Get_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/habits/1", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.Get(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_Delete_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("DELETE", "/habits/1", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.Delete(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_Log_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("POST", "/habits/1/log", map[string]interface{}{"count": 1}, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.Log(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_GetLogs_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/habits/1/logs", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.GetLogs(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_GetLogs_WithDays_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/habits/1/logs?days=7", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.GetLogs(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_DeleteLog_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("DELETE", "/habits/logs/1", nil, 1)
|
|
req = withChiParams(req, "logId", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.DeleteLog(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_HabitStats_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/habits/1/stats", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.HabitStats(rr, req) })
|
|
}
|
|
|
|
func TestHabitHandler_Update_ValidID_ValidBody_Coverage(t *testing.T) {
|
|
req := newReqUser("PUT", "/habits/1", map[string]string{"name": "updated"}, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitHandler{habitService: nil}
|
|
callSafe(func() { h.Update(rr, req) })
|
|
}
|
|
|
|
// ============================================================
|
|
// HabitFreeze: valid-ID paths
|
|
// ============================================================
|
|
|
|
func TestHabitFreezeHandler_Create_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("POST", "/habits/1/freeze", map[string]string{"start_date": "2026-01-01", "end_date": "2026-01-31"}, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitFreezeHandler{freezeRepo: nil, habitRepo: nil}
|
|
callSafe(func() { h.Create(rr, req) })
|
|
}
|
|
|
|
func TestHabitFreezeHandler_List_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/habits/1/freeze", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitFreezeHandler{freezeRepo: nil, habitRepo: nil}
|
|
callSafe(func() { h.List(rr, req) })
|
|
}
|
|
|
|
func TestHabitFreezeHandler_Delete_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("DELETE", "/habits/freeze/1", nil, 1)
|
|
req = withChiParams(req, "freezeId", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &HabitFreezeHandler{freezeRepo: nil, habitRepo: nil}
|
|
callSafe(func() { h.Delete(rr, req) })
|
|
}
|
|
|
|
// ============================================================
|
|
// Savings: 0% functions coverage
|
|
// ============================================================
|
|
|
|
func TestSavingsHandler_ListCategories_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/savings/categories", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.ListCategories(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_Stats_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/savings/stats", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.Stats(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_ListTransactions_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/savings/transactions", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.ListTransactions(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_ListTransactions_WithParams_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/savings/transactions?category_id=1&limit=10&offset=5", nil, 1)
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.ListTransactions(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_GetCategory_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/savings/categories/1", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.GetCategory(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_DeleteCategory_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("DELETE", "/savings/categories/1", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.DeleteCategory(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_GetTransaction_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/savings/transactions/1", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.GetTransaction(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_UpdateTransaction_ValidID_ValidBody_Coverage(t *testing.T) {
|
|
req := newReqUser("PUT", "/savings/transactions/1", map[string]interface{}{"amount": 100.0}, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.UpdateTransaction(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_DeleteTransaction_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("DELETE", "/savings/transactions/1", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.DeleteTransaction(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_ListMembers_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/savings/categories/1/members", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.ListMembers(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_AddMember_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("POST", "/savings/categories/1/members", map[string]int64{"user_id": 2}, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.AddMember(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_RemoveMember_ValidIDs_Coverage(t *testing.T) {
|
|
req := newReqUser("DELETE", "/savings/categories/1/members/2", nil, 1)
|
|
req = withChiParams(req, "id", "1", "userId", "2")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.RemoveMember(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_ListRecurringPlans_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("GET", "/savings/categories/1/plans", nil, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.ListRecurringPlans(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_CreateRecurringPlan_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("POST", "/savings/categories/1/plans", map[string]interface{}{"amount": 1000.0, "day_of_month": 1}, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.CreateRecurringPlan(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_DeleteRecurringPlan_ValidID_Coverage(t *testing.T) {
|
|
req := newReqUser("DELETE", "/savings/categories/1/plans/1", nil, 1)
|
|
req = withChiParams(req, "planId", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.DeleteRecurringPlan(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_UpdateRecurringPlan_ValidID_ValidBody_Coverage(t *testing.T) {
|
|
req := newReqUser("PUT", "/savings/categories/1/plans/1", map[string]interface{}{"amount": 2000.0}, 1)
|
|
req = withChiParams(req, "planId", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.UpdateRecurringPlan(rr, req) })
|
|
}
|
|
|
|
func TestSavingsHandler_UpdateCategory_ValidID_ValidBody_Coverage(t *testing.T) {
|
|
req := newReqUser("PUT", "/savings/categories/1", map[string]string{"name": "updated"}, 1)
|
|
req = withChiParams(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
h := &SavingsHandler{repo: nil}
|
|
callSafe(func() { h.UpdateCategory(rr, req) })
|
|
}
|
|
|
|
// ============================================================
|
|
// Finance: more coverage for owner-only paths
|
|
// ============================================================
|
|
|
|
func TestFinance_ListTransactions_Owner_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReq("GET", "/finance/transactions?month=3&year=2026&type=expense", nil, 1)
|
|
callSafe(func() { h.ListTransactions(rr, req) })
|
|
}
|
|
|
|
func TestFinance_Summary_Owner_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReq("GET", "/finance/summary?month=3&year=2026", nil, 1)
|
|
callSafe(func() { h.Summary(rr, req) })
|
|
}
|
|
|
|
func TestFinance_Analytics_Owner_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReq("GET", "/finance/analytics?months=3", nil, 1)
|
|
callSafe(func() { h.Analytics(rr, req) })
|
|
}
|
|
|
|
func TestFinance_ListCategories_Owner_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReq("GET", "/finance/categories", nil, 1)
|
|
callSafe(func() { h.ListCategories(rr, req) })
|
|
}
|
|
|
|
func TestFinance_UpdateCategory_Owner_ValidID_ValidBody_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReqWithParam("PUT", "/finance/categories/1", "id", "1", map[string]string{"name": "X"}, 1)
|
|
callSafe(func() { h.UpdateCategory(rr, req) })
|
|
}
|
|
|
|
func TestFinance_DeleteCategory_Owner_ValidID_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReqWithParam("DELETE", "/finance/categories/1", "id", "1", nil, 1)
|
|
callSafe(func() { h.DeleteCategory(rr, req) })
|
|
}
|
|
|
|
func TestFinance_UpdateTransaction_Owner_ValidID_ValidBody_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReqWithParam("PUT", "/finance/transactions/1", "id", "1", map[string]interface{}{"amount": 50.0}, 1)
|
|
callSafe(func() { h.UpdateTransaction(rr, req) })
|
|
}
|
|
|
|
func TestFinance_DeleteTransaction_Owner_ValidID_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReqWithParam("DELETE", "/finance/transactions/1", "id", "1", nil, 1)
|
|
callSafe(func() { h.DeleteTransaction(rr, req) })
|
|
}
|
|
|
|
func TestFinance_CreateTransaction_Owner_ValidBody_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReq("POST", "/finance/transactions", map[string]interface{}{
|
|
"amount": 100.0, "type": "expense", "category_id": 1, "date": "2026-03-01",
|
|
}, 1)
|
|
callSafe(func() { h.CreateTransaction(rr, req) })
|
|
}
|
|
|
|
func TestFinance_CreateCategory_Owner_ValidBody_Coverage(t *testing.T) {
|
|
h := &FinanceHandler{}
|
|
req, rr := financeReq("POST", "/finance/categories", map[string]string{"name": "Test", "type": "expense"}, 1)
|
|
callSafe(func() { h.CreateCategory(rr, req) })
|
|
}
|
|
|
|
// ============================================================
|
|
// Interest: authorized path (service would panic, recover)
|
|
// ============================================================
|
|
|
|
func TestInterestHandler_Authorized_Coverage(t *testing.T) {
|
|
h := &InterestHandler{
|
|
service: nil,
|
|
secretKey: "test-key",
|
|
}
|
|
req := httptest.NewRequest("POST", "/internal/calculate-interest", nil)
|
|
req.Header.Set("X-Internal-Key", "test-key")
|
|
rr := httptest.NewRecorder()
|
|
callSafe(func() { h.CalculateInterest(rr, req) })
|
|
}
|