c77ff07ce9
Remediation of the 2026-05-12/13 audits (78 findings + cluster gaps), tracked in deploy-k3s/SECURITY.md, plus fixes from two independent post-remediation reviews. Auth & sessions: - SHA-256 hashed auth-token storage (C1); prior-token cache eviction on re-login (MEDIUM-1) - local Google JWKS verification, iss/aud/exp checks (C2/C3) - constant-time login + generic errors (L1/LIVE-L11/LIVE-L13) - per-account login lockout keyed on distinct source IPs (M5/MEDIUM-3) - verified-email gating, login rate limiting (LIVE-L19, H1-H3) IAP & webhooks: - Apple/Google cross-account replay protection (C5/C6/C10/C13, H5/H6) - migrations 000003-000006 (token hashing, IAP replay, audit_log + webhook_event_log table creation, append-only audit log) Authorization & races: - file-ownership owner-OR-member fix (C7), atomic share-code join (C9/H9), device-token reassignment (C8/LOW-3) Secrets & deploy: - secrets file-mounted at /etc/honeydue/secrets, not env (F8); Redis password out of the ConfigMap (HIGH-1); B2 keys reconciled - digest-pinned images, admin ingress hardening, CSP/HSTS, /metrics lockdown; kubeconfig 0600, etcd secrets-encryption, fail2ban + unattended-upgrades at provision; secret-rotation runbook Build, vet, and the full test suite (incl. -race) pass; the goose migration chain is verified against PostgreSQL 16. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
628 lines
16 KiB
Go
628 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, 64) // SHA-256 hex hash (audit C1)
|
|
assert.Len(t, token.Plaintext, 40) // raw 20-byte token, returned to the client
|
|
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))
|
|
}
|