// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.MongoDB;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Moq;
using Xunit;
using MEVD = Microsoft.Extensions.VectorData;
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
///
/// Unit tests for class.
///
public sealed class MongoCollectionTests
{
private readonly Mock _mockMongoDatabase = new();
private readonly Mock> _mockMongoCollection = new();
public MongoCollectionTests()
{
this._mockMongoDatabase
.Setup(l => l.GetCollection(It.IsAny(), It.IsAny()))
.Returns(this._mockMongoCollection.Object);
}
[Fact]
public void ConstructorForModelWithoutKeyThrowsException()
{
// Act & Assert
var exception = Assert.Throws(() => new MongoCollection(this._mockMongoDatabase.Object, "collection"));
Assert.Contains("No key property found", exception.Message);
}
[Fact]
public void ConstructorWithDeclarativeModelInitializesCollection()
{
// Act & Assert
using var collection = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithImperativeModelInitializesCollection()
{
// Arrange
var definition = new VectorStoreCollectionDefinition
{
Properties = [new VectorStoreKeyProperty("Id", typeof(string))]
};
// Act
using var collection = new MongoCollection(
this._mockMongoDatabase.Object,
"collection",
new() { Definition = definition });
// Assert
Assert.NotNull(collection);
}
[Theory]
[MemberData(nameof(CollectionExistsData))]
public async Task CollectionExistsReturnsValidResultAsync(List collections, string collectionName, bool expectedResult)
{
// Arrange
var mockCursor = new Mock>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns(collections);
this._mockMongoDatabase
.Setup(l => l.ListCollectionNamesAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(mockCursor.Object);
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
collectionName);
// Act
var actualResult = await sut.CollectionExistsAsync();
// Assert
Assert.Equal(expectedResult, actualResult);
}
[Fact]
public async Task EnsureCollectionExistsInvokesValidMethodsAsync()
{
// Arrange
const string CollectionName = "collection";
var mockCursor = new Mock>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns([]);
this._mockMongoDatabase
.Setup(l => l.ListCollectionNamesAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(mockCursor.Object);
var mockIndexCursor = new Mock>();
mockIndexCursor
.SetupSequence(l => l.MoveNext(It.IsAny()))
.Returns(true)
.Returns(false);
mockIndexCursor
.Setup(l => l.Current)
.Returns([]);
var mockMongoIndexManager = new Mock>();
mockMongoIndexManager
.Setup(l => l.ListAsync(It.IsAny()))
.ReturnsAsync(mockIndexCursor.Object);
this._mockMongoCollection
.Setup(l => l.Indexes)
.Returns(mockMongoIndexManager.Object);
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
CollectionName);
// Act
await sut.EnsureCollectionExistsAsync();
// Assert
this._mockMongoDatabase.Verify(l => l.CreateCollectionAsync(
CollectionName,
It.IsAny(),
It.IsAny()), Times.Exactly(1));
this._mockMongoDatabase.Verify(l => l.ListCollectionNamesAsync(
It.IsAny(),
It.IsAny()), Times.Never);
}
[Fact]
public async Task DeleteInvokesValidMethodsAsync()
{
// Arrange
const string RecordKey = "key";
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer();
var expectedDefinition = Builders.Filter.Eq(document => document["_id"], RecordKey);
// Act
await sut.DeleteAsync(RecordKey);
// Assert
this._mockMongoCollection.Verify(l => l.DeleteOneAsync(
It.Is>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.IsAny()), Times.Once());
}
[Fact]
public async Task DeleteBatchInvokesValidMethodsAsync()
{
// Arrange
List recordKeys = ["key1", "key2"];
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer();
var expectedDefinition = Builders.Filter.In(document => document["_id"].AsString, recordKeys);
// Act
await sut.DeleteAsync(recordKeys);
// Assert
this._mockMongoCollection.Verify(l => l.DeleteManyAsync(
It.Is>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.IsAny()), Times.Once());
}
[Fact]
public async Task DeleteCollectionInvokesValidMethodsAsync()
{
// Arrange
const string CollectionName = "collection";
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
CollectionName);
// Act
await sut.EnsureCollectionDeletedAsync();
// Assert
this._mockMongoDatabase.Verify(l => l.DropCollectionAsync(
It.Is(name => name == CollectionName),
It.IsAny()), Times.Once());
}
[Fact]
public async Task GetReturnsValidRecordAsync()
{
// Arrange
const string RecordKey = "key";
var document = new BsonDocument { ["_id"] = RecordKey, ["HotelName"] = "Test Name" };
var mockCursor = new Mock>();
mockCursor
.Setup(l => l.MoveNextAsync(It.IsAny()))
.ReturnsAsync(true);
mockCursor
.Setup(l => l.Current)
.Returns([document]);
this._mockMongoCollection
.Setup(l => l.FindAsync(
It.IsAny>(),
It.IsAny>(),
It.IsAny()))
.ReturnsAsync(mockCursor.Object);
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
// Act
var result = await sut.GetAsync(RecordKey);
// Assert
Assert.NotNull(result);
Assert.Equal(RecordKey, result.HotelId);
Assert.Equal("Test Name", result.HotelName);
}
[Fact]
public async Task GetBatchReturnsValidRecordAsync()
{
// Arrange
var document1 = new BsonDocument { ["_id"] = "key1", ["HotelName"] = "Test Name 1" };
var document2 = new BsonDocument { ["_id"] = "key2", ["HotelName"] = "Test Name 2" };
var document3 = new BsonDocument { ["_id"] = "key3", ["HotelName"] = "Test Name 3" };
var mockCursor = new Mock>();
mockCursor
.SetupSequence(l => l.MoveNextAsync(It.IsAny()))
.ReturnsAsync(true)
.ReturnsAsync(false);
mockCursor
.Setup(l => l.Current)
.Returns([document1, document2, document3]);
this._mockMongoCollection
.Setup(l => l.FindAsync(
It.IsAny>(),
It.IsAny>(),
It.IsAny()))
.ReturnsAsync(mockCursor.Object);
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
// Act
var results = await sut.GetAsync(["key1", "key2", "key3"]).ToListAsync();
// Assert
Assert.NotNull(results[0]);
Assert.Equal("key1", results[0].HotelId);
Assert.Equal("Test Name 1", results[0].HotelName);
Assert.NotNull(results[1]);
Assert.Equal("key2", results[1].HotelId);
Assert.Equal("Test Name 2", results[1].HotelName);
Assert.NotNull(results[2]);
Assert.Equal("key3", results[2].HotelId);
Assert.Equal("Test Name 3", results[2].HotelName);
}
[Fact]
public async Task CanUpsertRecordAsync()
{
// Arrange
var hotel = new MongoHotelModel("key") { HotelName = "Test Name" };
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer();
var expectedDefinition = Builders.Filter.Eq(document => document["_id"], "key");
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
// Act
await sut.UpsertAsync(hotel);
// Assert
this._mockMongoCollection.Verify(l => l.ReplaceOneAsync(
It.Is>(definition =>
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
It.Is(document =>
document["_id"] == "key" &&
document["HotelName"] == "Test Name"),
It.IsAny(),
It.IsAny()), Times.Once());
}
[Fact]
public async Task CanUpsertManyRecordsAsync()
{
// Arrange
var hotel1 = new MongoHotelModel("key1") { HotelName = "Test Name 1" };
var hotel2 = new MongoHotelModel("key2") { HotelName = "Test Name 2" };
var hotel3 = new MongoHotelModel("key3") { HotelName = "Test Name 3" };
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
// Act
await sut.UpsertAsync([hotel1, hotel2, hotel3]);
}
[Fact]
public async Task UpsertWithModelWorksCorrectlyAsync()
{
var definition = new VectorStoreCollectionDefinition
{
Properties =
[
new VectorStoreKeyProperty("Id", typeof(string)),
new VectorStoreDataProperty("HotelName", typeof(string))
]
};
await this.TestUpsertWithModelAsync(
dataModel: new TestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "HotelName",
definition: definition);
}
[Fact]
public async Task UpsertWithVectorStoreModelWorksCorrectlyAsync()
{
await this.TestUpsertWithModelAsync(
dataModel: new VectorStoreTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "HotelName");
}
[Fact]
public async Task UpsertWithBsonModelWorksCorrectlyAsync()
{
var definition = new VectorStoreCollectionDefinition
{
Properties =
[
new VectorStoreKeyProperty("Id", typeof(string)),
new VectorStoreDataProperty("HotelName", typeof(string))
]
};
await this.TestUpsertWithModelAsync(
dataModel: new BsonTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "hotel_name",
definition: definition);
}
[Fact]
public async Task UpsertWithBsonVectorStoreModelWorksCorrectlyAsync()
{
await this.TestUpsertWithModelAsync(
dataModel: new BsonVectorStoreTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "hotel_name");
}
[Fact]
public async Task UpsertWithBsonVectorStoreWithNameModelWorksCorrectlyAsync()
{
await this.TestUpsertWithModelAsync(
dataModel: new BsonVectorStoreWithNameTestModel { Id = "key", HotelName = "Test Name" },
expectedPropertyName: "bson_hotel_name");
}
[Theory]
[MemberData(nameof(SearchVectorTypeData))]
public async Task SearchThrowsExceptionWithInvalidVectorTypeAsync(object vector, bool exceptionExpected)
{
// Arrange
this.MockCollectionForSearch();
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
// Act & Assert
if (exceptionExpected)
{
await Assert.ThrowsAsync(async () => await sut.SearchAsync(vector, top: 3).ToListAsync());
}
else
{
Assert.NotNull(await sut.SearchAsync(vector, top: 3).FirstOrDefaultAsync());
}
}
[Theory]
[InlineData("TestEmbedding1", "TestEmbedding1", 3, 3)]
[InlineData("TestEmbedding2", "test_embedding_2", 4, 4)]
public async Task SearchUsesValidQueryAsync(
string? vectorPropertyName,
string expectedVectorPropertyName,
int actualTop,
int expectedTop)
{
// Arrange
var vector = new ReadOnlyMemory([1f, 2f, 3f]);
var expectedSearch = new BsonDocument
{
{ "$vectorSearch",
new BsonDocument
{
{ "index", "vector_index" },
{ "queryVector", BsonArray.Create(vector.ToArray()) },
{ "path", expectedVectorPropertyName },
{ "limit", expectedTop },
{ "numCandidates", expectedTop * 10 },
}
}
};
var expectedProjection = new BsonDocument
{
{ "$project",
new BsonDocument
{
{ "similarityScore", new BsonDocument { { "$meta", "vectorSearchScore" } } },
{ "document", "$$ROOT" }
}
}
};
this.MockCollectionForSearch();
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
Expression>? vectorSelector = vectorPropertyName switch
{
"TestEmbedding1" => record => record.TestEmbedding1,
"TestEmbedding2" => record => record.TestEmbedding2,
_ => null
};
// Act
var actual = await sut.SearchAsync(vector, top: actualTop, new()
{
VectorProperty = vectorSelector,
}).FirstOrDefaultAsync();
// Assert
Assert.NotNull(actual);
this._mockMongoCollection.Verify(l => l.AggregateAsync(
It.Is>(pipeline =>
this.ComparePipeline(pipeline, expectedSearch, expectedProjection)),
It.IsAny(),
It.IsAny()), Times.Once());
}
[Fact]
public async Task SearchThrowsExceptionWithNonExistentVectorPropertyNameAsync()
{
// Arrange
this.MockCollectionForSearch();
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
var options = new MEVD.VectorSearchOptions { VectorProperty = r => "non-existent-property" };
// Act & Assert
await Assert.ThrowsAsync(async () => await sut.SearchAsync(new ReadOnlyMemory([1f, 2f, 3f]), top: 3, options).FirstOrDefaultAsync());
}
[Fact]
public async Task SearchReturnsRecordWithScoreAsync()
{
// Arrange
this.MockCollectionForSearch();
using var sut = new MongoCollection(
this._mockMongoDatabase.Object,
"collection");
// Act
var result = await sut.SearchAsync(new ReadOnlyMemory([1f, 2f, 3f]), top: 3).FirstOrDefaultAsync();
// Assert
Assert.NotNull(result);
Assert.Equal("key", result.Record.HotelId);
Assert.Equal("Test Name", result.Record.HotelName);
Assert.Equal(0.99f, result.Score);
}
public static TheoryData, string, bool> CollectionExistsData => new()
{
{ ["collection-2"], "collection-2", true },
{ [], "non-existent-collection", false }
};
public static TheoryData, int> EnsureCollectionExistsData => new()
{
{ ["collection"], 0 },
{ [], 1 }
};
public static TheoryData