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
14 changes: 14 additions & 0 deletions pkg/database/indexes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package database

import (
"context"

"go.mongodb.org/mongo-driver/mongo"
)

// IndexManager is the optional database capability for creating MongoDB
// collection indexes. Index definitions remain owned by consumers so this
// package does not need domain-specific index registries.
type IndexManager interface {
CreateIndexes(ctx context.Context, db string, collection string, indexes []mongo.IndexModel, opts ...any) ([]string, error)
}
64 changes: 64 additions & 0 deletions pkg/database/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)

// MockDatabase is a mock implementation of DatabaseInterface for testing
Expand All @@ -23,6 +24,9 @@ type MockDatabase struct {
// FindOneAndUpdateFunc allows customizing atomic find-and-update behavior.
FindOneAndUpdateFunc func(ctx context.Context, db string, collection string, filter any, update any, opts ...any) SingleResultInterface

// CreateIndexesFunc allows customizing index creation behavior.
CreateIndexesFunc func(ctx context.Context, db string, collection string, indexes []mongo.IndexModel, opts ...any) ([]string, error)

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

Expand All @@ -40,6 +44,7 @@ type MockDatabase struct {
FindQueue []FindResponse
FindOneQueue []FindOneResponse
FindOneAndUpdateQueue []FindOneResponse
CreateIndexesQueue []CreateIndexesResponse
UpdateOneQueue []UpdateOneResponse
DeleteOneQueue []DeleteResponse
DeleteManyQueue []DeleteResponse
Expand All @@ -50,13 +55,15 @@ type MockDatabase struct {
FindCalls []FindCall
FindOneCalls []FindOneCall
FindOneAndUpdateCalls []FindOneAndUpdateCall
CreateIndexesCalls []CreateIndexesCall
UpdateOneCalls []UpdateOneCall
DeleteOneCalls []DeleteCall
DeleteManyCalls []DeleteCall
CountCalls []CountCall
}

var _ DatabaseInterface = (*MockDatabase)(nil)
var _ IndexManager = (*MockDatabase)(nil)

// MockSingleResult implements SingleResultInterface for testing
type MockSingleResult struct {
Expand Down Expand Up @@ -210,6 +217,21 @@ type FindOneAndUpdateCall struct {
Opts []any
}

// CreateIndexesResponse represents a queued index creation response.
type CreateIndexesResponse struct {
Names []string
Err error
}

// CreateIndexesCall records an index creation call.
type CreateIndexesCall struct {
Ctx context.Context
Db string
Collection string
Indexes []mongo.IndexModel
Opts []any
}

// UpdateOneCall records a call to UpdateOne
type UpdateOneCall struct {
Ctx context.Context
Expand Down Expand Up @@ -259,6 +281,9 @@ func NewMockDatabase() *MockDatabase {
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")}
},
CreateIndexesFunc: func(ctx context.Context, db string, collection string, indexes []mongo.IndexModel, opts ...any) ([]string, error) {
return nil, nil
},
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 @@ -275,6 +300,7 @@ func NewMockDatabase() *MockDatabase {
FindCalls: []FindCall{},
FindOneCalls: []FindOneCall{},
FindOneAndUpdateCalls: []FindOneAndUpdateCall{},
CreateIndexesCalls: []CreateIndexesCall{},
UpdateOneCalls: []UpdateOneCall{},
DeleteOneCalls: []DeleteCall{},
DeleteManyCalls: []DeleteCall{},
Expand All @@ -283,6 +309,7 @@ func NewMockDatabase() *MockDatabase {
FindQueue: []FindResponse{},
FindOneQueue: []FindOneResponse{},
FindOneAndUpdateQueue: []FindOneResponse{},
CreateIndexesQueue: []CreateIndexesResponse{},
UpdateOneQueue: []UpdateOneResponse{},
DeleteOneQueue: []DeleteResponse{},
DeleteManyQueue: []DeleteResponse{},
Expand Down Expand Up @@ -421,6 +448,27 @@ func (m *MockDatabase) FindOneAndUpdate(ctx context.Context, db string, collecti
return &MockSingleResult{result: nil, err: fmt.Errorf("no document found")}
}

// CreateIndexes implements IndexManager.
func (m *MockDatabase) CreateIndexes(ctx context.Context, db string, collection string, indexes []mongo.IndexModel, opts ...any) ([]string, error) {
m.CreateIndexesCalls = append(m.CreateIndexesCalls, CreateIndexesCall{
Ctx: ctx,
Db: db,
Collection: collection,
Indexes: indexes,
Opts: opts,
})

if len(m.CreateIndexesQueue) > 0 {
response := m.CreateIndexesQueue[0]
m.CreateIndexesQueue = m.CreateIndexesQueue[1:]
return response.Names, response.Err
}
if m.CreateIndexesFunc != nil {
return m.CreateIndexesFunc(ctx, db, collection, indexes, opts...)
}
return nil, nil
}

// 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 @@ -635,6 +683,7 @@ func (m *MockDatabase) Reset() {
m.FindCalls = []FindCall{}
m.FindOneCalls = []FindOneCall{}
m.FindOneAndUpdateCalls = []FindOneAndUpdateCall{}
m.CreateIndexesCalls = []CreateIndexesCall{}
m.UpdateOneCalls = []UpdateOneCall{}
m.DeleteOneCalls = []DeleteCall{}
m.DeleteManyCalls = []DeleteCall{}
Expand All @@ -643,6 +692,7 @@ func (m *MockDatabase) Reset() {
m.FindQueue = []FindResponse{}
m.FindOneQueue = []FindOneResponse{}
m.FindOneAndUpdateQueue = []FindOneResponse{}
m.CreateIndexesQueue = []CreateIndexesResponse{}
m.UpdateOneQueue = []UpdateOneResponse{}
m.DeleteOneQueue = []DeleteResponse{}
m.DeleteManyQueue = []DeleteResponse{}
Expand Down Expand Up @@ -681,6 +731,14 @@ func (m *MockDatabase) ExpectFindOneAndUpdate(result any, err error) *MockDataba
return m
}

// ExpectCreateIndexes sets up an index creation response.
func (m *MockDatabase) ExpectCreateIndexes(names []string, err error) *MockDatabase {
m.CreateIndexesFunc = func(ctx context.Context, db string, collection string, indexes []mongo.IndexModel, opts ...any) ([]string, error) {
return names, 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 @@ -743,6 +801,12 @@ func (m *MockDatabase) QueueFindOneAndUpdate(result any, err error) *MockDatabas
return m
}

// QueueCreateIndexes adds an index creation response to the queue.
func (m *MockDatabase) QueueCreateIndexes(names []string, err error) *MockDatabase {
m.CreateIndexesQueue = append(m.CreateIndexesQueue, CreateIndexesResponse{Names: names, 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
46 changes: 46 additions & 0 deletions pkg/database/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import (
"errors"
"fmt"
"testing"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

func TestMockDatabase(t *testing.T) {
Expand Down Expand Up @@ -171,6 +175,48 @@ func TestMockDatabase(t *testing.T) {
}
})

t.Run("ExpectCreateIndexesWithCallTracking", func(t *testing.T) {
mock := NewMockDatabase().ExpectCreateIndexes([]string{"ownerId_1"}, nil)
indexes := []mongo.IndexModel{{Keys: bson.D{{Key: "ownerId", Value: 1}}}}
opts := options.CreateIndexes().SetMaxTime(10)

names, err := mock.CreateIndexes(context.Background(), "testdb", "organisation", indexes, opts)
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if len(names) != 1 || names[0] != "ownerId_1" {
t.Fatalf("created index names = %v", names)
}
if len(mock.CreateIndexesCalls) != 1 {
t.Fatalf("expected 1 CreateIndexes call, got %d", len(mock.CreateIndexesCalls))
}
call := mock.CreateIndexesCalls[0]
if call.Db != "testdb" || call.Collection != "organisation" || len(call.Indexes) != 1 {
t.Fatalf("unexpected CreateIndexes call: %#v", call)
}
if len(call.Opts) != 1 || call.Opts[0] != opts {
t.Fatalf("CreateIndexes options = %#v", call.Opts)
}
})

t.Run("QueueCreateIndexesErrorAndReset", func(t *testing.T) {
expectedErr := errors.New("index creation failed")
mock := NewMockDatabase().QueueCreateIndexes(nil, expectedErr)

_, err := mock.CreateIndexes(context.Background(), "testdb", "organisation", nil)
if !errors.Is(err, expectedErr) {
t.Fatalf("expected index creation failure, got %v", err)
}
if len(mock.CreateIndexesCalls) != 1 {
t.Fatalf("expected 1 CreateIndexes call, got %d", len(mock.CreateIndexesCalls))
}

mock.Reset()
if len(mock.CreateIndexesCalls) != 0 || len(mock.CreateIndexesQueue) != 0 {
t.Fatal("expected CreateIndexes state to be cleared")
}
})

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

Expand Down
18 changes: 18 additions & 0 deletions pkg/database/mongodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ type MongoClient struct {
}

var _ DatabaseInterface = (*MongoClient)(nil)
var _ IndexManager = (*MongoClient)(nil)

// NewMongoClient creates a new MongoClient with the provided MongoDB settings
func NewMongoClient(options *MongoOptions) (DatabaseInterface, error) {
Expand Down Expand Up @@ -357,6 +358,23 @@ func (m *MongoClient) GetTimeout() time.Duration {
return time.Duration(m.Options.Timeout) * time.Millisecond
}

// CreateIndexes creates the supplied indexes on a collection. Index
// definitions remain a consumer concern; this method only exposes MongoDB's
// idempotent CreateMany operation through the shared client.
// Supports *moptions.CreateIndexesOptions in opts.
func (m *MongoClient) CreateIndexes(ctx context.Context, db string, collection string, indexes []mongo.IndexModel, opts ...any) ([]string, error) {
coll := m.Client.Database(db).Collection(collection)

var createIndexesOpts []*moptions.CreateIndexesOptions
for _, opt := range opts {
if io, ok := opt.(*moptions.CreateIndexesOptions); ok {
createIndexesOpts = append(createIndexesOpts, io)
}
}

return coll.Indexes().CreateMany(ctx, indexes, createIndexesOpts...)
}

// FindResult wraps a MongoDB cursor for fluent API usage
type FindResult struct {
cursor *mongo.Cursor
Expand Down
Loading