Add PDF reports, file uploads, admin auth, and comprehensive tests
Features: - PDF service for generating task reports with ReportLab-style formatting - Storage service for file uploads (local and S3-compatible) - Admin authentication middleware with JWT support - Admin user model and repository Infrastructure: - Updated Docker configuration for admin panel builds - Email service enhancements for task notifications - Updated router with admin and file upload routes - Environment configuration updates Tests: - Unit tests for handlers (auth, residence, task) - Unit tests for models (user, residence, task) - Unit tests for repositories (user, residence, task) - Unit tests for services (residence, task) - Integration test setup - Test utilities for mocking database and services Database: - Admin user seed data - Updated test data seeds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
342
internal/testutil/testutil.go
Normal file
342
internal/testutil/testutil.go
Normal file
@@ -0,0 +1,342 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
// SetupTestDB creates an in-memory SQLite database for testing
|
||||
func SetupTestDB(t *testing.T) *gorm.DB {
|
||||
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.TaskStatus{},
|
||||
&models.TaskFrequency{},
|
||||
&models.TaskCompletion{},
|
||||
&models.Contractor{},
|
||||
&models.ContractorSpecialty{},
|
||||
&models.Document{},
|
||||
&models.Notification{},
|
||||
&models.NotificationPreference{},
|
||||
&models.APNSDevice{},
|
||||
&models.GCMDevice{},
|
||||
&models.UserSubscription{},
|
||||
&models.TierLimits{},
|
||||
&models.FeatureBenefit{},
|
||||
&models.UpgradeTrigger{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// SetupTestRouter creates a test Gin router
|
||||
func SetupTestRouter() *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
return gin.New()
|
||||
}
|
||||
|
||||
// MakeRequest makes a test HTTP request and returns the response
|
||||
func MakeRequest(router *gin.Engine, 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)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// CreateTestTaskStatus creates a test task status
|
||||
func CreateTestTaskStatus(t *testing.T, db *gorm.DB, name string) *models.TaskStatus {
|
||||
status := &models.TaskStatus{
|
||||
Name: name,
|
||||
DisplayOrder: 1,
|
||||
}
|
||||
err := db.Create(status).Error
|
||||
require.NoError(t, err)
|
||||
return status
|
||||
}
|
||||
|
||||
// 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 statuses
|
||||
statuses := []models.TaskStatus{
|
||||
{Name: "Pending", DisplayOrder: 1},
|
||||
{Name: "In Progress", DisplayOrder: 2},
|
||||
{Name: "Completed", DisplayOrder: 3},
|
||||
{Name: "Cancelled", DisplayOrder: 4},
|
||||
}
|
||||
for _, s := range statuses {
|
||||
db.Create(&s)
|
||||
}
|
||||
|
||||
// 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) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Set("auth_user", user)
|
||||
c.Set("auth_token", "test-token")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user