Files
worker/app/models/notification_get_contacts_test.go
Gleb Tv 2c884c5612
Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
refactor: adopt worker module path
2026-07-13 17:56:12 +03:00

147 строки
5.5 KiB
Go

package models_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"rocketgit.ru/rsmon/worker/app/models"
"rocketgit.ru/rsmon/worker/spec/factories"
)
// TestNotificationGetContactsIncludesSystemContact exercises the regression
// reported when the production dump was restored into dev: the
// contacts.is_system column was missing and the GORM preload panicked on
// GetContacts. The fix has two layers:
//
// 1. AutoMigrate must add is_system (and deletion_requested_at) before the
// notifier scheduler starts running.
// 2. GetContacts itself must not panic on a query error so a single bad row
// cannot tear down the scheduler goroutine.
//
// This test verifies both layers by:
// - asserting that the schema post-Migrate includes is_system, so the
// production-like scenario no longer panics; and
// - building a notification that contains a contact flagged is_system=true
// and checking GetContacts returns it.
func TestNotificationGetContactsIncludesSystemContact(t *testing.T) {
models.Drop()
models.Migrate()
// Column must exist after Migrate; otherwise GetContacts would fail
// with the same panic we saw in production.
assertColumnExists(t, "contacts", "is_system")
account := &models.Account{Name: "acct-get-contacts"}
require.NoError(t, models.DB().Create(account).Error)
accountID := account.ID
trueVal := true
contact := &models.Contact{
AccountID: &accountID,
Name: "system-admin",
Kind: "email",
Value: "ops@example.com",
IsSystem: &trueVal,
}
require.NoError(t, models.DB().Create(contact).Error)
notification := factories.PersistedNotification(
account, []int64{contact.ID}, nil, 300, false,
)
// Reload so the model has its persisted ID; the factory's PersistRelations
// may have left ContactIDs empty on the returned value depending on the
// GORM version, so fetch fresh.
require.NoError(t, models.DB().
Preload("Contacts").
First(&notification, notification.ID).Error)
got := notification.GetContacts()
ids := make([]int64, 0, len(got))
for _, c := range got {
ids = append(ids, c.ID)
}
assert.Contains(t, ids, contact.ID, "GetContacts must include the is_system contact")
}
// TestUserDeletionRequestedAtColumnAndRoundTrip verifies the second missing
// column reported by the panic: users.deletion_requested_at. It asserts that
// AutoMigrate creates the column and that the field round-trips through the
// DB correctly. ProcessPendingDeletions (the consumer of this column) relies
// on it being present and queryable.
func TestUserDeletionRequestedAtColumnAndRoundTrip(t *testing.T) {
models.Drop()
models.Migrate()
assertColumnExists(t, "users", "deletion_requested_at")
user := factories.PersistedUser("deletion-roundtrip@test.ru", "secret")
now := time.Now().UTC().Truncate(time.Microsecond)
user.DeletionRequestedAt = &now
require.NoError(t, models.DB().Save(&user).Error)
reloaded := models.User{}
require.NoError(t, models.DB().First(&reloaded, user.ID).Error)
require.NotNil(t, reloaded.DeletionRequestedAt, "deletion_requested_at must round-trip via Save/First")
assert.True(t, reloaded.DeletionRequestedAt.Equal(now),
"deletion_requested_at must preserve the timestamp value (got %v, want %v)",
reloaded.DeletionRequestedAt, now)
// ProcessPendingDeletions should not panic on the populated schema and
// must respect the cutoff: a recently-set deletion_requested_at is
// still inside the 7-day grace period, so no hard-delete must occur.
deleted, err := models.ProcessPendingDeletions()
require.NoError(t, err)
assert.Equal(t, 0, deleted, "users within the 7-day grace period must not be hard-deleted")
}
// TestProcessPendingDeletionsQueriesMissingColumnGracefully asserts that even
// if the deletion_requested_at column were missing, ProcessPendingDeletions
// would not panic (the panic-on-error pattern was historically present in
// other notifier helpers). We force the failure by renaming the column back,
// calling ProcessPendingDeletions, then restoring the column.
func TestProcessPendingDeletionsQueriesMissingColumnGracefully(t *testing.T) {
models.Drop()
models.Migrate()
assertColumnExists(t, "users", "deletion_requested_at")
// Simulate the production-missing-column scenario in a contained way:
// rename the column so the SELECT against deletion_requested_at fails.
require.NoError(t, models.DB().
Exec("ALTER TABLE users RENAME COLUMN deletion_requested_at TO deletion_requested_at_hidden").Error)
t.Cleanup(func() {
// Restore so subsequent tests in this package keep working.
_ = models.DB().
Exec("ALTER TABLE users RENAME COLUMN deletion_requested_at_hidden TO deletion_requested_at").Error
})
// Must not panic; must return an error.
assert.NotPanics(t, func() {
_, err := models.ProcessPendingDeletions()
assert.Error(t, err, "missing column must surface as an error, not a panic")
})
}
// assertColumnExists checks that the given table has the given column by
// querying information_schema. It is the canary for the AutoMigrate step
// ordering bug: if the column is missing, every test that touches it will
// panic with SQLSTATE 42703.
func assertColumnExists(t *testing.T, table, column string) {
t.Helper()
var n int
err := models.DB().Raw(
`SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = ? AND column_name = ?`,
table, column,
).Scan(&n).Error
require.NoError(t, err, "information_schema query must succeed")
assert.Equal(t, 1, n, "table %q must have column %q after Migrate()", table, column)
}