Files
honeyDueAPI/internal/models/models_coverage_test.go
T
Trey t 81578f6e27
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
feat(auth): replace hand-rolled auth with Ory Kratos — phase 2 backend
Delegates all credential management (login, register, password reset,
email verification, social sign-in) to Ory Kratos. The Go API now acts
as a resource server: the new KratosAuth middleware validates sessions
against the Kratos whoami endpoint, writes the local User mirror into
Echo context, and all existing domain handlers continue working
unchanged. Hand-rolled token auth, AuthToken model, apple_auth/
google_auth services, and the auth refresh flow are removed. Tests are
updated to use the fake-token middleware pattern so existing integration
assertions require no rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:55:56 -05:00

504 lines
13 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{}, &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)
}
// === 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)
}
// === 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))
}