Files
honeyDueAPI/internal/testutil/testutil.go
Trey t bb7493f033 Close all 25 codex audit findings and add KMP contract tests
Remediate all P0-S priority findings from cross-platform architecture audit:
- Add input validation and authorization checks across handlers
- Harden social auth (Apple/Google) token validation
- Add document ownership verification and file type validation
- Add rate limiting config and CORS origin restrictions
- Add subscription tier enforcement in handlers
- Add OpenAPI 3.0.3 spec (81 schemas, 104 operations)
- Add URL-level contract test (KMP API routes match spec paths)
- Add model-level contract test (65 schemas, 464 fields validated)
- Add CI workflow for backend tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:15:07 -06:00

358 lines
9.9 KiB
Go

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.DocumentImage{},
&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")
}