package testutil import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/treytartt/casera-api/internal/apperrors" "github.com/treytartt/casera-api/internal/i18n" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/validator" ) var i18nOnce sync.Once // SetupTestDB creates an in-memory SQLite database for testing func SetupTestDB(t *testing.T) *gorm.DB { // Initialize i18n once for all tests i18nOnce.Do(func() { i18n.Init() }) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err) // Migrate all models err = db.AutoMigrate( &models.User{}, &models.UserProfile{}, &models.AuthToken{}, &models.ConfirmationCode{}, &models.PasswordResetCode{}, &models.AdminUser{}, &models.Residence{}, &models.ResidenceType{}, &models.ResidenceShareCode{}, &models.Task{}, &models.TaskCategory{}, &models.TaskPriority{}, &models.TaskFrequency{}, &models.TaskCompletion{}, &models.TaskCompletionImage{}, &models.Contractor{}, &models.ContractorSpecialty{}, &models.Document{}, &models.Notification{}, &models.NotificationPreference{}, &models.APNSDevice{}, &models.GCMDevice{}, &models.UserSubscription{}, &models.SubscriptionSettings{}, &models.TierLimits{}, &models.FeatureBenefit{}, &models.UpgradeTrigger{}, &models.Promotion{}, ) require.NoError(t, err) return db } // SetupTestRouter creates a test Echo router with the custom error handler func SetupTestRouter() *echo.Echo { e := echo.New() e.Validator = validator.NewCustomValidator() e.HTTPErrorHandler = apperrors.HTTPErrorHandler return e } // MakeRequest makes a test HTTP request and returns the response func MakeRequest(router *echo.Echo, method, path string, body interface{}, token string) *httptest.ResponseRecorder { var reqBody *bytes.Buffer if body != nil { jsonBody, _ := json.Marshal(body) reqBody = bytes.NewBuffer(jsonBody) } else { reqBody = bytes.NewBuffer(nil) } req, _ := http.NewRequest(method, path, reqBody) req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Token "+token) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) return rec } // ParseJSON parses JSON response body into a map func ParseJSON(t *testing.T, body []byte) map[string]interface{} { var result map[string]interface{} err := json.Unmarshal(body, &result) require.NoError(t, err) return result } // ParseJSONArray parses JSON response body into an array func ParseJSONArray(t *testing.T, body []byte) []map[string]interface{} { var result []map[string]interface{} err := json.Unmarshal(body, &result) require.NoError(t, err) return result } // CreateTestUser creates a test user in the database func CreateTestUser(t *testing.T, db *gorm.DB, username, email, password string) *models.User { user := &models.User{ Username: username, Email: email, IsActive: true, } err := user.SetPassword(password) require.NoError(t, err) err = db.Create(user).Error require.NoError(t, err) return user } // CreateTestToken creates an auth token for a user func CreateTestToken(t *testing.T, db *gorm.DB, userID uint) *models.AuthToken { token, err := models.GetOrCreateToken(db, userID) require.NoError(t, err) return token } // CreateTestResidenceType creates a test residence type func CreateTestResidenceType(t *testing.T, db *gorm.DB, name string) *models.ResidenceType { rt := &models.ResidenceType{Name: name} err := db.Create(rt).Error require.NoError(t, err) return rt } // CreateTestResidence creates a test residence func CreateTestResidence(t *testing.T, db *gorm.DB, ownerID uint, name string) *models.Residence { residence := &models.Residence{ OwnerID: ownerID, Name: name, StreetAddress: "123 Test St", City: "Test City", StateProvince: "TS", PostalCode: "12345", Country: "USA", IsActive: true, IsPrimary: true, } err := db.Create(residence).Error require.NoError(t, err) return residence } // CreateTestTaskCategory creates a test task category func CreateTestTaskCategory(t *testing.T, db *gorm.DB, name string) *models.TaskCategory { cat := &models.TaskCategory{ Name: name, DisplayOrder: 1, } err := db.Create(cat).Error require.NoError(t, err) return cat } // CreateTestTaskPriority creates a test task priority func CreateTestTaskPriority(t *testing.T, db *gorm.DB, name string, level int) *models.TaskPriority { priority := &models.TaskPriority{ Name: name, Level: level, DisplayOrder: level, } err := db.Create(priority).Error require.NoError(t, err) return priority } // CreateTestTaskFrequency creates a test task frequency func CreateTestTaskFrequency(t *testing.T, db *gorm.DB, name string, days *int) *models.TaskFrequency { freq := &models.TaskFrequency{ Name: name, Days: days, DisplayOrder: 1, } err := db.Create(freq).Error require.NoError(t, err) return freq } // CreateTestTask creates a test task func CreateTestTask(t *testing.T, db *gorm.DB, residenceID, createdByID uint, title string) *models.Task { task := &models.Task{ ResidenceID: residenceID, CreatedByID: createdByID, Title: title, IsCancelled: false, IsArchived: false, } err := db.Create(task).Error require.NoError(t, err) return task } // SeedLookupData seeds all lookup tables with test data func SeedLookupData(t *testing.T, db *gorm.DB) { // Residence types residenceTypes := []models.ResidenceType{ {Name: "House"}, {Name: "Apartment"}, {Name: "Condo"}, {Name: "Townhouse"}, } for _, rt := range residenceTypes { db.Create(&rt) } // Task categories categories := []models.TaskCategory{ {Name: "Plumbing", DisplayOrder: 1}, {Name: "Electrical", DisplayOrder: 2}, {Name: "HVAC", DisplayOrder: 3}, {Name: "General", DisplayOrder: 99}, } for _, c := range categories { db.Create(&c) } // Task priorities priorities := []models.TaskPriority{ {Name: "Low", Level: 1, DisplayOrder: 1}, {Name: "Medium", Level: 2, DisplayOrder: 2}, {Name: "High", Level: 3, DisplayOrder: 3}, {Name: "Urgent", Level: 4, DisplayOrder: 4}, } for _, p := range priorities { db.Create(&p) } // Task frequencies days7 := 7 days30 := 30 frequencies := []models.TaskFrequency{ {Name: "Once", Days: nil, DisplayOrder: 1}, {Name: "Weekly", Days: &days7, DisplayOrder: 2}, {Name: "Monthly", Days: &days30, DisplayOrder: 3}, } for _, f := range frequencies { db.Create(&f) } // Contractor specialties specialties := []models.ContractorSpecialty{ {Name: "Plumber"}, {Name: "Electrician"}, {Name: "HVAC Technician"}, {Name: "Handyman"}, } for _, s := range specialties { db.Create(&s) } } // AssertJSONField asserts that a JSON field has the expected value func AssertJSONField(t *testing.T, data map[string]interface{}, field string, expected interface{}) { actual, ok := data[field] require.True(t, ok, "field %s not found in response", field) require.Equal(t, expected, actual, "field %s has unexpected value", field) } // AssertJSONFieldExists asserts that a JSON field exists func AssertJSONFieldExists(t *testing.T, data map[string]interface{}, field string) { _, ok := data[field] require.True(t, ok, "field %s not found in response", field) } // AssertStatusCode asserts the HTTP status code func AssertStatusCode(t *testing.T, w *httptest.ResponseRecorder, expected int) { require.Equal(t, expected, w.Code, "unexpected status code: %s", w.Body.String()) } // MockAuthMiddleware creates middleware that sets a test user in context func MockAuthMiddleware(user *models.User) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { c.Set("auth_user", user) c.Set("auth_token", "test-token") return next(c) } } } // CreateTestContractor creates a test contractor func CreateTestContractor(t *testing.T, db *gorm.DB, residenceID, createdByID uint, name string) *models.Contractor { contractor := &models.Contractor{ ResidenceID: &residenceID, CreatedByID: createdByID, Name: name, IsActive: true, } err := db.Create(contractor).Error require.NoError(t, err) return contractor } // CreateTestContractorSpecialty creates a test contractor specialty func CreateTestContractorSpecialty(t *testing.T, db *gorm.DB, name string) *models.ContractorSpecialty { specialty := &models.ContractorSpecialty{Name: name} err := db.Create(specialty).Error require.NoError(t, err) return specialty } // CreateTestDocument creates a test document func CreateTestDocument(t *testing.T, db *gorm.DB, residenceID, createdByID uint, title string) *models.Document { doc := &models.Document{ ResidenceID: residenceID, CreatedByID: createdByID, Title: title, DocumentType: "general", FileURL: "https://example.com/doc.pdf", } err := db.Create(doc).Error require.NoError(t, err) return doc } // AssertAppError asserts that an error is an AppError with a specific status code and message key func AssertAppError(t *testing.T, err error, expectedCode int, expectedMessageKey string) { require.Error(t, err, "expected an error") var appErr *apperrors.AppError require.ErrorAs(t, err, &appErr, "expected an AppError") require.Equal(t, expectedCode, appErr.Code, "unexpected status code") require.Equal(t, expectedMessageKey, appErr.MessageKey, "unexpected message key") } // AssertAppErrorCode asserts that an error is an AppError with a specific status code func AssertAppErrorCode(t *testing.T, err error, expectedCode int) { require.Error(t, err, "expected an error") var appErr *apperrors.AppError require.ErrorAs(t, err, &appErr, "expected an AppError") require.Equal(t, expectedCode, appErr.Code, "unexpected status code") }