| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- package repository
- import (
- "context"
- "regexp"
- "testing"
- "time"
- sqlmock "github.com/DATA-DOG/go-sqlmock"
- "github.com/stretchr/testify/require"
- )
- func TestSchedulerOutboxRepositoryFirstCreatedAtAfter(t *testing.T) {
- db, mock, err := sqlmock.New()
- require.NoError(t, err)
- defer func() { _ = db.Close() }()
- repo := &schedulerOutboxRepository{db: db}
- createdAt := time.Now().UTC().Truncate(time.Microsecond)
- const expectedSQL = `
- SELECT created_at
- FROM scheduler_outbox
- WHERE id > $1
- ORDER BY id ASC
- LIMIT 1
- `
- mock.ExpectQuery(regexp.QuoteMeta(expectedSQL)).
- WithArgs(int64(42)).
- WillReturnRows(sqlmock.NewRows([]string{"created_at"}).AddRow(createdAt))
- got, ok, err := repo.FirstCreatedAtAfter(context.Background(), 42)
- require.NoError(t, err)
- require.True(t, ok)
- require.Equal(t, createdAt, got)
- require.NoError(t, mock.ExpectationsWereMet())
- }
- func TestSchedulerOutboxRepositoryFirstCreatedAtAfterReturnsNotFound(t *testing.T) {
- db, mock, err := sqlmock.New()
- require.NoError(t, err)
- defer func() { _ = db.Close() }()
- repo := &schedulerOutboxRepository{db: db}
- const expectedSQL = `
- SELECT created_at
- FROM scheduler_outbox
- WHERE id > $1
- ORDER BY id ASC
- LIMIT 1
- `
- mock.ExpectQuery(regexp.QuoteMeta(expectedSQL)).
- WithArgs(int64(42)).
- WillReturnRows(sqlmock.NewRows([]string{"created_at"}))
- got, ok, err := repo.FirstCreatedAtAfter(context.Background(), 42)
- require.NoError(t, err)
- require.False(t, ok)
- require.True(t, got.IsZero())
- require.NoError(t, mock.ExpectationsWereMet())
- }
- func TestSchedulerOutboxRepositoryDeleteConsumedUpToUsesBoundedCTE(t *testing.T) {
- db, mock, err := sqlmock.New()
- require.NoError(t, err)
- defer func() { _ = db.Close() }()
- repo := &schedulerOutboxRepository{db: db}
- const expectedSQL = `
- WITH doomed AS (
- SELECT id
- FROM scheduler_outbox
- WHERE id <= $1
- AND created_at < NOW() - INTERVAL '10 seconds'
- ORDER BY id ASC
- LIMIT $2
- )
- DELETE FROM scheduler_outbox o
- USING doomed d
- WHERE o.id = d.id
- `
- mock.ExpectExec(regexp.QuoteMeta(expectedSQL)).
- WithArgs(int64(42), 5000).
- WillReturnResult(sqlmock.NewResult(0, 17))
- deleted, err := repo.DeleteConsumedUpTo(context.Background(), 42, 5000)
- require.NoError(t, err)
- require.EqualValues(t, 17, deleted)
- require.NoError(t, mock.ExpectationsWereMet())
- }
- func TestSchedulerOutboxRepositoryDeleteConsumedUpToSkipsNonPositiveWatermark(t *testing.T) {
- db, mock, err := sqlmock.New()
- require.NoError(t, err)
- defer func() { _ = db.Close() }()
- repo := &schedulerOutboxRepository{db: db}
- deleted, err := repo.DeleteConsumedUpTo(context.Background(), 0, 5000)
- require.NoError(t, err)
- require.EqualValues(t, 0, deleted)
- require.NoError(t, mock.ExpectationsWereMet())
- }
- func TestSchedulerOutboxRepositoryTryAcquireCleanupLock(t *testing.T) {
- db, mock, err := sqlmock.New()
- require.NoError(t, err)
- defer func() { _ = db.Close() }()
- repo := &schedulerOutboxRepository{db: db}
- mock.ExpectQuery(regexp.QuoteMeta("SELECT pg_try_advisory_lock(hashtext('scheduler_outbox_cleanup'))")).
- WillReturnRows(sqlmock.NewRows([]string{"pg_try_advisory_lock"}).AddRow(true))
- mock.ExpectExec(regexp.QuoteMeta("SELECT pg_advisory_unlock(hashtext('scheduler_outbox_cleanup'))")).
- WillReturnResult(sqlmock.NewResult(0, 1))
- lease, acquired, err := repo.TryAcquireCleanupLock(context.Background())
- require.NoError(t, err)
- require.True(t, acquired)
- require.NotNil(t, lease)
- lease.Release()
- require.NoError(t, mock.ExpectationsWereMet())
- }
- func TestSchedulerOutboxRepositoryTryAcquireCleanupLockUnavailable(t *testing.T) {
- db, mock, err := sqlmock.New()
- require.NoError(t, err)
- defer func() { _ = db.Close() }()
- repo := &schedulerOutboxRepository{db: db}
- mock.ExpectQuery(regexp.QuoteMeta("SELECT pg_try_advisory_lock(hashtext('scheduler_outbox_cleanup'))")).
- WillReturnRows(sqlmock.NewRows([]string{"pg_try_advisory_lock"}).AddRow(false))
- lease, acquired, err := repo.TryAcquireCleanupLock(context.Background())
- require.NoError(t, err)
- require.False(t, acquired)
- require.Nil(t, lease)
- require.NoError(t, mock.ExpectationsWereMet())
- }
- // buildSchedulerGroupPayload 在 groupIDs 为空时必须返回 untyped nil(any),
- // 否则 enqueueSchedulerOutbox 的 "payload != nil" 接口判空会被 typed-nil 欺骗,
- // 把 payload marshal 成 "null" 写入 dedup_key 哈希,破坏与其他 nil-payload
- // 调用的去重一致性。本测试用 ungrouped 账号场景验证两条路径的 dedup_key 一致。
- func TestEnqueueSchedulerOutbox_UngroupedAccountDedupesWithLiteralNilPayload(t *testing.T) {
- accountID := int64(42)
- // Path A: 显式 nil payload(如 SetError、SetStatus 等调用模式)
- keyLiteralNil := schedulerOutboxDedupKey("account_changed", &accountID, nil, nil)
- // Path B: buildSchedulerGroupPayload(account.GroupIDs) 当账号没有任何分组
- emptyGroupsPayload := buildSchedulerGroupPayload(nil)
- require.Nil(t, emptyGroupsPayload,
- "buildSchedulerGroupPayload(empty) must return untyped-nil any to avoid typed-nil marshal")
- // 模拟 enqueueSchedulerOutbox 内部的判空逻辑
- var payloadJSON []byte
- if emptyGroupsPayload != nil {
- t.Fatalf("typed-nil regression: buildSchedulerGroupPayload(empty) interface should be nil")
- }
- keyEmptyGroups := schedulerOutboxDedupKey("account_changed", &accountID, nil, payloadJSON)
- require.Equal(t, keyLiteralNil, keyEmptyGroups,
- "ungrouped-account account_changed must share dedup_key with other nil-payload variants")
- }
|