Files
honeyDueAPI/internal/handlers/residence_handler_test.go
T
Trey t 65a9aae4e5
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Migrate TaskService + ResidenceService to ctx-aware repos
Every public method on TaskService and ResidenceService now takes
ctx context.Context as the first arg and routes its repo calls through
.WithContext(ctx). With otelgorm registered, this means every API
endpoint backed by these two services produces a flame graph in Jaeger
where the SQL spans nest under the parent HTTP request span — instead
of appearing as orphaned queries.

Endpoints now fully traced (HTTP → service → SQL):
- GET    /api/tasks/                       (already shipped)
- GET    /api/tasks/by-residence/:id/      (already shipped)
- GET    /api/tasks/:id/
- POST   /api/tasks/
- POST   /api/tasks/bulk/
- PUT    /api/tasks/:id/
- DELETE /api/tasks/:id/
- POST   /api/tasks/:id/in-progress/
- POST   /api/tasks/:id/cancel/
- POST   /api/tasks/:id/uncancel/
- POST   /api/tasks/:id/archive/
- POST   /api/tasks/:id/unarchive/
- POST   /api/tasks/:id/complete/
- POST   /api/tasks/:id/quick-complete/
- GET    /api/tasks/completions/* (CRUD)
- GET    /api/static_data/ (categories, priorities, frequencies)
- GET    /api/residences/
- GET    /api/residences/my/
- GET    /api/residences/summary/
- GET    /api/residences/:id/
- POST   /api/residences/
- PUT    /api/residences/:id/
- DELETE /api/residences/:id/
- Share-code + member management endpoints
- GET    /api/residences/:id/report/

Mechanical work: ~50 method signatures, ~80 handler call sites,
~25 test call sites updated. Internal sendTaskCompletedNotification
helper also takes ctx so background notification SQL nests correctly.

The remaining services (ContractorService, DocumentService,
AuthService, NotificationService, SubscriptionService) follow the same
pattern; they continue to emit untraced SQL until migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:04:01 -05:00

732 lines
26 KiB
Go

package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/labstack/echo/v4"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/treytartt/honeydue-api/internal/config"
"github.com/treytartt/honeydue-api/internal/dto/requests"
"github.com/treytartt/honeydue-api/internal/repositories"
"github.com/treytartt/honeydue-api/internal/services"
"github.com/treytartt/honeydue-api/internal/testutil"
"gorm.io/gorm"
)
func setupResidenceHandler(t *testing.T) (*ResidenceHandler, *echo.Echo, *gorm.DB) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg)
handler := NewResidenceHandler(residenceService, nil, nil, true)
e := testutil.SetupTestRouter()
return handler, e, db
}
func TestResidenceHandler_CreateResidence(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/", handler.CreateResidence)
t.Run("successful creation", func(t *testing.T) {
req := requests.CreateResidenceRequest{
Name: "My House",
StreetAddress: "123 Main St",
City: "Austin",
StateProvince: "TX",
PostalCode: "78701",
}
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusCreated)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Response should be wrapped in WithSummaryResponse
assert.Contains(t, response, "data")
assert.Contains(t, response, "summary")
residenceData := response["data"].(map[string]interface{})
assert.Equal(t, "My House", residenceData["name"])
assert.Equal(t, "123 Main St", residenceData["street_address"])
assert.Equal(t, "Austin", residenceData["city"])
assert.Equal(t, "TX", residenceData["state_province"])
assert.Equal(t, "78701", residenceData["postal_code"])
assert.Equal(t, "USA", residenceData["country"]) // Default
assert.Equal(t, true, residenceData["is_primary"])
})
t.Run("creation with optional fields", func(t *testing.T) {
bedrooms := 3
bathrooms := decimal.NewFromFloat(2.5)
sqft := 2000
isPrimary := false
req := requests.CreateResidenceRequest{
Name: "Second House",
StreetAddress: "456 Oak Ave",
City: "Dallas",
StateProvince: "TX",
PostalCode: "75001",
Country: "USA",
Bedrooms: &bedrooms,
Bathrooms: &bathrooms,
SquareFootage: &sqft,
IsPrimary: &isPrimary,
}
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusCreated)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Response should be wrapped in WithSummaryResponse
assert.Contains(t, response, "data")
assert.Contains(t, response, "summary")
residenceData := response["data"].(map[string]interface{})
assert.Equal(t, float64(3), residenceData["bedrooms"])
assert.Equal(t, "2.5", residenceData["bathrooms"]) // Decimal serializes as string
assert.Equal(t, float64(2000), residenceData["square_footage"])
// Note: first residence becomes primary by default even if is_primary=false is specified
assert.Contains(t, []interface{}{true, false}, residenceData["is_primary"])
})
t.Run("creation with missing required fields", func(t *testing.T) {
// Only name is required; address fields are optional
req := map[string]string{
// Missing name - this is required
}
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
}
func TestResidenceHandler_GetResidence(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.GET("/:id/", handler.GetResidence)
otherAuthGroup := e.Group("/api/other-residences")
otherAuthGroup.Use(testutil.MockAuthMiddleware(otherUser))
otherAuthGroup.GET("/:id/", handler.GetResidence)
t.Run("get own residence", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/residences/%d/", residence.ID), nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, "Test House", response["name"])
assert.Equal(t, float64(residence.ID), response["id"])
})
t.Run("get residence with invalid ID", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", "/api/residences/invalid/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
t.Run("get non-existent residence", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", "/api/residences/9999/", nil, "test-token")
// Returns 403 (access denied) rather than 404 to not reveal whether an ID exists
testutil.AssertStatusCode(t, w, http.StatusForbidden)
})
t.Run("access denied for other user", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/other-residences/%d/", residence.ID), nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusForbidden)
})
}
func TestResidenceHandler_ListResidences(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
testutil.CreateTestResidence(t, db, user.ID, "House 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.GET("/", handler.ListResidences)
t.Run("list residences", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", "/api/residences/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response []map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Len(t, response, 2)
})
}
func TestResidenceHandler_UpdateResidence(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Original Name")
// Share with user
residenceRepo := repositories.NewResidenceRepository(db)
residenceRepo.AddUser(residence.ID, sharedUser.ID)
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.PUT("/:id/", handler.UpdateResidence)
sharedGroup := e.Group("/api/shared-residences")
sharedGroup.Use(testutil.MockAuthMiddleware(sharedUser))
sharedGroup.PUT("/:id/", handler.UpdateResidence)
t.Run("owner can update", func(t *testing.T) {
newName := "Updated Name"
newCity := "Dallas"
req := requests.UpdateResidenceRequest{
Name: &newName,
City: &newCity,
}
w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/residences/%d/", residence.ID), req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Response should be wrapped in WithSummaryResponse
assert.Contains(t, response, "data")
assert.Contains(t, response, "summary")
residenceData := response["data"].(map[string]interface{})
assert.Equal(t, "Updated Name", residenceData["name"])
assert.Equal(t, "Dallas", residenceData["city"])
})
t.Run("shared user cannot update", func(t *testing.T) {
newName := "Hacked Name"
req := requests.UpdateResidenceRequest{
Name: &newName,
}
w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/shared-residences/%d/", residence.ID), req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusForbidden)
})
}
func TestResidenceHandler_DeleteResidence(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "To Delete")
residenceRepo := repositories.NewResidenceRepository(db)
residenceRepo.AddUser(residence.ID, sharedUser.ID)
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.DELETE("/:id/", handler.DeleteResidence)
sharedGroup := e.Group("/api/shared-residences")
sharedGroup.Use(testutil.MockAuthMiddleware(sharedUser))
sharedGroup.DELETE("/:id/", handler.DeleteResidence)
t.Run("shared user cannot delete", func(t *testing.T) {
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/shared-residences/%d/", residence.ID), nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusForbidden)
})
t.Run("owner can delete", func(t *testing.T) {
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/residences/%d/", residence.ID), nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Response should be wrapped in WithSummaryResponse
assert.Contains(t, response, "data")
assert.Contains(t, response, "summary")
assert.Contains(t, response["data"], "deleted")
})
}
func TestResidenceHandler_GenerateShareCode(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Share Test")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/:id/generate-share-code/", handler.GenerateShareCode)
t.Run("generate share code", func(t *testing.T) {
req := requests.GenerateShareCodeRequest{
ExpiresInHours: 24,
}
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/residences/%d/generate-share-code/", residence.ID), req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
shareCode := response["share_code"].(map[string]interface{})
code := shareCode["code"].(string)
assert.Len(t, code, 6)
assert.NotEmpty(t, shareCode["expires_at"])
})
}
func TestResidenceHandler_JoinWithCode(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
newUser := testutil.CreateTestUser(t, db, "newuser", "new@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Join Test")
// Generate share code first
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg)
shareResp, _ := residenceService.GenerateShareCode(context.Background(), residence.ID, owner.ID, 24)
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(newUser))
authGroup.POST("/join-with-code/", handler.JoinWithCode)
ownerGroup := e.Group("/api/owner-residences")
ownerGroup.Use(testutil.MockAuthMiddleware(owner))
ownerGroup.POST("/join-with-code/", handler.JoinWithCode)
t.Run("join with valid code", func(t *testing.T) {
req := requests.JoinWithCodeRequest{
Code: shareResp.ShareCode.Code,
}
w := testutil.MakeRequest(e, "POST", "/api/residences/join-with-code/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// JoinResidenceResponse includes summary
assert.Contains(t, response, "residence")
assert.Contains(t, response, "summary")
residenceResp := response["residence"].(map[string]interface{})
assert.Equal(t, "Join Test", residenceResp["name"])
})
t.Run("owner tries to join own residence", func(t *testing.T) {
// Generate new code
shareResp2, _ := residenceService.GenerateShareCode(context.Background(), residence.ID, owner.ID, 24)
req := requests.JoinWithCodeRequest{
Code: shareResp2.ShareCode.Code,
}
w := testutil.MakeRequest(e, "POST", "/api/owner-residences/join-with-code/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusConflict)
})
t.Run("join with invalid code", func(t *testing.T) {
req := requests.JoinWithCodeRequest{
Code: "ABCDEF", // Valid length (6) but non-existent code
}
w := testutil.MakeRequest(e, "POST", "/api/residences/join-with-code/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusNotFound)
})
}
func TestResidenceHandler_GetResidenceUsers(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Users Test")
residenceRepo := repositories.NewResidenceRepository(db)
residenceRepo.AddUser(residence.ID, sharedUser.ID)
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(owner))
authGroup.GET("/:id/users/", handler.GetResidenceUsers)
t.Run("get residence users", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/residences/%d/users/", residence.ID), nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response []map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Len(t, response, 2) // owner + shared user
})
}
func TestResidenceHandler_RemoveUser(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Remove Test")
residenceRepo := repositories.NewResidenceRepository(db)
residenceRepo.AddUser(residence.ID, sharedUser.ID)
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(owner))
authGroup.DELETE("/:id/users/:user_id/", handler.RemoveResidenceUser)
t.Run("remove shared user", func(t *testing.T) {
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/residences/%d/users/%d/", residence.ID, sharedUser.ID), nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
response := testutil.ParseJSON(t, w.Body.Bytes())
assert.Contains(t, response["message"], "removed")
})
t.Run("cannot remove owner", func(t *testing.T) {
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/residences/%d/users/%d/", residence.ID, owner.ID), nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
}
func TestResidenceHandler_GetResidenceTypes(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
testutil.SeedLookupData(t, db)
user := testutil.CreateTestUser(t, db, "user", "user@test.com", "password")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.GET("/types/", handler.GetResidenceTypes)
t.Run("get residence types", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", "/api/residences/types/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response []map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Greater(t, len(response), 0)
})
}
func TestResidenceHandler_JSONResponses(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/", handler.CreateResidence)
authGroup.GET("/", handler.ListResidences)
t.Run("residence response has correct JSON structure", func(t *testing.T) {
req := requests.CreateResidenceRequest{
Name: "JSON Test House",
StreetAddress: "123 Test St",
City: "Austin",
StateProvince: "TX",
PostalCode: "78701",
}
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusCreated)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Response should be wrapped in WithSummaryResponse
assert.Contains(t, response, "data")
assert.Contains(t, response, "summary")
residenceData := response["data"].(map[string]interface{})
// Required fields in residence data
assert.Contains(t, residenceData, "id")
assert.Contains(t, residenceData, "name")
assert.Contains(t, residenceData, "street_address")
assert.Contains(t, residenceData, "city")
assert.Contains(t, residenceData, "state_province")
assert.Contains(t, residenceData, "postal_code")
assert.Contains(t, residenceData, "country")
assert.Contains(t, residenceData, "is_primary")
assert.Contains(t, residenceData, "is_active")
assert.Contains(t, residenceData, "created_at")
assert.Contains(t, residenceData, "updated_at")
// Type checks
assert.IsType(t, float64(0), residenceData["id"])
assert.IsType(t, "", residenceData["name"])
assert.IsType(t, true, residenceData["is_primary"])
// Summary should have expected fields
summary := response["summary"].(map[string]interface{})
assert.Contains(t, summary, "total_residences")
assert.Contains(t, summary, "total_tasks")
})
t.Run("list response returns array", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", "/api/residences/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response []map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Response should be an array of residences
assert.IsType(t, []map[string]interface{}{}, response)
})
}
func TestResidenceHandler_CreateResidence_NegativeBedrooms_Returns400(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/", handler.CreateResidence)
t.Run("negative bedrooms rejected", func(t *testing.T) {
bedrooms := -1
req := requests.CreateResidenceRequest{
Name: "Bad House",
Bedrooms: &bedrooms,
}
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
t.Run("negative square footage rejected", func(t *testing.T) {
sqft := -100
req := requests.CreateResidenceRequest{
Name: "Bad House",
SquareFootage: &sqft,
}
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
t.Run("zero bedrooms accepted", func(t *testing.T) {
bedrooms := 0
req := requests.CreateResidenceRequest{
Name: "Studio Apartment",
Bedrooms: &bedrooms,
}
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusCreated)
})
}
func TestResidenceHandler_GetMyResidences(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
testutil.CreateTestResidence(t, db, user.ID, "House 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.GET("/my-residences/", handler.GetMyResidences)
t.Run("successful my residences", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", "/api/residences/my-residences/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
// GetMyResidences returns MyResidencesResponse: {"residences": [...]}
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
residences := response["residences"].([]interface{})
assert.Len(t, residences, 2)
})
t.Run("user with no residences returns empty", func(t *testing.T) {
noResUser := testutil.CreateTestUser(t, db, "nores", "nores@test.com", "Password123")
e2 := testutil.SetupTestRouter()
authGroup2 := e2.Group("/api/residences")
authGroup2.Use(testutil.MockAuthMiddleware(noResUser))
authGroup2.GET("/my-residences/", handler.GetMyResidences)
w := testutil.MakeRequest(e2, "GET", "/api/residences/my-residences/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
// GetMyResidences returns MyResidencesResponse: {"residences": [...] or null}
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
if response["residences"] == nil {
// null residences means no residences
} else {
residences := response["residences"].([]interface{})
assert.Len(t, residences, 0)
}
})
}
func TestResidenceHandler_GetSummary(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
testutil.CreateTestResidence(t, db, user.ID, "House 1")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.GET("/summary/", handler.GetSummary)
t.Run("successful summary", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", "/api/residences/summary/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response, "total_residences")
assert.Contains(t, response, "total_tasks")
})
}
func TestResidenceHandler_UpdateResidence_InvalidID(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.PUT("/:id/", handler.UpdateResidence)
t.Run("invalid id returns 400", func(t *testing.T) {
newName := "Updated"
req := requests.UpdateResidenceRequest{Name: &newName}
w := testutil.MakeRequest(e, "PUT", "/api/residences/invalid/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
t.Run("non-existent id returns 403", func(t *testing.T) {
newName := "Updated"
req := requests.UpdateResidenceRequest{Name: &newName}
w := testutil.MakeRequest(e, "PUT", "/api/residences/9999/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusForbidden)
})
}
func TestResidenceHandler_DeleteResidence_InvalidID(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.DELETE("/:id/", handler.DeleteResidence)
t.Run("invalid id returns 400", func(t *testing.T) {
w := testutil.MakeRequest(e, "DELETE", "/api/residences/invalid/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
t.Run("non-existent id returns 403", func(t *testing.T) {
w := testutil.MakeRequest(e, "DELETE", "/api/residences/9999/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusForbidden)
})
}
func TestResidenceHandler_GetShareCode(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Share Code Test")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.GET("/:id/share-code/", handler.GetShareCode)
t.Run("no share code returns null", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/residences/%d/share-code/", residence.ID), nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Nil(t, response["share_code"])
})
t.Run("invalid id returns 400", func(t *testing.T) {
w := testutil.MakeRequest(e, "GET", "/api/residences/invalid/share-code/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
}
func TestResidenceHandler_GenerateSharePackage(t *testing.T) {
handler, e, db := setupResidenceHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Package Test")
authGroup := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/:id/generate-share-package/", handler.GenerateSharePackage)
t.Run("generate share package", func(t *testing.T) {
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/residences/%d/generate-share-package/", residence.ID), nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response, "share_code")
})
}