chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -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,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>SemanticKernel.Connectors.CosmosNoSql.UnitTests</AssemblyName>
<RootNamespace>SemanticKernel.Connectors.CosmosNoSql.UnitTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);SKEXP0001</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\CosmosNoSql\CosmosNoSql.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.VectorData;
using Microsoft.Extensions.VectorData.ProviderServices;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Xunit;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlCollectionQueryBuilder"/> class.
/// </summary>
public sealed class CosmosNoSqlCollectionQueryBuilderTests
{
private const string ScorePropertyName = "TestScore";
private readonly CollectionModel _model = new CosmosNoSqlModelBuilder().BuildDynamic(
new()
{
Properties =
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreVectorProperty("TestProperty1", typeof(ReadOnlyMemory<float>), 10) { StorageName = "test_property_1" },
new VectorStoreDataProperty("TestProperty2", typeof(string)) { StorageName = "test_property_2" },
new VectorStoreDataProperty("TestProperty3", typeof(string)) { StorageName = "test_property_3" }
]
},
defaultEmbeddingGenerator: null);
[Fact]
public void BuildSearchQueryWithoutFilterDoesNotContainWhereClause()
{
// Arrange
var vector = new ReadOnlyMemory<float>([1f, 2f, 3f]);
var vectorPropertyName = "test_property_1";
// Act
var queryDefinition = CosmosNoSqlCollectionQueryBuilder.BuildSearchQuery<DummyType>(
vector,
keywords: null,
this._model,
vectorPropertyName,
distanceFunction: null,
textPropertyName: null,
ScorePropertyName,
filter: null,
scoreThreshold: null,
10,
5,
includeVectors: true);
var queryText = queryDefinition.QueryText;
var queryParameters = queryDefinition.GetQueryParameters();
// Assert
Assert.DoesNotContain("WHERE", queryText);
Assert.Contains("OFFSET 5 LIMIT 10", queryText);
Assert.Equal("@vector", queryParameters[0].Name);
Assert.Equal(vector, queryParameters[0].Value);
}
#pragma warning disable CA1812 // An internal class that is apparently never instantiated. If so, remove the code from the assembly.
private sealed class DummyType;
#pragma warning restore CA1812
}
@@ -0,0 +1,803 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Moq;
using Xunit;
using DistanceFunction = Microsoft.Extensions.VectorData.DistanceFunction;
using IndexKind = Microsoft.Extensions.VectorData.IndexKind;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="CosmosNoSqlCollection{TKey, TRecord}"/> class.
/// </summary>
public sealed class CosmosNoSqlCollectionTests
{
private readonly Mock<Database> _mockDatabase = new();
private readonly Mock<Container> _mockContainer = new();
public CosmosNoSqlCollectionTests()
{
this._mockDatabase.Setup(l => l.GetContainer(It.IsAny<string>())).Returns(this._mockContainer.Object);
var mockClient = new Mock<CosmosClient>();
mockClient.Setup(l => l.ClientOptions).Returns(new CosmosClientOptions() { UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default });
this._mockDatabase
.Setup(l => l.Client)
.Returns(mockClient.Object);
}
[Fact]
public void ConstructorForModelWithoutKeyThrowsException()
{
// Act & Assert
var exception = Assert.Throws<NotSupportedException>(() => new CosmosNoSqlCollection<string, object>(this._mockDatabase.Object, "collection"));
Assert.Contains("No key property found", exception.Message);
}
[Fact]
public void ConstructorWithoutSystemTextJsonSerializerOptionsThrowsArgumentException()
{
// Arrange
var mockDatabase = new Mock<Database>();
var mockClient = new Mock<CosmosClient>();
mockDatabase.Setup(l => l.Client).Returns(mockClient.Object);
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(mockDatabase.Object, "collection"));
Assert.Contains(nameof(CosmosClientOptions.UseSystemTextJsonSerializerWithOptions), exception.Message);
}
[Fact]
public void ConstructorWithDeclarativeModelInitializesCollection()
{
// Act & Assert
using var collection = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.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 CosmosNoSqlCollection<string, TestModel>(
this._mockDatabase.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 mockFeedResponse = new Mock<FeedResponse<string>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns(collections);
var mockFeedIterator = new Mock<FeedIterator<string>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockDatabase
.Setup(l => l.GetContainerQueryIterator<string>(
It.IsAny<QueryDefinition>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
collectionName);
// Act
var actualResult = await sut.CollectionExistsAsync();
// Assert
Assert.Equal(expectedResult, actualResult);
}
[Theory]
[InlineData(IndexingMode.Consistent)]
[InlineData(IndexingMode.Lazy)]
[InlineData(IndexingMode.None)]
public async Task EnsureCollectionExistsUsesValidContainerPropertiesAsync(IndexingMode indexingMode)
{
// Arrange
const string CollectionName = "collection";
var mockFeedResponse = new Mock<FeedResponse<string>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns([]);
var mockFeedIterator = new Mock<FeedIterator<string>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockDatabase
.Setup(l => l.GetContainerQueryIterator<string>(
It.IsAny<QueryDefinition>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new CosmosNoSqlCollection<string, TestIndexingModel>(
this._mockDatabase.Object,
CollectionName,
new()
{
IndexingMode = indexingMode,
Automatic = indexingMode != IndexingMode.None
});
var expectedVectorEmbeddingPolicy = new VectorEmbeddingPolicy(
[
new Embedding
{
DataType = VectorDataType.Float32,
Dimensions = 2,
DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.Cosine,
Path = "/DescriptionEmbedding2"
},
new Embedding
{
DataType = VectorDataType.Uint8,
Dimensions = 3,
DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.DotProduct,
Path = "/DescriptionEmbedding3"
},
new Embedding
{
DataType = VectorDataType.Int8,
Dimensions = 4,
DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.Euclidean,
Path = "/DescriptionEmbedding4"
},
]);
var expectedIndexingPolicy = new IndexingPolicy
{
VectorIndexes =
[
new VectorIndexPath { Type = VectorIndexType.Flat, Path = "/DescriptionEmbedding2" },
new VectorIndexPath { Type = VectorIndexType.QuantizedFlat, Path = "/DescriptionEmbedding3" },
new VectorIndexPath { Type = VectorIndexType.DiskANN, Path = "/DescriptionEmbedding4" },
],
IndexingMode = indexingMode,
Automatic = indexingMode != IndexingMode.None
};
if (indexingMode != IndexingMode.None)
{
expectedIndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/IndexableData1/?" });
expectedIndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/IndexableData2/?" });
expectedIndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/" });
expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding2/*" });
expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding3/*" });
expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding4/*" });
}
var expectedContainerProperties = new ContainerProperties(CollectionName, "/id")
{
VectorEmbeddingPolicy = expectedVectorEmbeddingPolicy,
IndexingPolicy = expectedIndexingPolicy
};
// Act
await sut.EnsureCollectionExistsAsync();
// Assert
this._mockDatabase.Verify(l => l.CreateContainerAsync(
It.Is<ContainerProperties>(properties => this.VerifyContainerProperties(expectedContainerProperties, properties)),
It.IsAny<int?>(),
It.IsAny<RequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Theory]
[MemberData(nameof(EnsureCollectionExistsData))]
public async Task EnsureCollectionExistsInvokesValidMethodsAsync(List<string> collections, int actualCollectionCreations)
{
// Arrange
const string CollectionName = "collection";
var mockFeedResponse = new Mock<FeedResponse<string>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns(collections);
var mockFeedIterator = new Mock<FeedIterator<string>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockDatabase
.Setup(l => l.GetContainerQueryIterator<string>(
It.IsAny<QueryDefinition>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
CollectionName);
// Act
await sut.EnsureCollectionExistsAsync();
// Assert
this._mockDatabase.Verify(l => l.CreateContainerAsync(
It.IsAny<ContainerProperties>(),
It.IsAny<int?>(),
It.IsAny<RequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Exactly(actualCollectionCreations));
}
[Theory]
[InlineData("recordKey", false)]
[InlineData("partitionKey", true)]
public async Task DeleteInvokesValidMethodsAsync(
string expectedPartitionKey,
bool useExplicitPartitionKey)
{
// Arrange
const string RecordKey = "recordKey";
const string PartitionKey = "partitionKey";
// Act
if (useExplicitPartitionKey)
{
using var sut = new CosmosNoSqlCollection<CosmosNoSqlKey, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
await ((VectorStoreCollection<CosmosNoSqlKey, CosmosNoSqlHotel>)sut).DeleteAsync(
new CosmosNoSqlKey(RecordKey, PartitionKey));
}
else
{
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
await ((VectorStoreCollection<string, CosmosNoSqlHotel>)sut).DeleteAsync(
RecordKey);
}
// Assert
this._mockContainer.Verify(l => l.DeleteItemAsync<JsonObject>(
RecordKey,
new PartitionKey(expectedPartitionKey),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Fact]
public async Task DeleteBatchInvokesValidMethodsAsync()
{
// Arrange
List<string> recordKeys = ["key1", "key2"];
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.DeleteAsync(recordKeys);
// Assert
foreach (var key in recordKeys)
{
this._mockContainer.Verify(l => l.DeleteItemAsync<JsonObject>(
key,
new PartitionKey(key),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
}
[Fact]
public async Task DeleteCollectionInvokesValidMethodsAsync()
{
// Arrange
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.EnsureCollectionDeletedAsync();
// Assert
this._mockContainer.Verify(l => l.DeleteContainerAsync(
It.IsAny<ContainerRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Fact]
public async Task GetReturnsValidRecordAsync()
{
// Arrange
const string RecordKey = "key";
var jsonObject = new JsonObject { ["id"] = RecordKey, ["HotelName"] = "Test Name" };
var mockItemResponse = new Mock<ItemResponse<JsonObject>>();
mockItemResponse
.Setup(l => l.Resource)
.Returns(jsonObject);
this._mockContainer
.Setup(l => l.ReadItemAsync<JsonObject>(
RecordKey,
new PartitionKey(RecordKey),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockItemResponse.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.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 jsonObject1 = new JsonObject { ["id"] = "key1", ["HotelName"] = "Test Name 1" };
var jsonObject2 = new JsonObject { ["id"] = "key2", ["HotelName"] = "Test Name 2" };
var jsonObject3 = new JsonObject { ["id"] = "key3", ["HotelName"] = "Test Name 3" };
var mockFeedResponse = new Mock<FeedResponse<JsonObject>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns([jsonObject1, jsonObject2, jsonObject3]);
this._mockContainer
.Setup(l => l.ReadManyItemsAsync<JsonObject>(
It.IsAny<IReadOnlyList<(string id, PartitionKey partitionKey)>>(),
It.IsAny<ReadManyRequestOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.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 CosmosNoSqlHotel("key") { HotelName = "Test Name" };
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.UpsertAsync(hotel);
// Assert
this._mockContainer.Verify(l => l.UpsertItemAsync<JsonNode>(
It.Is<JsonNode>(node =>
node["id"]!.ToString() == "key" &&
node["HotelName"]!.ToString() == "Test Name"),
new PartitionKey("key"),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Fact]
public async Task CanUpsertManyRecordsAsync()
{
// Arrange
var hotel1 = new CosmosNoSqlHotel("key1") { HotelName = "Test Name 1" };
var hotel2 = new CosmosNoSqlHotel("key2") { HotelName = "Test Name 2" };
var hotel3 = new CosmosNoSqlHotel("key3") { HotelName = "Test Name 3" };
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.UpsertAsync([hotel1, hotel2, hotel3]);
}
[Fact]
public async Task VectorizedSearchReturnsValidRecordAsync()
{
// Arrange
const string RecordKey = "key";
const double ExpectedScore = 0.99;
var jsonObject = new JsonObject
{
["id"] = RecordKey,
["HotelName"] = "Test Name",
["SimilarityScore"] = ExpectedScore
};
var mockFeedResponse = new Mock<FeedResponse<JsonObject>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns([jsonObject]);
var mockFeedIterator = new Mock<FeedIterator<JsonObject>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockContainer
.Setup(l => l.GetItemQueryIterator<JsonObject>(
It.IsAny<QueryDefinition>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
var results = await sut.SearchAsync(new ReadOnlyMemory<float>([1f, 2f, 3f]), top: 3).ToListAsync();
var result = results[0];
// Assert
Assert.NotNull(result);
Assert.Equal(RecordKey, result.Record.HotelId);
Assert.Equal("Test Name", result.Record.HotelName);
Assert.Equal(ExpectedScore, result.Score);
}
[Fact]
public async Task VectorizedSearchWithUnsupportedVectorTypeThrowsExceptionAsync()
{
// Arrange
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act & Assert
await Assert.ThrowsAsync<NotSupportedException>(async () =>
await sut.SearchAsync(new List<double>([1, 2, 3]), top: 3).ToListAsync());
}
[Fact]
public async Task VectorizedSearchWithNonExistentVectorPropertyNameThrowsExceptionAsync()
{
// Arrange
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
var searchOptions = new VectorSearchOptions<CosmosNoSqlHotel> { VectorProperty = r => "non-existent-property" };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await sut.SearchAsync(new ReadOnlyMemory<float>([1f, 2f, 3f]), top: 3, searchOptions).ToListAsync());
}
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 }
};
#region
private bool VerifyContainerProperties(ContainerProperties expected, ContainerProperties actual)
{
Assert.Equal(expected.Id, actual.Id);
Assert.Equal(expected.PartitionKeyPath, actual.PartitionKeyPath);
Assert.Equal(expected.IndexingPolicy.IndexingMode, actual.IndexingPolicy.IndexingMode);
Assert.Equal(expected.IndexingPolicy.Automatic, actual.IndexingPolicy.Automatic);
if (expected.IndexingPolicy.IndexingMode != IndexingMode.None)
{
for (var i = 0; i < expected.VectorEmbeddingPolicy.Embeddings.Count; i++)
{
var expectedEmbedding = expected.VectorEmbeddingPolicy.Embeddings[i];
var actualEmbedding = actual.VectorEmbeddingPolicy.Embeddings[i];
Assert.Equal(expectedEmbedding.DataType, actualEmbedding.DataType);
Assert.Equal(expectedEmbedding.Dimensions, actualEmbedding.Dimensions);
Assert.Equal(expectedEmbedding.DistanceFunction, actualEmbedding.DistanceFunction);
Assert.Equal(expectedEmbedding.Path, actualEmbedding.Path);
}
for (var i = 0; i < expected.IndexingPolicy.VectorIndexes.Count; i++)
{
var expectedIndexPath = expected.IndexingPolicy.VectorIndexes[i];
var actualIndexPath = actual.IndexingPolicy.VectorIndexes[i];
Assert.Equal(expectedIndexPath.Type, actualIndexPath.Type);
Assert.Equal(expectedIndexPath.Path, actualIndexPath.Path);
}
for (var i = 0; i < expected.IndexingPolicy.IncludedPaths.Count; i++)
{
var expectedIncludedPath = expected.IndexingPolicy.IncludedPaths[i].Path;
var actualIncludedPath = actual.IndexingPolicy.IncludedPaths[i].Path;
Assert.Equal(expectedIncludedPath, actualIncludedPath);
}
for (var i = 0; i < expected.IndexingPolicy.ExcludedPaths.Count; i++)
{
var expectedExcludedPath = expected.IndexingPolicy.ExcludedPaths[i].Path;
var actualExcludedPath = actual.IndexingPolicy.ExcludedPaths[i].Path;
Assert.Equal(expectedExcludedPath, actualExcludedPath);
}
}
return true;
}
#pragma warning disable CA1812
private sealed class TestModel
{
public string? Id { get; set; }
public string? HotelName { get; set; }
}
private sealed class TestIndexingModel
{
[VectorStoreKey]
public string? Id { get; set; }
[VectorStoreVector(Dimensions: 2, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Flat)]
public ReadOnlyMemory<float>? DescriptionEmbedding2 { get; set; }
[VectorStoreVector(Dimensions: 3, DistanceFunction = DistanceFunction.DotProductSimilarity, IndexKind = IndexKind.QuantizedFlat)]
public ReadOnlyMemory<byte>? DescriptionEmbedding3 { get; set; }
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.EuclideanDistance, IndexKind = IndexKind.DiskAnn)]
public ReadOnlyMemory<sbyte>? DescriptionEmbedding4 { get; set; }
[VectorStoreData(IsIndexed = true)]
public string? IndexableData1 { get; set; }
[VectorStoreData(IsFullTextIndexed = true)]
public string? IndexableData2 { get; set; }
[VectorStoreData]
public string? NonIndexableData1 { get; set; }
}
#pragma warning restore CA1812
[Fact]
public void ConstructorWithStringKeyInitializesCollection()
{
// String TKey should work; partition key auto-defaults to the key property.
using var collection = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithStringKeyAndExplicitPartitionKeyInitializesCollection()
{
// String TKey should work when partition key explicitly points to the key property.
using var collection = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection",
new() { PartitionKeyProperties = ["HotelId"] });
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithStringKeyRejectsNonKeyPartitionKey()
{
// String TKey requires the partition key to be the key property.
var exception = Assert.Throws<ArgumentException>(() => new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection",
new() { PartitionKeyProperties = ["HotelName"] }));
Assert.Contains("partition key must be the key property", exception.Message);
}
[Fact]
public void ConstructorWithStringKeyRejectsEmptyPartitionKey()
{
// String TKey cannot use empty partition key (PartitionKey.None) - that requires CosmosNoSqlKey.
var exception = Assert.Throws<ArgumentException>(() => new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection",
new() { PartitionKeyProperties = [] }));
Assert.Contains("partition key must be the key property", exception.Message);
}
[Fact]
public void ConstructorWithGuidKeyInitializesCollection()
{
// Guid TKey should work; partition key auto-defaults to the key property.
using var collection = new CosmosNoSqlCollection<Guid, GuidKeyModel>(
this._mockDatabase.Object,
"collection");
Assert.NotNull(collection);
}
[Fact]
public void ConstructorWithGuidKeyRejectsNonKeyPartitionKey()
{
// Guid TKey requires the partition key to be the key property.
var exception = Assert.Throws<ArgumentException>(() => new CosmosNoSqlCollection<Guid, GuidKeyModel>(
this._mockDatabase.Object,
"collection",
new() { PartitionKeyProperties = ["Name"] }));
Assert.Contains("partition key must be the key property", exception.Message);
}
[Fact]
public void ConstructorWithUnsupportedKeyTypeThrowsException()
{
var exception = Assert.Throws<NotSupportedException>(() => new CosmosNoSqlCollection<int, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection"));
Assert.Contains("string, Guid, or CosmosNoSqlKey", exception.Message);
}
[Fact]
public async Task DeleteWithStringKeyInvokesValidMethodsAsync()
{
// Arrange
const string RecordKey = "recordKey";
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
await sut.DeleteAsync(RecordKey);
// Assert: with string key, partition key = document id
this._mockContainer.Verify(l => l.DeleteItemAsync<JsonObject>(
RecordKey,
new PartitionKey(RecordKey),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()),
Times.Once());
}
[Fact]
public async Task GetWithStringKeyReturnsValidRecordAsync()
{
// Arrange
const string RecordKey = "key";
var jsonObject = new JsonObject { ["id"] = RecordKey, ["HotelName"] = "Test Name" };
var mockItemResponse = new Mock<ItemResponse<JsonObject>>();
mockItemResponse
.Setup(l => l.Resource)
.Returns(jsonObject);
this._mockContainer
.Setup(l => l.ReadItemAsync<JsonObject>(
RecordKey,
new PartitionKey(RecordKey),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(mockItemResponse.Object);
using var sut = new CosmosNoSqlCollection<string, CosmosNoSqlHotel>(
this._mockDatabase.Object,
"collection");
// Act
var result = await sut.GetAsync(RecordKey);
// Assert
Assert.NotNull(result);
Assert.Equal(RecordKey, result.HotelId);
Assert.Equal("Test Name", result.HotelName);
}
#pragma warning disable CA1812
private sealed class GuidKeyModel
{
[VectorStoreKey]
public Guid Id { get; set; }
[VectorStoreData]
public string? Name { get; set; }
}
#pragma warning restore CA1812
#endregion
}
@@ -0,0 +1,313 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.VectorData;
using Microsoft.Extensions.VectorData.ProviderServices;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Xunit;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="CosmosNoSqlDynamicMapper"/> class.
/// </summary>
public sealed class CosmosNoSqlDynamicMapperTests
{
private static readonly JsonSerializerOptions s_jsonSerializerOptions = JsonSerializerOptions.Default;
private static readonly CollectionModel s_model = new CosmosNoSqlModelBuilder()
.BuildDynamic(
new VectorStoreCollectionDefinition
{
Properties =
[
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("DateTimeOffsetDataProp", typeof(DateTimeOffset)),
new VectorStoreDataProperty("NullableDateTimeOffsetDataProp", typeof(DateTimeOffset?)),
new VectorStoreDataProperty("TagListDataProp", typeof(List<string>)),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
new VectorStoreVectorProperty("NullableFloatVector", typeof(ReadOnlyMemory<float>?), 10),
new VectorStoreVectorProperty("ByteVector", typeof(ReadOnlyMemory<byte>), 10),
new VectorStoreVectorProperty("NullableByteVector", typeof(ReadOnlyMemory<byte>?), 10),
new VectorStoreVectorProperty("SByteVector", typeof(ReadOnlyMemory<sbyte>), 10),
new VectorStoreVectorProperty("NullableSByteVector", typeof(ReadOnlyMemory<sbyte>?), 10),
],
},
defaultEmbeddingGenerator: null);
private static readonly float[] s_floatVector = [1.0f, 2.0f, 3.0f];
private static readonly byte[] s_byteVector = [1, 2, 3];
private static readonly sbyte[] s_sbyteVector = [1, 2, 3];
private static readonly List<string> s_taglist = ["tag1", "tag2"];
[Fact]
public void MapFromDataToStorageModelMapsAllSupportedTypes()
{
// Arrange
var sut = new CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
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,
["DateTimeOffsetDataProp"] = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero),
["NullableDateTimeOffsetDataProp"] = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero),
["TagListDataProp"] = s_taglist,
["FloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
["NullableFloatVector"] = new ReadOnlyMemory<float>(s_floatVector),
["ByteVector"] = new ReadOnlyMemory<byte>(s_byteVector),
["NullableByteVector"] = new ReadOnlyMemory<byte>(s_byteVector),
["SByteVector"] = new ReadOnlyMemory<sbyte>(s_sbyteVector),
["NullableSByteVector"] = new ReadOnlyMemory<sbyte>(s_sbyteVector)
};
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.Equal("key", (string?)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"]);
Assert.Equal(6.0f, (float?)storageModel["NullableFloatDataProp"]);
Assert.Equal(7.0, (double?)storageModel["DoubleDataProp"]);
Assert.Equal(8.0, (double?)storageModel["NullableDoubleDataProp"]);
Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), (DateTimeOffset?)storageModel["DateTimeOffsetDataProp"]);
Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), (DateTimeOffset?)storageModel["NullableDateTimeOffsetDataProp"]);
Assert.Equal(s_taglist, storageModel["TagListDataProp"]!.AsArray().GetValues<string>().ToArray());
Assert.Equal(s_floatVector, storageModel["FloatVector"]!.AsArray().GetValues<float>().ToArray());
Assert.Equal(s_floatVector, storageModel["NullableFloatVector"]!.AsArray().GetValues<float>().ToArray());
Assert.Equal(s_byteVector, storageModel["ByteVector"]!.AsArray().GetValues<byte>().ToArray());
Assert.Equal(s_byteVector, storageModel["NullableByteVector"]!.AsArray().GetValues<byte>().ToArray());
Assert.Equal(s_sbyteVector, storageModel["SByteVector"]!.AsArray().GetValues<sbyte>().ToArray());
Assert.Equal(s_sbyteVector, storageModel["NullableSByteVector"]!.AsArray().GetValues<sbyte>().ToArray());
}
[Fact]
public void MapFromDataToStorageModelMapsNullValues()
{
// Arrange
VectorStoreCollectionDefinition definition = new()
{
Properties =
[
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 CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.Null(storageModel["StringDataProp"]);
Assert.Null(storageModel["NullableIntDataProp"]);
Assert.Null(storageModel["NullableFloatVector"]);
}
[Fact]
public void MapFromStorageToDataModelMapsAllSupportedTypes()
{
// Arrange
var sut = new CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
var storageModel = new JsonObject
{
["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,
["DateTimeOffsetDataProp"] = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero),
["NullableDateTimeOffsetDataProp"] = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero),
["TagListDataProp"] = new JsonArray(s_taglist.Select(l => (JsonValue)l).ToArray()),
["FloatVector"] = new JsonArray(s_floatVector.Select(l => (JsonValue)l).ToArray()),
["NullableFloatVector"] = new JsonArray(s_floatVector.Select(l => (JsonValue)l).ToArray()),
["ByteVector"] = new JsonArray(s_byteVector.Select(l => (JsonValue)l).ToArray()),
["NullableByteVector"] = new JsonArray(s_byteVector.Select(l => (JsonValue)l).ToArray()),
["SByteVector"] = new JsonArray(s_sbyteVector.Select(l => (JsonValue)l).ToArray()),
["NullableSByteVector"] = new JsonArray(s_sbyteVector.Select(l => (JsonValue)l).ToArray())
};
// 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(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), dataModel["DateTimeOffsetDataProp"]);
Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), dataModel["NullableDateTimeOffsetDataProp"]);
Assert.Equal(s_taglist, dataModel["TagListDataProp"]);
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["FloatVector"]!).ToArray());
Assert.Equal(s_floatVector, ((ReadOnlyMemory<float>)dataModel["NullableFloatVector"]!)!.ToArray());
Assert.Equal(s_byteVector, ((ReadOnlyMemory<byte>)dataModel["ByteVector"]!).ToArray());
Assert.Equal(s_byteVector, ((ReadOnlyMemory<byte>)dataModel["NullableByteVector"]!)!.ToArray());
Assert.Equal(s_sbyteVector, ((ReadOnlyMemory<sbyte>)dataModel["SByteVector"]!).ToArray());
Assert.Equal(s_sbyteVector, ((ReadOnlyMemory<sbyte>)dataModel["NullableSByteVector"]!)!.ToArray());
}
[Fact]
public void MapFromStorageToDataModelMapsNullValues()
{
// Arrange
VectorStoreCollectionDefinition definition = new()
{
Properties =
[
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 JsonObject
{
["id"] = "key",
["StringDataProp"] = null,
["NullableIntDataProp"] = null,
["NullableFloatVector"] = null
};
var sut = new CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
// 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 CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
var storageModel = new JsonObject();
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(
() => sut.MapFromStorageToDataModel(storageModel, includeVectors: true));
}
[Fact]
public void MapFromDataToStorageModelSkipsMissingProperties()
{
// Arrange
VectorStoreCollectionDefinition definition = new()
{
Properties =
[
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 CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
// Act
var storageModel = sut.MapFromDataToStorageModel(dataModel, recordIndex: 0, generatedEmbeddings: null);
// Assert
Assert.Equal("key", (string?)storageModel["id"]);
Assert.False(storageModel.ContainsKey("StringDataProp"));
Assert.False(storageModel.ContainsKey("FloatVector"));
}
[Fact]
public void MapFromStorageToDataModelSkipsMissingProperties()
{
// Arrange
VectorStoreCollectionDefinition definition = new()
{
Properties =
[
new VectorStoreKeyProperty("Key", typeof(string)),
new VectorStoreDataProperty("StringDataProp", typeof(string)),
new VectorStoreVectorProperty("FloatVector", typeof(ReadOnlyMemory<float>), 10),
],
};
var storageModel = new JsonObject
{
["id"] = "key"
};
var sut = new CosmosNoSqlDynamicMapper(s_model, s_jsonSerializerOptions);
// Act
var dataModel = sut.MapFromStorageToDataModel(storageModel, includeVectors: true);
// Assert
Assert.Equal("key", dataModel["Key"]);
Assert.False(dataModel.ContainsKey("StringDataProp"));
Assert.False(dataModel.ContainsKey("FloatVector"));
}
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.Extensions.VectorData;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
public class CosmosNoSqlHotel(string hotelId)
{
/// <summary>The key of the record.</summary>
[VectorStoreKey]
public string HotelId { get; init; } = hotelId;
/// <summary>A string metadata field.</summary>
[VectorStoreData]
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>
[VectorStoreData(StorageName = "parking_is_included")]
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>
[JsonPropertyName("description_embedding")]
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Flat)]
public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }
}
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Xunit;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="CosmosNoSqlMapper{TRecord}"/> class.
/// </summary>
public sealed class CosmosNoSqlMapperTests
{
private readonly CosmosNoSqlMapper<CosmosNoSqlHotel> _sut
= new(
new CosmosNoSqlModelBuilder().BuildDynamic(
new()
{
Properties =
[
new VectorStoreKeyProperty("HotelId", typeof(string)),
new VectorStoreVectorProperty("TestProperty1", typeof(ReadOnlyMemory<float>), 10) { StorageName = "test_property_1" },
new VectorStoreDataProperty("TestProperty2", typeof(string)) { StorageName = "test_property_2" },
new VectorStoreDataProperty("TestProperty3", typeof(string)) { StorageName = "test_property_3" }
]
},
defaultEmbeddingGenerator: null,
JsonSerializerOptions.Default),
JsonSerializerOptions.Default);
[Fact]
public void MapFromDataToStorageModelReturnsValidObject()
{
// Arrange
var hotel = new CosmosNoSqlHotel("key")
{
HotelName = "Test Name",
Tags = ["tag1", "tag2"],
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"]!.GetValue<string>());
Assert.Equal("Test Name", document["HotelName"]!.GetValue<string>());
Assert.Equal(["tag1", "tag2"], document["Tags"]!.AsArray().Select(l => l!.GetValue<string>()));
Assert.Equal([1f, 2f, 3f], document["description_embedding"]!.AsArray().Select(l => l!.GetValue<float>()));
}
[Fact]
public void MapFromStorageToDataModelReturnsValidObject()
{
// Arrange
var document = new JsonObject
{
["id"] = "key",
["HotelName"] = "Test Name",
["Tags"] = new JsonArray(new List<string> { "tag1", "tag2" }.Select(l => JsonValue.Create(l)).ToArray()),
["description_embedding"] = new JsonArray(new List<float> { 1f, 2f, 3f }.Select(l => JsonValue.Create(l)).ToArray()),
};
// Act
var hotel = this._sut.MapFromStorageToDataModel(document, new());
// Assert
Assert.NotNull(hotel);
Assert.Equal("key", hotel.HotelId);
Assert.Equal("Test Name", hotel.HotelName);
Assert.Equal(["tag1", "tag2"], hotel.Tags);
Assert.True(new ReadOnlyMemory<float>([1f, 2f, 3f]).Span.SequenceEqual(hotel.DescriptionEmbedding!.Value.Span));
}
}
@@ -0,0 +1,143 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using Microsoft.SemanticKernel.Connectors.CosmosNoSql;
using Moq;
using Xunit;
namespace SemanticKernel.Connectors.CosmosNoSql.UnitTests;
/// <summary>
/// Unit tests for <see cref="Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore"/> class.
/// </summary>
public sealed class CosmosNoSqlVectorStoreTests
{
private readonly Mock<Database> _mockDatabase = new();
public CosmosNoSqlVectorStoreTests()
{
var mockClient = new Mock<CosmosClient>();
mockClient.Setup(l => l.ClientOptions).Returns(new CosmosClientOptions() { UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default });
this._mockDatabase
.Setup(l => l.Client)
.Returns(mockClient.Object);
}
[Fact]
public void GetCollectionWithNotSupportedKeyThrowsException()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act & Assert
Assert.Throws<NotSupportedException>(() => sut.GetCollection<byte[], CosmosNoSqlHotel>("collection"));
}
[Fact]
public void GetCollectionWithSupportedKeyReturnsCollection()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var collection = sut.GetCollection<CosmosNoSqlKey, CosmosNoSqlHotel>("collection1");
// Assert
Assert.NotNull(collection);
}
[Fact]
public void GetCollectionWithStringKeyReturnsCollection()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var collection = sut.GetCollection<string, CosmosNoSqlHotel>("collection1");
// Assert
Assert.NotNull(collection);
}
[Fact]
public void GetCollectionWithGuidKeyReturnsCollection()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var collection = sut.GetCollection<Guid, GuidKeyHotel>("collection1");
// Assert
Assert.NotNull(collection);
}
[Fact]
public void GetCollectionWithoutFactoryReturnsDefaultCollection()
{
// Arrange
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var collection = sut.GetCollection<CosmosNoSqlKey, CosmosNoSqlHotel>("collection");
// Assert
Assert.NotNull(collection);
}
[Fact]
public async Task ListCollectionNamesReturnsCollectionNamesAsync()
{
// Arrange
var expectedCollectionNames = new List<string> { "collection-1", "collection-2", "collection-3" };
var mockFeedResponse = new Mock<FeedResponse<string>>();
mockFeedResponse
.Setup(l => l.Resource)
.Returns(expectedCollectionNames);
var mockFeedIterator = new Mock<FeedIterator<string>>();
mockFeedIterator
.SetupSequence(l => l.HasMoreResults)
.Returns(true)
.Returns(false);
mockFeedIterator
.Setup(l => l.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(mockFeedResponse.Object);
this._mockDatabase
.Setup(l => l.GetContainerQueryIterator<string>(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(mockFeedIterator.Object);
using var sut = new Microsoft.SemanticKernel.Connectors.CosmosNoSql.CosmosNoSqlVectorStore(this._mockDatabase.Object);
// Act
var actualCollectionNames = await sut.ListCollectionNamesAsync().ToListAsync();
// Assert
Assert.Equal(expectedCollectionNames, actualCollectionNames);
}
#pragma warning disable CA1812
private sealed class GuidKeyHotel
{
[Microsoft.Extensions.VectorData.VectorStoreKey]
public Guid Id { get; set; }
[Microsoft.Extensions.VectorData.VectorStoreData]
public string? Name { get; set; }
}
#pragma warning restore CA1812
}