65a9aae4e5
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>
732 lines
26 KiB
Go
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")
|
|
})
|
|
}
|