Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type DatabaseInterface interface {
Ping(context.Context) error
Find(ctx context.Context, db string, collection string, filter any, opts ...any) FindResultInterface
FindOne(ctx context.Context, db string, collection string, filter any, opts ...any) SingleResultInterface
FindOneAndUpdate(ctx context.Context, db string, collection string, filter any, update any, opts ...any) SingleResultInterface
UpdateOne(ctx context.Context, db string, collection string, filter any, update any, opts ...any) (UpdateResultInterface, error)
DeleteOne(ctx context.Context, db string, collection string, filter any, opts ...any) (DeleteResultInterface, error)
DeleteMany(ctx context.Context, db string, collection string, filter any, opts ...any) (DeleteResultInterface, error)
Expand Down
116 changes: 88 additions & 28 deletions pkg/database/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ type MockDatabase struct {
// FindOneFunc allows customizing FindOne behavior - returns a SingleResultInterface
FindOneFunc func(ctx context.Context, db string, collection string, filter any, opts ...any) SingleResultInterface

// FindOneAndUpdateFunc allows customizing atomic find-and-update behavior.
FindOneAndUpdateFunc func(ctx context.Context, db string, collection string, filter any, update any, opts ...any) SingleResultInterface

// UpdateOneFunc allows customizing UpdateOne behavior
UpdateOneFunc func(ctx context.Context, db string, collection string, filter any, update any, opts ...any) (UpdateResultInterface, error)

Expand All @@ -33,24 +36,28 @@ type MockDatabase struct {
CountFunc func(ctx context.Context, db string, collection string, filter any, opts ...any) (int64, error)

// Sequential response queues for multiple calls
PingQueue []PingResponse
FindQueue []FindResponse
FindOneQueue []FindOneResponse
UpdateOneQueue []UpdateOneResponse
DeleteOneQueue []DeleteResponse
DeleteManyQueue []DeleteResponse
CountQueue []CountResponse
PingQueue []PingResponse
FindQueue []FindResponse
FindOneQueue []FindOneResponse
FindOneAndUpdateQueue []FindOneResponse
UpdateOneQueue []UpdateOneResponse
DeleteOneQueue []DeleteResponse
DeleteManyQueue []DeleteResponse
CountQueue []CountResponse

// Call tracking
PingCalls []PingCall
FindCalls []FindCall
FindOneCalls []FindOneCall
UpdateOneCalls []UpdateOneCall
DeleteOneCalls []DeleteCall
DeleteManyCalls []DeleteCall
CountCalls []CountCall
PingCalls []PingCall
FindCalls []FindCall
FindOneCalls []FindOneCall
FindOneAndUpdateCalls []FindOneAndUpdateCall
UpdateOneCalls []UpdateOneCall
DeleteOneCalls []DeleteCall
DeleteManyCalls []DeleteCall
CountCalls []CountCall
}

var _ DatabaseInterface = (*MockDatabase)(nil)

// MockSingleResult implements SingleResultInterface for testing
type MockSingleResult struct {
result any
Expand Down Expand Up @@ -193,6 +200,16 @@ type FindOneCall struct {
Opts []any
}

// FindOneAndUpdateCall records an atomic find-and-update call.
type FindOneAndUpdateCall struct {
Ctx context.Context
Db string
Collection string
Filter any
Update any
Opts []any
}

// UpdateOneCall records a call to UpdateOne
type UpdateOneCall struct {
Ctx context.Context
Expand Down Expand Up @@ -239,6 +256,9 @@ func NewMockDatabase() *MockDatabase {
FindOneFunc: func(ctx context.Context, db string, collection string, filter any, opts ...any) SingleResultInterface {
return &MockSingleResult{result: nil, err: fmt.Errorf("no document found")}
},
FindOneAndUpdateFunc: func(ctx context.Context, db string, collection string, filter any, update any, opts ...any) SingleResultInterface {
return &MockSingleResult{result: nil, err: fmt.Errorf("no document found")}
},
UpdateOneFunc: func(ctx context.Context, db string, collection string, filter any, update any, opts ...any) (UpdateResultInterface, error) {
return &MockUpdateResult{matchedCount: 1, modifiedCount: 1}, nil
},
Expand All @@ -251,20 +271,22 @@ func NewMockDatabase() *MockDatabase {
CountFunc: func(ctx context.Context, db string, collection string, filter any, opts ...any) (int64, error) {
return 0, nil
},
PingCalls: []PingCall{},
FindCalls: []FindCall{},
FindOneCalls: []FindOneCall{},
UpdateOneCalls: []UpdateOneCall{},
DeleteOneCalls: []DeleteCall{},
DeleteManyCalls: []DeleteCall{},
CountCalls: []CountCall{},
PingQueue: []PingResponse{},
FindQueue: []FindResponse{},
FindOneQueue: []FindOneResponse{},
UpdateOneQueue: []UpdateOneResponse{},
DeleteOneQueue: []DeleteResponse{},
DeleteManyQueue: []DeleteResponse{},
CountQueue: []CountResponse{},
PingCalls: []PingCall{},
FindCalls: []FindCall{},
FindOneCalls: []FindOneCall{},
FindOneAndUpdateCalls: []FindOneAndUpdateCall{},
UpdateOneCalls: []UpdateOneCall{},
DeleteOneCalls: []DeleteCall{},
DeleteManyCalls: []DeleteCall{},
CountCalls: []CountCall{},
PingQueue: []PingResponse{},
FindQueue: []FindResponse{},
FindOneQueue: []FindOneResponse{},
FindOneAndUpdateQueue: []FindOneResponse{},
UpdateOneQueue: []UpdateOneResponse{},
DeleteOneQueue: []DeleteResponse{},
DeleteManyQueue: []DeleteResponse{},
CountQueue: []CountResponse{},
}
}

Expand Down Expand Up @@ -377,6 +399,28 @@ func (m *MockDatabase) FindOne(ctx context.Context, db string, collection string
return &MockSingleResult{result: result, err: err}
}

// FindOneAndUpdate implements DatabaseInterface.
func (m *MockDatabase) FindOneAndUpdate(ctx context.Context, db string, collection string, filter any, update any, opts ...any) SingleResultInterface {
m.FindOneAndUpdateCalls = append(m.FindOneAndUpdateCalls, FindOneAndUpdateCall{
Ctx: ctx,
Db: db,
Collection: collection,
Filter: filter,
Update: update,
Opts: opts,
})

if len(m.FindOneAndUpdateQueue) > 0 {
response := m.FindOneAndUpdateQueue[0]
m.FindOneAndUpdateQueue = m.FindOneAndUpdateQueue[1:]
return &MockSingleResult{result: response.Result, err: response.Err}
}
if m.FindOneAndUpdateFunc != nil {
return m.FindOneAndUpdateFunc(ctx, db, collection, filter, update, opts...)
}
return &MockSingleResult{result: nil, err: fmt.Errorf("no document found")}
}

// copyResult copies src into dest using BSON marshaling (for mock testing)
func copyResult(src any, dest any) error {
bytes, err := bson.Marshal(src)
Expand Down Expand Up @@ -590,13 +634,15 @@ func (m *MockDatabase) Reset() {
m.PingCalls = []PingCall{}
m.FindCalls = []FindCall{}
m.FindOneCalls = []FindOneCall{}
m.FindOneAndUpdateCalls = []FindOneAndUpdateCall{}
m.UpdateOneCalls = []UpdateOneCall{}
m.DeleteOneCalls = []DeleteCall{}
m.DeleteManyCalls = []DeleteCall{}
m.CountCalls = []CountCall{}
m.PingQueue = []PingResponse{}
m.FindQueue = []FindResponse{}
m.FindOneQueue = []FindOneResponse{}
m.FindOneAndUpdateQueue = []FindOneResponse{}
m.UpdateOneQueue = []UpdateOneResponse{}
m.DeleteOneQueue = []DeleteResponse{}
m.DeleteManyQueue = []DeleteResponse{}
Expand Down Expand Up @@ -627,6 +673,14 @@ func (m *MockDatabase) ExpectFindOne(result any, err error) *MockDatabase {
return m
}

// ExpectFindOneAndUpdate sets up an atomic find-and-update response.
func (m *MockDatabase) ExpectFindOneAndUpdate(result any, err error) *MockDatabase {
m.FindOneAndUpdateFunc = func(ctx context.Context, db string, collection string, filter any, update any, opts ...any) SingleResultInterface {
return &MockSingleResult{result: result, err: err}
}
return m
}

// ExpectUpdateOne sets up an expectation for UpdateOne
func (m *MockDatabase) ExpectUpdateOne(matchedCount, modifiedCount, upsertedCount int64, upsertedID any, err error) *MockDatabase {
m.UpdateOneFunc = func(ctx context.Context, db string, collection string, filter any, update any, opts ...any) (UpdateResultInterface, error) {
Expand Down Expand Up @@ -683,6 +737,12 @@ func (m *MockDatabase) QueueFindOne(result any, err error) *MockDatabase {
return m
}

// QueueFindOneAndUpdate adds an atomic find-and-update response to the queue.
func (m *MockDatabase) QueueFindOneAndUpdate(result any, err error) *MockDatabase {
m.FindOneAndUpdateQueue = append(m.FindOneAndUpdateQueue, FindOneResponse{Result: result, Err: err})
return m
}

// QueueUpdateOne adds an UpdateOne response to the queue for sequential calls
func (m *MockDatabase) QueueUpdateOne(matchedCount, modifiedCount, upsertedCount int64, upsertedID any, err error) *MockDatabase {
m.UpdateOneQueue = append(m.UpdateOneQueue, UpdateOneResponse{
Expand Down
41 changes: 41 additions & 0 deletions pkg/database/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,47 @@ func TestMockDatabase(t *testing.T) {
}
})

t.Run("ExpectFindOneAndUpdateWithResult", func(t *testing.T) {
mock := NewMockDatabase()
expected := map[string]any{"id": 1, "name": "Updated"}
filter := map[string]any{"id": 1}
update := map[string]any{"$set": map[string]any{"name": "Updated"}}
mock.ExpectFindOneAndUpdate(expected, nil)

var result map[string]any
err := mock.FindOneAndUpdate(context.Background(), "testdb", "users", filter, update).Into(&result)
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if result["name"] != "Updated" {
t.Fatalf("expected updated document, got %#v", result)
}
if len(mock.FindOneAndUpdateCalls) != 1 {
t.Fatalf("expected 1 findOneAndUpdate call, got %d", len(mock.FindOneAndUpdateCalls))
}
call := mock.FindOneAndUpdateCalls[0]
if call.Db != "testdb" || call.Collection != "users" {
t.Fatalf("unexpected call target %s.%s", call.Db, call.Collection)
}
})

t.Run("QueueFindOneAndUpdateErrorAndReset", func(t *testing.T) {
mock := NewMockDatabase().QueueFindOneAndUpdate(nil, errors.New("update failed"))

err := mock.FindOneAndUpdate(context.Background(), "testdb", "users", map[string]any{"id": 1}, map[string]any{"$set": map[string]any{"name": "Updated"}}).Err()
if err == nil || err.Error() != "update failed" {
t.Fatalf("expected update failure, got %v", err)
}
if len(mock.FindOneAndUpdateCalls) != 1 {
t.Fatalf("expected 1 findOneAndUpdate call, got %d", len(mock.FindOneAndUpdateCalls))
}

mock.Reset()
if len(mock.FindOneAndUpdateCalls) != 0 || len(mock.FindOneAndUpdateQueue) != 0 {
t.Fatal("expected findOneAndUpdate state to be cleared")
}
})

t.Run("CustomFindFunction", func(t *testing.T) {
mock := NewMockDatabase()

Expand Down
19 changes: 19 additions & 0 deletions pkg/database/mongodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ type MongoClient struct {
Options *MongoOptions
}

var _ DatabaseInterface = (*MongoClient)(nil)

// NewMongoClient creates a new MongoClient with the provided MongoDB settings
func NewMongoClient(options *MongoOptions) (DatabaseInterface, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(options.Timeout)*time.Millisecond)
Expand Down Expand Up @@ -421,6 +423,23 @@ func (m *MongoClient) FindOne(ctx context.Context, db string, collection string,
return &SingleResult{result: coll.FindOne(ctx, filter, findOneOpts...)}
}

// FindOneAndUpdate atomically updates one matching document and returns the
// document selected by the supplied options. Callers that need the updated
// document should pass options.FindOneAndUpdate().SetReturnDocument(options.After).
// Supports *moptions.FindOneAndUpdateOptions in opts.
func (m *MongoClient) FindOneAndUpdate(ctx context.Context, db string, collection string, filter any, update any, opts ...any) SingleResultInterface {
coll := m.Client.Database(db).Collection(collection)

var findOneAndUpdateOpts []*moptions.FindOneAndUpdateOptions
for _, opt := range opts {
if fo, ok := opt.(*moptions.FindOneAndUpdateOptions); ok {
findOneAndUpdateOpts = append(findOneAndUpdateOpts, fo)
}
}

return &SingleResult{result: coll.FindOneAndUpdate(ctx, filter, update, findOneAndUpdateOpts...)}
}

// InsertOne inserts a single document into the specified database and collection
func (m *MongoClient) InsertOne(ctx context.Context, db string, collection string, document any, opts ...any) (any, error) {
coll := m.Client.Database(db).Collection(collection)
Expand Down
Loading