feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
146
app/models/notification_get_contacts_test.go
Обычный файл
146
app/models/notification_get_contacts_test.go
Обычный файл
@@ -0,0 +1,146 @@
|
||||
package models_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
"rsgit.ru/rsmon/rsmon/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(¬ification, 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)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user