chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Suppressing errors for Test projects under dotnet folder
|
||||
[*.cs]
|
||||
dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
|
||||
dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
|
||||
dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
|
||||
dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
|
||||
@@ -0,0 +1,709 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="MongoCollection{TKey, TRecord}"/> class.
|
||||
/// </summary>
|
||||
public sealed class MongoCollectionTests
|
||||
{
|
||||
private readonly Mock<IMongoDatabase> _mockMongoDatabase = new();
|
||||
private readonly Mock<IMongoCollection<BsonDocument>> _mockMongoCollection = new();
|
||||
|
||||
public MongoCollectionTests()
|
||||
{
|
||||
this._mockMongoDatabase
|
||||
.Setup(l => l.GetCollection<BsonDocument>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()))
|
||||
.Returns(this._mockMongoCollection.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorForModelWithoutKeyThrowsException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<NotSupportedException>(() => new MongoCollection<string, object>(this._mockMongoDatabase.Object, "collection"));
|
||||
Assert.Contains("No key property found", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithDeclarativeModelInitializesCollection()
|
||||
{
|
||||
// Act & Assert
|
||||
using var collection = new MongoCollection<string, MongoHotelModel>(
|
||||
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<string, TestModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
"collection",
|
||||
new() { Definition = definition });
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(collection);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CollectionExistsData))]
|
||||
public async Task CollectionExistsReturnsValidResultAsync(List<string> collections, string collectionName, bool expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
var mockCursor = new Mock<IAsyncCursor<string>>();
|
||||
|
||||
mockCursor
|
||||
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
mockCursor
|
||||
.Setup(l => l.Current)
|
||||
.Returns(collections);
|
||||
|
||||
this._mockMongoDatabase
|
||||
.Setup(l => l.ListCollectionNamesAsync(It.IsAny<ListCollectionNamesOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(mockCursor.Object);
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
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<IAsyncCursor<string>>();
|
||||
mockCursor
|
||||
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
mockCursor
|
||||
.Setup(l => l.Current)
|
||||
.Returns([]);
|
||||
|
||||
this._mockMongoDatabase
|
||||
.Setup(l => l.ListCollectionNamesAsync(It.IsAny<ListCollectionNamesOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(mockCursor.Object);
|
||||
|
||||
var mockIndexCursor = new Mock<IAsyncCursor<BsonDocument>>();
|
||||
mockIndexCursor
|
||||
.SetupSequence(l => l.MoveNext(It.IsAny<CancellationToken>()))
|
||||
.Returns(true)
|
||||
.Returns(false);
|
||||
|
||||
mockIndexCursor
|
||||
.Setup(l => l.Current)
|
||||
.Returns([]);
|
||||
|
||||
var mockMongoIndexManager = new Mock<IMongoIndexManager<BsonDocument>>();
|
||||
|
||||
mockMongoIndexManager
|
||||
.Setup(l => l.ListAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(mockIndexCursor.Object);
|
||||
|
||||
this._mockMongoCollection
|
||||
.Setup(l => l.Indexes)
|
||||
.Returns(mockMongoIndexManager.Object);
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
CollectionName);
|
||||
|
||||
// Act
|
||||
await sut.EnsureCollectionExistsAsync();
|
||||
|
||||
// Assert
|
||||
this._mockMongoDatabase.Verify(l => l.CreateCollectionAsync(
|
||||
CollectionName,
|
||||
It.IsAny<CreateCollectionOptions>(),
|
||||
It.IsAny<CancellationToken>()), Times.Exactly(1));
|
||||
|
||||
this._mockMongoDatabase.Verify(l => l.ListCollectionNamesAsync(
|
||||
It.IsAny<ListCollectionNamesOptions>(),
|
||||
It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteInvokesValidMethodsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string RecordKey = "key";
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
"collection");
|
||||
|
||||
var serializerRegistry = BsonSerializer.SerializerRegistry;
|
||||
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
|
||||
var expectedDefinition = Builders<BsonDocument>.Filter.Eq(document => document["_id"], RecordKey);
|
||||
|
||||
// Act
|
||||
await sut.DeleteAsync(RecordKey);
|
||||
|
||||
// Assert
|
||||
this._mockMongoCollection.Verify(l => l.DeleteOneAsync(
|
||||
It.Is<FilterDefinition<BsonDocument>>(definition =>
|
||||
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
|
||||
It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteBatchInvokesValidMethodsAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<string> recordKeys = ["key1", "key2"];
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
"collection");
|
||||
|
||||
var serializerRegistry = BsonSerializer.SerializerRegistry;
|
||||
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
|
||||
var expectedDefinition = Builders<BsonDocument>.Filter.In(document => document["_id"].AsString, recordKeys);
|
||||
|
||||
// Act
|
||||
await sut.DeleteAsync(recordKeys);
|
||||
|
||||
// Assert
|
||||
this._mockMongoCollection.Verify(l => l.DeleteManyAsync(
|
||||
It.Is<FilterDefinition<BsonDocument>>(definition =>
|
||||
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
|
||||
It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteCollectionInvokesValidMethodsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string CollectionName = "collection";
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
CollectionName);
|
||||
|
||||
// Act
|
||||
await sut.EnsureCollectionDeletedAsync();
|
||||
|
||||
// Assert
|
||||
this._mockMongoDatabase.Verify(l => l.DropCollectionAsync(
|
||||
It.Is<string>(name => name == CollectionName),
|
||||
It.IsAny<CancellationToken>()), 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<IAsyncCursor<BsonDocument>>();
|
||||
mockCursor
|
||||
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
mockCursor
|
||||
.Setup(l => l.Current)
|
||||
.Returns([document]);
|
||||
|
||||
this._mockMongoCollection
|
||||
.Setup(l => l.FindAsync(
|
||||
It.IsAny<FilterDefinition<BsonDocument>>(),
|
||||
It.IsAny<FindOptions<BsonDocument>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(mockCursor.Object);
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
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<IAsyncCursor<BsonDocument>>();
|
||||
mockCursor
|
||||
.SetupSequence(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true)
|
||||
.ReturnsAsync(false);
|
||||
|
||||
mockCursor
|
||||
.Setup(l => l.Current)
|
||||
.Returns([document1, document2, document3]);
|
||||
|
||||
this._mockMongoCollection
|
||||
.Setup(l => l.FindAsync(
|
||||
It.IsAny<FilterDefinition<BsonDocument>>(),
|
||||
It.IsAny<FindOptions<BsonDocument>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(mockCursor.Object);
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
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<BsonDocument>();
|
||||
var expectedDefinition = Builders<BsonDocument>.Filter.Eq(document => document["_id"], "key");
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
"collection");
|
||||
|
||||
// Act
|
||||
await sut.UpsertAsync(hotel);
|
||||
|
||||
// Assert
|
||||
this._mockMongoCollection.Verify(l => l.ReplaceOneAsync(
|
||||
It.Is<FilterDefinition<BsonDocument>>(definition =>
|
||||
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
|
||||
It.Is<BsonDocument>(document =>
|
||||
document["_id"] == "key" &&
|
||||
document["HotelName"] == "Test Name"),
|
||||
It.IsAny<ReplaceOptions>(),
|
||||
It.IsAny<CancellationToken>()), 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<string, MongoHotelModel>(
|
||||
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<TestModel>(
|
||||
dataModel: new TestModel { Id = "key", HotelName = "Test Name" },
|
||||
expectedPropertyName: "HotelName",
|
||||
definition: definition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertWithVectorStoreModelWorksCorrectlyAsync()
|
||||
{
|
||||
await this.TestUpsertWithModelAsync<VectorStoreTestModel>(
|
||||
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<BsonTestModel>(
|
||||
dataModel: new BsonTestModel { Id = "key", HotelName = "Test Name" },
|
||||
expectedPropertyName: "hotel_name",
|
||||
definition: definition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertWithBsonVectorStoreModelWorksCorrectlyAsync()
|
||||
{
|
||||
await this.TestUpsertWithModelAsync<BsonVectorStoreTestModel>(
|
||||
dataModel: new BsonVectorStoreTestModel { Id = "key", HotelName = "Test Name" },
|
||||
expectedPropertyName: "hotel_name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertWithBsonVectorStoreWithNameModelWorksCorrectlyAsync()
|
||||
{
|
||||
await this.TestUpsertWithModelAsync<BsonVectorStoreWithNameTestModel>(
|
||||
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<string, MongoHotelModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
"collection");
|
||||
|
||||
// Act & Assert
|
||||
if (exceptionExpected)
|
||||
{
|
||||
await Assert.ThrowsAsync<NotSupportedException>(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<float>([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<string, VectorSearchModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
"collection");
|
||||
|
||||
Expression<Func<VectorSearchModel, object?>>? 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<PipelineDefinition<BsonDocument, BsonDocument>>(pipeline =>
|
||||
this.ComparePipeline(pipeline, expectedSearch, expectedProjection)),
|
||||
It.IsAny<AggregateOptions>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchThrowsExceptionWithNonExistentVectorPropertyNameAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.MockCollectionForSearch();
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
"collection");
|
||||
|
||||
var options = new MEVD.VectorSearchOptions<MongoHotelModel> { VectorProperty = r => "non-existent-property" };
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.SearchAsync(new ReadOnlyMemory<float>([1f, 2f, 3f]), top: 3, options).FirstOrDefaultAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchReturnsRecordWithScoreAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.MockCollectionForSearch();
|
||||
|
||||
using var sut = new MongoCollection<string, MongoHotelModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
"collection");
|
||||
|
||||
// Act
|
||||
var result = await sut.SearchAsync(new ReadOnlyMemory<float>([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<List<string>, string, bool> CollectionExistsData => new()
|
||||
{
|
||||
{ ["collection-2"], "collection-2", true },
|
||||
{ [], "non-existent-collection", false }
|
||||
};
|
||||
|
||||
public static TheoryData<List<string>, int> EnsureCollectionExistsData => new()
|
||||
{
|
||||
{ ["collection"], 0 },
|
||||
{ [], 1 }
|
||||
};
|
||||
|
||||
public static TheoryData<object, bool> SearchVectorTypeData => new()
|
||||
{
|
||||
{ new ReadOnlyMemory<float>([1f, 2f, 3f]), false },
|
||||
{ new ReadOnlyMemory<float>?(new([1f, 2f, 3f])), false },
|
||||
{ new List<float>([1f, 2f, 3f]), true },
|
||||
};
|
||||
|
||||
#region private
|
||||
|
||||
private bool ComparePipeline(
|
||||
PipelineDefinition<BsonDocument, BsonDocument> actualPipeline,
|
||||
BsonDocument expectedSearch,
|
||||
BsonDocument expectedProjection)
|
||||
{
|
||||
var serializerRegistry = BsonSerializer.SerializerRegistry;
|
||||
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
|
||||
|
||||
var documents = actualPipeline.Render(new RenderArgs<BsonDocument>(documentSerializer, serializerRegistry)).Documents;
|
||||
|
||||
return
|
||||
documents[0].ToJson() == expectedSearch.ToJson() &&
|
||||
documents[1].ToJson() == expectedProjection.ToJson();
|
||||
}
|
||||
|
||||
private void MockCollectionForSearch()
|
||||
{
|
||||
var document = new BsonDocument { ["_id"] = "key", ["HotelName"] = "Test Name" };
|
||||
var searchResult = new BsonDocument { ["document"] = document, ["similarityScore"] = 0.99f };
|
||||
|
||||
var mockCursor = new Mock<IAsyncCursor<BsonDocument>>();
|
||||
mockCursor
|
||||
.Setup(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
mockCursor
|
||||
.Setup(l => l.Current)
|
||||
.Returns([searchResult]);
|
||||
|
||||
this._mockMongoCollection
|
||||
.Setup(l => l.AggregateAsync(
|
||||
It.IsAny<PipelineDefinition<BsonDocument, BsonDocument>>(),
|
||||
It.IsAny<AggregateOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(mockCursor.Object);
|
||||
}
|
||||
|
||||
private async Task TestUpsertWithModelAsync<TDataModel>(
|
||||
TDataModel dataModel,
|
||||
string expectedPropertyName,
|
||||
VectorStoreCollectionDefinition? definition = null)
|
||||
where TDataModel : class
|
||||
{
|
||||
// Arrange
|
||||
var serializerRegistry = BsonSerializer.SerializerRegistry;
|
||||
var documentSerializer = serializerRegistry.GetSerializer<BsonDocument>();
|
||||
var expectedDefinition = Builders<BsonDocument>.Filter.Eq(document => document["_id"], "key");
|
||||
|
||||
MongoCollectionOptions? options = definition != null ?
|
||||
new() { Definition = definition } :
|
||||
null;
|
||||
|
||||
using var sut = new MongoCollection<string, TDataModel>(
|
||||
this._mockMongoDatabase.Object,
|
||||
"collection",
|
||||
options);
|
||||
|
||||
// Act
|
||||
await sut.UpsertAsync(dataModel);
|
||||
|
||||
// Assert
|
||||
this._mockMongoCollection.Verify(l => l.ReplaceOneAsync(
|
||||
It.Is<FilterDefinition<BsonDocument>>(definition =>
|
||||
CompareFilterDefinitions(definition, expectedDefinition, documentSerializer, serializerRegistry)),
|
||||
It.Is<BsonDocument>(document =>
|
||||
document["_id"] == "key" &&
|
||||
document.Contains(expectedPropertyName) &&
|
||||
document[expectedPropertyName] == "Test Name"),
|
||||
It.IsAny<ReplaceOptions>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
private static bool CompareFilterDefinitions(
|
||||
FilterDefinition<BsonDocument> actual,
|
||||
FilterDefinition<BsonDocument> expected,
|
||||
IBsonSerializer<BsonDocument> documentSerializer,
|
||||
IBsonSerializerRegistry serializerRegistry)
|
||||
{
|
||||
return actual.Render(new RenderArgs<BsonDocument>(documentSerializer, serializerRegistry)) ==
|
||||
expected.Render(new RenderArgs<BsonDocument>(documentSerializer, serializerRegistry));
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812
|
||||
private sealed class TestModel
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
|
||||
public string? HotelName { get; set; }
|
||||
}
|
||||
|
||||
private sealed class VectorStoreTestModel
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[VectorStoreData(StorageName = "hotel_name")]
|
||||
public string? HotelName { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BsonTestModel
|
||||
{
|
||||
[BsonId]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[BsonElement("hotel_name")]
|
||||
public string? HotelName { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BsonVectorStoreTestModel
|
||||
{
|
||||
[BsonId]
|
||||
[VectorStoreKey]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[BsonElement("hotel_name")]
|
||||
[VectorStoreData]
|
||||
public string? HotelName { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BsonVectorStoreWithNameTestModel
|
||||
{
|
||||
[BsonId]
|
||||
[VectorStoreKey]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[BsonElement("bson_hotel_name")]
|
||||
[VectorStoreData(StorageName = "storage_hotel_name")]
|
||||
public string? HotelName { get; set; }
|
||||
}
|
||||
|
||||
private sealed class VectorSearchModel
|
||||
{
|
||||
[BsonId]
|
||||
[VectorStoreKey]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public string? HotelName { get; set; }
|
||||
|
||||
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineDistance, IndexKind = IndexKind.IvfFlat, StorageName = "test_embedding_1")]
|
||||
public ReadOnlyMemory<float> TestEmbedding1 { get; set; }
|
||||
|
||||
[BsonElement("test_embedding_2")]
|
||||
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineDistance, IndexKind = IndexKind.IvfFlat)]
|
||||
public ReadOnlyMemory<float> TestEmbedding2 { get; set; }
|
||||
}
|
||||
#pragma warning restore CA1812
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SemanticKernel.Connectors.MongoDB.UnitTests</AssemblyName>
|
||||
<RootNamespace>SemanticKernel.Connectors.MongoDB.UnitTests</RootNamespace>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>$(NoWarn);SKEXP0001,VSTHRD111,CA2007,CS1591</NoWarn>
|
||||
<NoWarn>$(NoWarn);MEVD9001</NoWarn> <!-- Experimental MEVD connector-facing APIs -->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\VectorData\MongoDB\MongoDB.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.Extensions.VectorData.ProviderServices;
|
||||
using Microsoft.SemanticKernel.Connectors.MongoDB;
|
||||
using MongoDB.Bson;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="MongoDynamicMapper"/> class.
|
||||
/// </summary>
|
||||
public sealed class MongoDynamicMapperTests
|
||||
{
|
||||
private static readonly CollectionModel s_model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("BoolDataProp", typeof(bool)),
|
||||
new VectorStoreDataProperty("NullableBoolDataProp", typeof(bool?)),
|
||||
new VectorStoreDataProperty("StringDataProp", typeof(string)),
|
||||
new VectorStoreDataProperty("IntDataProp", typeof(int)),
|
||||
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
|
||||
new VectorStoreDataProperty("LongDataProp", typeof(long)),
|
||||
new VectorStoreDataProperty("NullableLongDataProp", typeof(long?)),
|
||||
new VectorStoreDataProperty("FloatDataProp", typeof(float)),
|
||||
new VectorStoreDataProperty("NullableFloatDataProp", typeof(float?)),
|
||||
new VectorStoreDataProperty("DoubleDataProp", typeof(double)),
|
||||
new VectorStoreDataProperty("NullableDoubleDataProp", typeof(double?)),
|
||||
new VectorStoreDataProperty("DecimalDataProp", typeof(decimal)),
|
||||
new VectorStoreDataProperty("NullableDecimalDataProp", typeof(decimal?)),
|
||||
new VectorStoreDataProperty("DateTimeDataProp", typeof(DateTime)),
|
||||
new VectorStoreDataProperty("NullableDateTimeDataProp", typeof(DateTime?)),
|
||||
new VectorStoreDataProperty("TagListDataProp", typeof(List<string>)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
|
||||
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10)
|
||||
]);
|
||||
|
||||
private static readonly float[] s_floatVector = [1.0f, 2.0f, 3.0f];
|
||||
private static readonly List<string> s_taglist = ["tag1", "tag2"];
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelMapsAllSupportedTypes()
|
||||
{
|
||||
// Arrange
|
||||
var sut = new MongoDynamicMapper(s_model);
|
||||
var dataModel = new Dictionary<string, object?>
|
||||
{
|
||||
["Key"] = "key",
|
||||
|
||||
["BoolDataProp"] = true,
|
||||
["NullableBoolDataProp"] = false,
|
||||
["StringDataProp"] = "string",
|
||||
["IntDataProp"] = 1,
|
||||
["NullableIntDataProp"] = 2,
|
||||
["LongDataProp"] = 3L,
|
||||
["NullableLongDataProp"] = 4L,
|
||||
["FloatDataProp"] = 5.0f,
|
||||
["NullableFloatDataProp"] = 6.0f,
|
||||
["DoubleDataProp"] = 7.0,
|
||||
["NullableDoubleDataProp"] = 8.0,
|
||||
["DecimalDataProp"] = 9.0m,
|
||||
["NullableDecimalDataProp"] = 10.0m,
|
||||
["DateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
|
||||
["NullableDateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
|
||||
["TagListDataProp"] = s_taglist,
|
||||
|
||||
["FloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
|
||||
["NullableFloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
|
||||
};
|
||||
|
||||
// Act
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", storageModel["_id"]);
|
||||
Assert.Equal(true, (bool?)storageModel["BoolDataProp"]);
|
||||
Assert.Equal(false, (bool?)storageModel["NullableBoolDataProp"]);
|
||||
Assert.Equal("string", (string?)storageModel["StringDataProp"]);
|
||||
Assert.Equal(1, (int?)storageModel["IntDataProp"]);
|
||||
Assert.Equal(2, (int?)storageModel["NullableIntDataProp"]);
|
||||
Assert.Equal(3L, (long?)storageModel["LongDataProp"]);
|
||||
Assert.Equal(4L, (long?)storageModel["NullableLongDataProp"]);
|
||||
Assert.Equal(5.0f, (float?)storageModel["FloatDataProp"].AsDouble);
|
||||
Assert.Equal(6.0f, (float?)storageModel["NullableFloatDataProp"].AsNullableDouble);
|
||||
Assert.Equal(7.0, (double?)storageModel["DoubleDataProp"]);
|
||||
Assert.Equal(8.0, (double?)storageModel["NullableDoubleDataProp"]);
|
||||
Assert.Equal(9.0m, (decimal?)storageModel["DecimalDataProp"]);
|
||||
Assert.Equal(10.0m, (decimal?)storageModel["NullableDecimalDataProp"]);
|
||||
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), storageModel["DateTimeDataProp"].ToUniversalTime());
|
||||
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), storageModel["NullableDateTimeDataProp"].ToUniversalTime());
|
||||
Assert.Equal(s_taglist, storageModel["TagListDataProp"]!.AsBsonArray.Select(x => (string)x!).ToArray());
|
||||
Assert.Equal(s_floatVector, storageModel["FloatVector"]!.AsBsonArray.Select(x => (float)x.AsDouble!).ToArray());
|
||||
Assert.Equal(s_floatVector, storageModel["NullableFloatVector"]!.AsBsonArray.Select(x => (float)x.AsNullableDouble!).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelMapsNullValues()
|
||||
{
|
||||
// Arrange
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("StringDataProp", typeof(string)),
|
||||
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
|
||||
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10)
|
||||
]);
|
||||
|
||||
var dataModel = new Dictionary<string, object?>
|
||||
{
|
||||
["Key"] = "key",
|
||||
["StringDataProp"] = null,
|
||||
["NullableIntDataProp"] = null,
|
||||
["NullableFloatVector"] = null
|
||||
};
|
||||
|
||||
var sut = new MongoDynamicMapper(model);
|
||||
|
||||
// Act
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(BsonNull.Value, storageModel["StringDataProp"]);
|
||||
Assert.Equal(BsonNull.Value, storageModel["NullableIntDataProp"]);
|
||||
Assert.Empty(storageModel["NullableFloatVector"].AsBsonArray);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelMapsAllSupportedTypes()
|
||||
{
|
||||
// Arrange
|
||||
var sut = new MongoDynamicMapper(s_model);
|
||||
var storageModel = new BsonDocument
|
||||
{
|
||||
["_id"] = "key",
|
||||
["BoolDataProp"] = true,
|
||||
["NullableBoolDataProp"] = false,
|
||||
["StringDataProp"] = "string",
|
||||
["IntDataProp"] = 1,
|
||||
["NullableIntDataProp"] = 2,
|
||||
["LongDataProp"] = 3L,
|
||||
["NullableLongDataProp"] = 4L,
|
||||
["FloatDataProp"] = 5.0f,
|
||||
["NullableFloatDataProp"] = 6.0f,
|
||||
["DoubleDataProp"] = 7.0,
|
||||
["NullableDoubleDataProp"] = 8.0,
|
||||
["DecimalDataProp"] = 9.0m,
|
||||
["NullableDecimalDataProp"] = 10.0m,
|
||||
["DateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
|
||||
["NullableDateTimeDataProp"] = new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(),
|
||||
["TagListDataProp"] = BsonArray.Create(s_taglist),
|
||||
["FloatVector"] = BsonArray.Create(s_floatVector),
|
||||
["NullableFloatVector"] = BsonArray.Create(s_floatVector),
|
||||
};
|
||||
|
||||
// Act
|
||||
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", dataModel["Key"]);
|
||||
Assert.Equal(true, dataModel["BoolDataProp"]);
|
||||
Assert.Equal(false, dataModel["NullableBoolDataProp"]);
|
||||
Assert.Equal("string", dataModel["StringDataProp"]);
|
||||
Assert.Equal(1, dataModel["IntDataProp"]);
|
||||
Assert.Equal(2, dataModel["NullableIntDataProp"]);
|
||||
Assert.Equal(3L, dataModel["LongDataProp"]);
|
||||
Assert.Equal(4L, dataModel["NullableLongDataProp"]);
|
||||
Assert.Equal(5.0f, dataModel["FloatDataProp"]);
|
||||
Assert.Equal(6.0f, dataModel["NullableFloatDataProp"]);
|
||||
Assert.Equal(7.0, dataModel["DoubleDataProp"]);
|
||||
Assert.Equal(8.0, dataModel["NullableDoubleDataProp"]);
|
||||
Assert.Equal(9.0m, dataModel["DecimalDataProp"]);
|
||||
Assert.Equal(10.0m, dataModel["NullableDecimalDataProp"]);
|
||||
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), dataModel["DateTimeDataProp"]);
|
||||
Assert.Equal(new DateTime(2021, 1, 1, 0, 0, 0).ToUniversalTime(), dataModel["NullableDateTimeDataProp"]);
|
||||
Assert.Equal(s_taglist, dataModel["TagListDataProp"]);
|
||||
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["FloatVector"]!).ToArray());
|
||||
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["NullableFloatVector"]!)!.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelMapsNullValues()
|
||||
{
|
||||
// Arrange
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("StringDataProp", typeof(string)),
|
||||
new VectorStoreDataProperty("NullableIntDataProp", typeof(int?)),
|
||||
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10)
|
||||
]);
|
||||
|
||||
var storageModel = new BsonDocument
|
||||
{
|
||||
["_id"] = "key",
|
||||
["StringDataProp"] = BsonNull.Value,
|
||||
["NullableIntDataProp"] = BsonNull.Value,
|
||||
["NullableFloatVector"] = BsonNull.Value
|
||||
};
|
||||
|
||||
var sut = new MongoDynamicMapper(model);
|
||||
|
||||
// Act
|
||||
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", dataModel["Key"]);
|
||||
Assert.Null(dataModel["StringDataProp"]);
|
||||
Assert.Null(dataModel["NullableIntDataProp"]);
|
||||
Assert.Null(dataModel["NullableFloatVector"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelThrowsForMissingKey()
|
||||
{
|
||||
// Arrange
|
||||
var sut = new MongoDynamicMapper(s_model);
|
||||
var storageModel = new BsonDocument();
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(
|
||||
() => sut.MapFromStorageToDataModel(storageModel, includeVectors: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelSkipsMissingProperties()
|
||||
{
|
||||
// Arrange
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("StringDataProp", typeof(string)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
|
||||
]);
|
||||
|
||||
var dataModel = new Dictionary<string, object?> { ["Key"] = "key" };
|
||||
var sut = new MongoDynamicMapper(model);
|
||||
|
||||
// Act
|
||||
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", (string?)storageModel["_id"]);
|
||||
Assert.False(storageModel.Contains("StringDataProp"));
|
||||
Assert.False(storageModel.Contains("FloatVector"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelSkipsMissingProperties()
|
||||
{
|
||||
// Arrange
|
||||
var model = BuildModel(
|
||||
[
|
||||
new VectorStoreKeyProperty("Key", typeof(string)),
|
||||
new VectorStoreDataProperty("StringDataProp", typeof(string)),
|
||||
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
|
||||
]);
|
||||
|
||||
var storageModel = new BsonDocument
|
||||
{
|
||||
["_id"] = "key"
|
||||
};
|
||||
|
||||
var sut = new MongoDynamicMapper(model);
|
||||
|
||||
// Act
|
||||
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("key", dataModel["Key"]);
|
||||
Assert.False(dataModel.ContainsKey("StringDataProp"));
|
||||
Assert.False(dataModel.ContainsKey("FloatVector"));
|
||||
}
|
||||
|
||||
private static CollectionModel BuildModel(List<VectorStoreProperty> properties)
|
||||
=> new MongoModelBuilder().BuildDynamic(new() { Properties = properties }, defaultEmbeddingGenerator: null);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
|
||||
|
||||
public class MongoHotelModel(string hotelId)
|
||||
{
|
||||
/// <summary>The key of the record.</summary>
|
||||
[VectorStoreKey]
|
||||
public string HotelId { get; init; } = hotelId;
|
||||
|
||||
/// <summary>A string metadata field.</summary>
|
||||
[VectorStoreData(IsIndexed = true)]
|
||||
public string? HotelName { get; set; }
|
||||
|
||||
/// <summary>An int metadata field.</summary>
|
||||
[VectorStoreData]
|
||||
public int HotelCode { get; set; }
|
||||
|
||||
/// <summary>A float metadata field.</summary>
|
||||
[VectorStoreData]
|
||||
public float? HotelRating { get; set; }
|
||||
|
||||
/// <summary>A bool metadata field.</summary>
|
||||
[BsonElement("parking_is_included")]
|
||||
[VectorStoreData]
|
||||
public bool ParkingIncluded { get; set; }
|
||||
|
||||
/// <summary>An array metadata field.</summary>
|
||||
[VectorStoreData]
|
||||
public List<string> Tags { get; set; } = [];
|
||||
|
||||
/// <summary>A data field.</summary>
|
||||
[VectorStoreData]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>A vector field.</summary>
|
||||
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineSimilarity)]
|
||||
public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.MongoDB;
|
||||
using MongoDB.Bson;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="MongoMapper{TRecord}"/> class.
|
||||
/// </summary>
|
||||
public sealed class MongoMapperTests
|
||||
{
|
||||
private readonly MongoMapper<MongoHotelModel> _sut;
|
||||
|
||||
public MongoMapperTests()
|
||||
{
|
||||
var keyProperty = new VectorStoreKeyProperty("HotelId", typeof(string));
|
||||
|
||||
var definition = new VectorStoreCollectionDefinition
|
||||
{
|
||||
Properties =
|
||||
[
|
||||
keyProperty,
|
||||
new VectorStoreDataProperty("HotelName", typeof(string)),
|
||||
new VectorStoreDataProperty("Tags", typeof(List<string>)),
|
||||
new VectorStoreDataProperty("ParkingIncluded", typeof(bool)),
|
||||
new VectorStoreVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory<float>?), 10)
|
||||
]
|
||||
};
|
||||
|
||||
this._sut = new(new MongoModelBuilder().Build(typeof(MongoHotelModel), typeof(string), definition, defaultEmbeddingGenerator: null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromDataToStorageModelReturnsValidObject()
|
||||
{
|
||||
// Arrange
|
||||
var hotel = new MongoHotelModel("key")
|
||||
{
|
||||
HotelName = "Test Name",
|
||||
Tags = ["tag1", "tag2"],
|
||||
ParkingIncluded = true,
|
||||
DescriptionEmbedding = new ReadOnlyMemory<float>([1f, 2f, 3f])
|
||||
};
|
||||
|
||||
// Act
|
||||
var document = this._sut.MapFromDataToStorageModel(hotel, recordIndex: 0, generatedEmbeddings: null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(document);
|
||||
|
||||
Assert.Equal("key", document["_id"]);
|
||||
Assert.Equal("Test Name", document["HotelName"]);
|
||||
Assert.Equal(["tag1", "tag2"], document["Tags"].AsBsonArray);
|
||||
Assert.True(document["parking_is_included"].AsBoolean);
|
||||
Assert.Equal([1f, 2f, 3f], document["DescriptionEmbedding"].AsBsonArray);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapFromStorageToDataModelReturnsValidObject()
|
||||
{
|
||||
// Arrange
|
||||
var document = new BsonDocument
|
||||
{
|
||||
["_id"] = "key",
|
||||
["HotelName"] = "Test Name",
|
||||
["Tags"] = BsonArray.Create(new List<string> { "tag1", "tag2" }),
|
||||
["parking_is_included"] = BsonValue.Create(true),
|
||||
["DescriptionEmbedding"] = BsonArray.Create(new List<float> { 1f, 2f, 3f })
|
||||
};
|
||||
|
||||
// Act
|
||||
var hotel = this._sut.MapFromStorageToDataModel(document, includeVectors: true);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(hotel);
|
||||
|
||||
Assert.Equal("key", hotel.HotelId);
|
||||
Assert.Equal("Test Name", hotel.HotelName);
|
||||
Assert.Equal(["tag1", "tag2"], hotel.Tags);
|
||||
Assert.True(hotel.ParkingIncluded);
|
||||
Assert.True(new ReadOnlyMemory<float>([1f, 2f, 3f]).Span.SequenceEqual(hotel.DescriptionEmbedding!.Value.Span));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Connectors.MongoDB;
|
||||
using MongoDB.Driver;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.MongoDB.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="MongoVectorStore"/> class.
|
||||
/// </summary>
|
||||
public sealed class MongoVectorStoreTests
|
||||
{
|
||||
private readonly Mock<IMongoDatabase> _mockMongoDatabase = new();
|
||||
|
||||
[Fact]
|
||||
public void GetCollectionWithNotSupportedKeyThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
using var sut = new MongoVectorStore(this._mockMongoDatabase.Object);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException>(() => sut.GetCollection<byte[], MongoHotelModel>("collection"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCollectionWithoutFactoryReturnsDefaultCollection()
|
||||
{
|
||||
// Arrange
|
||||
using var sut = new MongoVectorStore(this._mockMongoDatabase.Object);
|
||||
|
||||
// Act
|
||||
var collection = sut.GetCollection<string, MongoHotelModel>("collection");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(collection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCollectionNamesReturnsCollectionNamesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var expectedCollectionNames = new List<string> { "collection-1", "collection-2", "collection-3" };
|
||||
|
||||
var mockCursor = new Mock<IAsyncCursor<string>>();
|
||||
mockCursor
|
||||
.SetupSequence(l => l.MoveNextAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true)
|
||||
.ReturnsAsync(false);
|
||||
|
||||
mockCursor
|
||||
.Setup(l => l.Current)
|
||||
.Returns(expectedCollectionNames);
|
||||
|
||||
this._mockMongoDatabase
|
||||
.Setup(l => l.ListCollectionNamesAsync(It.IsAny<ListCollectionNamesOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(mockCursor.Object);
|
||||
|
||||
using var sut = new MongoVectorStore(this._mockMongoDatabase.Object);
|
||||
|
||||
// Act
|
||||
var actualCollectionNames = await sut.ListCollectionNamesAsync().ToListAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedCollectionNames, actualCollectionNames);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user