Files
microsoft--semantic-kernel/dotnet/test/VectorData/CosmosNoSql.UnitTests/CosmosNoSqlCollectionTests.cs
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

804 lines
28 KiB
C#

// 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
}