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)) }