Files
honeyDueAPI/internal/integration/integration_test.go
Trey t c7dc56e2d2 Rebrand from MyCrib to Casera
- Update Go module from mycrib-api to casera-api
- Update all import statements across 69 Go files
- Update admin panel branding (title, sidebar, login form)
- Update email templates (subjects, bodies, signatures)
- Update PDF report generation branding
- Update Docker container names and network
- Update config defaults (database name, email sender, APNS topic)
- Update README and documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 21:10:48 -06:00

716 lines
24 KiB
Go

package integration
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/treytartt/casera-api/internal/config"
"github.com/treytartt/casera-api/internal/handlers"
"github.com/treytartt/casera-api/internal/middleware"
"github.com/treytartt/casera-api/internal/repositories"
"github.com/treytartt/casera-api/internal/services"
"github.com/treytartt/casera-api/internal/testutil"
"gorm.io/gorm"
)
// TestApp holds all components for integration testing
type TestApp struct {
DB *gorm.DB
Router *gin.Engine
AuthHandler *handlers.AuthHandler
ResidenceHandler *handlers.ResidenceHandler
TaskHandler *handlers.TaskHandler
UserRepo *repositories.UserRepository
ResidenceRepo *repositories.ResidenceRepository
TaskRepo *repositories.TaskRepository
AuthService *services.AuthService
}
func setupIntegrationTest(t *testing.T) *TestApp {
gin.SetMode(gin.TestMode)
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
// Create repositories
userRepo := repositories.NewUserRepository(db)
residenceRepo := repositories.NewResidenceRepository(db)
taskRepo := repositories.NewTaskRepository(db)
// Create config
cfg := &config.Config{
Security: config.SecurityConfig{
SecretKey: "test-secret-key-for-integration-tests",
PasswordResetExpiry: 15 * time.Minute,
ConfirmationExpiry: 24 * time.Hour,
MaxPasswordResetRate: 3,
},
}
// Create services
authService := services.NewAuthService(userRepo, cfg)
residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg)
taskService := services.NewTaskService(taskRepo, residenceRepo)
// Create handlers
authHandler := handlers.NewAuthHandler(authService, nil, nil)
residenceHandler := handlers.NewResidenceHandler(residenceService, nil, nil)
taskHandler := handlers.NewTaskHandler(taskService, nil)
// Create router with real middleware
router := gin.New()
// Public routes
auth := router.Group("/api/auth")
{
auth.POST("/register", authHandler.Register)
auth.POST("/login", authHandler.Login)
}
// Protected routes - use AuthMiddleware without Redis cache for testing
authMiddleware := middleware.NewAuthMiddleware(db, nil)
api := router.Group("/api")
api.Use(authMiddleware.TokenAuth())
{
api.GET("/auth/me", authHandler.CurrentUser)
api.POST("/auth/logout", authHandler.Logout)
residences := api.Group("/residences")
{
residences.GET("", residenceHandler.ListResidences)
residences.POST("", residenceHandler.CreateResidence)
residences.GET("/:id", residenceHandler.GetResidence)
residences.PUT("/:id", residenceHandler.UpdateResidence)
residences.DELETE("/:id", residenceHandler.DeleteResidence)
residences.POST("/:id/generate-share-code", residenceHandler.GenerateShareCode)
residences.GET("/:id/users", residenceHandler.GetResidenceUsers)
residences.DELETE("/:id/users/:userId", residenceHandler.RemoveResidenceUser)
}
api.POST("/residences/join-with-code", residenceHandler.JoinWithCode)
api.GET("/residence-types", residenceHandler.GetResidenceTypes)
tasks := api.Group("/tasks")
{
tasks.GET("", taskHandler.ListTasks)
tasks.POST("", taskHandler.CreateTask)
tasks.GET("/:id", taskHandler.GetTask)
tasks.PUT("/:id", taskHandler.UpdateTask)
tasks.DELETE("/:id", taskHandler.DeleteTask)
tasks.POST("/:id/cancel", taskHandler.CancelTask)
tasks.POST("/:id/uncancel", taskHandler.UncancelTask)
tasks.POST("/:id/archive", taskHandler.ArchiveTask)
tasks.POST("/:id/unarchive", taskHandler.UnarchiveTask)
tasks.POST("/:id/mark-in-progress", taskHandler.MarkInProgress)
}
api.GET("/tasks/by-residence/:residence_id", taskHandler.GetTasksByResidence)
completions := api.Group("/completions")
{
completions.GET("", taskHandler.ListCompletions)
completions.POST("", taskHandler.CreateCompletion)
completions.GET("/:id", taskHandler.GetCompletion)
completions.DELETE("/:id", taskHandler.DeleteCompletion)
}
api.GET("/task-categories", taskHandler.GetCategories)
api.GET("/task-priorities", taskHandler.GetPriorities)
api.GET("/task-statuses", taskHandler.GetStatuses)
api.GET("/task-frequencies", taskHandler.GetFrequencies)
}
return &TestApp{
DB: db,
Router: router,
AuthHandler: authHandler,
ResidenceHandler: residenceHandler,
TaskHandler: taskHandler,
UserRepo: userRepo,
ResidenceRepo: residenceRepo,
TaskRepo: taskRepo,
AuthService: authService,
}
}
// Helper to make authenticated requests
func (app *TestApp) makeAuthenticatedRequest(t *testing.T, method, path string, body interface{}, token string) *httptest.ResponseRecorder {
var reqBody []byte
var err error
if body != nil {
reqBody, err = json.Marshal(body)
require.NoError(t, err)
}
req := httptest.NewRequest(method, path, bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Token "+token)
}
w := httptest.NewRecorder()
app.Router.ServeHTTP(w, req)
return w
}
// Helper to register and login a user, returns token
func (app *TestApp) registerAndLogin(t *testing.T, username, email, password string) string {
// Register
registerBody := map[string]string{
"username": username,
"email": email,
"password": password,
}
w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody, "")
require.Equal(t, http.StatusCreated, w.Code)
// Login
loginBody := map[string]string{
"username": username,
"password": password,
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/login", loginBody, "")
require.Equal(t, http.StatusOK, w.Code)
var loginResp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &loginResp)
require.NoError(t, err)
return loginResp["token"].(string)
}
// ============ Authentication Flow Tests ============
func TestIntegration_AuthenticationFlow(t *testing.T) {
app := setupIntegrationTest(t)
// 1. Register a new user
registerBody := map[string]string{
"username": "testuser",
"email": "test@example.com",
"password": "SecurePass123!",
}
w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody, "")
assert.Equal(t, http.StatusCreated, w.Code)
var registerResp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &registerResp)
require.NoError(t, err)
assert.NotEmpty(t, registerResp["token"])
assert.NotNil(t, registerResp["user"])
// 2. Login with the same credentials
loginBody := map[string]string{
"username": "testuser",
"password": "SecurePass123!",
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/login", loginBody, "")
assert.Equal(t, http.StatusOK, w.Code)
var loginResp map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &loginResp)
require.NoError(t, err)
token := loginResp["token"].(string)
assert.NotEmpty(t, token)
// 3. Get current user with token
w = app.makeAuthenticatedRequest(t, "GET", "/api/auth/me", nil, token)
assert.Equal(t, http.StatusOK, w.Code)
var meResp map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &meResp)
require.NoError(t, err)
assert.Equal(t, "testuser", meResp["username"])
assert.Equal(t, "test@example.com", meResp["email"])
// 4. Access protected route without token should fail
w = app.makeAuthenticatedRequest(t, "GET", "/api/auth/me", nil, "")
assert.Equal(t, http.StatusUnauthorized, w.Code)
// 5. Access protected route with invalid token should fail
w = app.makeAuthenticatedRequest(t, "GET", "/api/auth/me", nil, "invalid-token")
assert.Equal(t, http.StatusUnauthorized, w.Code)
// 6. Logout
w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/logout", nil, token)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestIntegration_RegistrationValidation(t *testing.T) {
app := setupIntegrationTest(t)
tests := []struct {
name string
body map[string]string
expectedStatus int
}{
{
name: "missing username",
body: map[string]string{"email": "test@example.com", "password": "pass123"},
expectedStatus: http.StatusBadRequest,
},
{
name: "missing email",
body: map[string]string{"username": "testuser", "password": "pass123"},
expectedStatus: http.StatusBadRequest,
},
{
name: "missing password",
body: map[string]string{"username": "testuser", "email": "test@example.com"},
expectedStatus: http.StatusBadRequest,
},
{
name: "invalid email",
body: map[string]string{"username": "testuser", "email": "invalid", "password": "pass123"},
expectedStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", tt.body, "")
assert.Equal(t, tt.expectedStatus, w.Code)
})
}
}
func TestIntegration_DuplicateRegistration(t *testing.T) {
app := setupIntegrationTest(t)
// Register first user (password must be >= 8 chars)
registerBody := map[string]string{
"username": "testuser",
"email": "test@example.com",
"password": "password123",
}
w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody, "")
assert.Equal(t, http.StatusCreated, w.Code)
// Try to register with same username - returns 400 (BadRequest)
registerBody2 := map[string]string{
"username": "testuser",
"email": "different@example.com",
"password": "password123",
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody2, "")
assert.Equal(t, http.StatusBadRequest, w.Code)
// Try to register with same email - returns 400 (BadRequest)
registerBody3 := map[string]string{
"username": "differentuser",
"email": "test@example.com",
"password": "password123",
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody3, "")
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// ============ Residence Flow Tests ============
func TestIntegration_ResidenceFlow(t *testing.T) {
app := setupIntegrationTest(t)
token := app.registerAndLogin(t, "owner", "owner@test.com", "password123")
// 1. Create a residence
createBody := map[string]interface{}{
"name": "My House",
"street_address": "123 Main St",
"city": "Austin",
"state_province": "TX",
"postal_code": "78701",
}
w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", createBody, token)
assert.Equal(t, http.StatusCreated, w.Code)
var createResp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &createResp)
require.NoError(t, err)
residenceID := createResp["id"].(float64)
assert.NotZero(t, residenceID)
assert.Equal(t, "My House", createResp["name"])
assert.True(t, createResp["is_primary"].(bool))
// 2. Get the residence
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, token)
assert.Equal(t, http.StatusOK, w.Code)
var getResp map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &getResp)
require.NoError(t, err)
assert.Equal(t, "My House", getResp["name"])
// 3. List residences
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences", nil, token)
assert.Equal(t, http.StatusOK, w.Code)
var listResp []map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &listResp)
require.NoError(t, err)
assert.Len(t, listResp, 1)
// 4. Update the residence
updateBody := map[string]interface{}{
"name": "My Updated House",
"city": "Dallas",
}
w = app.makeAuthenticatedRequest(t, "PUT", "/api/residences/"+formatID(residenceID), updateBody, token)
assert.Equal(t, http.StatusOK, w.Code)
var updateResp map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &updateResp)
require.NoError(t, err)
assert.Equal(t, "My Updated House", updateResp["name"])
assert.Equal(t, "Dallas", updateResp["city"])
// 5. Delete the residence (returns 200 with message, not 204)
w = app.makeAuthenticatedRequest(t, "DELETE", "/api/residences/"+formatID(residenceID), nil, token)
assert.Equal(t, http.StatusOK, w.Code)
// 6. Verify it's deleted (should return 403 - access denied since it doesn't exist/inactive)
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, token)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestIntegration_ResidenceSharingFlow(t *testing.T) {
app := setupIntegrationTest(t)
// Create owner and another user
ownerToken := app.registerAndLogin(t, "owner", "owner@test.com", "password123")
userToken := app.registerAndLogin(t, "shareduser", "shared@test.com", "password123")
// Create residence as owner
createBody := map[string]interface{}{
"name": "Shared House",
}
w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", createBody, ownerToken)
require.Equal(t, http.StatusCreated, w.Code)
var createResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &createResp)
residenceID := createResp["id"].(float64)
// Other user cannot access initially
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, userToken)
assert.Equal(t, http.StatusForbidden, w.Code)
// Generate share code
w = app.makeAuthenticatedRequest(t, "POST", "/api/residences/"+formatID(residenceID)+"/generate-share-code", nil, ownerToken)
assert.Equal(t, http.StatusOK, w.Code)
var shareResp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &shareResp)
require.NoError(t, err)
shareCodeObj, ok := shareResp["share_code"].(map[string]interface{})
require.True(t, ok, "Expected share_code object in response")
shareCode := shareCodeObj["code"].(string)
assert.Len(t, shareCode, 6)
// User joins with code
joinBody := map[string]interface{}{
"code": shareCode,
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/residences/join-with-code", joinBody, userToken)
assert.Equal(t, http.StatusOK, w.Code)
// Now user can access
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, userToken)
assert.Equal(t, http.StatusOK, w.Code)
// Get users list - returns array directly, not wrapped in {"users": ...}
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID)+"/users", nil, ownerToken)
assert.Equal(t, http.StatusOK, w.Code)
var users []interface{}
err = json.Unmarshal(w.Body.Bytes(), &users)
require.NoError(t, err)
assert.Len(t, users, 2) // owner + shared user
}
// ============ Task Flow Tests ============
func TestIntegration_TaskFlow(t *testing.T) {
app := setupIntegrationTest(t)
token := app.registerAndLogin(t, "owner", "owner@test.com", "password123")
// Create residence first
residenceBody := map[string]interface{}{"name": "Task House"}
w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, token)
require.Equal(t, http.StatusCreated, w.Code)
var residenceResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &residenceResp)
residenceID := uint(residenceResp["id"].(float64))
// 1. Create a task
taskBody := map[string]interface{}{
"residence_id": residenceID,
"title": "Fix leaky faucet",
"description": "Kitchen faucet is dripping",
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks", taskBody, token)
assert.Equal(t, http.StatusCreated, w.Code)
var taskResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &taskResp)
taskID := taskResp["id"].(float64)
assert.NotZero(t, taskID)
assert.Equal(t, "Fix leaky faucet", taskResp["title"])
// 2. Get the task
w = app.makeAuthenticatedRequest(t, "GET", "/api/tasks/"+formatID(taskID), nil, token)
assert.Equal(t, http.StatusOK, w.Code)
// 3. Update the task
updateBody := map[string]interface{}{
"title": "Fix kitchen faucet",
"description": "Updated description",
}
w = app.makeAuthenticatedRequest(t, "PUT", "/api/tasks/"+formatID(taskID), updateBody, token)
assert.Equal(t, http.StatusOK, w.Code)
var updateResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &updateResp)
assert.Equal(t, "Fix kitchen faucet", updateResp["title"])
// 4. Mark as in progress
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/mark-in-progress", nil, token)
assert.Equal(t, http.StatusOK, w.Code)
var progressResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &progressResp)
task := progressResp["task"].(map[string]interface{})
status := task["status"].(map[string]interface{})
assert.Equal(t, "In Progress", status["name"])
// 5. Complete the task
completionBody := map[string]interface{}{
"task_id": taskID,
"notes": "Fixed the faucet",
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/completions", completionBody, token)
assert.Equal(t, http.StatusCreated, w.Code)
var completionResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &completionResp)
completionID := completionResp["id"].(float64)
assert.NotZero(t, completionID)
assert.Equal(t, "Fixed the faucet", completionResp["notes"])
// 6. List completions
w = app.makeAuthenticatedRequest(t, "GET", "/api/completions?task_id="+formatID(taskID), nil, token)
assert.Equal(t, http.StatusOK, w.Code)
// 7. Archive the task
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/archive", nil, token)
assert.Equal(t, http.StatusOK, w.Code)
var archiveResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &archiveResp)
archivedTask := archiveResp["task"].(map[string]interface{})
assert.True(t, archivedTask["is_archived"].(bool))
// 8. Unarchive the task
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/unarchive", nil, token)
assert.Equal(t, http.StatusOK, w.Code)
// 9. Cancel the task
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/cancel", nil, token)
assert.Equal(t, http.StatusOK, w.Code)
var cancelResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &cancelResp)
cancelledTask := cancelResp["task"].(map[string]interface{})
assert.True(t, cancelledTask["is_cancelled"].(bool))
// 10. Delete the task (returns 200 with message, not 204)
w = app.makeAuthenticatedRequest(t, "DELETE", "/api/tasks/"+formatID(taskID), nil, token)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestIntegration_TasksByResidenceKanban(t *testing.T) {
app := setupIntegrationTest(t)
token := app.registerAndLogin(t, "owner", "owner@test.com", "password123")
// Create residence
residenceBody := map[string]interface{}{"name": "Kanban House"}
w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, token)
require.Equal(t, http.StatusCreated, w.Code)
var residenceResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &residenceResp)
residenceID := uint(residenceResp["id"].(float64))
// Create multiple tasks
for i := 1; i <= 3; i++ {
taskBody := map[string]interface{}{
"residence_id": residenceID,
"title": "Task " + formatID(float64(i)),
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks", taskBody, token)
require.Equal(t, http.StatusCreated, w.Code)
}
// Get tasks by residence (kanban view)
w = app.makeAuthenticatedRequest(t, "GET", "/api/tasks/by-residence/"+formatID(float64(residenceID)), nil, token)
assert.Equal(t, http.StatusOK, w.Code)
var kanbanResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &kanbanResp)
columns := kanbanResp["columns"].([]interface{})
assert.Greater(t, len(columns), 0)
// Check column structure
for _, col := range columns {
column := col.(map[string]interface{})
assert.NotEmpty(t, column["name"])
assert.NotEmpty(t, column["display_name"])
assert.NotNil(t, column["tasks"])
assert.NotNil(t, column["count"])
}
}
// ============ Lookup Data Tests ============
func TestIntegration_LookupEndpoints(t *testing.T) {
app := setupIntegrationTest(t)
token := app.registerAndLogin(t, "user", "user@test.com", "password123")
tests := []struct {
name string
endpoint string
}{
{"residence types", "/api/residence-types"},
{"task categories", "/api/task-categories"},
{"task priorities", "/api/task-priorities"},
{"task statuses", "/api/task-statuses"},
{"task frequencies", "/api/task-frequencies"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := app.makeAuthenticatedRequest(t, "GET", tt.endpoint, nil, token)
assert.Equal(t, http.StatusOK, w.Code)
// All lookup endpoints return arrays directly
var items []interface{}
err := json.Unmarshal(w.Body.Bytes(), &items)
require.NoError(t, err)
assert.Greater(t, len(items), 0)
// Check item structure
for _, item := range items {
obj := item.(map[string]interface{})
assert.NotZero(t, obj["id"])
assert.NotEmpty(t, obj["name"])
}
})
}
}
// ============ Access Control Tests ============
func TestIntegration_CrossUserAccessDenied(t *testing.T) {
app := setupIntegrationTest(t)
// Create two users with their own residences
user1Token := app.registerAndLogin(t, "user1", "user1@test.com", "password123")
user2Token := app.registerAndLogin(t, "user2", "user2@test.com", "password123")
// User1 creates a residence
residenceBody := map[string]interface{}{"name": "User1's House"}
w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, user1Token)
require.Equal(t, http.StatusCreated, w.Code)
var residenceResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &residenceResp)
residenceID := residenceResp["id"].(float64)
// User1 creates a task
taskBody := map[string]interface{}{
"residence_id": residenceID,
"title": "User1's Task",
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks", taskBody, user1Token)
require.Equal(t, http.StatusCreated, w.Code)
var taskResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &taskResp)
taskID := taskResp["id"].(float64)
// User2 cannot access User1's residence
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, user2Token)
assert.Equal(t, http.StatusForbidden, w.Code)
// User2 cannot access User1's task
w = app.makeAuthenticatedRequest(t, "GET", "/api/tasks/"+formatID(taskID), nil, user2Token)
assert.Equal(t, http.StatusForbidden, w.Code)
// User2 cannot update User1's residence
updateBody := map[string]interface{}{"name": "Hacked!"}
w = app.makeAuthenticatedRequest(t, "PUT", "/api/residences/"+formatID(residenceID), updateBody, user2Token)
assert.Equal(t, http.StatusForbidden, w.Code)
// User2 cannot delete User1's residence
w = app.makeAuthenticatedRequest(t, "DELETE", "/api/residences/"+formatID(residenceID), nil, user2Token)
assert.Equal(t, http.StatusForbidden, w.Code)
// User2 cannot create task in User1's residence
taskBody2 := map[string]interface{}{
"residence_id": residenceID,
"title": "Malicious Task",
}
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks", taskBody2, user2Token)
assert.Equal(t, http.StatusForbidden, w.Code)
}
// ============ JSON Response Structure Tests ============
func TestIntegration_ResponseStructure(t *testing.T) {
app := setupIntegrationTest(t)
token := app.registerAndLogin(t, "user", "user@test.com", "password123")
// Create residence
residenceBody := map[string]interface{}{
"name": "Response Test House",
"street_address": "123 Test St",
"city": "Austin",
"state_province": "TX",
"postal_code": "78701",
}
w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, token)
require.Equal(t, http.StatusCreated, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
// Verify all expected fields are present
expectedFields := []string{
"id", "owner_id", "name", "street_address", "city",
"state_province", "postal_code", "country",
"is_primary", "is_active", "created_at", "updated_at",
}
for _, field := range expectedFields {
_, exists := resp[field]
assert.True(t, exists, "Expected field %s to be present", field)
}
// Check that nullable fields can be null
assert.Nil(t, resp["bedrooms"])
assert.Nil(t, resp["bathrooms"])
}
// ============ Helper Functions ============
func formatID(id float64) string {
return fmt.Sprintf("%d", uint(id))
}