Files
honeyDueAPI/internal/models/models_coverage_test.go
Trey T bec880886b Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests
- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests)
- Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests)
- Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests)
- Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests)
- Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests)
- Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:30:09 -05:00

627 lines
16 KiB
Go

package models
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// setupModelsTestDB creates a minimal in-memory SQLite for model-level tests
// that require database interaction (e.g., BeforeCreate hooks).
func setupModelsTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
require.NoError(t, err)
err = db.AutoMigrate(&User{}, &AuthToken{}, &UserProfile{})
require.NoError(t, err)
return db
}
// === Residence model tests ===
func TestResidence_GetAllUsers(t *testing.T) {
owner := User{Username: "owner"}
owner.ID = 1
member1 := User{Username: "member1"}
member1.ID = 2
member2 := User{Username: "member2"}
member2.ID = 3
residence := &Residence{
OwnerID: owner.ID,
Owner: owner,
Users: []User{member1, member2},
}
allUsers := residence.GetAllUsers()
assert.Len(t, allUsers, 3)
assert.Equal(t, "owner", allUsers[0].Username)
}
func TestResidence_HasAccess(t *testing.T) {
owner := User{Username: "owner"}
owner.ID = 1
member := User{Username: "member"}
member.ID = 2
residence := &Residence{
OwnerID: owner.ID,
Owner: owner,
Users: []User{member},
}
tests := []struct {
name string
userID uint
expected bool
}{
{"owner has access", 1, true},
{"member has access", 2, true},
{"stranger no access", 99, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, residence.HasAccess(tt.userID))
})
}
}
func TestResidence_IsPrimaryOwner(t *testing.T) {
residence := &Residence{OwnerID: 1}
assert.True(t, residence.IsPrimaryOwner(1))
assert.False(t, residence.IsPrimaryOwner(2))
}
// === Document model tests ===
func TestDocument_TableName_DocumentImage(t *testing.T) {
di := DocumentImage{}
assert.Equal(t, "task_documentimage", di.TableName())
}
func TestDocument_IsWarrantyExpiringSoon(t *testing.T) {
future30 := time.Now().UTC().AddDate(0, 0, 15)
future90 := time.Now().UTC().AddDate(0, 0, 60)
past := time.Now().UTC().AddDate(0, 0, -5)
tests := []struct {
name string
doc Document
days int
expected bool
}{
{
name: "warranty expiring within threshold",
doc: Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &future30},
days: 30,
expected: true,
},
{
name: "warranty not expiring within threshold",
doc: Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &future90},
days: 30,
expected: false,
},
{
name: "warranty already expired",
doc: Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &past},
days: 30,
expected: false,
},
{
name: "non-warranty document",
doc: Document{DocumentType: DocumentTypeGeneral, ExpiryDate: &future30},
days: 30,
expected: false,
},
{
name: "warranty with nil expiry",
doc: Document{DocumentType: DocumentTypeWarranty, ExpiryDate: nil},
days: 30,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.doc.IsWarrantyExpiringSoon(tt.days))
})
}
}
func TestDocument_IsWarrantyExpired(t *testing.T) {
past := time.Now().UTC().AddDate(0, 0, -5)
future := time.Now().UTC().AddDate(0, 0, 30)
tests := []struct {
name string
doc Document
expected bool
}{
{
name: "expired warranty",
doc: Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &past},
expected: true,
},
{
name: "active warranty",
doc: Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &future},
expected: false,
},
{
name: "non-warranty",
doc: Document{DocumentType: DocumentTypeGeneral, ExpiryDate: &past},
expected: false,
},
{
name: "warranty nil expiry",
doc: Document{DocumentType: DocumentTypeWarranty, ExpiryDate: nil},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.doc.IsWarrantyExpired())
})
}
}
func TestDocumentType_Constants(t *testing.T) {
// Verify document type constants have expected values
assert.Equal(t, DocumentType("general"), DocumentTypeGeneral)
assert.Equal(t, DocumentType("warranty"), DocumentTypeWarranty)
assert.Equal(t, DocumentType("receipt"), DocumentTypeReceipt)
assert.Equal(t, DocumentType("contract"), DocumentTypeContract)
assert.Equal(t, DocumentType("insurance"), DocumentTypeInsurance)
assert.Equal(t, DocumentType("manual"), DocumentTypeManual)
}
// === Notification model tests ===
func TestNotification_TableName(t *testing.T) {
n := Notification{}
assert.Equal(t, "notifications_notification", n.TableName())
}
func TestNotificationPreference_TableName(t *testing.T) {
np := NotificationPreference{}
assert.Equal(t, "notifications_notificationpreference", np.TableName())
}
func TestAPNSDevice_TableName(t *testing.T) {
d := APNSDevice{}
assert.Equal(t, "push_notifications_apnsdevice", d.TableName())
}
func TestGCMDevice_TableName(t *testing.T) {
d := GCMDevice{}
assert.Equal(t, "push_notifications_gcmdevice", d.TableName())
}
func TestNotification_MarkAsRead(t *testing.T) {
n := &Notification{Read: false}
n.MarkAsRead()
assert.True(t, n.Read)
assert.NotNil(t, n.ReadAt)
}
func TestNotification_MarkAsSent(t *testing.T) {
n := &Notification{Sent: false}
n.MarkAsSent()
assert.True(t, n.Sent)
assert.NotNil(t, n.SentAt)
}
func TestNotificationType_Constants(t *testing.T) {
assert.Equal(t, NotificationType("task_due_soon"), NotificationTaskDueSoon)
assert.Equal(t, NotificationType("task_overdue"), NotificationTaskOverdue)
assert.Equal(t, NotificationType("task_completed"), NotificationTaskCompleted)
assert.Equal(t, NotificationType("task_assigned"), NotificationTaskAssigned)
assert.Equal(t, NotificationType("residence_shared"), NotificationResidenceShared)
assert.Equal(t, NotificationType("warranty_expiring"), NotificationWarrantyExpiring)
}
// === AuthToken model tests ===
func TestAuthToken_BeforeCreate_GeneratesKey(t *testing.T) {
db := setupModelsTestDB(t)
user := &User{
Username: "tokenuser",
Email: "token@test.com",
Password: "dummy",
IsActive: true,
}
err := db.Create(user).Error
require.NoError(t, err)
token := &AuthToken{UserID: user.ID}
err = db.Create(token).Error
require.NoError(t, err)
assert.NotEmpty(t, token.Key)
assert.Len(t, token.Key, 40) // 20 bytes = 40 hex chars
assert.False(t, token.Created.IsZero())
}
func TestAuthToken_BeforeCreate_PreservesExistingKey(t *testing.T) {
db := setupModelsTestDB(t)
user := &User{
Username: "tokenuser",
Email: "token@test.com",
Password: "dummy",
IsActive: true,
}
err := db.Create(user).Error
require.NoError(t, err)
existingKey := "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
token := &AuthToken{
Key: existingKey,
UserID: user.ID,
}
err = db.Create(token).Error
require.NoError(t, err)
assert.Equal(t, existingKey, token.Key)
}
func TestGetOrCreateToken_CreatesNew(t *testing.T) {
db := setupModelsTestDB(t)
user := &User{
Username: "newtoken",
Email: "newtoken@test.com",
Password: "dummy",
IsActive: true,
}
err := db.Create(user).Error
require.NoError(t, err)
token, err := GetOrCreateToken(db, user.ID)
require.NoError(t, err)
assert.NotEmpty(t, token.Key)
assert.Equal(t, user.ID, token.UserID)
}
func TestGetOrCreateToken_ReturnsExisting(t *testing.T) {
db := setupModelsTestDB(t)
user := &User{
Username: "existingtoken",
Email: "existingtoken@test.com",
Password: "dummy",
IsActive: true,
}
err := db.Create(user).Error
require.NoError(t, err)
token1, err := GetOrCreateToken(db, user.ID)
require.NoError(t, err)
token2, err := GetOrCreateToken(db, user.ID)
require.NoError(t, err)
assert.Equal(t, token1.Key, token2.Key)
}
// === User model additional tests ===
func TestUser_SetPassword_And_CheckPassword_Integration(t *testing.T) {
user := &User{}
err := user.SetPassword("Password123")
require.NoError(t, err)
assert.True(t, user.CheckPassword("Password123"))
assert.False(t, user.CheckPassword("WrongPassword"))
assert.False(t, user.CheckPassword(""))
assert.False(t, user.CheckPassword("password123")) // case sensitive
}
// === Task model additional tests ===
func TestTask_IsOverdue_CancelledNotOverdue(t *testing.T) {
yesterday := time.Now().UTC().AddDate(0, 0, -2)
task := &Task{
DueDate: &yesterday,
IsCancelled: true,
}
assert.False(t, task.IsOverdue())
}
func TestTask_IsOverdue_ArchivedNotOverdue(t *testing.T) {
yesterday := time.Now().UTC().AddDate(0, 0, -2)
task := &Task{
DueDate: &yesterday,
IsArchived: true,
}
assert.False(t, task.IsOverdue())
}
func TestTask_IsOverdue_NoDueDateNotOverdue(t *testing.T) {
task := &Task{
DueDate: nil,
}
assert.False(t, task.IsOverdue())
}
func TestTask_IsOverdue_CompletedNotOverdue(t *testing.T) {
yesterday := time.Now().UTC().AddDate(0, 0, -2)
task := &Task{
DueDate: &yesterday,
NextDueDate: nil,
Completions: []TaskCompletion{{CompletedAt: time.Now().UTC()}},
}
assert.False(t, task.IsOverdue())
}
func TestTask_IsOverdue_CompletionCountNotOverdue(t *testing.T) {
yesterday := time.Now().UTC().AddDate(0, 0, -2)
task := &Task{
DueDate: &yesterday,
NextDueDate: nil,
CompletionCount: 1,
}
assert.False(t, task.IsOverdue())
}
func TestTask_IsOverdue_UsesNextDueDate(t *testing.T) {
// DueDate is overdue, but NextDueDate is in the future
pastDue := time.Now().UTC().AddDate(0, 0, -10)
futureDue := time.Now().UTC().AddDate(0, 0, 10)
task := &Task{
DueDate: &pastDue,
NextDueDate: &futureDue,
}
assert.False(t, task.IsOverdue())
}
func TestTask_IsDueSoon_CancelledNotDueSoon(t *testing.T) {
futureDue := time.Now().UTC().AddDate(0, 0, 5)
task := &Task{
DueDate: &futureDue,
IsCancelled: true,
}
assert.False(t, task.IsDueSoon(30))
}
func TestTask_IsDueSoon_NoDueDateNotDueSoon(t *testing.T) {
task := &Task{
DueDate: nil,
}
assert.False(t, task.IsDueSoon(30))
}
func TestTask_IsDueSoon_WithinThreshold(t *testing.T) {
futureDue := time.Now().UTC().AddDate(0, 0, 5)
task := &Task{
DueDate: &futureDue,
}
assert.True(t, task.IsDueSoon(30))
assert.True(t, task.IsDueSoon(10))
assert.False(t, task.IsDueSoon(3))
}
func TestTask_IsDueSoon_CompletedNotDueSoon(t *testing.T) {
futureDue := time.Now().UTC().AddDate(0, 0, 5)
task := &Task{
DueDate: &futureDue,
NextDueDate: nil,
Completions: []TaskCompletion{{CompletedAt: time.Now().UTC()}},
}
assert.False(t, task.IsDueSoon(30))
}
func TestTaskCompletionImage_TableName(t *testing.T) {
tci := TaskCompletionImage{}
assert.Equal(t, "task_taskcompletionimage", tci.TableName())
}
// === Subscription model additional tests ===
func TestSubscription_TableNames(t *testing.T) {
assert.Equal(t, "subscription_subscriptionsettings", SubscriptionSettings{}.TableName())
assert.Equal(t, "subscription_usersubscription", UserSubscription{}.TableName())
assert.Equal(t, "subscription_upgradetrigger", UpgradeTrigger{}.TableName())
assert.Equal(t, "subscription_featurebenefit", FeatureBenefit{}.TableName())
assert.Equal(t, "subscription_promotion", Promotion{}.TableName())
assert.Equal(t, "subscription_tierlimits", TierLimits{}.TableName())
}
func TestSubscription_IsActive(t *testing.T) {
future := time.Now().UTC().Add(24 * time.Hour)
past := time.Now().UTC().Add(-24 * time.Hour)
tests := []struct {
name string
sub *UserSubscription
expected bool
}{
{
name: "pro with future expiry is active",
sub: &UserSubscription{Tier: TierPro, ExpiresAt: &future},
expected: true,
},
{
name: "pro with nil expiry is active",
sub: &UserSubscription{Tier: TierPro, ExpiresAt: nil},
expected: true,
},
{
name: "pro with past expiry is not active",
sub: &UserSubscription{Tier: TierPro, ExpiresAt: &past},
expected: false,
},
{
name: "free with active trial is active",
sub: &UserSubscription{Tier: TierFree, TrialEnd: &future},
expected: true,
},
{
name: "free without trial is not active",
sub: &UserSubscription{Tier: TierFree},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.sub.IsActive())
})
}
}
func TestSubscription_SubscriptionSource(t *testing.T) {
sub := &UserSubscription{Platform: "ios"}
assert.Equal(t, "ios", sub.SubscriptionSource())
}
func TestPromotion_IsCurrentlyActive(t *testing.T) {
now := time.Now().UTC()
tests := []struct {
name string
promo Promotion
expected bool
}{
{
name: "active promotion within dates",
promo: Promotion{
IsActive: true,
StartDate: now.Add(-1 * time.Hour),
EndDate: now.Add(1 * time.Hour),
},
expected: true,
},
{
name: "inactive promotion",
promo: Promotion{
IsActive: false,
StartDate: now.Add(-1 * time.Hour),
EndDate: now.Add(1 * time.Hour),
},
expected: false,
},
{
name: "promotion not yet started",
promo: Promotion{
IsActive: true,
StartDate: now.Add(1 * time.Hour),
EndDate: now.Add(2 * time.Hour),
},
expected: false,
},
{
name: "promotion already ended",
promo: Promotion{
IsActive: true,
StartDate: now.Add(-2 * time.Hour),
EndDate: now.Add(-1 * time.Hour),
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.promo.IsCurrentlyActive())
})
}
}
func TestGetDefaultFreeLimits(t *testing.T) {
limits := GetDefaultFreeLimits()
assert.Equal(t, TierFree, limits.Tier)
require.NotNil(t, limits.PropertiesLimit)
require.NotNil(t, limits.TasksLimit)
require.NotNil(t, limits.ContractorsLimit)
require.NotNil(t, limits.DocumentsLimit)
assert.Equal(t, 1, *limits.PropertiesLimit)
assert.Equal(t, 10, *limits.TasksLimit)
assert.Equal(t, 0, *limits.ContractorsLimit)
assert.Equal(t, 0, *limits.DocumentsLimit)
}
func TestGetDefaultProLimits(t *testing.T) {
limits := GetDefaultProLimits()
assert.Equal(t, TierPro, limits.Tier)
assert.Nil(t, limits.PropertiesLimit)
assert.Nil(t, limits.TasksLimit)
assert.Nil(t, limits.ContractorsLimit)
assert.Nil(t, limits.DocumentsLimit)
}
// === ConfirmationCode additional tests ===
func TestConfirmationCode_TableName(t *testing.T) {
cc := ConfirmationCode{}
assert.Equal(t, "user_confirmationcode", cc.TableName())
}
// === PasswordResetCode additional tests ===
func TestPasswordResetCode_TableName(t *testing.T) {
prc := PasswordResetCode{}
assert.Equal(t, "user_passwordresetcode", prc.TableName())
}
// === Social Auth TableName tests ===
func TestAppleSocialAuth_TableName(t *testing.T) {
a := AppleSocialAuth{}
assert.Equal(t, "user_applesocialauth", a.TableName())
}
func TestGoogleSocialAuth_TableName(t *testing.T) {
g := GoogleSocialAuth{}
assert.Equal(t, "user_googlesocialauth", g.TableName())
}
// === BaseModel tests ===
func TestBaseModel_BeforeCreate(t *testing.T) {
b := &BaseModel{}
err := b.BeforeCreate(nil)
require.NoError(t, err)
assert.False(t, b.CreatedAt.IsZero())
assert.False(t, b.UpdatedAt.IsZero())
}
func TestBaseModel_BeforeCreate_PreservesExisting(t *testing.T) {
existingTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
b := &BaseModel{
CreatedAt: existingTime,
UpdatedAt: existingTime,
}
err := b.BeforeCreate(nil)
require.NoError(t, err)
assert.Equal(t, existingTime, b.CreatedAt)
assert.Equal(t, existingTime, b.UpdatedAt)
}
func TestBaseModel_BeforeUpdate(t *testing.T) {
oldTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
b := &BaseModel{
UpdatedAt: oldTime,
}
err := b.BeforeUpdate(nil)
require.NoError(t, err)
assert.True(t, b.UpdatedAt.After(oldTime))
}