实现七月迭代公共技术基础
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
This commit is contained in:
129
internal/application/outbox/recovery_integration_test.go
Normal file
129
internal/application/outbox/recovery_integration_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package outbox_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
outboxapp "github.com/break/junhong_cmp_fiber/internal/application/outbox"
|
||||
infraoutbox "github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/testutil"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
type databaseAuditWriter struct {
|
||||
fail bool
|
||||
}
|
||||
|
||||
func (w *databaseAuditWriter) WriteRecovery(_ context.Context, tx *gorm.DB, audit outboxapp.RecoveryAudit) error {
|
||||
if w.fail {
|
||||
return stderrors.New("测试审计不可用")
|
||||
}
|
||||
return tx.Exec(`INSERT INTO test_outbox_recovery_audit (batch_id, operation_type, event_count)
|
||||
VALUES (?, ?, ?)`, audit.BatchID, audit.OperationType, len(audit.EventIDs)).Error
|
||||
}
|
||||
|
||||
func TestSelectiveReplayPreservesIdentityAndWritesAudit(t *testing.T) {
|
||||
db := testutil.NewPostgresTransaction(t)
|
||||
testutil.CreateTemporaryOutboxTable(t, db)
|
||||
createTemporaryRecoveryAuditTable(t, db)
|
||||
now := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC)
|
||||
repository := infraoutbox.NewRepository()
|
||||
event, err := repository.Append(context.Background(), db, infraoutbox.Envelope{
|
||||
EventID: "recovery-stable", EventType: "example.recovery", AggregateType: "example", AggregateID: "1",
|
||||
ResourceType: "example", ResourceID: "1", CorrelationID: "correlation-stable",
|
||||
Payload: struct {
|
||||
Secret string `json:"secret"`
|
||||
}{Secret: "preserved-in-database-only"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("准备恢复事件失败:%v", err)
|
||||
}
|
||||
if err := db.Model(&model.OutboxEvent{}).Where("id = ?", event.ID).Updates(map[string]any{
|
||||
"status": constants.OutboxStatusFailed, "retry_count": 10, "last_error_code": "FINAL",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("设置最终失败状态失败:%v", err)
|
||||
}
|
||||
var storedBefore model.OutboxEvent
|
||||
if err := db.First(&storedBefore, event.ID).Error; err != nil {
|
||||
t.Fatalf("读取重放前事件失败:%v", err)
|
||||
}
|
||||
|
||||
service, err := outboxapp.NewRecoveryService(db, &databaseAuditWriter{}, func() time.Time { return now })
|
||||
if err != nil {
|
||||
t.Fatalf("创建恢复服务失败:%v", err)
|
||||
}
|
||||
batchID, err := service.Replay(context.Background(), outboxapp.Operator{
|
||||
ID: 7, SuperAdmin: true, RequestID: "request-recovery", CorrelationID: "correlation-recovery",
|
||||
}, []uint{event.ID}, "确认消费者已具备幂等保护")
|
||||
if err != nil || batchID == "" {
|
||||
t.Fatalf("选择性重放失败:%v", err)
|
||||
}
|
||||
var recovered model.OutboxEvent
|
||||
if err := db.First(&recovered, event.ID).Error; err != nil {
|
||||
t.Fatalf("读取重放事件失败:%v", err)
|
||||
}
|
||||
if recovered.Status != constants.OutboxStatusPending || recovered.EventID != event.EventID ||
|
||||
recovered.CorrelationID != event.CorrelationID || string(recovered.Payload) != string(storedBefore.Payload) {
|
||||
t.Fatalf("重放改变了事件身份或内容:%+v", recovered)
|
||||
}
|
||||
var auditCount int64
|
||||
if err := db.Table("test_outbox_recovery_audit").Where("batch_id = ?", batchID).Count(&auditCount).Error; err != nil || auditCount != 1 {
|
||||
t.Fatalf("恢复审计事实缺失:%v,数量:%d", err, auditCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveryRejectsActiveLeaseUnauthorizedOperatorAndAuditFailure(t *testing.T) {
|
||||
db := testutil.NewPostgresTransaction(t)
|
||||
testutil.CreateTemporaryOutboxTable(t, db)
|
||||
createTemporaryRecoveryAuditTable(t, db)
|
||||
now := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC)
|
||||
repository := infraoutbox.NewRepository()
|
||||
event, err := repository.Append(context.Background(), db, infraoutbox.Envelope{
|
||||
EventID: "active-lease", EventType: "example.recovery", AggregateType: "example", AggregateID: "2",
|
||||
ResourceType: "example", ResourceID: "2", Payload: struct {
|
||||
Visible bool `json:"visible"`
|
||||
}{Visible: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("准备有效租约事件失败:%v", err)
|
||||
}
|
||||
if err := db.Model(&model.OutboxEvent{}).Where("id = ?", event.ID).Updates(map[string]any{
|
||||
"status": constants.OutboxStatusDelivering, "lease_owner": "active-worker", "lease_expires_at": now.Add(time.Minute),
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("设置有效租约失败:%v", err)
|
||||
}
|
||||
service, _ := outboxapp.NewRecoveryService(db, &databaseAuditWriter{}, func() time.Time { return now })
|
||||
if _, err := service.ReleaseExpiredLeases(context.Background(), outboxapp.Operator{ID: 7, SuperAdmin: true}, []uint{event.ID}, "测试释放"); err == nil {
|
||||
t.Fatal("不得释放仍然有效的租约")
|
||||
}
|
||||
if _, err := service.Replay(context.Background(), outboxapp.Operator{ID: 8, SuperAdmin: false}, []uint{event.ID}, "越权测试"); err == nil {
|
||||
t.Fatal("非超级管理员不得执行人工恢复")
|
||||
}
|
||||
|
||||
if err := db.Model(&model.OutboxEvent{}).Where("id = ?", event.ID).Update("status", constants.OutboxStatusFailed).Error; err != nil {
|
||||
t.Fatalf("设置失败事件失败:%v", err)
|
||||
}
|
||||
failClosed, _ := outboxapp.NewRecoveryService(db, &databaseAuditWriter{fail: true}, func() time.Time { return now })
|
||||
if _, err := failClosed.Replay(context.Background(), outboxapp.Operator{ID: 7, SuperAdmin: true}, []uint{event.ID}, "审计失败测试"); err == nil {
|
||||
t.Fatal("统一审计不可用时恢复事务必须失败")
|
||||
}
|
||||
var unchanged model.OutboxEvent
|
||||
if err := db.First(&unchanged, event.ID).Error; err != nil || unchanged.Status != constants.OutboxStatusFailed {
|
||||
t.Fatalf("审计失败后事件状态未回滚:%v,状态:%d", err, unchanged.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func createTemporaryRecoveryAuditTable(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
if err := db.Exec(`CREATE TEMP TABLE test_outbox_recovery_audit (
|
||||
id bigserial PRIMARY KEY, batch_id varchar(64) NOT NULL,
|
||||
operation_type varchar(100) NOT NULL, event_count integer NOT NULL
|
||||
) ON COMMIT DROP`).Error; err != nil {
|
||||
t.Fatalf("创建恢复审计测试表失败:%v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user