package store import ( "context" "errors" "testing" "time" "github.com/tech/sendico/ledger/storage/model" "github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository/builder" rd "github.com/tech/sendico/pkg/db/repository/decoder" "github.com/tech/sendico/pkg/db/storable" "github.com/tech/sendico/pkg/merrors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" ) func TestOutboxStore_Create(t *testing.T) { ctx := context.Background() logger := zap.NewNop() t.Run("Success", func(t *testing.T) { var insertedEvent *model.OutboxEvent stub := &repositoryStub{ InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { insertedEvent = object.(*model.OutboxEvent) return nil }, } store := &outboxStore{logger: logger, repo: stub} event := &model.OutboxEvent{ EventID: "evt_12345", Subject: "ledger.entry.created", Payload: []byte(`{"entryId":"123"}`), Status: model.OutboxStatusPending, } err := store.Create(ctx, event) require.NoError(t, err) assert.NotNil(t, insertedEvent) assert.Equal(t, "evt_12345", insertedEvent.EventID) assert.Equal(t, "ledger.entry.created", insertedEvent.Subject) assert.Equal(t, model.OutboxStatusPending, insertedEvent.Status) }) t.Run("NilEvent", func(t *testing.T) { stub := &repositoryStub{} store := &outboxStore{logger: logger, repo: stub} err := store.Create(ctx, nil) require.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) }) t.Run("DuplicateEventID", func(t *testing.T) { stub := &repositoryStub{ InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { return mongo.WriteException{ WriteErrors: []mongo.WriteError{ {Code: 11000}, // Duplicate key error }, } }, } store := &outboxStore{logger: logger, repo: stub} event := &model.OutboxEvent{ EventID: "duplicate_event", Subject: "test.subject", Status: model.OutboxStatusPending, } err := store.Create(ctx, event) require.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrDataConflict)) }) t.Run("InsertError", func(t *testing.T) { expectedErr := errors.New("database error") stub := &repositoryStub{ InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { return expectedErr }, } store := &outboxStore{logger: logger, repo: stub} event := &model.OutboxEvent{ EventID: "evt_123", Subject: "test.subject", } err := store.Create(ctx, event) require.Error(t, err) assert.Equal(t, expectedErr, err) }) } func TestOutboxStore_ListPending(t *testing.T) { ctx := context.Background() logger := zap.NewNop() t.Run("Success", func(t *testing.T) { called := false stub := &repositoryStub{ FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { called = true return nil }, } store := &outboxStore{logger: logger, repo: stub} results, err := store.ListPending(ctx, 10) require.NoError(t, err) assert.True(t, called) assert.NotNil(t, results) }) t.Run("EmptyResult", func(t *testing.T) { stub := &repositoryStub{ FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { return nil }, } store := &outboxStore{logger: logger, repo: stub} results, err := store.ListPending(ctx, 10) require.NoError(t, err) assert.Len(t, results, 0) }) t.Run("WithLimit", func(t *testing.T) { called := false stub := &repositoryStub{ FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { called = true return nil }, } store := &outboxStore{logger: logger, repo: stub} results, err := store.ListPending(ctx, 3) require.NoError(t, err) assert.True(t, called) assert.NotNil(t, results) }) t.Run("FindError", func(t *testing.T) { expectedErr := errors.New("database error") stub := &repositoryStub{ FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { return expectedErr }, } store := &outboxStore{logger: logger, repo: stub} results, err := store.ListPending(ctx, 10) require.Error(t, err) assert.Nil(t, results) assert.Equal(t, expectedErr, err) }) } func TestOutboxStore_MarkSent(t *testing.T) { ctx := context.Background() logger := zap.NewNop() eventRef := primitive.NewObjectID() sentTime := time.Now() t.Run("Success", func(t *testing.T) { var patchedID primitive.ObjectID stub := &repositoryStub{ PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { patchedID = id return nil }, } store := &outboxStore{logger: logger, repo: stub} err := store.MarkSent(ctx, eventRef, sentTime) require.NoError(t, err) assert.Equal(t, eventRef, patchedID) }) t.Run("ZeroEventID", func(t *testing.T) { stub := &repositoryStub{} store := &outboxStore{logger: logger, repo: stub} err := store.MarkSent(ctx, primitive.NilObjectID, sentTime) require.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) }) t.Run("PatchError", func(t *testing.T) { expectedErr := errors.New("database error") stub := &repositoryStub{ PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { return expectedErr }, } store := &outboxStore{logger: logger, repo: stub} err := store.MarkSent(ctx, eventRef, sentTime) require.Error(t, err) assert.Equal(t, expectedErr, err) }) } func TestOutboxStore_MarkFailed(t *testing.T) { ctx := context.Background() logger := zap.NewNop() eventRef := primitive.NewObjectID() t.Run("Success", func(t *testing.T) { var patchedID primitive.ObjectID stub := &repositoryStub{ PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { patchedID = id return nil }, } store := &outboxStore{logger: logger, repo: stub} err := store.MarkFailed(ctx, eventRef) require.NoError(t, err) assert.Equal(t, eventRef, patchedID) }) t.Run("ZeroEventID", func(t *testing.T) { stub := &repositoryStub{} store := &outboxStore{logger: logger, repo: stub} err := store.MarkFailed(ctx, primitive.NilObjectID) require.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) }) t.Run("PatchError", func(t *testing.T) { expectedErr := errors.New("database error") stub := &repositoryStub{ PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { return expectedErr }, } store := &outboxStore{logger: logger, repo: stub} err := store.MarkFailed(ctx, eventRef) require.Error(t, err) assert.Equal(t, expectedErr, err) }) } func TestOutboxStore_IncrementAttempts(t *testing.T) { ctx := context.Background() logger := zap.NewNop() eventRef := primitive.NewObjectID() t.Run("Success", func(t *testing.T) { var patchedID primitive.ObjectID stub := &repositoryStub{ PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { patchedID = id return nil }, } store := &outboxStore{logger: logger, repo: stub} err := store.IncrementAttempts(ctx, eventRef) require.NoError(t, err) assert.Equal(t, eventRef, patchedID) }) t.Run("ZeroEventID", func(t *testing.T) { stub := &repositoryStub{} store := &outboxStore{logger: logger, repo: stub} err := store.IncrementAttempts(ctx, primitive.NilObjectID) require.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) }) t.Run("PatchError", func(t *testing.T) { expectedErr := errors.New("database error") stub := &repositoryStub{ PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { return expectedErr }, } store := &outboxStore{logger: logger, repo: stub} err := store.IncrementAttempts(ctx, eventRef) require.Error(t, err) assert.Equal(t, expectedErr, err) }) t.Run("MultipleIncrements", func(t *testing.T) { var callCount int stub := &repositoryStub{ PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { callCount++ return nil }, } store := &outboxStore{logger: logger, repo: stub} // Simulate multiple retry attempts for i := 0; i < 3; i++ { err := store.IncrementAttempts(ctx, eventRef) require.NoError(t, err) } assert.Equal(t, 3, callCount) }) }