Organize backend into product executable and shared Go modules
This commit is contained in:
1 parent
981e84a4b4
commit
f0720ec66b
382 files changed
+1944
-6989
No files matched your search
@@ -0,0 +1,143 @@
|
||||
package repositoryimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
ri "git.sendico.io/tech/sxchnge/shared/database/repository/index"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func (r *MongoRepository) CreateIndex(def *ri.Definition) error {
|
||||
if r.collection == nil {
|
||||
return merrors.NoData("data collection is not set")
|
||||
}
|
||||
if len(def.Keys) == 0 {
|
||||
return merrors.InvalidArgument("Index definition has no keys", "index.keys")
|
||||
}
|
||||
|
||||
// ----- build BSON keys --------------------------------------------------
|
||||
keys := bson.D{}
|
||||
for _, k := range def.Keys {
|
||||
var value any
|
||||
switch {
|
||||
case k.Type != "":
|
||||
value = k.Type // text, 2dsphere, …
|
||||
case k.Sort == ri.Desc:
|
||||
value = int8(-1)
|
||||
default:
|
||||
value = int8(1) // default to Asc
|
||||
}
|
||||
keys = append(keys, bson.E{Key: k.Field, Value: value})
|
||||
}
|
||||
|
||||
opts := options.Index().
|
||||
SetUnique(def.Unique)
|
||||
if def.TTL != nil {
|
||||
opts.SetExpireAfterSeconds(*def.TTL)
|
||||
}
|
||||
if def.Name != "" {
|
||||
opts.SetName(def.Name)
|
||||
}
|
||||
if def.PartialFilter != nil {
|
||||
opts.SetPartialFilterExpression(def.PartialFilter.BuildQuery())
|
||||
}
|
||||
if def.Sparse {
|
||||
opts.SetSparse(def.Sparse)
|
||||
}
|
||||
|
||||
_, err := r.collection.Indexes().CreateOne(
|
||||
context.Background(),
|
||||
mongo.IndexModel{Keys: keys, Options: opts},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MongoRepository) DropIndex(ctx context.Context, name string) error {
|
||||
if r.collection == nil {
|
||||
return merrors.NoData("data collection is not set")
|
||||
}
|
||||
if name == "" {
|
||||
return merrors.InvalidArgument("index name is required", "index.name")
|
||||
}
|
||||
return r.collection.Indexes().DropOne(ctx, name)
|
||||
}
|
||||
|
||||
func (r *MongoRepository) ListIndexDefinitions(ctx context.Context) ([]*ri.Definition, error) {
|
||||
if r.collection == nil {
|
||||
return nil, merrors.NoData("data collection is not set")
|
||||
}
|
||||
cursor, err := r.collection.Indexes().List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = cursor.Close(ctx) }()
|
||||
|
||||
defs := make([]*ri.Definition, 0)
|
||||
for cursor.Next(ctx) {
|
||||
var doc mongoIndexDocument
|
||||
if err := cursor.Decode(&doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defs = append(defs, indexDefinitionFromDocument(doc))
|
||||
}
|
||||
if err := cursor.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return defs, nil
|
||||
}
|
||||
|
||||
type mongoIndexDocument struct {
|
||||
Name string `bson:"name"`
|
||||
Key bson.D `bson:"key"`
|
||||
Unique bool `bson:"unique"`
|
||||
Sparse bool `bson:"sparse"`
|
||||
ExpireAfterSeconds *int32 `bson:"expireAfterSeconds,omitempty"`
|
||||
}
|
||||
|
||||
func indexDefinitionFromDocument(doc mongoIndexDocument) *ri.Definition {
|
||||
keys := make([]ri.Key, 0, len(doc.Key))
|
||||
for _, key := range doc.Key {
|
||||
keys = append(keys, indexKeyFromBSON(key))
|
||||
}
|
||||
return &ri.Definition{
|
||||
Keys: keys,
|
||||
Unique: doc.Unique,
|
||||
Sparse: doc.Sparse,
|
||||
TTL: doc.ExpireAfterSeconds,
|
||||
Name: doc.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func indexKeyFromBSON(key bson.E) ri.Key {
|
||||
out := ri.Key{Field: key.Key}
|
||||
switch typed := key.Value.(type) {
|
||||
case string:
|
||||
out.Type = ri.IndexType(typed)
|
||||
case int:
|
||||
out.Sort = indexSort(typed)
|
||||
case int8:
|
||||
out.Sort = indexSort(typed)
|
||||
case int16:
|
||||
out.Sort = indexSort(typed)
|
||||
case int32:
|
||||
out.Sort = indexSort(typed)
|
||||
case int64:
|
||||
out.Sort = indexSort(typed)
|
||||
case float64:
|
||||
out.Sort = indexSort(typed)
|
||||
default:
|
||||
out.Type = ri.IndexType(fmt.Sprint(typed))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func indexSort[T ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float64](value T) ri.Sort {
|
||||
if value < 0 {
|
||||
return ri.Desc
|
||||
}
|
||||
return ri.Asc
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package repositoryimp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/database/repository"
|
||||
ri "git.sendico.io/tech/sxchnge/shared/database/repository/index"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func TestCreateIndex_WithPartialFilter(t *testing.T) {
|
||||
startCtx, startCancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer startCancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(startCtx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForListeningPort("27017/tcp").WithStartupTimeout(2*time.Minute)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(startCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := mongo.Connect(options.Client().ApplyURI(mongoURI))
|
||||
require.NoError(t, err)
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
database := client.Database("test_partial_index_" + t.Name())
|
||||
defer database.Drop(context.Background())
|
||||
|
||||
repo := repository.CreateMongoRepository(database, "partial_index_items")
|
||||
|
||||
def := &ri.Definition{
|
||||
Keys: []ri.Key{
|
||||
{Field: "field", Sort: ri.Asc},
|
||||
},
|
||||
Unique: true,
|
||||
Name: "partial_unique_field_true",
|
||||
PartialFilter: repository.Filter("flag", true),
|
||||
}
|
||||
|
||||
require.NoError(t, repo.CreateIndex(def))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
defs, err := repo.ListIndexDefinitions(ctx)
|
||||
require.NoError(t, err)
|
||||
listed := false
|
||||
for _, listedDef := range defs {
|
||||
if listedDef.Name == def.Name {
|
||||
listed = true
|
||||
assert.True(t, listedDef.Unique)
|
||||
assert.Equal(t, []ri.Key{{Field: "field", Sort: ri.Asc}}, listedDef.Keys)
|
||||
}
|
||||
}
|
||||
assert.True(t, listed, "created index was not returned by ListIndexDefinitions")
|
||||
|
||||
cursor, err := database.Collection(repo.Collection()).Indexes().List(ctx)
|
||||
require.NoError(t, err)
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
found := false
|
||||
for cursor.Next(ctx) {
|
||||
var idx bson.M
|
||||
require.NoError(t, cursor.Decode(&idx))
|
||||
if idx["name"] == def.Name {
|
||||
found = true
|
||||
assert.Equal(t, true, idx["unique"])
|
||||
assert.Equal(t, bson.M{"field": int32(1)}, idx["key"])
|
||||
partial, ok := idx["partialFilterExpression"].(bson.M)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, bson.M{"flag": true}, partial)
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "partial unique index was not created")
|
||||
|
||||
termCtx, termCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer termCancel()
|
||||
_ = mongoContainer.Terminate(termCtx)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package repositoryimp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
ri "git.sendico.io/tech/sxchnge/shared/database/repository/index"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestIndexDefinitionFromDocumentMapsIndexMetadata(t *testing.T) {
|
||||
ttl := int32(3600)
|
||||
def := indexDefinitionFromDocument(mongoIndexDocument{
|
||||
Name: "account_ref_unique",
|
||||
Key: bson.D{{Key: "accountRef", Value: int32(1)}, {Key: "version", Value: int64(-1)}},
|
||||
Unique: true,
|
||||
Sparse: true,
|
||||
ExpireAfterSeconds: &ttl,
|
||||
})
|
||||
|
||||
if def.Name != "account_ref_unique" || !def.Unique || !def.Sparse {
|
||||
t.Fatalf("unexpected definition metadata: %#v", def)
|
||||
}
|
||||
if def.TTL == nil || *def.TTL != ttl {
|
||||
t.Fatalf("TTL = %v, want %d", def.TTL, ttl)
|
||||
}
|
||||
wantKeys := []ri.Key{
|
||||
{Field: "accountRef", Sort: ri.Asc},
|
||||
{Field: "version", Sort: ri.Desc},
|
||||
}
|
||||
if len(def.Keys) != len(wantKeys) {
|
||||
t.Fatalf("keys = %#v, want %#v", def.Keys, wantKeys)
|
||||
}
|
||||
for i := range wantKeys {
|
||||
if def.Keys[i] != wantKeys[i] {
|
||||
t.Fatalf("keys[%d] = %#v, want %#v", i, def.Keys[i], wantKeys[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexDefinitionFromDocumentMapsTypedIndex(t *testing.T) {
|
||||
def := indexDefinitionFromDocument(mongoIndexDocument{
|
||||
Name: "text_index",
|
||||
Key: bson.D{{Key: "description", Value: "text"}},
|
||||
})
|
||||
|
||||
if len(def.Keys) != 1 {
|
||||
t.Fatalf("keys = %#v", def.Keys)
|
||||
}
|
||||
if def.Keys[0] != (ri.Key{Field: "description", Type: ri.IndexTypeText}) {
|
||||
t.Fatalf("key = %#v", def.Keys[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package repositoryimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/database/record"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/repository/builder"
|
||||
rd "git.sendico.io/tech/sxchnge/shared/database/repository/decoder"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/transaction"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type MongoRepository struct {
|
||||
collectionName string
|
||||
collection *mongo.Collection
|
||||
}
|
||||
|
||||
func idFilter(id bson.ObjectID) bson.D {
|
||||
return bson.D{
|
||||
{Key: storable.IDField, Value: id},
|
||||
}
|
||||
}
|
||||
|
||||
func NewMongoRepository(db *mongo.Database, collection string) *MongoRepository {
|
||||
return &MongoRepository{
|
||||
collectionName: collection,
|
||||
collection: db.Collection(collection),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Collection() string {
|
||||
return r.collectionName
|
||||
}
|
||||
|
||||
func prepareForWrite(obj storable.Storable) bson.ObjectID {
|
||||
id := obj.GetID()
|
||||
if id == nil || id.IsZero() {
|
||||
obj.SetID(bson.NewObjectID())
|
||||
id = obj.GetID()
|
||||
}
|
||||
obj.Update()
|
||||
return *id
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Insert(ctx context.Context, obj storable.Storable, getFilter builder.Query) error {
|
||||
prepareForWrite(obj)
|
||||
_, err := r.collection.InsertOne(ctx, obj)
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
if getFilter != nil {
|
||||
if err = r.FindOneByFilter(ctx, getFilter, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return merrors.DataConflict("duplicate_key")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MongoRepository) InsertMany(ctx context.Context, objects []storable.Storable) error {
|
||||
if len(objects) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
docs := make([]interface{}, len(objects))
|
||||
for i, obj := range objects {
|
||||
prepareForWrite(obj)
|
||||
docs[i] = obj
|
||||
}
|
||||
|
||||
_, err := r.collection.InsertMany(ctx, docs)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MongoRepository) findOneByFilterImp(ctx context.Context, filter bson.D, errMessage string, result storable.Storable) error {
|
||||
err := r.collection.FindOne(ctx, filter).Decode(result)
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return merrors.NoData(errMessage)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Get(ctx context.Context, id bson.ObjectID, result storable.Storable) error {
|
||||
if id.IsZero() {
|
||||
return merrors.InvalidArgument("zero id provided while fetching "+result.Collection(), "id")
|
||||
}
|
||||
return r.findOneByFilterImp(ctx, idFilter(id), fmt.Sprintf("%s with ID = %s not found", result.Collection(), id.Hex()), result)
|
||||
}
|
||||
|
||||
type QueryFunc func(ctx context.Context, collection *mongo.Collection) (*mongo.Cursor, error)
|
||||
|
||||
func (r *MongoRepository) executeQuery(ctx context.Context, queryFunc QueryFunc, decoder rd.DecodingFunc) error {
|
||||
cursor, err := queryFunc(ctx, r.collection)
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return merrors.NoData("no_items_in_array")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = cursor.Close(ctx)
|
||||
}()
|
||||
|
||||
for cursor.Next(ctx) {
|
||||
if err = decoder(cursor); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Aggregate(ctx context.Context, pipeline builder.Pipeline, decoder rd.DecodingFunc) error {
|
||||
queryFunc := func(ctx context.Context, collection *mongo.Collection) (*mongo.Cursor, error) {
|
||||
return collection.Aggregate(ctx, pipeline.Build())
|
||||
}
|
||||
return r.executeQuery(ctx, queryFunc, decoder)
|
||||
}
|
||||
|
||||
func (r *MongoRepository) FindManyByFilter(ctx context.Context, query builder.Query, decoder rd.DecodingFunc) error {
|
||||
queryFunc := func(ctx context.Context, collection *mongo.Collection) (*mongo.Cursor, error) {
|
||||
return collection.Find(ctx, query.BuildQuery(), query.BuildOptions())
|
||||
}
|
||||
return r.executeQuery(ctx, queryFunc, decoder)
|
||||
}
|
||||
|
||||
func (r *MongoRepository) FindAllByFilter(ctx context.Context, query builder.Query, result any) error {
|
||||
if query == nil {
|
||||
cursor, err := r.collection.Find(ctx, bson.D{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = cursor.Close(ctx)
|
||||
}()
|
||||
return cursor.All(ctx, result)
|
||||
}
|
||||
cursor, err := r.collection.Find(ctx, query.BuildQuery(), query.BuildOptions())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = cursor.Close(ctx)
|
||||
}()
|
||||
return cursor.All(ctx, result)
|
||||
}
|
||||
|
||||
func (r *MongoRepository) FindOneByFilter(ctx context.Context, query builder.Query, result storable.Storable) error {
|
||||
return r.findOneByFilterImp(ctx, query.BuildQuery(), result.Collection()+" not found by filter", result)
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Update(ctx context.Context, obj storable.Storable) error {
|
||||
obj.Update()
|
||||
return r.collection.FindOneAndReplace(ctx, idFilter(*obj.GetID()), obj).Err()
|
||||
}
|
||||
|
||||
func (r *MongoRepository) ReplaceOneByFilter(ctx context.Context, query builder.Query, obj storable.Storable) (bool, error) {
|
||||
if query == nil {
|
||||
return false, merrors.InvalidArgument("filter is required", "filter")
|
||||
}
|
||||
if obj == nil {
|
||||
return false, merrors.InvalidArgument("object is required", "obj")
|
||||
}
|
||||
result, err := r.collection.ReplaceOne(ctx, query.BuildQuery(), obj)
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return false, merrors.DataConflict("duplicate_key")
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result.MatchedCount > 0, nil
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Upsert(ctx context.Context, obj storable.Storable) error {
|
||||
id := prepareForWrite(obj)
|
||||
_, err := r.collection.ReplaceOne(ctx, idFilter(id), obj, options.Replace().SetUpsert(true))
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Patch(ctx context.Context, id bson.ObjectID, patch builder.Patch) error {
|
||||
if id.IsZero() {
|
||||
return merrors.InvalidArgument("zero id provided while patching", "id")
|
||||
}
|
||||
_, err := r.collection.UpdateByID(ctx, id, patch.Build())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MongoRepository) PatchMany(ctx context.Context, query builder.Query, patch builder.Patch) (int, error) {
|
||||
result, err := r.collection.UpdateMany(ctx, query.BuildQuery(), patch.Build())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(result.ModifiedCount), nil
|
||||
}
|
||||
|
||||
func (r *MongoRepository) ListIDs(ctx context.Context, query builder.Query) ([]bson.ObjectID, error) {
|
||||
filter := query.BuildQuery()
|
||||
findOptions := query.BuildOptions().SetProjection(bson.M{storable.IDField: 1})
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, findOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = cursor.Close(ctx)
|
||||
}()
|
||||
|
||||
var ids []bson.ObjectID
|
||||
for cursor.Next(ctx) {
|
||||
var doc struct {
|
||||
ID bson.ObjectID `bson:"_id"`
|
||||
}
|
||||
if err := cursor.Decode(&doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, doc.ID)
|
||||
}
|
||||
if err := cursor.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Delete(ctx context.Context, id bson.ObjectID) error {
|
||||
_, err := r.collection.DeleteOne(ctx, idFilter(id))
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MongoRepository) DeleteMany(ctx context.Context, query builder.Query) error {
|
||||
_, err := r.collection.DeleteMany(ctx, query.BuildQuery())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MongoRepository) Name() string {
|
||||
return r.collection.Name()
|
||||
}
|
||||
|
||||
func (r *MongoRepository) WithTransaction(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||
if cb == nil {
|
||||
return nil, merrors.InvalidArgument("transaction callback is required", "callback")
|
||||
}
|
||||
session, err := r.collection.Database().Client().StartSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.EndSession(ctx)
|
||||
return session.WithTransaction(ctx, cb)
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package repositoryimp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/database/internal/builder"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/internal/repository"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/repository/builder"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func TestMongoRepository_Insert(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("Insert_WithoutID", func(t *testing.T) {
|
||||
testObj := &TestObject{Name: "testInsert"}
|
||||
// ID should be nil/zero initially
|
||||
assert.True(t, testObj.GetID().IsZero())
|
||||
|
||||
err := repository.Insert(ctx, testObj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ID should be assigned after insert
|
||||
assert.False(t, testObj.GetID().IsZero())
|
||||
assert.NotEmpty(t, testObj.CreatedAt)
|
||||
assert.NotEmpty(t, testObj.UpdatedAt)
|
||||
})
|
||||
|
||||
t.Run("Insert_WithExistingID", func(t *testing.T) {
|
||||
existingID := bson.NewObjectID()
|
||||
testObj := &TestObject{Name: "testInsertWithID"}
|
||||
testObj.SetID(existingID)
|
||||
|
||||
err := repository.Insert(ctx, testObj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ID should remain the same
|
||||
assert.Equal(t, existingID, *testObj.GetID())
|
||||
})
|
||||
|
||||
t.Run("Insert_DuplicateKey", func(t *testing.T) {
|
||||
// Insert first object
|
||||
testObj1 := &TestObject{Name: "duplicate"}
|
||||
err := repository.Insert(ctx, testObj1, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to insert object with same ID
|
||||
testObj2 := &TestObject{Name: "duplicate2"}
|
||||
testObj2.SetID(*testObj1.GetID())
|
||||
|
||||
err = repository.Insert(ctx, testObj2, nil)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, merrors.ErrDataConflict))
|
||||
})
|
||||
|
||||
t.Run("Insert_DuplicateKeyWithGetFilter", func(t *testing.T) {
|
||||
// Insert first object
|
||||
testObj1 := &TestObject{Name: "duplicateWithFilter"}
|
||||
err := repository.Insert(ctx, testObj1, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to insert object with same ID, but with getFilter
|
||||
testObj2 := &TestObject{Name: "duplicateWithFilter2"}
|
||||
testObj2.SetID(*testObj1.GetID())
|
||||
|
||||
getFilter := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("_id"), builder.Eq, *testObj1.GetID())
|
||||
|
||||
err = repository.Insert(ctx, testObj2, getFilter)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, merrors.ErrDataConflict))
|
||||
|
||||
// But testObj2 should be populated with the existing object data
|
||||
assert.Equal(t, testObj1.Name, testObj2.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_Update(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("Update_ExistingObject", func(t *testing.T) {
|
||||
// Insert object first
|
||||
testObj := &TestObject{Name: "originalName"}
|
||||
err := repository.Insert(ctx, testObj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
originalUpdatedAt := testObj.UpdatedAt
|
||||
|
||||
// Update the object
|
||||
testObj.Name = "updatedName"
|
||||
time.Sleep(10 * time.Millisecond) // Ensure time difference
|
||||
|
||||
err = repository.Update(ctx, testObj)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the object was updated
|
||||
result := &TestObject{}
|
||||
err = repository.Get(ctx, *testObj.GetID(), result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "updatedName", result.Name)
|
||||
assert.True(t, result.UpdatedAt.After(originalUpdatedAt))
|
||||
})
|
||||
|
||||
t.Run("Update_NonExistentObject", func(t *testing.T) {
|
||||
nonExistentID := bson.NewObjectID()
|
||||
testObj := &TestObject{Name: "nonExistent"}
|
||||
testObj.SetID(nonExistentID)
|
||||
|
||||
err := repository.Update(ctx, testObj)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, mongo.ErrNoDocuments))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_Delete(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("Delete_ExistingObject", func(t *testing.T) {
|
||||
// Insert object first
|
||||
testObj := &TestObject{Name: "toDelete"}
|
||||
err := repository.Insert(ctx, testObj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete the object
|
||||
err = repository.Delete(ctx, *testObj.GetID())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the object was deleted
|
||||
result := &TestObject{}
|
||||
err = repository.Get(ctx, *testObj.GetID(), result)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, merrors.ErrNoData))
|
||||
})
|
||||
|
||||
t.Run("Delete_NonExistentObject", func(t *testing.T) {
|
||||
nonExistentID := bson.NewObjectID()
|
||||
|
||||
err := repository.Delete(ctx, nonExistentID)
|
||||
// Delete should not return error even if object doesn't exist
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_FindOneByFilter(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("FindOneByFilter_MatchingFilter", func(t *testing.T) {
|
||||
// Insert test objects
|
||||
testObjs := []*TestObject{
|
||||
{Name: "findMe"},
|
||||
{Name: "dontFindMe"},
|
||||
{Name: "findMeToo"},
|
||||
}
|
||||
|
||||
for _, obj := range testObjs {
|
||||
err := repository.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Find by filter
|
||||
query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("name"), builder.Eq, "findMe")
|
||||
result := &TestObject{}
|
||||
|
||||
err := repository.FindOneByFilter(ctx, query, result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "findMe", result.Name)
|
||||
})
|
||||
|
||||
t.Run("FindOneByFilter_NoMatch", func(t *testing.T) {
|
||||
query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("name"), builder.Eq, "nonExistentName")
|
||||
result := &TestObject{}
|
||||
|
||||
err := repository.FindOneByFilter(ctx, query, result)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, merrors.ErrNoData))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_FindManyByFilter(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("FindManyByFilter_MultipleResults", func(t *testing.T) {
|
||||
// Insert test objects
|
||||
testObjs := []*TestObject{
|
||||
{Name: "findMany1"},
|
||||
{Name: "findMany2"},
|
||||
{Name: "dontFind"},
|
||||
}
|
||||
|
||||
for _, obj := range testObjs {
|
||||
err := repository.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Find objects with names starting with "findMany"
|
||||
query := builderimp.NewQueryImp().RegEx(builderimp.NewFieldImp("name"), "^findMany", "")
|
||||
|
||||
var results []*TestObject
|
||||
decoder := func(cursor *mongo.Cursor) error {
|
||||
var obj TestObject
|
||||
if err := cursor.Decode(&obj); err != nil {
|
||||
return err
|
||||
}
|
||||
results = append(results, &obj)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := repository.FindManyByFilter(ctx, query, decoder)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
|
||||
names := make([]string, len(results))
|
||||
for i, obj := range results {
|
||||
names[i] = obj.Name
|
||||
}
|
||||
assert.Contains(t, names, "findMany1")
|
||||
assert.Contains(t, names, "findMany2")
|
||||
})
|
||||
|
||||
t.Run("FindManyByFilter_NoResults", func(t *testing.T) {
|
||||
query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("name"), builder.Eq, "nonExistentPattern")
|
||||
|
||||
var results []*TestObject
|
||||
decoder := func(cursor *mongo.Cursor) error {
|
||||
var obj TestObject
|
||||
if err := cursor.Decode(&obj); err != nil {
|
||||
return err
|
||||
}
|
||||
results = append(results, &obj)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := repository.FindManyByFilter(ctx, query, decoder)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, results)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_DeleteMany(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("DeleteMany_MultipleDocuments", func(t *testing.T) {
|
||||
// Insert test objects
|
||||
testObjs := []*TestObject{
|
||||
{Name: "deleteMany1"},
|
||||
{Name: "deleteMany2"},
|
||||
{Name: "keepMe"},
|
||||
}
|
||||
|
||||
for _, obj := range testObjs {
|
||||
err := repository.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Delete objects with names starting with "deleteMany"
|
||||
query := builderimp.NewQueryImp().RegEx(builderimp.NewFieldImp("name"), "^deleteMany", "")
|
||||
|
||||
err := repository.DeleteMany(ctx, query)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify deletions
|
||||
queryAll := builderimp.NewQueryImp()
|
||||
var results []*TestObject
|
||||
decoder := func(cursor *mongo.Cursor) error {
|
||||
var obj TestObject
|
||||
if err := cursor.Decode(&obj); err != nil {
|
||||
return err
|
||||
}
|
||||
results = append(results, &obj)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = repository.FindManyByFilter(ctx, queryAll, decoder)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, "keepMe", results[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_Name(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "mycollection")
|
||||
|
||||
t.Run("Name_ReturnsCollectionName", func(t *testing.T) {
|
||||
name := repository.Name()
|
||||
assert.Equal(t, "mycollection", name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_UpdateTimestamp(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("Update_Should_Update_Timestamp", func(t *testing.T) {
|
||||
// Create test object
|
||||
obj := &TestObject{
|
||||
Name: "Test Object",
|
||||
}
|
||||
|
||||
// Set ID and initial timestamps
|
||||
obj.SetID(bson.NewObjectID())
|
||||
originalCreatedAt := obj.CreatedAt
|
||||
originalUpdatedAt := obj.UpdatedAt
|
||||
|
||||
// Insert the object
|
||||
err := repository.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait a moment to ensure timestamp difference
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Update the object
|
||||
obj.Name = "Updated Object"
|
||||
err = repository.Update(ctx, obj)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify timestamps
|
||||
assert.Equal(t, originalCreatedAt, obj.CreatedAt, "CreatedAt should not change")
|
||||
assert.True(t, obj.UpdatedAt.After(originalUpdatedAt), "UpdatedAt should be updated")
|
||||
|
||||
// Verify the object was actually updated in the database
|
||||
var retrieved TestObject
|
||||
err = repository.Get(ctx, *obj.GetID(), &retrieved)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Updated Object", retrieved.Name, "Name should be updated")
|
||||
assert.WithinDuration(t, originalCreatedAt, retrieved.CreatedAt, time.Second, "CreatedAt should not change in DB")
|
||||
assert.True(t, retrieved.UpdatedAt.After(originalUpdatedAt), "UpdatedAt should be updated in DB")
|
||||
assert.WithinDuration(t, obj.UpdatedAt, retrieved.UpdatedAt, time.Second, "UpdatedAt should match between object and DB")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package repositoryimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/database/record"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/repository/builder"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func (r *MongoRepository) FindFirstByFilter(ctx context.Context, query builder.Query, result storable.Storable) error {
|
||||
opts := query.BuildOptions().SetLimit(1)
|
||||
cursor, err := r.collection.Find(ctx, query.BuildQuery(), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = cursor.Close(ctx)
|
||||
}()
|
||||
if !cursor.Next(ctx) {
|
||||
if err := cursor.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return merrors.NoData(result.Collection() + " not found by filter")
|
||||
}
|
||||
return cursor.Decode(result)
|
||||
}
|
||||
|
||||
func (r *MongoRepository) CountByFilter(ctx context.Context, query builder.Query) (int64, error) {
|
||||
return r.collection.CountDocuments(ctx, query.BuildQuery())
|
||||
}
|
||||
|
||||
func (r *MongoRepository) PatchOneByFilter(ctx context.Context, query builder.Query, patch builder.Patch, result storable.Storable) (bool, error) {
|
||||
if query == nil {
|
||||
return false, merrors.InvalidArgument("filter is required", "filter")
|
||||
}
|
||||
if patch == nil {
|
||||
return false, merrors.InvalidArgument("patch is required", "patch")
|
||||
}
|
||||
if result == nil {
|
||||
return false, merrors.InvalidArgument("result is required", "result")
|
||||
}
|
||||
err := r.collection.FindOneAndUpdate(
|
||||
ctx,
|
||||
query.BuildQuery(),
|
||||
patch.Build(),
|
||||
options.FindOneAndUpdate().SetReturnDocument(options.After),
|
||||
).Decode(result)
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package repositoryimp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/database/internal/repository"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/record"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func TestMongoRepository_InsertMany(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("InsertMany_Success", func(t *testing.T) {
|
||||
objects := []storable.Storable{
|
||||
&TestObject{Name: "test1"},
|
||||
&TestObject{Name: "test2"},
|
||||
&TestObject{Name: "test3"},
|
||||
}
|
||||
|
||||
err := repository.InsertMany(ctx, objects)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all objects were inserted and have IDs
|
||||
for _, obj := range objects {
|
||||
assert.NotNil(t, obj.GetID())
|
||||
assert.False(t, obj.GetID().IsZero())
|
||||
|
||||
// Verify we can retrieve each object
|
||||
result := &TestObject{}
|
||||
err := repository.Get(ctx, *obj.GetID(), result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, obj.(*TestObject).Name, result.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InsertMany_EmptySlice", func(t *testing.T) {
|
||||
objects := []storable.Storable{}
|
||||
|
||||
err := repository.InsertMany(ctx, objects)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("InsertMany_WithExistingIDs", func(t *testing.T) {
|
||||
id1 := bson.NewObjectID()
|
||||
id2 := bson.NewObjectID()
|
||||
|
||||
objects := []storable.Storable{
|
||||
&TestObject{Base: storable.Base{ID: id1}, Name: "preassigned1"},
|
||||
&TestObject{Base: storable.Base{ID: id2}, Name: "preassigned2"},
|
||||
}
|
||||
|
||||
err := repository.InsertMany(ctx, objects)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify objects were inserted with pre-assigned IDs
|
||||
result1 := &TestObject{}
|
||||
err = repository.Get(ctx, id1, result1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "preassigned1", result1.Name)
|
||||
|
||||
result2 := &TestObject{}
|
||||
err = repository.Get(ctx, id2, result2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "preassigned2", result2.Name)
|
||||
})
|
||||
|
||||
t.Run("InsertMany_MixedTypes", func(t *testing.T) {
|
||||
objects := []storable.Storable{
|
||||
&TestObject{Name: "test1"},
|
||||
&AnotherObject{Description: "desc1"},
|
||||
&TestObject{Name: "test2"},
|
||||
}
|
||||
|
||||
err := repository.InsertMany(ctx, objects)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all objects were inserted
|
||||
for _, obj := range objects {
|
||||
assert.NotNil(t, obj.GetID())
|
||||
assert.False(t, obj.GetID().IsZero())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InsertMany_DuplicateKey", func(t *testing.T) {
|
||||
id := bson.NewObjectID()
|
||||
|
||||
// Insert first object
|
||||
obj1 := &TestObject{Base: storable.Base{ID: id}, Name: "original"}
|
||||
err := repository.Insert(ctx, obj1, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to insert multiple objects including one with duplicate ID
|
||||
objects := []storable.Storable{
|
||||
&TestObject{Name: "test1"},
|
||||
&TestObject{Base: storable.Base{ID: id}, Name: "duplicate"},
|
||||
}
|
||||
|
||||
err = repository.InsertMany(ctx, objects)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, mongo.IsDuplicateKeyError(err))
|
||||
})
|
||||
|
||||
t.Run("InsertMany_UpdateTimestamps", func(t *testing.T) {
|
||||
objects := []storable.Storable{
|
||||
&TestObject{Name: "test1"},
|
||||
&TestObject{Name: "test2"},
|
||||
}
|
||||
|
||||
err := repository.InsertMany(ctx, objects)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify timestamps were set
|
||||
for _, obj := range objects {
|
||||
testObj := obj.(*TestObject)
|
||||
assert.NotZero(t, testObj.CreatedAt)
|
||||
assert.NotZero(t, testObj.UpdatedAt)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package repositoryimp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/database/internal/repository"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/repository"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/repository/builder"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func TestMongoRepository_PatchOperations(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repo := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("Patch_SingleDocument", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "old"}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
original := obj.UpdatedAt
|
||||
|
||||
patch := repository.Patch().Set(repository.Field("name"), "new")
|
||||
err = repo.Patch(ctx, *obj.GetID(), patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "new", result.Name)
|
||||
assert.True(t, result.UpdatedAt.After(original))
|
||||
})
|
||||
|
||||
t.Run("PatchMany_MultipleDocuments", func(t *testing.T) {
|
||||
objs := []*TestObject{{Name: "match"}, {Name: "match"}, {Name: "other"}}
|
||||
for _, o := range objs {
|
||||
err := repo.Insert(ctx, o, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
query := repository.Query().Comparison(repository.Field("name"), builder.Eq, "match")
|
||||
patch := repository.Patch().Set(repository.Field("name"), "patched")
|
||||
modified, err := repo.PatchMany(ctx, query, patch)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, modified)
|
||||
|
||||
verify := repository.Query().Comparison(repository.Field("name"), builder.Eq, "patched")
|
||||
var results []TestObject
|
||||
decoder := func(cursor *mongo.Cursor) error {
|
||||
var obj TestObject
|
||||
if err := cursor.Decode(&obj); err != nil {
|
||||
return err
|
||||
}
|
||||
results = append(results, obj)
|
||||
return nil
|
||||
}
|
||||
err = repo.FindManyByFilter(ctx, verify, decoder)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
})
|
||||
|
||||
t.Run("PatchOneByFilter_MatchesAndReturnsUpdatedDocument", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "patch-one-match", Tags: []string{"before"}}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
originalUpdatedAt := obj.UpdatedAt
|
||||
|
||||
query := repository.Query().Filter(repository.Field("name"), "patch-one-match")
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field("name"), "patch-one-updated").
|
||||
Push(repository.Field("tags"), "after")
|
||||
var result TestObject
|
||||
matched, err := repo.PatchOneByFilter(ctx, query, patch, &result)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, matched)
|
||||
require.Equal(t, *obj.GetID(), *result.GetID())
|
||||
require.Equal(t, "patch-one-updated", result.Name)
|
||||
require.Equal(t, []string{"before", "after"}, result.Tags)
|
||||
require.True(t, result.UpdatedAt.After(originalUpdatedAt))
|
||||
|
||||
var persisted TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &persisted)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, result.Name, persisted.Name)
|
||||
require.Equal(t, result.Tags, persisted.Tags)
|
||||
})
|
||||
|
||||
t.Run("PatchOneByFilter_NoMatch", func(t *testing.T) {
|
||||
patch := repository.Patch().Set(repository.Field("name"), "not-applied")
|
||||
var result TestObject
|
||||
|
||||
matched, err := repo.PatchOneByFilter(ctx, repository.Query().Filter(repository.Field("name"), "missing-patch-one"), patch, &result)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.False(t, matched)
|
||||
require.Empty(t, result.Name)
|
||||
})
|
||||
|
||||
t.Run("PatchOneByFilter_InvalidArguments", func(t *testing.T) {
|
||||
var result TestObject
|
||||
_, err := repo.PatchOneByFilter(ctx, nil, repository.Patch().Set(repository.Field("name"), "x"), &result)
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, merrors.ErrInvalidArg))
|
||||
|
||||
_, err = repo.PatchOneByFilter(ctx, repository.Query().Filter(repository.Field("name"), "x"), nil, &result)
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, merrors.ErrInvalidArg))
|
||||
|
||||
_, err = repo.PatchOneByFilter(ctx, repository.Query().Filter(repository.Field("name"), "x"), repository.Patch().Set(repository.Field("name"), "y"), nil)
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, merrors.ErrInvalidArg))
|
||||
})
|
||||
|
||||
t.Run("Patch_PushArray", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "test", Tags: []string{"tag1"}}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
patch := repository.Patch().Push(repository.Field("tags"), "tag2")
|
||||
err = repo.Patch(ctx, *obj.GetID(), patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"tag1", "tag2"}, result.Tags)
|
||||
})
|
||||
|
||||
t.Run("Patch_PullArray", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "test", Tags: []string{"tag1", "tag2", "tag3"}}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
patch := repository.Patch().Pull(repository.Field("tags"), "tag2")
|
||||
err = repo.Patch(ctx, *obj.GetID(), patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"tag1", "tag3"}, result.Tags)
|
||||
})
|
||||
|
||||
t.Run("Patch_AddToSetArray", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "test", Tags: []string{"tag1"}}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add new tag
|
||||
patch := repository.Patch().AddToSet(repository.Field("tags"), "tag2")
|
||||
err = repo.Patch(ctx, *obj.GetID(), patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"tag1", "tag2"}, result.Tags)
|
||||
|
||||
// Try to add duplicate tag - should not add
|
||||
patch = repository.Patch().AddToSet(repository.Field("tags"), "tag1")
|
||||
err = repo.Patch(ctx, *obj.GetID(), patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Get(ctx, *obj.GetID(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"tag1", "tag2"}, result.Tags)
|
||||
})
|
||||
|
||||
t.Run("Patch_PushToEmptyArray", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "test", Tags: []string{}}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
patch := repository.Patch().Push(repository.Field("tags"), "tag1")
|
||||
err = repo.Patch(ctx, *obj.GetID(), patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"tag1"}, result.Tags)
|
||||
})
|
||||
|
||||
t.Run("Patch_PullFromEmptyArray", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "test", Tags: []string{}}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
patch := repository.Patch().Pull(repository.Field("tags"), "nonexistent")
|
||||
err = repo.Patch(ctx, *obj.GetID(), patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{}, result.Tags)
|
||||
})
|
||||
|
||||
t.Run("Patch_PullNonExistentElement", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "test", Tags: []string{"tag1", "tag2"}}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
patch := repository.Patch().Pull(repository.Field("tags"), "nonexistent")
|
||||
err = repo.Patch(ctx, *obj.GetID(), patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"tag1", "tag2"}, result.Tags)
|
||||
})
|
||||
|
||||
t.Run("Patch_ChainedArrayOperations", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "test", Tags: []string{"tag1"}}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Note: MongoDB doesn't allow multiple operations on the same array field in a single update
|
||||
// This test demonstrates that chained array operations on the same field will fail
|
||||
patch := repository.Patch().
|
||||
Push(repository.Field("tags"), "tag2").
|
||||
AddToSet(repository.Field("tags"), "tag3").
|
||||
Pull(repository.Field("tags"), "tag1")
|
||||
err = repo.Patch(ctx, *obj.GetID(), patch)
|
||||
require.Error(t, err) // This should fail due to MongoDB's limitation
|
||||
assert.Contains(t, err.Error(), "conflict")
|
||||
})
|
||||
|
||||
t.Run("PatchMany_ArrayOperations", func(t *testing.T) {
|
||||
objs := []*TestObject{
|
||||
{Name: "obj1", Tags: []string{"tag1"}},
|
||||
{Name: "obj2", Tags: []string{"tag2"}},
|
||||
{Name: "obj3", Tags: []string{"tag3"}},
|
||||
}
|
||||
for _, o := range objs {
|
||||
err := repo.Insert(ctx, o, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
query := repository.Query().Comparison(repository.Field("name"), builder.In, []string{"obj1", "obj2"})
|
||||
patch := repository.Patch().Push(repository.Field("tags"), "common")
|
||||
modified, err := repo.PatchMany(ctx, query, patch)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, modified)
|
||||
|
||||
// Verify the changes
|
||||
for _, name := range []string{"obj1", "obj2"} {
|
||||
var result TestObject
|
||||
err = repo.FindOneByFilter(ctx, repository.Query().Comparison(repository.Field("name"), builder.Eq, name), &result)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.Tags, "common")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package repositoryimp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/database/internal/builder"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/internal/repository"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/record"
|
||||
"git.sendico.io/tech/sxchnge/shared/database/repository/builder"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type TestObject struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
Name string `bson:"name"`
|
||||
Tags []string `bson:"tags"`
|
||||
}
|
||||
|
||||
func (t *TestObject) Collection() string {
|
||||
return "testObject"
|
||||
}
|
||||
|
||||
type AnotherObject struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
Description string `bson:"description"`
|
||||
}
|
||||
|
||||
func (a *AnotherObject) Collection() string {
|
||||
return "anotherObject"
|
||||
}
|
||||
|
||||
func terminate(ctx context.Context, t *testing.T, container *mongodb.MongoDBContainer) {
|
||||
err := container.Terminate(ctx)
|
||||
require.NoError(t, err, "failed to terminate MongoDB container")
|
||||
}
|
||||
|
||||
func disconnect(ctx context.Context, t *testing.T, client *mongo.Client) {
|
||||
err := client.Disconnect(ctx)
|
||||
require.NoError(t, err, "failed to disconnect from MongoDB")
|
||||
}
|
||||
|
||||
func TestMongoRepository_Get(t *testing.T) {
|
||||
// Use a context with timeout, so if container spinning or DB ops hang,
|
||||
// the test won't run indefinitely.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("Get_Success", func(t *testing.T) {
|
||||
testObj := &TestObject{Name: "testName"}
|
||||
err := repository.Insert(ctx, testObj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := &TestObject{}
|
||||
err = repository.Get(ctx, testObj.ID, result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testObj.Name, result.Name)
|
||||
assert.Equal(t, testObj.ID, result.ID)
|
||||
})
|
||||
|
||||
t.Run("Get_NotFound", func(t *testing.T) {
|
||||
nonExistentID := bson.NewObjectID()
|
||||
result := &TestObject{}
|
||||
|
||||
err := repository.Get(ctx, nonExistentID, result)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, merrors.ErrNoData))
|
||||
})
|
||||
|
||||
t.Run("Get_InvalidID", func(t *testing.T) {
|
||||
invalidID := bson.ObjectID{} // zero value
|
||||
result := &TestObject{}
|
||||
|
||||
err := repository.Get(ctx, invalidID, result)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
|
||||
})
|
||||
|
||||
t.Run("Get_DifferentTypes", func(t *testing.T) {
|
||||
anotherObj := &AnotherObject{Description: "testDescription"}
|
||||
err := repository.Insert(ctx, anotherObj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := &AnotherObject{}
|
||||
err = repository.Get(ctx, anotherObj.ID, result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, anotherObj.Description, result.Description)
|
||||
assert.Equal(t, anotherObj.ID, result.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMongoRepository_ListIDs(t *testing.T) {
|
||||
// Use a context with timeout, so if container spinning or DB ops hang,
|
||||
// the test won't run indefinitely.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repository := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("ListIDs_Success", func(t *testing.T) {
|
||||
// Insert test data
|
||||
testObjs := []*TestObject{
|
||||
{Name: "testName1"},
|
||||
{Name: "testName2"},
|
||||
{Name: "testName3"},
|
||||
}
|
||||
for _, obj := range testObjs {
|
||||
err := repository.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Define a query to match all objects
|
||||
query := builderimp.NewQueryImp()
|
||||
|
||||
// Call ListIDs
|
||||
ids, err := repository.ListIDs(ctx, query)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert the IDs are correct
|
||||
require.Len(t, ids, len(testObjs))
|
||||
for _, obj := range testObjs {
|
||||
assert.Contains(t, ids, obj.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListIDs_EmptyResult", func(t *testing.T) {
|
||||
// Define a query that matches no objects
|
||||
query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("name"), builder.Eq, "nonExistentName")
|
||||
|
||||
// Call ListIDs
|
||||
ids, err := repository.ListIDs(ctx, query)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert no IDs are returned
|
||||
assert.Empty(t, ids)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package repositoryimp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/database/internal/repository"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func TestMongoRepository_Upsert(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
defer terminate(ctx, t, mongoContainer)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
defer disconnect(ctx, t, client)
|
||||
|
||||
db := client.Database("testdb")
|
||||
repo := repositoryimp.NewMongoRepository(db, "testcollection")
|
||||
|
||||
t.Run("Upsert_InsertsWhenIDIsZero", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "inserted-via-upsert"}
|
||||
require.True(t, obj.GetID().IsZero())
|
||||
|
||||
err := repo.Upsert(ctx, obj)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, obj.GetID().IsZero())
|
||||
assert.NotZero(t, obj.CreatedAt)
|
||||
assert.NotZero(t, obj.UpdatedAt)
|
||||
|
||||
var stored TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &stored)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "inserted-via-upsert", stored.Name)
|
||||
})
|
||||
|
||||
t.Run("Upsert_UpdatesExistingDocument", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "before-upsert"}
|
||||
err := repo.Insert(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
createdAt := obj.CreatedAt
|
||||
updatedAt := obj.UpdatedAt
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
obj.Name = "after-upsert"
|
||||
err = repo.Upsert(ctx, obj)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, createdAt, obj.CreatedAt)
|
||||
assert.True(t, obj.UpdatedAt.After(updatedAt))
|
||||
|
||||
var stored TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &stored)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "after-upsert", stored.Name)
|
||||
assert.WithinDuration(t, createdAt, stored.CreatedAt, time.Second)
|
||||
assert.True(t, stored.UpdatedAt.After(updatedAt))
|
||||
})
|
||||
|
||||
t.Run("Upsert_InsertsWhenIDDoesNotExist", func(t *testing.T) {
|
||||
obj := &TestObject{Name: "upsert-with-preassigned-id"}
|
||||
obj.SetID(bson.NewObjectID())
|
||||
|
||||
err := repo.Upsert(ctx, obj)
|
||||
require.NoError(t, err)
|
||||
|
||||
var stored TestObject
|
||||
err = repo.Get(ctx, *obj.GetID(), &stored)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "upsert-with-preassigned-id", stored.Name)
|
||||
assert.Equal(t, *obj.GetID(), stored.ID)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user