From c327a5d08dcb482a328c5c0e38fb25772f63d059 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 23 May 2026 18:19:35 +0000
Subject: [PATCH 01/33] Initial plan
From d921eca5a7a8eead60501596bff4777c5b03ffb4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 23 May 2026 18:24:15 +0000
Subject: [PATCH 02/33] Add Cosmos emulator integration test infrastructure and
scenario plan
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/1b70d529-777e-4c18-a4a2-953806949258
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
.../FSharp.Azure.Cosmos.Tests.fsproj | 2 +
.../Cosmos.Tests/IntegrationInfrastructure.fs | 101 ++++++++++++++++++
tests/Cosmos.Tests/IntegrationTestPlan.fs | 17 +++
3 files changed, 120 insertions(+)
create mode 100644 tests/Cosmos.Tests/IntegrationInfrastructure.fs
create mode 100644 tests/Cosmos.Tests/IntegrationTestPlan.fs
diff --git a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
index 9588d7d..847c599 100644
--- a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
+++ b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
@@ -17,6 +17,8 @@
+
+
diff --git a/tests/Cosmos.Tests/IntegrationInfrastructure.fs b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
new file mode 100644
index 0000000..ea4f770
--- /dev/null
+++ b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
@@ -0,0 +1,101 @@
+namespace Tests.Integration
+
+open System
+open System.Text.RegularExpressions
+open System.Threading
+open System.Threading.Tasks
+
+open Microsoft.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type TestBase () =
+
+ member val TestContext = Unchecked.defaultof with get, set
+
+ member this.CancellationToken = this.TestContext.CancellationTokenSource.Token
+
+type DatabaseTestApplicationFactory(testContext : TestContext) =
+ let endpoint = "https://127.0.0.1:8081"
+
+ let primaryKey =
+ "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
+
+ let buildDatabaseId () =
+ let className =
+ match testContext.FullyQualifiedTestClassName with
+ | null -> "CosmosTests"
+ | fullyQualifiedTestClassName when String.IsNullOrWhiteSpace fullyQualifiedTestClassName -> "CosmosTests"
+ | fullyQualifiedTestClassName -> fullyQualifiedTestClassName
+
+ let sanitizedClassName = Regex.Replace(className, "[^a-zA-Z0-9-_]", "-")
+ let shortClassName = sanitizedClassName[..(min 40 (sanitizedClassName.Length - 1))]
+ $"{shortClassName}-{Guid.NewGuid():N}"
+
+ let databaseId = buildDatabaseId ()
+ let client = new CosmosClient(endpoint, primaryKey, CosmosClientOptions(ConnectionMode = ConnectionMode.Gateway))
+ let mutable database = ValueNone
+
+ member _.Client = client
+ member _.DatabaseId = databaseId
+ member _.Database = database
+
+ member _.InitializeAsync(cancellationToken : CancellationToken) : Task =
+ task {
+ let! createdDatabase = client.CreateDatabaseIfNotExistsAsync(databaseId, cancellationToken = cancellationToken)
+ database <- ValueSome createdDatabase.Database
+ }
+
+ member _.CleanupAsync(cancellationToken : CancellationToken) : Task =
+ task {
+ match database with
+ | ValueNone -> ()
+ | ValueSome existingDatabase ->
+ let! _ = existingDatabase.DeleteAsync(cancellationToken = cancellationToken)
+ database <- ValueNone
+ }
+
+ abstract SeedDataAsync : CancellationToken -> Task
+ default _.SeedDataAsync(_ : CancellationToken) = Task.CompletedTask
+
+ interface IAsyncDisposable with
+ member this.DisposeAsync() =
+ task {
+ do! this.CleanupAsync(CancellationToken.None)
+ client.Dispose()
+ }
+ |> ValueTask
+
+[]
+type IntegrationTestBase () =
+ inherit TestBase()
+
+ []
+ val mutable private application : DatabaseTestApplicationFactory voption
+
+ member this.Application =
+ match this.application with
+ | ValueNone -> invalidOp "Integration test application is not initialized."
+ | ValueSome application -> application
+
+ abstract CreateApplication : TestContext -> DatabaseTestApplicationFactory
+ default _.CreateApplication context = DatabaseTestApplicationFactory(context)
+
+ []
+ member this.Initialize() : Task =
+ task {
+ let application = this.CreateApplication(this.TestContext)
+ this.application <- ValueSome application
+ do! application.InitializeAsync(this.CancellationToken)
+ do! application.SeedDataAsync(this.CancellationToken)
+ }
+
+ []
+ member this.Cleanup() : Task =
+ task {
+ match this.application with
+ | ValueNone -> ()
+ | ValueSome application ->
+ do! (application :> IAsyncDisposable).DisposeAsync().AsTask()
+ this.application <- ValueNone
+ }
diff --git a/tests/Cosmos.Tests/IntegrationTestPlan.fs b/tests/Cosmos.Tests/IntegrationTestPlan.fs
new file mode 100644
index 0000000..5b0c76b
--- /dev/null
+++ b/tests/Cosmos.Tests/IntegrationTestPlan.fs
@@ -0,0 +1,17 @@
+namespace Tests.Integration
+
+module IntegrationTestPlan =
+
+ let PlannedScenarios : string array =
+ [|
+ "Create item: validates id/partition key persistence and response diagnostics."
+ "Read item: validates successful reads and not-found responses."
+ "Upsert item: validates insert-then-update behavior and returned resource state."
+ "Replace item: validates optimistic concurrency with ETag preconditions."
+ "Patch item: validates additive and replace patch operations for partial updates."
+ "Delete item: validates deletion and subsequent not-found behavior."
+ "Read many: validates batch read behavior across partitions."
+ "Iterator/taskSeq reads: validates paging and continuation behavior."
+ "Unique key constraints: validates duplicate write failures."
+ "Seeded data scenarios: empty container, single partition set, multi-partition set."
+ |]
From 527ea3689ef1c1529bc58e6f490866e0007a9562 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 23 May 2026 18:28:31 +0000
Subject: [PATCH 03/33] Refine Cosmos test infrastructure and planned scenario
list
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/1b70d529-777e-4c18-a4a2-953806949258
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
.../Cosmos.Tests/IntegrationInfrastructure.fs | 35 ++++++++++++++-----
tests/Cosmos.Tests/IntegrationTestPlan.fs | 4 +--
2 files changed, 28 insertions(+), 11 deletions(-)
diff --git a/tests/Cosmos.Tests/IntegrationInfrastructure.fs b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
index ea4f770..a871456 100644
--- a/tests/Cosmos.Tests/IntegrationInfrastructure.fs
+++ b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
@@ -16,20 +16,38 @@ type TestBase () =
member this.CancellationToken = this.TestContext.CancellationTokenSource.Token
type DatabaseTestApplicationFactory(testContext : TestContext) =
+ []
+ let defaultClassName = "CosmosTests"
+
+ []
let endpoint = "https://127.0.0.1:8081"
+ []
let primaryKey =
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
+ []
+ let maxClassNameLength = 40
+
let buildDatabaseId () =
let className =
match testContext.FullyQualifiedTestClassName with
- | null -> "CosmosTests"
- | fullyQualifiedTestClassName when String.IsNullOrWhiteSpace fullyQualifiedTestClassName -> "CosmosTests"
- | fullyQualifiedTestClassName -> fullyQualifiedTestClassName
+ | null -> defaultClassName
+ | fullyQualifiedTestClassName ->
+ if fullyQualifiedTestClassName.Trim().Length = 0 then
+ defaultClassName
+ else
+ fullyQualifiedTestClassName
let sanitizedClassName = Regex.Replace(className, "[^a-zA-Z0-9-_]", "-")
- let shortClassName = sanitizedClassName[..(min 40 (sanitizedClassName.Length - 1))]
+ let validClassName =
+ if String.IsNullOrWhiteSpace sanitizedClassName then
+ defaultClassName
+ else
+ sanitizedClassName
+
+ let maxAllowedIndex = min (maxClassNameLength - 1) (validClassName.Length - 1)
+ let shortClassName = validClassName[..maxAllowedIndex]
$"{shortClassName}-{Guid.NewGuid():N}"
let databaseId = buildDatabaseId ()
@@ -55,8 +73,8 @@ type DatabaseTestApplicationFactory(testContext : TestContext) =
database <- ValueNone
}
- abstract SeedDataAsync : CancellationToken -> Task
- default _.SeedDataAsync(_ : CancellationToken) = Task.CompletedTask
+ abstract SeedDataAsync : cancellationToken : CancellationToken -> Task
+ default _.SeedDataAsync(cancellationToken : CancellationToken) = Task.CompletedTask
interface IAsyncDisposable with
member this.DisposeAsync() =
@@ -70,12 +88,11 @@ type DatabaseTestApplicationFactory(testContext : TestContext) =
type IntegrationTestBase () =
inherit TestBase()
- []
- val mutable private application : DatabaseTestApplicationFactory voption
+ member val private application : DatabaseTestApplicationFactory voption = ValueNone with get, set
member this.Application =
match this.application with
- | ValueNone -> invalidOp "Integration test application is not initialized."
+ | ValueNone -> invalidOp "Application not initialized. Ensure test runs within TestInitialize/TestCleanup lifecycle."
| ValueSome application -> application
abstract CreateApplication : TestContext -> DatabaseTestApplicationFactory
diff --git a/tests/Cosmos.Tests/IntegrationTestPlan.fs b/tests/Cosmos.Tests/IntegrationTestPlan.fs
index 5b0c76b..3ca1f00 100644
--- a/tests/Cosmos.Tests/IntegrationTestPlan.fs
+++ b/tests/Cosmos.Tests/IntegrationTestPlan.fs
@@ -2,7 +2,7 @@ namespace Tests.Integration
module IntegrationTestPlan =
- let PlannedScenarios : string array =
+ let plannedScenarios : string array =
[|
"Create item: validates id/partition key persistence and response diagnostics."
"Read item: validates successful reads and not-found responses."
@@ -11,7 +11,7 @@ module IntegrationTestPlan =
"Patch item: validates additive and replace patch operations for partial updates."
"Delete item: validates deletion and subsequent not-found behavior."
"Read many: validates batch read behavior across partitions."
- "Iterator/taskSeq reads: validates paging and continuation behavior."
+ "Iterator/TaskSeq reads: validates paging and continuation behavior."
"Unique key constraints: validates duplicate write failures."
"Seeded data scenarios: empty container, single partition set, multi-partition set."
|]
From f98ebe51ea21211c6d1aff5767625d476f384f7a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 23 May 2026 22:30:30 +0000
Subject: [PATCH 04/33] Remove DB name normalization and format integration
files
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/6a9fbc0d-9c2b-4b57-ac0e-308e2d1edeae
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
.../Cosmos.Tests/IntegrationInfrastructure.fs | 88 ++++++++-----------
tests/Cosmos.Tests/IntegrationTestPlan.fs | 25 +++---
2 files changed, 48 insertions(+), 65 deletions(-)
diff --git a/tests/Cosmos.Tests/IntegrationInfrastructure.fs b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
index a871456..c9374c7 100644
--- a/tests/Cosmos.Tests/IntegrationInfrastructure.fs
+++ b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
@@ -1,7 +1,6 @@
namespace Tests.Integration
open System
-open System.Text.RegularExpressions
open System.Threading
open System.Threading.Tasks
@@ -15,7 +14,7 @@ type TestBase () =
member this.CancellationToken = this.TestContext.CancellationTokenSource.Token
-type DatabaseTestApplicationFactory(testContext : TestContext) =
+type DatabaseTestApplicationFactory (testContext : TestContext) =
[]
let defaultClassName = "CosmosTests"
@@ -26,11 +25,8 @@ type DatabaseTestApplicationFactory(testContext : TestContext) =
let primaryKey =
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
- []
- let maxClassNameLength = 40
-
let buildDatabaseId () =
- let className =
+ let testClassName =
match testContext.FullyQualifiedTestClassName with
| null -> defaultClassName
| fullyQualifiedTestClassName ->
@@ -39,54 +35,44 @@ type DatabaseTestApplicationFactory(testContext : TestContext) =
else
fullyQualifiedTestClassName
- let sanitizedClassName = Regex.Replace(className, "[^a-zA-Z0-9-_]", "-")
- let validClassName =
- if String.IsNullOrWhiteSpace sanitizedClassName then
- defaultClassName
- else
- sanitizedClassName
-
- let maxAllowedIndex = min (maxClassNameLength - 1) (validClassName.Length - 1)
- let shortClassName = validClassName[..maxAllowedIndex]
- $"{shortClassName}-{Guid.NewGuid():N}"
+ $"{testClassName}-{Guid.NewGuid ():N}"
let databaseId = buildDatabaseId ()
- let client = new CosmosClient(endpoint, primaryKey, CosmosClientOptions(ConnectionMode = ConnectionMode.Gateway))
+ let client =
+ new CosmosClient (endpoint, primaryKey, CosmosClientOptions (ConnectionMode = ConnectionMode.Gateway))
let mutable database = ValueNone
member _.Client = client
member _.DatabaseId = databaseId
member _.Database = database
- member _.InitializeAsync(cancellationToken : CancellationToken) : Task =
- task {
- let! createdDatabase = client.CreateDatabaseIfNotExistsAsync(databaseId, cancellationToken = cancellationToken)
- database <- ValueSome createdDatabase.Database
- }
-
- member _.CleanupAsync(cancellationToken : CancellationToken) : Task =
- task {
- match database with
- | ValueNone -> ()
- | ValueSome existingDatabase ->
- let! _ = existingDatabase.DeleteAsync(cancellationToken = cancellationToken)
- database <- ValueNone
- }
+ member _.InitializeAsync (cancellationToken : CancellationToken) : Task = task {
+ let! createdDatabase = client.CreateDatabaseIfNotExistsAsync (databaseId, cancellationToken = cancellationToken)
+ database <- ValueSome createdDatabase.Database
+ }
+
+ member _.CleanupAsync (cancellationToken : CancellationToken) : Task = task {
+ match database with
+ | ValueNone -> ()
+ | ValueSome existingDatabase ->
+ let! _ = existingDatabase.DeleteAsync (cancellationToken = cancellationToken)
+ database <- ValueNone
+ }
abstract SeedDataAsync : cancellationToken : CancellationToken -> Task
- default _.SeedDataAsync(cancellationToken : CancellationToken) = Task.CompletedTask
+ default _.SeedDataAsync (cancellationToken : CancellationToken) = Task.CompletedTask
interface IAsyncDisposable with
- member this.DisposeAsync() =
+ member this.DisposeAsync () =
task {
- do! this.CleanupAsync(CancellationToken.None)
- client.Dispose()
+ do! this.CleanupAsync (CancellationToken.None)
+ client.Dispose ()
}
|> ValueTask
[]
type IntegrationTestBase () =
- inherit TestBase()
+ inherit TestBase ()
member val private application : DatabaseTestApplicationFactory voption = ValueNone with get, set
@@ -96,23 +82,21 @@ type IntegrationTestBase () =
| ValueSome application -> application
abstract CreateApplication : TestContext -> DatabaseTestApplicationFactory
- default _.CreateApplication context = DatabaseTestApplicationFactory(context)
+ default _.CreateApplication context = DatabaseTestApplicationFactory (context)
[]
- member this.Initialize() : Task =
- task {
- let application = this.CreateApplication(this.TestContext)
- this.application <- ValueSome application
- do! application.InitializeAsync(this.CancellationToken)
- do! application.SeedDataAsync(this.CancellationToken)
- }
+ member this.Initialize () : Task = task {
+ let application = this.CreateApplication (this.TestContext)
+ this.application <- ValueSome application
+ do! application.InitializeAsync (this.CancellationToken)
+ do! application.SeedDataAsync (this.CancellationToken)
+ }
[]
- member this.Cleanup() : Task =
- task {
- match this.application with
- | ValueNone -> ()
- | ValueSome application ->
- do! (application :> IAsyncDisposable).DisposeAsync().AsTask()
- this.application <- ValueNone
- }
+ member this.Cleanup () : Task = task {
+ match this.application with
+ | ValueNone -> ()
+ | ValueSome application ->
+ do! (application :> IAsyncDisposable).DisposeAsync().AsTask ()
+ this.application <- ValueNone
+ }
diff --git a/tests/Cosmos.Tests/IntegrationTestPlan.fs b/tests/Cosmos.Tests/IntegrationTestPlan.fs
index 3ca1f00..d74b934 100644
--- a/tests/Cosmos.Tests/IntegrationTestPlan.fs
+++ b/tests/Cosmos.Tests/IntegrationTestPlan.fs
@@ -2,16 +2,15 @@ namespace Tests.Integration
module IntegrationTestPlan =
- let plannedScenarios : string array =
- [|
- "Create item: validates id/partition key persistence and response diagnostics."
- "Read item: validates successful reads and not-found responses."
- "Upsert item: validates insert-then-update behavior and returned resource state."
- "Replace item: validates optimistic concurrency with ETag preconditions."
- "Patch item: validates additive and replace patch operations for partial updates."
- "Delete item: validates deletion and subsequent not-found behavior."
- "Read many: validates batch read behavior across partitions."
- "Iterator/TaskSeq reads: validates paging and continuation behavior."
- "Unique key constraints: validates duplicate write failures."
- "Seeded data scenarios: empty container, single partition set, multi-partition set."
- |]
+ let plannedScenarios : string array = [|
+ "Create item: validates id/partition key persistence and response diagnostics."
+ "Read item: validates successful reads and not-found responses."
+ "Upsert item: validates insert-then-update behavior and returned resource state."
+ "Replace item: validates optimistic concurrency with ETag preconditions."
+ "Patch item: validates additive and replace patch operations for partial updates."
+ "Delete item: validates deletion and subsequent not-found behavior."
+ "Read many: validates batch read behavior across partitions."
+ "Iterator/TaskSeq reads: validates paging and continuation behavior."
+ "Unique key constraints: validates duplicate write failures."
+ "Seeded data scenarios: empty container, single partition set, multi-partition set."
+ |]
From 67c9905b8d03a8f6b0fb3880ef4e64e729eee7ec Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 00:14:57 +0000
Subject: [PATCH 05/33] Use TestContext-based database identifier for
integration tests
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/ed2b3897-981f-4bb6-afae-f9e0089c252a
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
.../Cosmos.Tests/IntegrationInfrastructure.fs | 40 ++++++++++++-------
1 file changed, 26 insertions(+), 14 deletions(-)
diff --git a/tests/Cosmos.Tests/IntegrationInfrastructure.fs b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
index c9374c7..9e541f1 100644
--- a/tests/Cosmos.Tests/IntegrationInfrastructure.fs
+++ b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
@@ -7,6 +7,31 @@ open System.Threading.Tasks
open Microsoft.Azure.Cosmos
open Microsoft.VisualStudio.TestTools.UnitTesting
+[]
+module TestContextExtensions =
+
+ type TestContext with
+
+ member ctx.GetTestDatabaseIdentifier () =
+ match ctx.TestData with
+ | null -> ctx.TestName
+ | testData ->
+ let dataHash =
+ testData
+ |> Array.fold
+ (fun acc item ->
+ let itemHash =
+ match item with
+ | null -> 0
+ | item -> item.GetHashCode ()
+
+ HashCode.Combine (acc, itemHash)
+ )
+ 0
+ |> abs
+
+ $"{ctx.TestName}_{dataHash}"
+
[]
type TestBase () =
@@ -15,9 +40,6 @@ type TestBase () =
member this.CancellationToken = this.TestContext.CancellationTokenSource.Token
type DatabaseTestApplicationFactory (testContext : TestContext) =
- []
- let defaultClassName = "CosmosTests"
-
[]
let endpoint = "https://127.0.0.1:8081"
@@ -25,17 +47,7 @@ type DatabaseTestApplicationFactory (testContext : TestContext) =
let primaryKey =
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
- let buildDatabaseId () =
- let testClassName =
- match testContext.FullyQualifiedTestClassName with
- | null -> defaultClassName
- | fullyQualifiedTestClassName ->
- if fullyQualifiedTestClassName.Trim().Length = 0 then
- defaultClassName
- else
- fullyQualifiedTestClassName
-
- $"{testClassName}-{Guid.NewGuid ():N}"
+ let buildDatabaseId () = testContext.GetTestDatabaseIdentifier ()
let databaseId = buildDatabaseId ()
let client =
From 44baaefa566335aef741bb21da99e2a2621cb436 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 00:15:43 +0000
Subject: [PATCH 06/33] Harden test data hash conversion for database
identifier
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/ed2b3897-981f-4bb6-afae-f9e0089c252a
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/IntegrationInfrastructure.fs | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/Cosmos.Tests/IntegrationInfrastructure.fs b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
index 9e541f1..099a6c1 100644
--- a/tests/Cosmos.Tests/IntegrationInfrastructure.fs
+++ b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
@@ -28,6 +28,7 @@ module TestContextExtensions =
HashCode.Combine (acc, itemHash)
)
0
+ |> int64
|> abs
$"{ctx.TestName}_{dataHash}"
From e177988dae431f68a25f0953e585ba47db43756b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 01:00:34 +0000
Subject: [PATCH 07/33] Add integration tests for
create/read/upsert/replace/patch/delete/readmany operations
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/c46a3cd3-c537-46ff-a93f-15d1ad4d611d
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/Tests.fs | 299 +++++++++++++++++++++++++++++++++++-
1 file changed, 296 insertions(+), 3 deletions(-)
diff --git a/tests/Cosmos.Tests/Tests.fs b/tests/Cosmos.Tests/Tests.fs
index 7afcb8b..83f9f0a 100644
--- a/tests/Cosmos.Tests/Tests.fs
+++ b/tests/Cosmos.Tests/Tests.fs
@@ -1,10 +1,303 @@
-namespace Tests
+namespace Tests.Integration
open System
+open System.Net
+open System.Threading.Tasks
+open FSharp.Azure.Cosmos
+open Microsoft.Azure.Cosmos
open Microsoft.VisualStudio.TestTools.UnitTesting
+type private TestItem = { id : string; partitionKey : string; name : string; quantity : int }
+
[]
-type TestClass () =
+type OperationIntegrationTests () =
+ inherit IntegrationTestBase ()
+
+ []
+ let containerId = "operation-tests"
+
+ member private this.GetContainerAsync () : Task = task {
+ let database =
+ match this.Application.Database with
+ | ValueSome database -> database
+ | ValueNone -> invalidOp "Database is not initialized."
+
+ let! containerResponse =
+ database.CreateContainerIfNotExistsAsync (
+ ContainerProperties (containerId, "/partitionKey"),
+ cancellationToken = this.CancellationToken
+ )
+
+ return containerResponse.Container
+ }
+
+ member private this.NewItem (suffix : string) : TestItem = {
+ id = $"{this.TestContext.TestName}-{suffix}"
+ partitionKey = "integration"
+ name = $"item-{suffix}"
+ quantity = 1
+ }
+
+ []
+ member this.Create_execute_returns_created_resource () : Task = task {
+ let! container = this.GetContainerAsync ()
+ let testItem = this.NewItem "create"
+
+ let! response =
+ container.ExecuteAsync (
+ createAndRead {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match response.Result with
+ | CreateResult.Ok created ->
+ Assert.IsTrue (testItem.id = created.id, "Create should persist the item id.")
+ Assert.IsTrue (testItem.partitionKey = created.partitionKey, "Create should persist the item partition key.")
+ Assert.IsFalse (String.IsNullOrWhiteSpace response.ActivityId, "Create should return a valid activity id.")
+ Assert.IsTrue (response.RequestCharge > 0.0, "Create should report positive request charge.")
+ | result -> Assert.Fail ($"Expected create success but received {result}.")
+ }
+
+ []
+ member this.Read_execute_returns_existing_and_not_found_states () : Task = task {
+ let! container = this.GetContainerAsync ()
+ let testItem = this.NewItem "read"
+
+ let! _ =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let! foundResponse =
+ container.ExecuteAsync (
+ read {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match foundResponse.Result with
+ | ReadResult.Ok found ->
+ Assert.IsTrue (testItem.id = found.id, "Read should return the item that was created.")
+ Assert.IsTrue (foundResponse.HttpStatusCode = HttpStatusCode.OK, "Read success should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected successful read but received {result}.")
+
+ let missingId = $"{testItem.id}-missing"
+
+ let! missingResponse =
+ container.ExecuteAsync (
+ read {
+ id missingId
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match missingResponse.Result with
+ | ReadResult.NotFound _ ->
+ Assert.IsTrue (
+ missingResponse.HttpStatusCode = HttpStatusCode.NotFound,
+ "Read of missing item should return HTTP 404."
+ )
+ | result -> Assert.Fail ($"Expected read not found for missing item but received {result}.")
+ }
[]
- member this.TestMethodPassing () = Assert.IsTrue (true)
+ member this.Upsert_execute_overwrite_creates_then_updates_item () : Task = task {
+ let! container = this.GetContainerAsync ()
+ let testItem = this.NewItem "upsert"
+
+ let! createdResponse =
+ container.ExecuteOverwriteAsync (
+ upsertAndRead {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match createdResponse.Result with
+ | UpsertResult.Ok created ->
+ Assert.IsTrue (testItem.name = created.name, "Upsert create should store initial payload.")
+ Assert.IsTrue (createdResponse.HttpStatusCode = HttpStatusCode.Created, "Initial upsert should create item.")
+ | result -> Assert.Fail ($"Expected upsert create success but received {result}.")
+
+ let updated = { testItem with name = "item-upsert-updated"; quantity = 5 }
+
+ let! updatedResponse =
+ container.ExecuteOverwriteAsync (
+ upsertAndRead {
+ item updated
+ partitionKey updated.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match updatedResponse.Result with
+ | UpsertResult.Ok upserted ->
+ Assert.IsTrue (updated.name = upserted.name, "Upsert update should persist new payload.")
+ Assert.IsTrue (updated.quantity = upserted.quantity, "Upsert update should persist new quantity.")
+ Assert.IsTrue (updatedResponse.HttpStatusCode = HttpStatusCode.OK, "Second upsert should replace existing item.")
+ | result -> Assert.Fail ($"Expected upsert update success but received {result}.")
+ }
+
+ []
+ member this.Replace_execute_overwrite_replaces_existing_item () : Task = task {
+ let! container = this.GetContainerAsync ()
+ let testItem = this.NewItem "replace"
+
+ let! _ =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let replacement = { testItem with name = "item-replaced"; quantity = 3 }
+
+ let! replaceResponse =
+ container.ExecuteOverwriteAsync (
+ replaceAndRead {
+ id replacement.id
+ item replacement
+ partitionKey replacement.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match replaceResponse.Result with
+ | ReplaceResult.Ok replaced ->
+ Assert.IsTrue (replacement.name = replaced.name, "Replace should persist replacement name.")
+ Assert.IsTrue (replacement.quantity = replaced.quantity, "Replace should persist replacement quantity.")
+ Assert.IsTrue (replaceResponse.HttpStatusCode = HttpStatusCode.OK, "Replace should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected replace success but received {result}.")
+ }
+
+ []
+ member this.Patch_execute_overwrite_updates_targeted_field () : Task = task {
+ let! container = this.GetContainerAsync ()
+ let testItem = this.NewItem "patch"
+
+ let! _ =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let! patchResponse =
+ container.ExecuteOverwriteAsync (
+ patchAndRead {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ operation (Microsoft.Azure.Cosmos.PatchOperation.Replace ("/name", "item-patched"))
+ operation (Microsoft.Azure.Cosmos.PatchOperation.Replace ("/quantity", 9))
+ },
+ this.CancellationToken
+ )
+
+ match patchResponse.Result with
+ | PatchResult.Ok patched ->
+ Assert.IsTrue ("item-patched" = patched.name, "Patch should update the name field.")
+ Assert.IsTrue (9 = patched.quantity, "Patch should update the quantity field.")
+ Assert.IsTrue (patchResponse.HttpStatusCode = HttpStatusCode.OK, "Patch should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected patch success but received {result}.")
+ }
+
+ []
+ member this.Delete_execute_removes_item_and_subsequent_read_is_not_found () : Task = task {
+ let! container = this.GetContainerAsync ()
+ let testItem = this.NewItem "delete"
+
+ let! _ =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let! deleteResponse =
+ container.ExecuteAsync (
+ delete {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match deleteResponse.Result with
+ | DeleteResult.Ok _ ->
+ Assert.IsTrue (deleteResponse.HttpStatusCode = HttpStatusCode.NoContent, "Delete should return HTTP 204.")
+ | result -> Assert.Fail ($"Expected delete success but received {result}.")
+
+ let! missingResponse =
+ container.ExecuteAsync (
+ read {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match missingResponse.Result with
+ | ReadResult.NotFound _ ->
+ Assert.IsTrue (missingResponse.HttpStatusCode = HttpStatusCode.NotFound, "Read after delete should return HTTP 404.")
+ | result -> Assert.Fail ($"Expected read not found after delete but received {result}.")
+ }
+
+ []
+ member this.ReadMany_execute_returns_matching_items () : Task = task {
+ let! container = this.GetContainerAsync ()
+ let firstItem = this.NewItem "readmany-1"
+ let secondItem = this.NewItem "readmany-2"
+
+ let! _ =
+ container.ExecuteAsync (
+ create {
+ item firstItem
+ partitionKey firstItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let! _ =
+ container.ExecuteAsync (
+ create {
+ item secondItem
+ partitionKey secondItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let! readManyResponse =
+ container.ExecuteAsync (
+ readMany {
+ item firstItem.id firstItem.partitionKey
+ item secondItem.id secondItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match readManyResponse.Result with
+ | ReadManyResult.Ok (feed : FeedResponse) ->
+ let returnedIds = feed |> Seq.map _.id |> Set.ofSeq
+ Assert.IsTrue (feed.Count = 2, "ReadMany should return the requested number of existing items.")
+ Assert.IsTrue (returnedIds.Contains firstItem.id, "ReadMany should include first requested item.")
+ Assert.IsTrue (returnedIds.Contains secondItem.id, "ReadMany should include second requested item.")
+ Assert.IsTrue (readManyResponse.HttpStatusCode = HttpStatusCode.OK, "ReadMany should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected read many success but received {result}.")
+ }
From 7d70871929efefe4c3cd2b6fa992068f32129eb3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 01:01:46 +0000
Subject: [PATCH 08/33] Address integration test review feedback in patch
scenario
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/c46a3cd3-c537-46ff-a93f-15d1ad4d611d
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/Tests.fs | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/tests/Cosmos.Tests/Tests.fs b/tests/Cosmos.Tests/Tests.fs
index 83f9f0a..26f1bc3 100644
--- a/tests/Cosmos.Tests/Tests.fs
+++ b/tests/Cosmos.Tests/Tests.fs
@@ -13,7 +13,6 @@ type private TestItem = { id : string; partitionKey : string; name : string; qua
type OperationIntegrationTests () =
inherit IntegrationTestBase ()
- []
let containerId = "operation-tests"
member private this.GetContainerAsync () : Task = task {
@@ -187,6 +186,8 @@ type OperationIntegrationTests () =
member this.Patch_execute_overwrite_updates_targeted_field () : Task = task {
let! container = this.GetContainerAsync ()
let testItem = this.NewItem "patch"
+ let patchedName = "item-patched"
+ let patchedQuantity = 9
let! _ =
container.ExecuteAsync (
@@ -202,16 +203,18 @@ type OperationIntegrationTests () =
patchAndRead {
id testItem.id
partitionKey testItem.partitionKey
- operation (Microsoft.Azure.Cosmos.PatchOperation.Replace ("/name", "item-patched"))
- operation (Microsoft.Azure.Cosmos.PatchOperation.Replace ("/quantity", 9))
+ operation (Microsoft.Azure.Cosmos.PatchOperation.Replace ("/name", patchedName))
+ operation (Microsoft.Azure.Cosmos.PatchOperation.Replace ("/quantity", patchedQuantity))
},
this.CancellationToken
)
match patchResponse.Result with
| PatchResult.Ok patched ->
- Assert.IsTrue ("item-patched" = patched.name, "Patch should update the name field.")
- Assert.IsTrue (9 = patched.quantity, "Patch should update the quantity field.")
+ let isNamePatched = patchedName = patched.name
+ let isQuantityPatched = patchedQuantity = patched.quantity
+ Assert.IsTrue (isNamePatched, "Patch should update the name field.")
+ Assert.IsTrue (isQuantityPatched, "Patch should update the quantity field.")
Assert.IsTrue (patchResponse.HttpStatusCode = HttpStatusCode.OK, "Patch should return HTTP 200.")
| result -> Assert.Fail ($"Expected patch success but received {result}.")
}
From 660a0cc0db7aafb0e2fc986f997cd272b0f9cbee Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 01:15:19 +0000
Subject: [PATCH 09/33] Add CosmosAssert helper and simplify integration test
assertions
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/727cc9a9-e850-45de-b65f-4e8eb8ff33fc
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/CosmosAssert.fs | 90 +++++++++++++++++++
.../FSharp.Azure.Cosmos.Tests.fsproj | 1 +
tests/Cosmos.Tests/Tests.fs | 70 +++++++--------
3 files changed, 122 insertions(+), 39 deletions(-)
create mode 100644 tests/Cosmos.Tests/CosmosAssert.fs
diff --git a/tests/Cosmos.Tests/CosmosAssert.fs b/tests/Cosmos.Tests/CosmosAssert.fs
new file mode 100644
index 0000000..5b7d2ac
--- /dev/null
+++ b/tests/Cosmos.Tests/CosmosAssert.fs
@@ -0,0 +1,90 @@
+namespace Tests.Integration
+
+open System.Diagnostics
+open System.Net
+open System.Runtime.InteropServices
+open FSharp.Azure.Cosmos.Create
+open FSharp.Azure.Cosmos.Delete
+open FSharp.Azure.Cosmos.Read
+open FSharp.Azure.Cosmos.Replace
+open Microsoft.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type CosmosAssert private () =
+
+ static member WantOk<'T> (response : ItemResponse<'T>, [] message) =
+ match response.StatusCode with
+ | HttpStatusCode.OK
+ | HttpStatusCode.Created -> response.Resource
+ | _ ->
+ Assert.Fail (message)
+ Unchecked.defaultof<_>
+
+ static member IsOk (response : ItemResponse<'T>, [] message) = CosmosAssert.WantOk (response, message) |> ignore
+
+ static member WantOk<'T> (result : CreateResult<'T>, [] message) =
+ match result with
+ | CreateResult.Ok ok -> ok
+ | _ ->
+ Assert.Fail ($"Expected CreateResult.Ok, but {result} given. {message}")
+ Unchecked.defaultof<_>
+
+ static member IsOk (result : CreateResult<'T>, [] message) = CosmosAssert.WantOk (result, message) |> ignore
+
+ static member WantOk<'T> (result : ReadResult<'T>, [] message) =
+ match result with
+ | ReadResult.Ok ok -> ok
+ | _ ->
+ Assert.Fail (message)
+ Unchecked.defaultof<_>
+
+ static member IsOk (result : ReadResult<'T>, [] message) = CosmosAssert.WantOk (result, message) |> ignore
+
+ static member WantOk<'T> (result : ReplaceResult<'T>, [] message) =
+ match result with
+ | ReplaceResult.Ok ok -> ok
+ | _ ->
+ Assert.Fail (message)
+ Unchecked.defaultof<_>
+
+ static member IsOk (result : ReplaceResult<'T>, [] message) = CosmosAssert.WantOk (result, message) |> ignore
+
+ static member WantOk<'T> (result : DeleteResult<'T>, [] message) =
+ match result with
+ | DeleteResult.Ok ok -> ok
+ | _ ->
+ Assert.Fail (message)
+ Unchecked.defaultof<_>
+
+ static member IsOk (result : DeleteResult<'T>, [] message) = CosmosAssert.WantOk (result, message) |> ignore
+
+ static member WantNotFound<'T> (result : ReadResult<'T>, [] message) =
+ match result with
+ | ReadResult.NotFound response -> response
+ | _ ->
+ Assert.Fail (message)
+ Unchecked.defaultof<_>
+
+ static member IsNotFound (result : ReadResult<'T>, [] message) =
+ CosmosAssert.WantNotFound (result, message) |> ignore
+
+ static member WantNotFound<'T> (result : DeleteResult<'T>, [] message) =
+ match result with
+ | DeleteResult.NotFound response -> response
+ | _ ->
+ Assert.Fail (message)
+ Unchecked.defaultof<_>
+
+ static member IsNotFound (result : DeleteResult<'T>, [] message) =
+ CosmosAssert.WantNotFound (result, message) |> ignore
+
+ static member WantConflict (result : CreateResult<'T>, [] message) =
+ match result with
+ | CreateResult.IdAlreadyExists _ -> ()
+ | _ ->
+ Assert.Fail (message)
+ Unchecked.defaultof<_>
+
+ static member IsConflict (result : CreateResult<'T>, [] message) =
+ CosmosAssert.WantConflict (result, message) |> ignore
diff --git a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
index 847c599..e306248 100644
--- a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
+++ b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
@@ -18,6 +18,7 @@
+
diff --git a/tests/Cosmos.Tests/Tests.fs b/tests/Cosmos.Tests/Tests.fs
index 26f1bc3..54689ef 100644
--- a/tests/Cosmos.Tests/Tests.fs
+++ b/tests/Cosmos.Tests/Tests.fs
@@ -51,13 +51,11 @@ type OperationIntegrationTests () =
this.CancellationToken
)
- match response.Result with
- | CreateResult.Ok created ->
- Assert.IsTrue (testItem.id = created.id, "Create should persist the item id.")
- Assert.IsTrue (testItem.partitionKey = created.partitionKey, "Create should persist the item partition key.")
- Assert.IsFalse (String.IsNullOrWhiteSpace response.ActivityId, "Create should return a valid activity id.")
- Assert.IsTrue (response.RequestCharge > 0.0, "Create should report positive request charge.")
- | result -> Assert.Fail ($"Expected create success but received {result}.")
+ let created = CosmosAssert.WantOk (response.Result, "Create should return CreateResult.Ok.")
+ Assert.IsTrue (testItem.id = created.id, "Create should persist the item id.")
+ Assert.IsTrue (testItem.partitionKey = created.partitionKey, "Create should persist the item partition key.")
+ Assert.IsFalse (String.IsNullOrWhiteSpace response.ActivityId, "Create should return a valid activity id.")
+ Assert.IsTrue (response.RequestCharge > 0.0, "Create should report positive request charge.")
}
[]
@@ -65,7 +63,7 @@ type OperationIntegrationTests () =
let! container = this.GetContainerAsync ()
let testItem = this.NewItem "read"
- let! _ =
+ let! createdResponse =
container.ExecuteAsync (
create {
item testItem
@@ -73,6 +71,7 @@ type OperationIntegrationTests () =
},
this.CancellationToken
)
+ CosmosAssert.IsOk (createdResponse.Result, "Seed create should succeed.")
let! foundResponse =
container.ExecuteAsync (
@@ -83,11 +82,10 @@ type OperationIntegrationTests () =
this.CancellationToken
)
- match foundResponse.Result with
- | ReadResult.Ok found ->
- Assert.IsTrue (testItem.id = found.id, "Read should return the item that was created.")
- Assert.IsTrue (foundResponse.HttpStatusCode = HttpStatusCode.OK, "Read success should return HTTP 200.")
- | result -> Assert.Fail ($"Expected successful read but received {result}.")
+ let found =
+ CosmosAssert.WantOk (foundResponse.Result, "Read should return ReadResult.Ok for existing item.")
+ Assert.IsTrue (testItem.id = found.id, "Read should return the item that was created.")
+ Assert.IsTrue (foundResponse.HttpStatusCode = HttpStatusCode.OK, "Read success should return HTTP 200.")
let missingId = $"{testItem.id}-missing"
@@ -100,13 +98,8 @@ type OperationIntegrationTests () =
this.CancellationToken
)
- match missingResponse.Result with
- | ReadResult.NotFound _ ->
- Assert.IsTrue (
- missingResponse.HttpStatusCode = HttpStatusCode.NotFound,
- "Read of missing item should return HTTP 404."
- )
- | result -> Assert.Fail ($"Expected read not found for missing item but received {result}.")
+ CosmosAssert.IsNotFound (missingResponse.Result, "Read of missing item should return ReadResult.NotFound.")
+ Assert.IsTrue (missingResponse.HttpStatusCode = HttpStatusCode.NotFound, "Read of missing item should return HTTP 404.")
}
[]
@@ -153,7 +146,7 @@ type OperationIntegrationTests () =
let! container = this.GetContainerAsync ()
let testItem = this.NewItem "replace"
- let! _ =
+ let! createdResponse =
container.ExecuteAsync (
create {
item testItem
@@ -161,6 +154,7 @@ type OperationIntegrationTests () =
},
this.CancellationToken
)
+ CosmosAssert.IsOk (createdResponse.Result, "Seed create should succeed.")
let replacement = { testItem with name = "item-replaced"; quantity = 3 }
@@ -174,12 +168,10 @@ type OperationIntegrationTests () =
this.CancellationToken
)
- match replaceResponse.Result with
- | ReplaceResult.Ok replaced ->
- Assert.IsTrue (replacement.name = replaced.name, "Replace should persist replacement name.")
- Assert.IsTrue (replacement.quantity = replaced.quantity, "Replace should persist replacement quantity.")
- Assert.IsTrue (replaceResponse.HttpStatusCode = HttpStatusCode.OK, "Replace should return HTTP 200.")
- | result -> Assert.Fail ($"Expected replace success but received {result}.")
+ let replaced = CosmosAssert.WantOk (replaceResponse.Result, "Replace should return ReplaceResult.Ok.")
+ Assert.IsTrue (replacement.name = replaced.name, "Replace should persist replacement name.")
+ Assert.IsTrue (replacement.quantity = replaced.quantity, "Replace should persist replacement quantity.")
+ Assert.IsTrue (replaceResponse.HttpStatusCode = HttpStatusCode.OK, "Replace should return HTTP 200.")
}
[]
@@ -189,7 +181,7 @@ type OperationIntegrationTests () =
let patchedName = "item-patched"
let patchedQuantity = 9
- let! _ =
+ let! createdResponse =
container.ExecuteAsync (
create {
item testItem
@@ -197,6 +189,7 @@ type OperationIntegrationTests () =
},
this.CancellationToken
)
+ CosmosAssert.IsOk (createdResponse.Result, "Seed create should succeed.")
let! patchResponse =
container.ExecuteOverwriteAsync (
@@ -224,7 +217,7 @@ type OperationIntegrationTests () =
let! container = this.GetContainerAsync ()
let testItem = this.NewItem "delete"
- let! _ =
+ let! createdResponse =
container.ExecuteAsync (
create {
item testItem
@@ -232,6 +225,7 @@ type OperationIntegrationTests () =
},
this.CancellationToken
)
+ CosmosAssert.IsOk (createdResponse.Result, "Seed create should succeed.")
let! deleteResponse =
container.ExecuteAsync (
@@ -242,10 +236,8 @@ type OperationIntegrationTests () =
this.CancellationToken
)
- match deleteResponse.Result with
- | DeleteResult.Ok _ ->
- Assert.IsTrue (deleteResponse.HttpStatusCode = HttpStatusCode.NoContent, "Delete should return HTTP 204.")
- | result -> Assert.Fail ($"Expected delete success but received {result}.")
+ CosmosAssert.IsOk (deleteResponse.Result, "Delete should return DeleteResult.Ok.")
+ Assert.IsTrue (deleteResponse.HttpStatusCode = HttpStatusCode.NoContent, "Delete should return HTTP 204.")
let! missingResponse =
container.ExecuteAsync (
@@ -256,10 +248,8 @@ type OperationIntegrationTests () =
this.CancellationToken
)
- match missingResponse.Result with
- | ReadResult.NotFound _ ->
- Assert.IsTrue (missingResponse.HttpStatusCode = HttpStatusCode.NotFound, "Read after delete should return HTTP 404.")
- | result -> Assert.Fail ($"Expected read not found after delete but received {result}.")
+ CosmosAssert.IsNotFound (missingResponse.Result, "Read after delete should return ReadResult.NotFound.")
+ Assert.IsTrue (missingResponse.HttpStatusCode = HttpStatusCode.NotFound, "Read after delete should return HTTP 404.")
}
[]
@@ -268,7 +258,7 @@ type OperationIntegrationTests () =
let firstItem = this.NewItem "readmany-1"
let secondItem = this.NewItem "readmany-2"
- let! _ =
+ let! firstCreatedResponse =
container.ExecuteAsync (
create {
item firstItem
@@ -276,8 +266,9 @@ type OperationIntegrationTests () =
},
this.CancellationToken
)
+ CosmosAssert.IsOk (firstCreatedResponse.Result, "First seed create should succeed.")
- let! _ =
+ let! secondCreatedResponse =
container.ExecuteAsync (
create {
item secondItem
@@ -285,6 +276,7 @@ type OperationIntegrationTests () =
},
this.CancellationToken
)
+ CosmosAssert.IsOk (secondCreatedResponse.Result, "Second seed create should succeed.")
let! readManyResponse =
container.ExecuteAsync (
From 771f88d10a7baed1be0c576057a3c518436613f3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 01:16:10 +0000
Subject: [PATCH 10/33] Refine CosmosAssert failure handling consistency
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/727cc9a9-e850-45de-b65f-4e8eb8ff33fc
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/CosmosAssert.fs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/Cosmos.Tests/CosmosAssert.fs b/tests/Cosmos.Tests/CosmosAssert.fs
index 5b7d2ac..e3336e2 100644
--- a/tests/Cosmos.Tests/CosmosAssert.fs
+++ b/tests/Cosmos.Tests/CosmosAssert.fs
@@ -27,7 +27,7 @@ type CosmosAssert private () =
match result with
| CreateResult.Ok ok -> ok
| _ ->
- Assert.Fail ($"Expected CreateResult.Ok, but {result} given. {message}")
+ Assert.Fail (message)
Unchecked.defaultof<_>
static member IsOk (result : CreateResult<'T>, [] message) = CosmosAssert.WantOk (result, message) |> ignore
@@ -84,7 +84,7 @@ type CosmosAssert private () =
| CreateResult.IdAlreadyExists _ -> ()
| _ ->
Assert.Fail (message)
- Unchecked.defaultof<_>
+ ()
static member IsConflict (result : CreateResult<'T>, [] message) =
CosmosAssert.WantConflict (result, message) |> ignore
From f9d3251e69aaccd40499e4cb1fa9b3f23e25000e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 01:17:38 +0000
Subject: [PATCH 11/33] Plan split operation tests into separate files with
AndRead coverage
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/727cc9a9-e850-45de-b65f-4e8eb8ff33fc
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/CosmosAssert.fs | 24 ++++++++++++++----------
1 file changed, 14 insertions(+), 10 deletions(-)
diff --git a/tests/Cosmos.Tests/CosmosAssert.fs b/tests/Cosmos.Tests/CosmosAssert.fs
index e3336e2..2dc7274 100644
--- a/tests/Cosmos.Tests/CosmosAssert.fs
+++ b/tests/Cosmos.Tests/CosmosAssert.fs
@@ -1,5 +1,6 @@
namespace Tests.Integration
+open System
open System.Diagnostics
open System.Net
open System.Runtime.InteropServices
@@ -12,13 +13,18 @@ open Microsoft.VisualStudio.TestTools.UnitTesting
[]
type CosmosAssert private () =
+ static member private GetMessageOrDefault (message : string) (defaultMessage : string) =
+ if String.IsNullOrWhiteSpace message then
+ defaultMessage
+ else
+ message
static member WantOk<'T> (response : ItemResponse<'T>, [] message) =
match response.StatusCode with
| HttpStatusCode.OK
| HttpStatusCode.Created -> response.Resource
| _ ->
- Assert.Fail (message)
+ Assert.Fail (CosmosAssert.GetMessageOrDefault message $"Expected OK or Created but got {response.StatusCode}.")
Unchecked.defaultof<_>
static member IsOk (response : ItemResponse<'T>, [] message) = CosmosAssert.WantOk (response, message) |> ignore
@@ -27,7 +33,7 @@ type CosmosAssert private () =
match result with
| CreateResult.Ok ok -> ok
| _ ->
- Assert.Fail (message)
+ Assert.Fail (CosmosAssert.GetMessageOrDefault message $"Expected CreateResult.Ok but got {result}.")
Unchecked.defaultof<_>
static member IsOk (result : CreateResult<'T>, [] message) = CosmosAssert.WantOk (result, message) |> ignore
@@ -36,7 +42,7 @@ type CosmosAssert private () =
match result with
| ReadResult.Ok ok -> ok
| _ ->
- Assert.Fail (message)
+ Assert.Fail (CosmosAssert.GetMessageOrDefault message $"Expected ReadResult.Ok but got {result}.")
Unchecked.defaultof<_>
static member IsOk (result : ReadResult<'T>, [] message) = CosmosAssert.WantOk (result, message) |> ignore
@@ -45,7 +51,7 @@ type CosmosAssert private () =
match result with
| ReplaceResult.Ok ok -> ok
| _ ->
- Assert.Fail (message)
+ Assert.Fail (CosmosAssert.GetMessageOrDefault message $"Expected ReplaceResult.Ok but got {result}.")
Unchecked.defaultof<_>
static member IsOk (result : ReplaceResult<'T>, [] message) = CosmosAssert.WantOk (result, message) |> ignore
@@ -54,7 +60,7 @@ type CosmosAssert private () =
match result with
| DeleteResult.Ok ok -> ok
| _ ->
- Assert.Fail (message)
+ Assert.Fail (CosmosAssert.GetMessageOrDefault message $"Expected DeleteResult.Ok but got {result}.")
Unchecked.defaultof<_>
static member IsOk (result : DeleteResult<'T>, [] message) = CosmosAssert.WantOk (result, message) |> ignore
@@ -63,7 +69,7 @@ type CosmosAssert private () =
match result with
| ReadResult.NotFound response -> response
| _ ->
- Assert.Fail (message)
+ Assert.Fail (CosmosAssert.GetMessageOrDefault message $"Expected ReadResult.NotFound but got {result}.")
Unchecked.defaultof<_>
static member IsNotFound (result : ReadResult<'T>, [] message) =
@@ -73,7 +79,7 @@ type CosmosAssert private () =
match result with
| DeleteResult.NotFound response -> response
| _ ->
- Assert.Fail (message)
+ Assert.Fail (CosmosAssert.GetMessageOrDefault message $"Expected DeleteResult.NotFound but got {result}.")
Unchecked.defaultof<_>
static member IsNotFound (result : DeleteResult<'T>, [] message) =
@@ -82,9 +88,7 @@ type CosmosAssert private () =
static member WantConflict (result : CreateResult<'T>, [] message) =
match result with
| CreateResult.IdAlreadyExists _ -> ()
- | _ ->
- Assert.Fail (message)
- ()
+ | _ -> Assert.Fail (CosmosAssert.GetMessageOrDefault message $"Expected CreateResult.IdAlreadyExists but got {result}.")
static member IsConflict (result : CreateResult<'T>, [] message) =
CosmosAssert.WantConflict (result, message) |> ignore
From 6d5d59f9f2393ac96d1fa0030a65c4ac09427002 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 01:19:55 +0000
Subject: [PATCH 12/33] Split operation integration tests into per-operation
files with AndRead coverage
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/727cc9a9-e850-45de-b65f-4e8eb8ff33fc
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/CreateOperationTests.fs | 60 ++++
tests/Cosmos.Tests/DeleteOperationTests.fs | 51 +++
.../FSharp.Azure.Cosmos.Tests.fsproj | 9 +
.../OperationTestInfrastructure.fs | 38 +++
tests/Cosmos.Tests/PatchOperationTests.fs | 101 ++++++
tests/Cosmos.Tests/ReadManyOperationTests.fs | 58 ++++
tests/Cosmos.Tests/ReadOperationTests.fs | 53 ++++
tests/Cosmos.Tests/ReplaceOperationTests.fs | 90 ++++++
tests/Cosmos.Tests/Tests.fs | 298 ------------------
tests/Cosmos.Tests/UpsertOperationTests.fs | 101 ++++++
10 files changed, 561 insertions(+), 298 deletions(-)
create mode 100644 tests/Cosmos.Tests/CreateOperationTests.fs
create mode 100644 tests/Cosmos.Tests/DeleteOperationTests.fs
create mode 100644 tests/Cosmos.Tests/OperationTestInfrastructure.fs
create mode 100644 tests/Cosmos.Tests/PatchOperationTests.fs
create mode 100644 tests/Cosmos.Tests/ReadManyOperationTests.fs
create mode 100644 tests/Cosmos.Tests/ReadOperationTests.fs
create mode 100644 tests/Cosmos.Tests/ReplaceOperationTests.fs
delete mode 100644 tests/Cosmos.Tests/Tests.fs
create mode 100644 tests/Cosmos.Tests/UpsertOperationTests.fs
diff --git a/tests/Cosmos.Tests/CreateOperationTests.fs b/tests/Cosmos.Tests/CreateOperationTests.fs
new file mode 100644
index 0000000..df76a1a
--- /dev/null
+++ b/tests/Cosmos.Tests/CreateOperationTests.fs
@@ -0,0 +1,60 @@
+namespace Tests.Integration
+
+open System.Net
+open System.Threading.Tasks
+open FSharp.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type CreateOperationIntegrationTests () =
+ inherit OperationTestBase ()
+
+ []
+ member this.Create_execute_persists_item () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "create"
+
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, "Create should return CreateResult.Ok.")
+ Assert.IsTrue (createResponse.HttpStatusCode = HttpStatusCode.Created, "Create should return HTTP 201.")
+
+ let! readResponse =
+ container.ExecuteAsync (
+ read {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let created = CosmosAssert.WantOk (readResponse.Result, "Created item should be readable.")
+ Assert.IsTrue (testItem.id = created.id, "Create should persist item id.")
+ Assert.IsTrue (testItem.partitionKey = created.partitionKey, "Create should persist partition key.")
+ }
+
+ []
+ member this.CreateAndRead_execute_returns_created_resource () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "create-and-read"
+
+ let! response =
+ container.ExecuteAsync (
+ createAndRead {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let created = CosmosAssert.WantOk (response.Result, "CreateAndRead should return CreateResult.Ok.")
+ Assert.IsTrue (testItem.id = created.id, "CreateAndRead should return created item id.")
+ Assert.IsTrue (testItem.partitionKey = created.partitionKey, "CreateAndRead should return created partition key.")
+ }
diff --git a/tests/Cosmos.Tests/DeleteOperationTests.fs b/tests/Cosmos.Tests/DeleteOperationTests.fs
new file mode 100644
index 0000000..c633b81
--- /dev/null
+++ b/tests/Cosmos.Tests/DeleteOperationTests.fs
@@ -0,0 +1,51 @@
+namespace Tests.Integration
+
+open System.Net
+open System.Threading.Tasks
+open FSharp.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type DeleteOperationIntegrationTests () =
+ inherit OperationTestBase ()
+
+ []
+ member this.Delete_execute_removes_item_and_subsequent_read_is_not_found () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "delete"
+
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, "Seed create should succeed.")
+
+ let! deleteResponse =
+ container.ExecuteAsync (
+ delete {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (deleteResponse.Result, "Delete should return DeleteResult.Ok.")
+ Assert.IsTrue (deleteResponse.HttpStatusCode = HttpStatusCode.NoContent, "Delete should return HTTP 204.")
+
+ let! missingResponse =
+ container.ExecuteAsync (
+ read {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsNotFound (missingResponse.Result, "Read after delete should return ReadResult.NotFound.")
+ Assert.IsTrue (missingResponse.HttpStatusCode = HttpStatusCode.NotFound, "Read after delete should return HTTP 404.")
+ }
diff --git a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
index e306248..cc8cae0 100644
--- a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
+++ b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
@@ -19,8 +19,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/Cosmos.Tests/OperationTestInfrastructure.fs b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
new file mode 100644
index 0000000..9668e05
--- /dev/null
+++ b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
@@ -0,0 +1,38 @@
+namespace Tests.Integration
+
+open System
+open System.Threading.Tasks
+open Microsoft.Azure.Cosmos
+
+type TestItem = { id : string; partitionKey : string; name : string; quantity : int }
+
+[]
+type OperationTestBase () =
+ inherit IntegrationTestBase ()
+
+ []
+ let containerId = "operation-tests"
+
+ member private this.GetDatabase () =
+ match this.Application.Database with
+ | ValueSome database -> database
+ | ValueNone -> invalidOp "Database is not initialized."
+
+ member this.GetContainer () : Task = task {
+ let database = this.GetDatabase ()
+
+ let! containerResponse =
+ database.CreateContainerIfNotExistsAsync (
+ ContainerProperties (containerId, "/partitionKey"),
+ cancellationToken = this.CancellationToken
+ )
+
+ return containerResponse.Container
+ }
+
+ member this.NewItem (suffix : string) : TestItem = {
+ id = $"{this.TestContext.TestName}-{suffix}"
+ partitionKey = "integration"
+ name = $"item-{suffix}"
+ quantity = 1
+ }
diff --git a/tests/Cosmos.Tests/PatchOperationTests.fs b/tests/Cosmos.Tests/PatchOperationTests.fs
new file mode 100644
index 0000000..d57ee12
--- /dev/null
+++ b/tests/Cosmos.Tests/PatchOperationTests.fs
@@ -0,0 +1,101 @@
+namespace Tests.Integration
+
+open System.Net
+open System.Threading.Tasks
+open FSharp.Azure.Cosmos
+open Microsoft.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type PatchOperationIntegrationTests () =
+ inherit OperationTestBase ()
+
+ []
+ member this.Patch_execute_overwrite_updates_item () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "patch"
+
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, "Seed create should succeed.")
+
+ let patchedName = "item-patched"
+ let patchedQuantity = 9
+
+ let! patchResponse =
+ container.ExecuteOverwriteAsync (
+ patch {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ operation (PatchOperation.Replace ("/name", patchedName))
+ operation (PatchOperation.Replace ("/quantity", patchedQuantity))
+ },
+ this.CancellationToken
+ )
+
+ match patchResponse.Result with
+ | PatchResult.Ok _ -> Assert.IsTrue (patchResponse.HttpStatusCode = HttpStatusCode.OK, "Patch should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected patch success, got {result}.")
+
+ let! readResponse =
+ container.ExecuteAsync (
+ read {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let persisted = CosmosAssert.WantOk (readResponse.Result, "Patched item should be readable.")
+ let isNamePatched = patchedName = persisted.name
+ let isQuantityPatched = patchedQuantity = persisted.quantity
+ Assert.IsTrue (isNamePatched, "Patch should persist patched name.")
+ Assert.IsTrue (isQuantityPatched, "Patch should persist patched quantity.")
+ }
+
+ []
+ member this.PatchAndRead_execute_overwrite_returns_updated_item () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "patch-and-read"
+
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, "Seed create should succeed.")
+
+ let patchedName = "item-patched-and-read"
+ let patchedQuantity = 11
+
+ let! patchResponse =
+ container.ExecuteOverwriteAsync (
+ patchAndRead {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ operation (PatchOperation.Replace ("/name", patchedName))
+ operation (PatchOperation.Replace ("/quantity", patchedQuantity))
+ },
+ this.CancellationToken
+ )
+
+ match patchResponse.Result with
+ | PatchResult.Ok patched ->
+ let isNamePatched = patchedName = patched.name
+ let isQuantityPatched = patchedQuantity = patched.quantity
+ Assert.IsTrue (isNamePatched, "PatchAndRead should return patched name.")
+ Assert.IsTrue (isQuantityPatched, "PatchAndRead should return patched quantity.")
+ Assert.IsTrue (patchResponse.HttpStatusCode = HttpStatusCode.OK, "PatchAndRead should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected patchAndRead success, got {result}.")
+ }
diff --git a/tests/Cosmos.Tests/ReadManyOperationTests.fs b/tests/Cosmos.Tests/ReadManyOperationTests.fs
new file mode 100644
index 0000000..8e02113
--- /dev/null
+++ b/tests/Cosmos.Tests/ReadManyOperationTests.fs
@@ -0,0 +1,58 @@
+namespace Tests.Integration
+
+open System.Net
+open System.Threading.Tasks
+open FSharp.Azure.Cosmos
+open Microsoft.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type ReadManyOperationIntegrationTests () =
+ inherit OperationTestBase ()
+
+ []
+ member this.ReadMany_execute_returns_matching_items () : Task = task {
+ let! container = this.GetContainer ()
+ let firstItem = this.NewItem "readmany-1"
+ let secondItem = this.NewItem "readmany-2"
+
+ let! firstCreateResponse =
+ container.ExecuteAsync (
+ create {
+ item firstItem
+ partitionKey firstItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (firstCreateResponse.Result, "First seed create should succeed.")
+
+ let! secondCreateResponse =
+ container.ExecuteAsync (
+ create {
+ item secondItem
+ partitionKey secondItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (secondCreateResponse.Result, "Second seed create should succeed.")
+
+ let! readManyResponse =
+ container.ExecuteAsync (
+ readMany {
+ item firstItem.id firstItem.partitionKey
+ item secondItem.id secondItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match readManyResponse.Result with
+ | ReadManyResult.Ok (feed : FeedResponse) ->
+ let returnedIds = feed |> Seq.map _.id |> Set.ofSeq
+ Assert.IsTrue (feed.Count = 2, "ReadMany should return requested number of items.")
+ Assert.IsTrue (returnedIds.Contains firstItem.id, "ReadMany should include first item.")
+ Assert.IsTrue (returnedIds.Contains secondItem.id, "ReadMany should include second item.")
+ Assert.IsTrue (readManyResponse.HttpStatusCode = HttpStatusCode.OK, "ReadMany should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected read many success, got {result}.")
+ }
diff --git a/tests/Cosmos.Tests/ReadOperationTests.fs b/tests/Cosmos.Tests/ReadOperationTests.fs
new file mode 100644
index 0000000..233f39b
--- /dev/null
+++ b/tests/Cosmos.Tests/ReadOperationTests.fs
@@ -0,0 +1,53 @@
+namespace Tests.Integration
+
+open System.Net
+open System.Threading.Tasks
+open FSharp.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type ReadOperationIntegrationTests () =
+ inherit OperationTestBase ()
+
+ []
+ member this.Read_execute_returns_existing_and_not_found_states () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "read"
+
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, "Seed create should succeed.")
+
+ let! foundResponse =
+ container.ExecuteAsync (
+ read {
+ id testItem.id
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let found =
+ CosmosAssert.WantOk (foundResponse.Result, "Read should return ReadResult.Ok for existing item.")
+ Assert.IsTrue (testItem.id = found.id, "Read should return created item.")
+ Assert.IsTrue (foundResponse.HttpStatusCode = HttpStatusCode.OK, "Read should return HTTP 200 for existing item.")
+
+ let! missingResponse =
+ container.ExecuteAsync (
+ read {
+ id $"{testItem.id}-missing"
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsNotFound (missingResponse.Result, "Read should return ReadResult.NotFound for missing item.")
+ Assert.IsTrue (missingResponse.HttpStatusCode = HttpStatusCode.NotFound, "Read missing should return HTTP 404.")
+ }
diff --git a/tests/Cosmos.Tests/ReplaceOperationTests.fs b/tests/Cosmos.Tests/ReplaceOperationTests.fs
new file mode 100644
index 0000000..5684f5b
--- /dev/null
+++ b/tests/Cosmos.Tests/ReplaceOperationTests.fs
@@ -0,0 +1,90 @@
+namespace Tests.Integration
+
+open System.Net
+open System.Threading.Tasks
+open FSharp.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type ReplaceOperationIntegrationTests () =
+ inherit OperationTestBase ()
+
+ []
+ member this.Replace_execute_overwrite_replaces_existing_item () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "replace"
+
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, "Seed create should succeed.")
+
+ let replacement = { testItem with name = "item-replaced"; quantity = 3 }
+
+ let! replaceResponse =
+ container.ExecuteOverwriteAsync (
+ replace {
+ id replacement.id
+ item replacement
+ partitionKey replacement.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (replaceResponse.Result, "Replace should return ReplaceResult.Ok.")
+ Assert.IsTrue (replaceResponse.HttpStatusCode = HttpStatusCode.OK, "Replace should return HTTP 200.")
+
+ let! readResponse =
+ container.ExecuteAsync (
+ read {
+ id replacement.id
+ partitionKey replacement.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let persisted = CosmosAssert.WantOk (readResponse.Result, "Replaced item should be readable.")
+ Assert.IsTrue (replacement.name = persisted.name, "Replace should persist replacement name.")
+ Assert.IsTrue (replacement.quantity = persisted.quantity, "Replace should persist replacement quantity.")
+ }
+
+ []
+ member this.ReplaceAndRead_execute_overwrite_returns_replaced_item () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "replace-and-read"
+
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, "Seed create should succeed.")
+
+ let replacement = { testItem with name = "item-replaced-and-read"; quantity = 6 }
+
+ let! replaceResponse =
+ container.ExecuteOverwriteAsync (
+ replaceAndRead {
+ id replacement.id
+ item replacement
+ partitionKey replacement.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let replaced =
+ CosmosAssert.WantOk (replaceResponse.Result, "ReplaceAndRead should return ReplaceResult.Ok.")
+ Assert.IsTrue (replacement.name = replaced.name, "ReplaceAndRead should return replacement name.")
+ Assert.IsTrue (replacement.quantity = replaced.quantity, "ReplaceAndRead should return replacement quantity.")
+ Assert.IsTrue (replaceResponse.HttpStatusCode = HttpStatusCode.OK, "ReplaceAndRead should return HTTP 200.")
+ }
diff --git a/tests/Cosmos.Tests/Tests.fs b/tests/Cosmos.Tests/Tests.fs
deleted file mode 100644
index 54689ef..0000000
--- a/tests/Cosmos.Tests/Tests.fs
+++ /dev/null
@@ -1,298 +0,0 @@
-namespace Tests.Integration
-
-open System
-open System.Net
-open System.Threading.Tasks
-open FSharp.Azure.Cosmos
-open Microsoft.Azure.Cosmos
-open Microsoft.VisualStudio.TestTools.UnitTesting
-
-type private TestItem = { id : string; partitionKey : string; name : string; quantity : int }
-
-[]
-type OperationIntegrationTests () =
- inherit IntegrationTestBase ()
-
- let containerId = "operation-tests"
-
- member private this.GetContainerAsync () : Task = task {
- let database =
- match this.Application.Database with
- | ValueSome database -> database
- | ValueNone -> invalidOp "Database is not initialized."
-
- let! containerResponse =
- database.CreateContainerIfNotExistsAsync (
- ContainerProperties (containerId, "/partitionKey"),
- cancellationToken = this.CancellationToken
- )
-
- return containerResponse.Container
- }
-
- member private this.NewItem (suffix : string) : TestItem = {
- id = $"{this.TestContext.TestName}-{suffix}"
- partitionKey = "integration"
- name = $"item-{suffix}"
- quantity = 1
- }
-
- []
- member this.Create_execute_returns_created_resource () : Task = task {
- let! container = this.GetContainerAsync ()
- let testItem = this.NewItem "create"
-
- let! response =
- container.ExecuteAsync (
- createAndRead {
- item testItem
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
-
- let created = CosmosAssert.WantOk (response.Result, "Create should return CreateResult.Ok.")
- Assert.IsTrue (testItem.id = created.id, "Create should persist the item id.")
- Assert.IsTrue (testItem.partitionKey = created.partitionKey, "Create should persist the item partition key.")
- Assert.IsFalse (String.IsNullOrWhiteSpace response.ActivityId, "Create should return a valid activity id.")
- Assert.IsTrue (response.RequestCharge > 0.0, "Create should report positive request charge.")
- }
-
- []
- member this.Read_execute_returns_existing_and_not_found_states () : Task = task {
- let! container = this.GetContainerAsync ()
- let testItem = this.NewItem "read"
-
- let! createdResponse =
- container.ExecuteAsync (
- create {
- item testItem
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
- CosmosAssert.IsOk (createdResponse.Result, "Seed create should succeed.")
-
- let! foundResponse =
- container.ExecuteAsync (
- read {
- id testItem.id
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
-
- let found =
- CosmosAssert.WantOk (foundResponse.Result, "Read should return ReadResult.Ok for existing item.")
- Assert.IsTrue (testItem.id = found.id, "Read should return the item that was created.")
- Assert.IsTrue (foundResponse.HttpStatusCode = HttpStatusCode.OK, "Read success should return HTTP 200.")
-
- let missingId = $"{testItem.id}-missing"
-
- let! missingResponse =
- container.ExecuteAsync (
- read {
- id missingId
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
-
- CosmosAssert.IsNotFound (missingResponse.Result, "Read of missing item should return ReadResult.NotFound.")
- Assert.IsTrue (missingResponse.HttpStatusCode = HttpStatusCode.NotFound, "Read of missing item should return HTTP 404.")
- }
-
- []
- member this.Upsert_execute_overwrite_creates_then_updates_item () : Task = task {
- let! container = this.GetContainerAsync ()
- let testItem = this.NewItem "upsert"
-
- let! createdResponse =
- container.ExecuteOverwriteAsync (
- upsertAndRead {
- item testItem
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
-
- match createdResponse.Result with
- | UpsertResult.Ok created ->
- Assert.IsTrue (testItem.name = created.name, "Upsert create should store initial payload.")
- Assert.IsTrue (createdResponse.HttpStatusCode = HttpStatusCode.Created, "Initial upsert should create item.")
- | result -> Assert.Fail ($"Expected upsert create success but received {result}.")
-
- let updated = { testItem with name = "item-upsert-updated"; quantity = 5 }
-
- let! updatedResponse =
- container.ExecuteOverwriteAsync (
- upsertAndRead {
- item updated
- partitionKey updated.partitionKey
- },
- this.CancellationToken
- )
-
- match updatedResponse.Result with
- | UpsertResult.Ok upserted ->
- Assert.IsTrue (updated.name = upserted.name, "Upsert update should persist new payload.")
- Assert.IsTrue (updated.quantity = upserted.quantity, "Upsert update should persist new quantity.")
- Assert.IsTrue (updatedResponse.HttpStatusCode = HttpStatusCode.OK, "Second upsert should replace existing item.")
- | result -> Assert.Fail ($"Expected upsert update success but received {result}.")
- }
-
- []
- member this.Replace_execute_overwrite_replaces_existing_item () : Task = task {
- let! container = this.GetContainerAsync ()
- let testItem = this.NewItem "replace"
-
- let! createdResponse =
- container.ExecuteAsync (
- create {
- item testItem
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
- CosmosAssert.IsOk (createdResponse.Result, "Seed create should succeed.")
-
- let replacement = { testItem with name = "item-replaced"; quantity = 3 }
-
- let! replaceResponse =
- container.ExecuteOverwriteAsync (
- replaceAndRead {
- id replacement.id
- item replacement
- partitionKey replacement.partitionKey
- },
- this.CancellationToken
- )
-
- let replaced = CosmosAssert.WantOk (replaceResponse.Result, "Replace should return ReplaceResult.Ok.")
- Assert.IsTrue (replacement.name = replaced.name, "Replace should persist replacement name.")
- Assert.IsTrue (replacement.quantity = replaced.quantity, "Replace should persist replacement quantity.")
- Assert.IsTrue (replaceResponse.HttpStatusCode = HttpStatusCode.OK, "Replace should return HTTP 200.")
- }
-
- []
- member this.Patch_execute_overwrite_updates_targeted_field () : Task = task {
- let! container = this.GetContainerAsync ()
- let testItem = this.NewItem "patch"
- let patchedName = "item-patched"
- let patchedQuantity = 9
-
- let! createdResponse =
- container.ExecuteAsync (
- create {
- item testItem
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
- CosmosAssert.IsOk (createdResponse.Result, "Seed create should succeed.")
-
- let! patchResponse =
- container.ExecuteOverwriteAsync (
- patchAndRead {
- id testItem.id
- partitionKey testItem.partitionKey
- operation (Microsoft.Azure.Cosmos.PatchOperation.Replace ("/name", patchedName))
- operation (Microsoft.Azure.Cosmos.PatchOperation.Replace ("/quantity", patchedQuantity))
- },
- this.CancellationToken
- )
-
- match patchResponse.Result with
- | PatchResult.Ok patched ->
- let isNamePatched = patchedName = patched.name
- let isQuantityPatched = patchedQuantity = patched.quantity
- Assert.IsTrue (isNamePatched, "Patch should update the name field.")
- Assert.IsTrue (isQuantityPatched, "Patch should update the quantity field.")
- Assert.IsTrue (patchResponse.HttpStatusCode = HttpStatusCode.OK, "Patch should return HTTP 200.")
- | result -> Assert.Fail ($"Expected patch success but received {result}.")
- }
-
- []
- member this.Delete_execute_removes_item_and_subsequent_read_is_not_found () : Task = task {
- let! container = this.GetContainerAsync ()
- let testItem = this.NewItem "delete"
-
- let! createdResponse =
- container.ExecuteAsync (
- create {
- item testItem
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
- CosmosAssert.IsOk (createdResponse.Result, "Seed create should succeed.")
-
- let! deleteResponse =
- container.ExecuteAsync (
- delete {
- id testItem.id
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
-
- CosmosAssert.IsOk (deleteResponse.Result, "Delete should return DeleteResult.Ok.")
- Assert.IsTrue (deleteResponse.HttpStatusCode = HttpStatusCode.NoContent, "Delete should return HTTP 204.")
-
- let! missingResponse =
- container.ExecuteAsync (
- read {
- id testItem.id
- partitionKey testItem.partitionKey
- },
- this.CancellationToken
- )
-
- CosmosAssert.IsNotFound (missingResponse.Result, "Read after delete should return ReadResult.NotFound.")
- Assert.IsTrue (missingResponse.HttpStatusCode = HttpStatusCode.NotFound, "Read after delete should return HTTP 404.")
- }
-
- []
- member this.ReadMany_execute_returns_matching_items () : Task = task {
- let! container = this.GetContainerAsync ()
- let firstItem = this.NewItem "readmany-1"
- let secondItem = this.NewItem "readmany-2"
-
- let! firstCreatedResponse =
- container.ExecuteAsync (
- create {
- item firstItem
- partitionKey firstItem.partitionKey
- },
- this.CancellationToken
- )
- CosmosAssert.IsOk (firstCreatedResponse.Result, "First seed create should succeed.")
-
- let! secondCreatedResponse =
- container.ExecuteAsync (
- create {
- item secondItem
- partitionKey secondItem.partitionKey
- },
- this.CancellationToken
- )
- CosmosAssert.IsOk (secondCreatedResponse.Result, "Second seed create should succeed.")
-
- let! readManyResponse =
- container.ExecuteAsync (
- readMany {
- item firstItem.id firstItem.partitionKey
- item secondItem.id secondItem.partitionKey
- },
- this.CancellationToken
- )
-
- match readManyResponse.Result with
- | ReadManyResult.Ok (feed : FeedResponse) ->
- let returnedIds = feed |> Seq.map _.id |> Set.ofSeq
- Assert.IsTrue (feed.Count = 2, "ReadMany should return the requested number of existing items.")
- Assert.IsTrue (returnedIds.Contains firstItem.id, "ReadMany should include first requested item.")
- Assert.IsTrue (returnedIds.Contains secondItem.id, "ReadMany should include second requested item.")
- Assert.IsTrue (readManyResponse.HttpStatusCode = HttpStatusCode.OK, "ReadMany should return HTTP 200.")
- | result -> Assert.Fail ($"Expected read many success but received {result}.")
- }
diff --git a/tests/Cosmos.Tests/UpsertOperationTests.fs b/tests/Cosmos.Tests/UpsertOperationTests.fs
new file mode 100644
index 0000000..2557435
--- /dev/null
+++ b/tests/Cosmos.Tests/UpsertOperationTests.fs
@@ -0,0 +1,101 @@
+namespace Tests.Integration
+
+open System.Net
+open System.Threading.Tasks
+open FSharp.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type UpsertOperationIntegrationTests () =
+ inherit OperationTestBase ()
+
+ []
+ member this.Upsert_execute_overwrite_creates_then_updates_item () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "upsert"
+
+ let! createResult =
+ container.ExecuteOverwriteAsync (
+ upsert {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match createResult.Result with
+ | UpsertResult.Ok _ ->
+ Assert.IsTrue (createResult.HttpStatusCode = HttpStatusCode.Created, "First upsert should create item (HTTP 201).")
+ | result -> Assert.Fail ($"Expected first upsert success, got {result}.")
+
+ let updated = { testItem with name = "item-upsert-updated"; quantity = 5 }
+
+ let! updateResult =
+ container.ExecuteOverwriteAsync (
+ upsert {
+ item updated
+ partitionKey updated.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match updateResult.Result with
+ | UpsertResult.Ok _ ->
+ Assert.IsTrue (updateResult.HttpStatusCode = HttpStatusCode.OK, "Second upsert should update item (HTTP 200).")
+ | result -> Assert.Fail ($"Expected second upsert success, got {result}.")
+
+ let! readResponse =
+ container.ExecuteAsync (
+ read {
+ id updated.id
+ partitionKey updated.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ let persisted = CosmosAssert.WantOk (readResponse.Result, "Updated upsert item should be readable.")
+ Assert.IsTrue (updated.name = persisted.name, "Upsert should persist updated name.")
+ Assert.IsTrue (updated.quantity = persisted.quantity, "Upsert should persist updated quantity.")
+ }
+
+ []
+ member this.UpsertAndRead_execute_overwrite_returns_updated_item () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "upsert-and-read"
+
+ let! createdResponse =
+ container.ExecuteOverwriteAsync (
+ upsertAndRead {
+ item testItem
+ partitionKey testItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match createdResponse.Result with
+ | UpsertResult.Ok created ->
+ Assert.IsTrue (testItem.name = created.name, "UpsertAndRead create should return created resource.")
+ Assert.IsTrue (
+ createdResponse.HttpStatusCode = HttpStatusCode.Created,
+ "UpsertAndRead create should return HTTP 201."
+ )
+ | result -> Assert.Fail ($"Expected upsertAndRead create success, got {result}.")
+
+ let updated = { testItem with name = "item-upsert-and-read-updated"; quantity = 9 }
+
+ let! updatedResponse =
+ container.ExecuteOverwriteAsync (
+ upsertAndRead {
+ item updated
+ partitionKey updated.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ match updatedResponse.Result with
+ | UpsertResult.Ok upserted ->
+ Assert.IsTrue (updated.name = upserted.name, "UpsertAndRead update should return updated name.")
+ Assert.IsTrue (updated.quantity = upserted.quantity, "UpsertAndRead update should return updated quantity.")
+ Assert.IsTrue (updatedResponse.HttpStatusCode = HttpStatusCode.OK, "UpsertAndRead update should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected upsertAndRead update success, got {result}.")
+ }
From 4a47672beb75a3fa322e615b25a279e643be0c69 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 01:21:05 +0000
Subject: [PATCH 13/33] Adjust shared operation test fixture visibility
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/727cc9a9-e850-45de-b65f-4e8eb8ff33fc
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/OperationTestInfrastructure.fs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/Cosmos.Tests/OperationTestInfrastructure.fs b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
index 9668e05..c3f4e26 100644
--- a/tests/Cosmos.Tests/OperationTestInfrastructure.fs
+++ b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
@@ -4,7 +4,7 @@ open System
open System.Threading.Tasks
open Microsoft.Azure.Cosmos
-type TestItem = { id : string; partitionKey : string; name : string; quantity : int }
+type internal TestItem = { id : string; partitionKey : string; name : string; quantity : int }
[]
type OperationTestBase () =
@@ -30,7 +30,7 @@ type OperationTestBase () =
return containerResponse.Container
}
- member this.NewItem (suffix : string) : TestItem = {
+ member internal this.NewItem (suffix : string) : TestItem = {
id = $"{this.TestContext.TestName}-{suffix}"
partitionKey = "integration"
name = $"item-{suffix}"
From 0bf340cb65c05b3cccd7fae1e9b4e743b7411cc9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 01:22:08 +0000
Subject: [PATCH 14/33] Remove class-scope literal from operation test
infrastructure
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/727cc9a9-e850-45de-b65f-4e8eb8ff33fc
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/OperationTestInfrastructure.fs | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/Cosmos.Tests/OperationTestInfrastructure.fs b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
index c3f4e26..ba7f91f 100644
--- a/tests/Cosmos.Tests/OperationTestInfrastructure.fs
+++ b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
@@ -10,7 +10,6 @@ type internal TestItem = { id : string; partitionKey : string; name : string; qu
type OperationTestBase () =
inherit IntegrationTestBase ()
- []
let containerId = "operation-tests"
member private this.GetDatabase () =
From 03795ebd07db48ed67721397c41292838b711997 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 01:30:34 +0000
Subject: [PATCH 15/33] Rename test methods add read extension coverage and
builder tests
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/63ac5990-d78f-452f-9f2f-85e08fdbff91
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/BuilderUnitTests.fs | 234 ++++++++++++++++++
.../Cosmos.Tests/CosmosReadExtensionsTests.fs | 68 +++++
tests/Cosmos.Tests/CreateOperationTests.fs | 4 +-
tests/Cosmos.Tests/DeleteOperationTests.fs | 2 +-
.../FSharp.Azure.Cosmos.Tests.fsproj | 3 +
.../Cosmos.Tests/IterationExtensionsTests.fs | 56 +++++
.../OperationTestInfrastructure.fs | 15 ++
tests/Cosmos.Tests/PatchOperationTests.fs | 4 +-
tests/Cosmos.Tests/ReadManyOperationTests.fs | 2 +-
tests/Cosmos.Tests/ReadOperationTests.fs | 2 +-
tests/Cosmos.Tests/ReplaceOperationTests.fs | 63 ++++-
tests/Cosmos.Tests/UpsertOperationTests.fs | 65 ++++-
12 files changed, 507 insertions(+), 11 deletions(-)
create mode 100644 tests/Cosmos.Tests/BuilderUnitTests.fs
create mode 100644 tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
create mode 100644 tests/Cosmos.Tests/IterationExtensionsTests.fs
diff --git a/tests/Cosmos.Tests/BuilderUnitTests.fs b/tests/Cosmos.Tests/BuilderUnitTests.fs
new file mode 100644
index 0000000..913c45c
--- /dev/null
+++ b/tests/Cosmos.Tests/BuilderUnitTests.fs
@@ -0,0 +1,234 @@
+namespace Tests.Unit
+
+open System
+open FSharp.Azure.Cosmos
+open Microsoft.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+type private BuilderTestItem = { id : string; partitionKey : string; value : int }
+
+[]
+type BuilderUnitTests () =
+
+ []
+ member _.``Create builders configure operation and content response mode`` () =
+ let createItem = { id = "create-id"; partitionKey = "pk"; value = 1 }
+
+ let createOperation = create {
+ item createItem
+ partitionKey createItem.partitionKey
+ sessionToken "create-session"
+ }
+
+ let createAndReadOperation = createAndRead {
+ item createItem
+ partitionKey createItem.partitionKey
+ sessionToken "create-and-read-session"
+ }
+
+ Assert.IsTrue (createOperation.PartitionKey |> ValueOption.isSome, "Create builder should set partition key.")
+ Assert.IsTrue (createOperation.RequestOptions.SessionToken = "create-session", "Create builder should set session token.")
+ Assert.IsFalse (
+ createOperation.RequestOptions.EnableContentResponseOnWrite,
+ "Create builder should disable content response."
+ )
+ Assert.IsTrue (
+ createAndReadOperation.RequestOptions.EnableContentResponseOnWrite,
+ "CreateAndRead builder should enable content response."
+ )
+
+ []
+ member _.``Read builder configures id partition key and request options`` () =
+ let operation = read {
+ id "read-id"
+ partitionKey "pk"
+ eTag "etag-value"
+ sessionToken "read-session"
+ }
+
+ Assert.IsTrue (operation.Id = "read-id", "Read builder should set id.")
+ Assert.IsFalse (isNull operation.RequestOptions, "Read builder should initialize request options when needed.")
+ Assert.IsTrue (operation.RequestOptions.IfNoneMatchEtag = "etag-value", "Read builder should set eTag option.")
+ Assert.IsTrue (operation.RequestOptions.SessionToken = "read-session", "Read builder should set session token.")
+
+ []
+ member _.``ReadMany builder collects item tuples and request options`` () =
+ let operation = readMany {
+ item "item-1" "pk"
+ item "item-2" (PartitionKey "pk")
+ sessionToken "readmany-session"
+ }
+
+ Assert.IsTrue (operation.Items.Length = 2, "ReadMany builder should collect all item tuples.")
+ Assert.IsFalse (isNull operation.RequestOptions, "ReadMany builder should create request options when needed.")
+ Assert.IsTrue (operation.RequestOptions.SessionToken = "readmany-session", "ReadMany builder should set session token.")
+
+ []
+ member _.``Replace builders configure operation and content response mode`` () =
+ let replaceItem = { id = "replace-id"; partitionKey = "pk"; value = 1 }
+
+ let replaceOperation = replace {
+ id replaceItem.id
+ item replaceItem
+ partitionKey replaceItem.partitionKey
+ eTag "replace-etag"
+ }
+
+ let replaceAndReadOperation = replaceAndRead {
+ id replaceItem.id
+ item replaceItem
+ partitionKey replaceItem.partitionKey
+ }
+
+ Assert.IsTrue (replaceOperation.Id = replaceItem.id, "Replace builder should set id.")
+ Assert.IsTrue (replaceOperation.RequestOptions.IfMatchEtag = "replace-etag", "Replace builder should set eTag.")
+ Assert.IsFalse (
+ replaceOperation.RequestOptions.EnableContentResponseOnWrite,
+ "Replace builder should disable content response."
+ )
+ Assert.IsTrue (
+ replaceAndReadOperation.RequestOptions.EnableContentResponseOnWrite,
+ "ReplaceAndRead builder should enable content response."
+ )
+
+ []
+ member _.``Replace concurrently builders configure update function and response mode`` () =
+ let replaceConcurrentlyOperation = replaceConcurrenly {
+ id "replace-concurrent-id"
+ partitionKey "pk"
+ update (fun item -> async { return Result.Ok { item with value = item.value + 1 } })
+ }
+
+ let replaceConcurrentlyAndReadOperation = replaceConcurrenlyAndRead {
+ id "replace-concurrent-and-read-id"
+ partitionKey "pk"
+ update (fun item -> async { return Result.Ok item })
+ }
+
+ let updateResult =
+ replaceConcurrentlyOperation.Update { id = "id"; partitionKey = "pk"; value = 2 }
+ |> Async.RunSynchronously
+
+ Assert.IsTrue (replaceConcurrentlyOperation.Id = "replace-concurrent-id", "Replace concurrently builder should set id.")
+ Assert.IsTrue (Result.isOk updateResult, "Replace concurrently builder should set update function.")
+ Assert.IsFalse (
+ replaceConcurrentlyOperation.RequestOptions.EnableContentResponseOnWrite,
+ "Replace concurrently builder should disable content response."
+ )
+ Assert.IsTrue (
+ replaceConcurrentlyAndReadOperation.RequestOptions.EnableContentResponseOnWrite,
+ "Replace concurrently and read builder should enable content response."
+ )
+
+ []
+ member _.``Upsert builders configure operation and content response mode`` () =
+ let upsertItem = { id = "upsert-id"; partitionKey = "pk"; value = 1 }
+
+ let upsertOperation = upsert {
+ item upsertItem
+ partitionKey upsertItem.partitionKey
+ eTag "upsert-etag"
+ }
+
+ let upsertAndReadOperation = upsertAndRead {
+ item upsertItem
+ partitionKey upsertItem.partitionKey
+ }
+
+ Assert.IsTrue (upsertOperation.PartitionKey |> ValueOption.isSome, "Upsert builder should set partition key.")
+ Assert.IsTrue (upsertOperation.RequestOptions.IfMatchEtag = "upsert-etag", "Upsert builder should set eTag.")
+ Assert.IsFalse (
+ upsertOperation.RequestOptions.EnableContentResponseOnWrite,
+ "Upsert builder should disable content response."
+ )
+ Assert.IsTrue (
+ upsertAndReadOperation.RequestOptions.EnableContentResponseOnWrite,
+ "UpsertAndRead builder should enable content response."
+ )
+
+ []
+ member _.``Upsert concurrently builders configure updateOrCreate and response mode`` () =
+ let upsertConcurrentlyOperation = upsertConcurrenly {
+ id "upsert-concurrent-id"
+ partitionKey "pk"
+ updateOrCreate (fun maybeItem -> async {
+ match maybeItem with
+ | Some item -> return Result.Ok { item with value = item.value + 1 }
+ | None -> return Result.Ok { id = "new-id"; partitionKey = "pk"; value = 1 }
+ })
+ }
+
+ let upsertConcurrentlyAndReadOperation = upsertConcurrenlyAndRead {
+ id "upsert-concurrent-and-read-id"
+ partitionKey "pk"
+ updateOrCreate (fun _ -> async { return Error "custom-error" })
+ }
+
+ let updateResult =
+ upsertConcurrentlyOperation.UpdateOrCreate None
+ |> Async.RunSynchronously
+
+ Assert.IsTrue (upsertConcurrentlyOperation.Id = "upsert-concurrent-id", "Upsert concurrently builder should set id.")
+ Assert.IsTrue (Result.isOk updateResult, "Upsert concurrently builder should set updateOrCreate function.")
+ Assert.IsFalse (
+ upsertConcurrentlyOperation.RequestOptions.EnableContentResponseOnWrite,
+ "Upsert concurrently builder should disable content response."
+ )
+ Assert.IsTrue (
+ upsertConcurrentlyAndReadOperation.RequestOptions.EnableContentResponseOnWrite,
+ "Upsert concurrently and read builder should enable content response."
+ )
+
+ []
+ member _.``Patch builders configure operations and content response mode`` () =
+ let patchOperation = patch {
+ id "patch-id"
+ partitionKey "pk"
+ operation (PatchOperation.Replace ("/value", 2))
+ filterPredicate "FROM c WHERE c.partitionKey = 'pk'"
+ }
+
+ let patchAndReadOperation = patchAndRead {
+ id "patch-and-read-id"
+ partitionKey "pk"
+ operation (PatchOperation.Replace ("/value", 5))
+ }
+
+ Assert.IsTrue (patchOperation.Id = "patch-id", "Patch builder should set id.")
+ Assert.IsTrue (patchOperation.Operations.Length = 1, "Patch builder should collect operations.")
+ Assert.IsTrue (
+ patchOperation.RequestOptions.FilterPredicate = "FROM c WHERE c.partitionKey = 'pk'",
+ "Patch builder should set filter predicate."
+ )
+ Assert.IsFalse (
+ patchOperation.RequestOptions.EnableContentResponseOnWrite,
+ "Patch builder should disable content response."
+ )
+ Assert.IsTrue (
+ patchAndReadOperation.RequestOptions.EnableContentResponseOnWrite,
+ "PatchAndRead builder should enable content response."
+ )
+
+ []
+ member _.``Delete builder configures id partition key and request options`` () =
+ let operation = delete {
+ id "delete-id"
+ partitionKey "pk"
+ eTag "delete-etag"
+ sessionToken "delete-session"
+ }
+
+ Assert.IsTrue (operation.Id = "delete-id", "Delete builder should set id.")
+ Assert.IsTrue (operation.RequestOptions |> ValueOption.isSome, "Delete builder should initialize request options.")
+
+ let options = operation.RequestOptions |> ValueOption.get
+ Assert.IsTrue (options.IfNoneMatchEtag = "delete-etag", "Delete builder should set eTag.")
+ Assert.IsTrue (options.SessionToken = "delete-session", "Delete builder should set session token.")
+
+ []
+ member _.``Unique key builders configure key and policy paths`` () =
+ let uniqueKeyDefinition = uniqueKey { paths [ "/tenantId"; "/email" ] }
+ let policy = uniqueKeyPolicy { key uniqueKeyDefinition }
+
+ Assert.IsTrue (uniqueKeyDefinition.Paths.Count = 2, "UniqueKey builder should add all paths.")
+ Assert.IsTrue (policy.UniqueKeys.Count = 1, "UniqueKeyPolicy builder should add unique key.")
diff --git a/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs b/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
new file mode 100644
index 0000000..4dd36b5
--- /dev/null
+++ b/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
@@ -0,0 +1,68 @@
+namespace Tests.Integration
+
+open System.Net
+open System.Threading.Tasks
+open FSharp.Azure.Cosmos
+open Microsoft.Azure.Cosmos
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type CosmosReadExtensionsIntegrationTests () =
+ inherit OperationTestBase ()
+
+ []
+ member this.``CountAsync and LongCountAsync return seeded item counts`` () : Task = task {
+ let! container = this.GetContainer ()
+ let seededItems = [ this.NewItem "count-1"; this.NewItem "count-2"; this.NewItem "count-3" ]
+ do! this.SeedItemsAsync (container, seededItems)
+
+ let! countByPartition = container.CountAsync ("integration", cancellationToken = this.CancellationToken)
+ let! countByQuery = container.CountAsync (QueryRequestOptions (), cancellationToken = this.CancellationToken)
+ let! longCountByPartition =
+ container.LongCountAsync (PartitionKey "integration", cancellationToken = this.CancellationToken)
+
+ Assert.IsTrue ((countByPartition = 3), "CountAsync by partition should return seeded item count.")
+ Assert.IsTrue ((countByQuery = 3), "CountAsync by query options should return seeded item count.")
+ Assert.IsTrue ((longCountByPartition = 3L), "LongCountAsync should return seeded item count.")
+ }
+
+ []
+ member this.``ExistsAsync and IsNotDeletedAsync return expected values`` () : Task = task {
+ let! container = this.GetContainer ()
+ let firstItem = this.NewItem "exists-1"
+ let secondItem = this.NewItem "exists-2"
+ do! this.SeedItemsAsync (container, [ firstItem; secondItem ])
+
+ let! existsWithPartition =
+ container.ExistsAsync (firstItem.id, PartitionKey firstItem.partitionKey, this.CancellationToken)
+
+ let! existsWithoutPartition = container.ExistsAsync (firstItem.id, cancellationToken = this.CancellationToken)
+
+ let! missingExists = container.ExistsAsync ($"{firstItem.id}-missing", cancellationToken = this.CancellationToken)
+
+ Assert.IsTrue (existsWithPartition, "ExistsAsync with partition key should return true for existing item.")
+ Assert.IsTrue (existsWithoutPartition, "ExistsAsync without partition key should return true for existing item.")
+ Assert.IsFalse (missingExists, "ExistsAsync should return false for missing item.")
+
+ let! notDeletedBeforePatch = container.IsNotDeletedAsync "deletedAt" secondItem.id
+
+ Assert.IsTrue (notDeletedBeforePatch, "IsNotDeletedAsync should return true before deleted marker is set.")
+
+ let! patchResponse =
+ container.ExecuteOverwriteAsync (
+ patch {
+ id secondItem.id
+ partitionKey secondItem.partitionKey
+ operation (PatchOperation.Set ("/deletedAt", "2026-05-24T00:00:00Z"))
+ },
+ this.CancellationToken
+ )
+
+ match patchResponse.Result with
+ | PatchResult.Ok _ -> Assert.IsTrue (patchResponse.HttpStatusCode = HttpStatusCode.OK, "Patch should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected patch success, got {result}.")
+
+ let! notDeletedAfterPatch = container.IsNotDeletedAsync "deletedAt" secondItem.id
+
+ Assert.IsFalse (notDeletedAfterPatch, "IsNotDeletedAsync should return false after deleted marker is set.")
+ }
diff --git a/tests/Cosmos.Tests/CreateOperationTests.fs b/tests/Cosmos.Tests/CreateOperationTests.fs
index df76a1a..3062d8d 100644
--- a/tests/Cosmos.Tests/CreateOperationTests.fs
+++ b/tests/Cosmos.Tests/CreateOperationTests.fs
@@ -10,7 +10,7 @@ type CreateOperationIntegrationTests () =
inherit OperationTestBase ()
[]
- member this.Create_execute_persists_item () : Task = task {
+ member this.``Create execute persists item`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "create"
@@ -41,7 +41,7 @@ type CreateOperationIntegrationTests () =
}
[]
- member this.CreateAndRead_execute_returns_created_resource () : Task = task {
+ member this.``CreateAndRead execute returns created resource`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "create-and-read"
diff --git a/tests/Cosmos.Tests/DeleteOperationTests.fs b/tests/Cosmos.Tests/DeleteOperationTests.fs
index c633b81..0d4ae7b 100644
--- a/tests/Cosmos.Tests/DeleteOperationTests.fs
+++ b/tests/Cosmos.Tests/DeleteOperationTests.fs
@@ -10,7 +10,7 @@ type DeleteOperationIntegrationTests () =
inherit OperationTestBase ()
[]
- member this.Delete_execute_removes_item_and_subsequent_read_is_not_found () : Task = task {
+ member this.``Delete execute removes item and subsequent read is not found`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "delete"
diff --git a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
index cc8cae0..768bf45 100644
--- a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
+++ b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
@@ -29,6 +29,9 @@
+
+
+
diff --git a/tests/Cosmos.Tests/IterationExtensionsTests.fs b/tests/Cosmos.Tests/IterationExtensionsTests.fs
new file mode 100644
index 0000000..009b57a
--- /dev/null
+++ b/tests/Cosmos.Tests/IterationExtensionsTests.fs
@@ -0,0 +1,56 @@
+namespace Tests.Integration
+
+open System.Threading.Tasks
+open FSharp.Control
+open Microsoft.Azure.Cosmos
+open Microsoft.Azure.Cosmos.Linq
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+type IterationExtensionsIntegrationTests () =
+ inherit OperationTestBase ()
+
+ []
+ member this.``FeedIterator AsAsyncEnumerable iterates seeded items`` () : Task = task {
+ let! container = this.GetContainer ()
+ let firstItem = this.NewItem "iterator-1"
+ let secondItem = this.NewItem "iterator-2"
+ do! this.SeedItemsAsync (container, [ firstItem; secondItem ])
+
+ let query =
+ QueryDefinition("SELECT * FROM c WHERE c.partitionKey = @partitionKey").WithParameter ("@partitionKey", "integration")
+
+ let iterator = container.GetItemQueryIterator (query)
+ let expectedIds = set [ firstItem.id; secondItem.id ]
+ let! iteratedItems =
+ iterator.AsAsyncEnumerable (this.CancellationToken)
+ |> TaskSeq.toListAsync
+ let foundCount =
+ iteratedItems
+ |> List.filter (fun item -> expectedIds.Contains item.id)
+ |> List.length
+ Assert.IsTrue ((foundCount = 2), "FeedIterator.AsAsyncEnumerable should iterate seeded items.")
+ }
+
+ []
+ member this.``IQueryable AsAsyncEnumerable iterates seeded items`` () : Task = task {
+ let! container = this.GetContainer ()
+ let firstItem = this.NewItem "queryable-1"
+ let secondItem = this.NewItem "queryable-2"
+ do! this.SeedItemsAsync (container, [ firstItem; secondItem ])
+
+ let queryable =
+ container.GetItemLinqQueryable (
+ requestOptions = QueryRequestOptions (PartitionKey = PartitionKey "integration")
+ )
+
+ let expectedIds = set [ firstItem.id; secondItem.id ]
+ let! iteratedItems =
+ queryable.AsAsyncEnumerable (this.CancellationToken)
+ |> TaskSeq.toListAsync
+ let foundCount =
+ iteratedItems
+ |> List.filter (fun item -> expectedIds.Contains item.id)
+ |> List.length
+ Assert.IsTrue ((foundCount = 2), "IQueryable.AsAsyncEnumerable should iterate seeded items.")
+ }
diff --git a/tests/Cosmos.Tests/OperationTestInfrastructure.fs b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
index ba7f91f..05c0b56 100644
--- a/tests/Cosmos.Tests/OperationTestInfrastructure.fs
+++ b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
@@ -2,6 +2,7 @@ namespace Tests.Integration
open System
open System.Threading.Tasks
+open FSharp.Azure.Cosmos
open Microsoft.Azure.Cosmos
type internal TestItem = { id : string; partitionKey : string; name : string; quantity : int }
@@ -35,3 +36,17 @@ type OperationTestBase () =
name = $"item-{suffix}"
quantity = 1
}
+
+ member internal this.SeedItemsAsync (container : Container, items : TestItem seq) : Task = task {
+ for seedItem in items do
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item seedItem
+ partitionKey seedItem.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, $"Seed create should succeed for item '{seedItem.id}'.")
+ }
diff --git a/tests/Cosmos.Tests/PatchOperationTests.fs b/tests/Cosmos.Tests/PatchOperationTests.fs
index d57ee12..9676d57 100644
--- a/tests/Cosmos.Tests/PatchOperationTests.fs
+++ b/tests/Cosmos.Tests/PatchOperationTests.fs
@@ -11,7 +11,7 @@ type PatchOperationIntegrationTests () =
inherit OperationTestBase ()
[]
- member this.Patch_execute_overwrite_updates_item () : Task = task {
+ member this.``Patch execute overwrite updates item`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "patch"
@@ -61,7 +61,7 @@ type PatchOperationIntegrationTests () =
}
[]
- member this.PatchAndRead_execute_overwrite_returns_updated_item () : Task = task {
+ member this.``PatchAndRead execute overwrite returns updated item`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "patch-and-read"
diff --git a/tests/Cosmos.Tests/ReadManyOperationTests.fs b/tests/Cosmos.Tests/ReadManyOperationTests.fs
index 8e02113..615798e 100644
--- a/tests/Cosmos.Tests/ReadManyOperationTests.fs
+++ b/tests/Cosmos.Tests/ReadManyOperationTests.fs
@@ -11,7 +11,7 @@ type ReadManyOperationIntegrationTests () =
inherit OperationTestBase ()
[]
- member this.ReadMany_execute_returns_matching_items () : Task = task {
+ member this.``ReadMany execute returns matching items`` () : Task = task {
let! container = this.GetContainer ()
let firstItem = this.NewItem "readmany-1"
let secondItem = this.NewItem "readmany-2"
diff --git a/tests/Cosmos.Tests/ReadOperationTests.fs b/tests/Cosmos.Tests/ReadOperationTests.fs
index 233f39b..74a0808 100644
--- a/tests/Cosmos.Tests/ReadOperationTests.fs
+++ b/tests/Cosmos.Tests/ReadOperationTests.fs
@@ -10,7 +10,7 @@ type ReadOperationIntegrationTests () =
inherit OperationTestBase ()
[]
- member this.Read_execute_returns_existing_and_not_found_states () : Task = task {
+ member this.``Read execute returns existing and not found states`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "read"
diff --git a/tests/Cosmos.Tests/ReplaceOperationTests.fs b/tests/Cosmos.Tests/ReplaceOperationTests.fs
index 5684f5b..f915813 100644
--- a/tests/Cosmos.Tests/ReplaceOperationTests.fs
+++ b/tests/Cosmos.Tests/ReplaceOperationTests.fs
@@ -10,7 +10,7 @@ type ReplaceOperationIntegrationTests () =
inherit OperationTestBase ()
[]
- member this.Replace_execute_overwrite_replaces_existing_item () : Task = task {
+ member this.``Replace execute overwrite replaces existing item`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "replace"
@@ -55,7 +55,7 @@ type ReplaceOperationIntegrationTests () =
}
[]
- member this.ReplaceAndRead_execute_overwrite_returns_replaced_item () : Task = task {
+ member this.``ReplaceAndRead execute overwrite returns replaced item`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "replace-and-read"
@@ -88,3 +88,62 @@ type ReplaceOperationIntegrationTests () =
Assert.IsTrue (replacement.quantity = replaced.quantity, "ReplaceAndRead should return replacement quantity.")
Assert.IsTrue (replaceResponse.HttpStatusCode = HttpStatusCode.OK, "ReplaceAndRead should return HTTP 200.")
}
+
+ []
+ member this.``Replace concurrently retries and applies update`` () : Task = task {
+ let! container = this.GetContainer ()
+ let original = this.NewItem "replace-concurrent"
+
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item original
+ partitionKey original.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, "Seed create should succeed.")
+
+ let mutable conflictInjected = false
+
+ let operation = replaceConcurrenly {
+ id original.id
+ partitionKey original.partitionKey
+ update (fun current -> async {
+ if not conflictInjected then
+ conflictInjected <- true
+
+ let competingUpdate = { current with name = "competing-update" }
+
+ let! _ =
+ container.ExecuteOverwriteAsync (
+ replace {
+ id competingUpdate.id
+ item competingUpdate
+ partitionKey competingUpdate.partitionKey
+ },
+ this.CancellationToken
+ )
+ |> Async.AwaitTask
+
+ ()
+
+ return
+ Result.Ok {
+ current with
+ name = "replace-concurrent-updated"
+ quantity = current.quantity + 10
+ }
+ })
+ }
+
+ let! concurrentResponse = container.ExecuteConcurrentlyAsync (operation, 3, this.CancellationToken)
+
+ match concurrentResponse.Result with
+ | ReplaceConcurrentResult.Ok updated ->
+ Assert.IsTrue (conflictInjected, "Replace concurrently test should inject a conflicting update at least once.")
+ Assert.IsTrue (updated.name = "replace-concurrent-updated", "Replace concurrently should persist updated name.")
+ Assert.IsTrue (updated.quantity = original.quantity + 10, "Replace concurrently should persist updated quantity.")
+ | result -> Assert.Fail ($"Expected replace concurrently success after retry, got {result}.")
+ }
diff --git a/tests/Cosmos.Tests/UpsertOperationTests.fs b/tests/Cosmos.Tests/UpsertOperationTests.fs
index 2557435..f775baa 100644
--- a/tests/Cosmos.Tests/UpsertOperationTests.fs
+++ b/tests/Cosmos.Tests/UpsertOperationTests.fs
@@ -10,7 +10,7 @@ type UpsertOperationIntegrationTests () =
inherit OperationTestBase ()
[]
- member this.Upsert_execute_overwrite_creates_then_updates_item () : Task = task {
+ member this.``Upsert execute overwrite creates then updates item`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "upsert"
@@ -59,7 +59,7 @@ type UpsertOperationIntegrationTests () =
}
[]
- member this.UpsertAndRead_execute_overwrite_returns_updated_item () : Task = task {
+ member this.``UpsertAndRead execute overwrite returns updated item`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "upsert-and-read"
@@ -99,3 +99,64 @@ type UpsertOperationIntegrationTests () =
Assert.IsTrue (updatedResponse.HttpStatusCode = HttpStatusCode.OK, "UpsertAndRead update should return HTTP 200.")
| result -> Assert.Fail ($"Expected upsertAndRead update success, got {result}.")
}
+
+ []
+ member this.``Upsert concurrently retries and applies update`` () : Task = task {
+ let! container = this.GetContainer ()
+ let original = this.NewItem "upsert-concurrent"
+
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item original
+ partitionKey original.partitionKey
+ },
+ this.CancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, "Seed create should succeed.")
+
+ let mutable conflictInjected = false
+
+ let operation = upsertConcurrenly {
+ id original.id
+ partitionKey original.partitionKey
+ updateOrCreate (fun maybeCurrent -> async {
+ match maybeCurrent with
+ | Some current ->
+ if not conflictInjected then
+ conflictInjected <- true
+
+ let competingUpdate = { current with name = "competing-upsert-update" }
+
+ let! _ =
+ container.ExecuteOverwriteAsync (
+ upsert {
+ item competingUpdate
+ partitionKey competingUpdate.partitionKey
+ },
+ this.CancellationToken
+ )
+ |> Async.AwaitTask
+
+ ()
+
+ return
+ Result.Ok {
+ current with
+ name = "upsert-concurrent-updated"
+ quantity = current.quantity + 7
+ }
+ | None -> return Result.Error "Expected existing item for concurrent upsert test."
+ })
+ }
+
+ let! concurrentResponse = container.ExecuteConcurrentlyAsync (operation, 3, this.CancellationToken)
+
+ match concurrentResponse.Result with
+ | UpsertConcurrentResult.Ok updated ->
+ Assert.IsTrue (conflictInjected, "Upsert concurrently test should inject a conflicting update at least once.")
+ Assert.IsTrue (updated.name = "upsert-concurrent-updated", "Upsert concurrently should persist updated name.")
+ Assert.IsTrue (updated.quantity = original.quantity + 7, "Upsert concurrently should persist updated quantity.")
+ | result -> Assert.Fail ($"Expected upsert concurrently success after retry, got {result}.")
+ }
From 31d54c0c9cf03816f082ba92dc96815c59bf0ed2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 14:32:46 +0000
Subject: [PATCH 16/33] Add Assert extensions and align test namespaces
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/6f71a1f0-316c-4294-88a6-23f8995238fc
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/Assert.fs | 72 +++++++++++++++++++
tests/Cosmos.Tests/BuilderUnitTests.fs | 15 ++--
tests/Cosmos.Tests/CosmosAssert.fs | 2 +-
.../Cosmos.Tests/CosmosReadExtensionsTests.fs | 2 +-
tests/Cosmos.Tests/CreateOperationTests.fs | 2 +-
tests/Cosmos.Tests/DeleteOperationTests.fs | 2 +-
.../FSharp.Azure.Cosmos.Tests.fsproj | 1 +
.../Cosmos.Tests/IntegrationInfrastructure.fs | 2 +-
tests/Cosmos.Tests/IntegrationTestPlan.fs | 2 +-
.../Cosmos.Tests/IterationExtensionsTests.fs | 2 +-
.../OperationTestInfrastructure.fs | 2 +-
tests/Cosmos.Tests/PatchOperationTests.fs | 2 +-
tests/Cosmos.Tests/ReadManyOperationTests.fs | 2 +-
tests/Cosmos.Tests/ReadOperationTests.fs | 2 +-
tests/Cosmos.Tests/ReplaceOperationTests.fs | 2 +-
tests/Cosmos.Tests/UpsertOperationTests.fs | 2 +-
16 files changed, 93 insertions(+), 21 deletions(-)
create mode 100644 tests/Cosmos.Tests/Assert.fs
diff --git a/tests/Cosmos.Tests/Assert.fs b/tests/Cosmos.Tests/Assert.fs
new file mode 100644
index 0000000..c73402e
--- /dev/null
+++ b/tests/Cosmos.Tests/Assert.fs
@@ -0,0 +1,72 @@
+namespace FSharp.Azure.Cosmos.Tests
+
+open System.Runtime.InteropServices
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+[]
+module AssertExtensions =
+
+ type Assert with
+
+ static member WantSome (value, [] message) =
+ match value with
+ | Some some -> some
+ | None ->
+ Assert.Fail (message)
+ Unchecked.defaultof<_>
+
+ static member IsSome (value, [] message) = Assert.WantSome (value, message) |> ignore
+
+ static member IsNone (value, [] message) =
+ match value with
+ | Some _ -> Assert.Fail (message)
+ | None -> ()
+
+ static member WantValueSome (value, [] message) =
+ match value with
+ | ValueSome some -> some
+ | ValueNone ->
+ Assert.Fail (message)
+ Unchecked.defaultof<_>
+
+ static member IsValueSome (value, [] message) = Assert.WantValueSome (value, message) |> ignore
+
+ static member IsValueNone (value, [] message) =
+ match value with
+ | ValueSome _ -> Assert.Fail (message)
+ | ValueNone -> ()
+
+ static member WantOk (value, [] message : string | null) =
+ match value with
+ | Ok ok -> ok
+ | Error error ->
+ match message with
+ | null -> Assert.Fail (string error)
+ | message -> Assert.Fail ($"'{message}': {error}")
+ Unchecked.defaultof<_>
+
+ static member IsOk (value, [] message) = Assert.WantOk (value, message) |> ignore
+
+ static member WantError (value, [] message : string | null) =
+ match value with
+ | Error error -> error
+ | Ok value ->
+ match message with
+ | null -> Assert.Fail (string value)
+ | message -> Assert.Fail ($"'{message}': {value}")
+ Unchecked.defaultof<_>
+
+ static member IsError (value, [] message) = Assert.WantError (value, message) |> ignore
+
+ static member inline IsDefaultOf< ^T> (value : ^T, [] message : string) =
+ Assert.AreEqual (box value, box Unchecked.defaultof< ^T>, message)
+
+ static member inline OkEquals< ^R, 'E> (expected : ^R, actual : Result< ^R, 'E >, [] message) =
+ Assert.AreEqual (box expected, box (Assert.WantOk (actual, message)), message)
+
+ static member inline ErrorEquals<'R, ^E> (expected : ^E, actual : Result<'R, ^E>, [] message) =
+ Assert.AreEqual (box expected, box (Assert.WantError (actual, message)), message)
+
+ static member FailWithData<'T> ([] message) =
+ Assert.Fail (message)
+ Unchecked.defaultof<'T>
diff --git a/tests/Cosmos.Tests/BuilderUnitTests.fs b/tests/Cosmos.Tests/BuilderUnitTests.fs
index 913c45c..3eb5478 100644
--- a/tests/Cosmos.Tests/BuilderUnitTests.fs
+++ b/tests/Cosmos.Tests/BuilderUnitTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Unit
+namespace FSharp.Azure.Cosmos.Tests
open System
open FSharp.Azure.Cosmos
@@ -26,7 +26,7 @@ type BuilderUnitTests () =
sessionToken "create-and-read-session"
}
- Assert.IsTrue (createOperation.PartitionKey |> ValueOption.isSome, "Create builder should set partition key.")
+ Assert.IsValueSome (createOperation.PartitionKey, "Create builder should set partition key.")
Assert.IsTrue (createOperation.RequestOptions.SessionToken = "create-session", "Create builder should set session token.")
Assert.IsFalse (
createOperation.RequestOptions.EnableContentResponseOnWrite,
@@ -110,7 +110,7 @@ type BuilderUnitTests () =
|> Async.RunSynchronously
Assert.IsTrue (replaceConcurrentlyOperation.Id = "replace-concurrent-id", "Replace concurrently builder should set id.")
- Assert.IsTrue (Result.isOk updateResult, "Replace concurrently builder should set update function.")
+ Assert.IsOk (updateResult, "Replace concurrently builder should set update function.")
Assert.IsFalse (
replaceConcurrentlyOperation.RequestOptions.EnableContentResponseOnWrite,
"Replace concurrently builder should disable content response."
@@ -135,7 +135,7 @@ type BuilderUnitTests () =
partitionKey upsertItem.partitionKey
}
- Assert.IsTrue (upsertOperation.PartitionKey |> ValueOption.isSome, "Upsert builder should set partition key.")
+ Assert.IsValueSome (upsertOperation.PartitionKey, "Upsert builder should set partition key.")
Assert.IsTrue (upsertOperation.RequestOptions.IfMatchEtag = "upsert-etag", "Upsert builder should set eTag.")
Assert.IsFalse (
upsertOperation.RequestOptions.EnableContentResponseOnWrite,
@@ -169,7 +169,7 @@ type BuilderUnitTests () =
|> Async.RunSynchronously
Assert.IsTrue (upsertConcurrentlyOperation.Id = "upsert-concurrent-id", "Upsert concurrently builder should set id.")
- Assert.IsTrue (Result.isOk updateResult, "Upsert concurrently builder should set updateOrCreate function.")
+ Assert.IsOk (updateResult, "Upsert concurrently builder should set updateOrCreate function.")
Assert.IsFalse (
upsertConcurrentlyOperation.RequestOptions.EnableContentResponseOnWrite,
"Upsert concurrently builder should disable content response."
@@ -219,9 +219,8 @@ type BuilderUnitTests () =
}
Assert.IsTrue (operation.Id = "delete-id", "Delete builder should set id.")
- Assert.IsTrue (operation.RequestOptions |> ValueOption.isSome, "Delete builder should initialize request options.")
-
- let options = operation.RequestOptions |> ValueOption.get
+ let options =
+ Assert.WantValueSome (operation.RequestOptions, "Delete builder should initialize request options.")
Assert.IsTrue (options.IfNoneMatchEtag = "delete-etag", "Delete builder should set eTag.")
Assert.IsTrue (options.SessionToken = "delete-session", "Delete builder should set session token.")
diff --git a/tests/Cosmos.Tests/CosmosAssert.fs b/tests/Cosmos.Tests/CosmosAssert.fs
index 2dc7274..6571565 100644
--- a/tests/Cosmos.Tests/CosmosAssert.fs
+++ b/tests/Cosmos.Tests/CosmosAssert.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System
open System.Diagnostics
diff --git a/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs b/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
index 4dd36b5..487e0eb 100644
--- a/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
+++ b/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
diff --git a/tests/Cosmos.Tests/CreateOperationTests.fs b/tests/Cosmos.Tests/CreateOperationTests.fs
index 3062d8d..fd0914e 100644
--- a/tests/Cosmos.Tests/CreateOperationTests.fs
+++ b/tests/Cosmos.Tests/CreateOperationTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
diff --git a/tests/Cosmos.Tests/DeleteOperationTests.fs b/tests/Cosmos.Tests/DeleteOperationTests.fs
index 0d4ae7b..25518a2 100644
--- a/tests/Cosmos.Tests/DeleteOperationTests.fs
+++ b/tests/Cosmos.Tests/DeleteOperationTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
diff --git a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
index 768bf45..c8ecfe0 100644
--- a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
+++ b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
@@ -18,6 +18,7 @@
+
diff --git a/tests/Cosmos.Tests/IntegrationInfrastructure.fs b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
index 099a6c1..46011f1 100644
--- a/tests/Cosmos.Tests/IntegrationInfrastructure.fs
+++ b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System
open System.Threading
diff --git a/tests/Cosmos.Tests/IntegrationTestPlan.fs b/tests/Cosmos.Tests/IntegrationTestPlan.fs
index d74b934..8409cc5 100644
--- a/tests/Cosmos.Tests/IntegrationTestPlan.fs
+++ b/tests/Cosmos.Tests/IntegrationTestPlan.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
module IntegrationTestPlan =
diff --git a/tests/Cosmos.Tests/IterationExtensionsTests.fs b/tests/Cosmos.Tests/IterationExtensionsTests.fs
index 009b57a..5da81d8 100644
--- a/tests/Cosmos.Tests/IterationExtensionsTests.fs
+++ b/tests/Cosmos.Tests/IterationExtensionsTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Threading.Tasks
open FSharp.Control
diff --git a/tests/Cosmos.Tests/OperationTestInfrastructure.fs b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
index 05c0b56..263e3c1 100644
--- a/tests/Cosmos.Tests/OperationTestInfrastructure.fs
+++ b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System
open System.Threading.Tasks
diff --git a/tests/Cosmos.Tests/PatchOperationTests.fs b/tests/Cosmos.Tests/PatchOperationTests.fs
index 9676d57..8db5ae7 100644
--- a/tests/Cosmos.Tests/PatchOperationTests.fs
+++ b/tests/Cosmos.Tests/PatchOperationTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
diff --git a/tests/Cosmos.Tests/ReadManyOperationTests.fs b/tests/Cosmos.Tests/ReadManyOperationTests.fs
index 615798e..ac9fec4 100644
--- a/tests/Cosmos.Tests/ReadManyOperationTests.fs
+++ b/tests/Cosmos.Tests/ReadManyOperationTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
diff --git a/tests/Cosmos.Tests/ReadOperationTests.fs b/tests/Cosmos.Tests/ReadOperationTests.fs
index 74a0808..25f5642 100644
--- a/tests/Cosmos.Tests/ReadOperationTests.fs
+++ b/tests/Cosmos.Tests/ReadOperationTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
diff --git a/tests/Cosmos.Tests/ReplaceOperationTests.fs b/tests/Cosmos.Tests/ReplaceOperationTests.fs
index f915813..05c90b0 100644
--- a/tests/Cosmos.Tests/ReplaceOperationTests.fs
+++ b/tests/Cosmos.Tests/ReplaceOperationTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
diff --git a/tests/Cosmos.Tests/UpsertOperationTests.fs b/tests/Cosmos.Tests/UpsertOperationTests.fs
index f775baa..fad153d 100644
--- a/tests/Cosmos.Tests/UpsertOperationTests.fs
+++ b/tests/Cosmos.Tests/UpsertOperationTests.fs
@@ -1,4 +1,4 @@
-namespace Tests.Integration
+namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
From d63fbff76b291e182c24319f97df9800c293c414 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 14:33:40 +0000
Subject: [PATCH 17/33] Fix Assert extension signatures and keep test helper
usage
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/6f71a1f0-316c-4294-88a6-23f8995238fc
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/Assert.fs | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/tests/Cosmos.Tests/Assert.fs b/tests/Cosmos.Tests/Assert.fs
index c73402e..7c6e450 100644
--- a/tests/Cosmos.Tests/Assert.fs
+++ b/tests/Cosmos.Tests/Assert.fs
@@ -8,30 +8,30 @@ module AssertExtensions =
type Assert with
- static member WantSome (value, [] message) =
+ static member WantSome (value, [] message : string | null) =
match value with
| Some some -> some
| None ->
Assert.Fail (message)
Unchecked.defaultof<_>
- static member IsSome (value, [] message) = Assert.WantSome (value, message) |> ignore
+ static member IsSome (value, [] message : string | null) = Assert.WantSome (value, message) |> ignore
- static member IsNone (value, [] message) =
+ static member IsNone (value, [] message : string | null) =
match value with
| Some _ -> Assert.Fail (message)
| None -> ()
- static member WantValueSome (value, [] message) =
+ static member WantValueSome (value, [] message : string | null) =
match value with
| ValueSome some -> some
| ValueNone ->
Assert.Fail (message)
Unchecked.defaultof<_>
- static member IsValueSome (value, [] message) = Assert.WantValueSome (value, message) |> ignore
+ static member IsValueSome (value, [] message : string | null) = Assert.WantValueSome (value, message) |> ignore
- static member IsValueNone (value, [] message) =
+ static member IsValueNone (value, [] message : string | null) =
match value with
| ValueSome _ -> Assert.Fail (message)
| ValueNone -> ()
@@ -45,7 +45,7 @@ module AssertExtensions =
| message -> Assert.Fail ($"'{message}': {error}")
Unchecked.defaultof<_>
- static member IsOk (value, [] message) = Assert.WantOk (value, message) |> ignore
+ static member IsOk (value, [] message : string | null) = Assert.WantOk (value, message) |> ignore
static member WantError (value, [] message : string | null) =
match value with
@@ -56,17 +56,17 @@ module AssertExtensions =
| message -> Assert.Fail ($"'{message}': {value}")
Unchecked.defaultof<_>
- static member IsError (value, [] message) = Assert.WantError (value, message) |> ignore
+ static member IsError (value, [] message : string | null) = Assert.WantError (value, message) |> ignore
static member inline IsDefaultOf< ^T> (value : ^T, [] message : string) =
Assert.AreEqual (box value, box Unchecked.defaultof< ^T>, message)
- static member inline OkEquals< ^R, 'E> (expected : ^R, actual : Result< ^R, 'E >, [] message) =
+ static member inline OkEquals< ^R, 'E> (expected : ^R, actual : Result< ^R, 'E >, [] message : string | null) =
Assert.AreEqual (box expected, box (Assert.WantOk (actual, message)), message)
- static member inline ErrorEquals<'R, ^E> (expected : ^E, actual : Result<'R, ^E>, [] message) =
+ static member inline ErrorEquals<'R, ^E> (expected : ^E, actual : Result<'R, ^E>, [] message : string | null) =
Assert.AreEqual (box expected, box (Assert.WantError (actual, message)), message)
- static member FailWithData<'T> ([] message) =
+ static member FailWithData<'T> ([] message : string | null) =
Assert.Fail (message)
Unchecked.defaultof<'T>
From 60e97b6773fa3e7b40cf9bba22a07277a01b2fed Mon Sep 17 00:00:00 2001
From: Andrii Chebukin
Date: Sun, 24 May 2026 16:50:55 +0200
Subject: [PATCH 18/33] fixup! Split operation integration tests into
per-operation files with AndRead coverage
---
tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj | 2 --
1 file changed, 2 deletions(-)
diff --git a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
index c8ecfe0..5971707 100644
--- a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
+++ b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
@@ -22,7 +22,6 @@
-
@@ -33,7 +32,6 @@
-
From e1305d37c55ebe20977b58a39457d2d248e04a93 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 15:14:30 +0000
Subject: [PATCH 19/33] Delete IntegrationTestPlan file per PR feedback
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/a905fb7f-9390-4144-84f7-22ff34a8fb8c
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
.../FSharp.Azure.Cosmos.Tests.fsproj | 1 -
tests/Cosmos.Tests/IntegrationTestPlan.fs | 16 ----------------
2 files changed, 17 deletions(-)
delete mode 100644 tests/Cosmos.Tests/IntegrationTestPlan.fs
diff --git a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
index 5971707..e3180d3 100644
--- a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
+++ b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
@@ -21,7 +21,6 @@
-
diff --git a/tests/Cosmos.Tests/IntegrationTestPlan.fs b/tests/Cosmos.Tests/IntegrationTestPlan.fs
deleted file mode 100644
index 8409cc5..0000000
--- a/tests/Cosmos.Tests/IntegrationTestPlan.fs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace FSharp.Azure.Cosmos.Tests.Integration
-
-module IntegrationTestPlan =
-
- let plannedScenarios : string array = [|
- "Create item: validates id/partition key persistence and response diagnostics."
- "Read item: validates successful reads and not-found responses."
- "Upsert item: validates insert-then-update behavior and returned resource state."
- "Replace item: validates optimistic concurrency with ETag preconditions."
- "Patch item: validates additive and replace patch operations for partial updates."
- "Delete item: validates deletion and subsequent not-found behavior."
- "Read many: validates batch read behavior across partitions."
- "Iterator/TaskSeq reads: validates paging and continuation behavior."
- "Unique key constraints: validates duplicate write failures."
- "Seeded data scenarios: empty container, single partition set, multi-partition set."
- |]
From 5d796346fcee81c857b523a310b6a47d4327533d Mon Sep 17 00:00:00 2001
From: Andrii Chebukin
Date: Sun, 24 May 2026 17:41:35 +0200
Subject: [PATCH 20/33] Apply suggestions from code review
Co-authored-by: Andrii Chebukin
---
tests/Cosmos.Tests/CosmosAssert.fs | 1 +
tests/Cosmos.Tests/IterationExtensionsTests.fs | 8 ++++----
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/tests/Cosmos.Tests/CosmosAssert.fs b/tests/Cosmos.Tests/CosmosAssert.fs
index 6571565..96b1edd 100644
--- a/tests/Cosmos.Tests/CosmosAssert.fs
+++ b/tests/Cosmos.Tests/CosmosAssert.fs
@@ -13,6 +13,7 @@ open Microsoft.VisualStudio.TestTools.UnitTesting
[]
type CosmosAssert private () =
+
static member private GetMessageOrDefault (message : string) (defaultMessage : string) =
if String.IsNullOrWhiteSpace message then
defaultMessage
diff --git a/tests/Cosmos.Tests/IterationExtensionsTests.fs b/tests/Cosmos.Tests/IterationExtensionsTests.fs
index 5da81d8..f3bc66f 100644
--- a/tests/Cosmos.Tests/IterationExtensionsTests.fs
+++ b/tests/Cosmos.Tests/IterationExtensionsTests.fs
@@ -27,8 +27,8 @@ type IterationExtensionsIntegrationTests () =
|> TaskSeq.toListAsync
let foundCount =
iteratedItems
- |> List.filter (fun item -> expectedIds.Contains item.id)
- |> List.length
+ |> Seq.filter (fun item -> expectedIds.Contains item.id)
+ |> Seq.length
Assert.IsTrue ((foundCount = 2), "FeedIterator.AsAsyncEnumerable should iterate seeded items.")
}
@@ -50,7 +50,7 @@ type IterationExtensionsIntegrationTests () =
|> TaskSeq.toListAsync
let foundCount =
iteratedItems
- |> List.filter (fun item -> expectedIds.Contains item.id)
- |> List.length
+ |> Seq.filter (fun item -> expectedIds.Contains item.id)
+ |> Seq.length
Assert.IsTrue ((foundCount = 2), "IQueryable.AsAsyncEnumerable should iterate seeded items.")
}
From f62634da3b3321afa52ea25bc37705abc6c9fd99 Mon Sep 17 00:00:00 2001
From: Andrii Chebukin
Date: Sun, 24 May 2026 21:01:21 +0200
Subject: [PATCH 21/33] fixup! ci(cosmos): use separate common action to check
Azure Cosmos Emulator readiness
---
.../Cosmos.Tests/IntegrationInfrastructure.fs | 27 ++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/tests/Cosmos.Tests/IntegrationInfrastructure.fs b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
index 46011f1..8bc90b4 100644
--- a/tests/Cosmos.Tests/IntegrationInfrastructure.fs
+++ b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
@@ -1,6 +1,9 @@
namespace FSharp.Azure.Cosmos.Tests.Integration
open System
+open System.Net
+open System.Net.Http
+open System.Net.Security
open System.Threading
open System.Threading.Tasks
@@ -51,8 +54,30 @@ type DatabaseTestApplicationFactory (testContext : TestContext) =
let buildDatabaseId () = testContext.GetTestDatabaseIdentifier ()
let databaseId = buildDatabaseId ()
+
+ let isLocalEmulatorHost (uri : Uri) =
+ uri.Host.Equals ("localhost", StringComparison.OrdinalIgnoreCase)
+ || uri.Host.Equals ("127.0.0.1", StringComparison.OrdinalIgnoreCase)
+
+ let createHttpClient () =
+ let handler = new HttpClientHandler ()
+
+ handler.ServerCertificateCustomValidationCallback <-
+ (fun request _ _ errors ->
+ match request.RequestUri with
+ | null -> errors = SslPolicyErrors.None
+ | requestUri when errors = SslPolicyErrors.None -> true
+ | requestUri -> isLocalEmulatorHost requestUri
+ )
+
+ new HttpClient (handler, true)
+
let client =
- new CosmosClient (endpoint, primaryKey, CosmosClientOptions (ConnectionMode = ConnectionMode.Gateway))
+ new CosmosClient (
+ endpoint,
+ primaryKey,
+ CosmosClientOptions (ConnectionMode = ConnectionMode.Gateway, HttpClientFactory = Func createHttpClient)
+ )
let mutable database = ValueNone
member _.Client = client
From cd491da9f4f312b7bfef18c93138d77f87e1c743 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 21:32:50 +0000
Subject: [PATCH 22/33] Fix failing Cosmos emulator tests
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/b2eda2fa-a0cb-4604-b8ca-51bd73075963
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
src/Cosmos/Cosmos.fs | 3 ++-
tests/Cosmos.Tests/OperationTestInfrastructure.fs | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/Cosmos/Cosmos.fs b/src/Cosmos/Cosmos.fs
index 5f0e742..13e4069 100644
--- a/src/Cosmos/Cosmos.fs
+++ b/src/Cosmos/Cosmos.fs
@@ -223,7 +223,8 @@ module Operations =
QueryDefinition(
$"SELECT VALUE COUNT(1) \
FROM item \
- WHERE item.id = @Id AND IS_NULL(item.{deletedFieldName})"
+ WHERE item.id = @Id \
+ AND (NOT IS_DEFINED(item.{deletedFieldName}) OR IS_NULL(item.{deletedFieldName}))"
)
.WithParameter ("@Id", id)
let! count =
diff --git a/tests/Cosmos.Tests/OperationTestInfrastructure.fs b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
index 263e3c1..200dbdb 100644
--- a/tests/Cosmos.Tests/OperationTestInfrastructure.fs
+++ b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
@@ -5,7 +5,8 @@ open System.Threading.Tasks
open FSharp.Azure.Cosmos
open Microsoft.Azure.Cosmos
-type internal TestItem = { id : string; partitionKey : string; name : string; quantity : int }
+[]
+type TestItem = { id : string; partitionKey : string; name : string; quantity : int }
[]
type OperationTestBase () =
From d53c998bc7e5c3b3ed7725050b867c881ba124be Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 21:33:27 +0000
Subject: [PATCH 23/33] Handle undefined deleted marker in IsNotDeletedAsync
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/b2eda2fa-a0cb-4604-b8ca-51bd73075963
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
src/Cosmos/Cosmos.fs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Cosmos/Cosmos.fs b/src/Cosmos/Cosmos.fs
index 13e4069..b102108 100644
--- a/src/Cosmos/Cosmos.fs
+++ b/src/Cosmos/Cosmos.fs
@@ -221,10 +221,10 @@ module Operations =
task {
let query =
QueryDefinition(
- $"SELECT VALUE COUNT(1) \
- FROM item \
- WHERE item.id = @Id \
- AND (NOT IS_DEFINED(item.{deletedFieldName}) OR IS_NULL(item.{deletedFieldName}))"
+ $"""SELECT VALUE COUNT(1)
+ FROM item
+ WHERE item.id = @Id
+ AND (NOT IS_DEFINED(item.{deletedFieldName}) OR IS_NULL(item.{deletedFieldName}))"""
)
.WithParameter ("@Id", id)
let! count =
From 829a0537149cbe72f1850ba14b5c7e3ee4071302 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 21:34:06 +0000
Subject: [PATCH 24/33] Validate deleted field name in IsNotDeletedAsync query
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/b2eda2fa-a0cb-4604-b8ca-51bd73075963
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
src/Cosmos/Cosmos.fs | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/src/Cosmos/Cosmos.fs b/src/Cosmos/Cosmos.fs
index b102108..5b21e35 100644
--- a/src/Cosmos/Cosmos.fs
+++ b/src/Cosmos/Cosmos.fs
@@ -219,6 +219,16 @@ module Operations =
(id : string, [] requiestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken)
=
task {
+ let isValidDeletedFieldName =
+ not (String.IsNullOrWhiteSpace deletedFieldName)
+ && (deletedFieldName[0] = '_' || Char.IsLetter deletedFieldName[0])
+ && deletedFieldName |> Seq.forall (fun c -> c = '_' || Char.IsLetterOrDigit c)
+
+ if not isValidDeletedFieldName then
+ invalidArg
+ (nameof deletedFieldName)
+ "Deleted field name must start with a letter or underscore and contain only letters, digits, or underscores."
+
let query =
QueryDefinition(
$"""SELECT VALUE COUNT(1)
From a06c43a351a4282aa925cbdac1a6230cd7a2b38c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 21:35:06 +0000
Subject: [PATCH 25/33] Harden IsNotDeletedAsync field-name validation
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/b2eda2fa-a0cb-4604-b8ca-51bd73075963
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
src/Cosmos/Cosmos.fs | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/src/Cosmos/Cosmos.fs b/src/Cosmos/Cosmos.fs
index 5b21e35..fc53b5e 100644
--- a/src/Cosmos/Cosmos.fs
+++ b/src/Cosmos/Cosmos.fs
@@ -215,14 +215,22 @@ module Operations =
/// Partition key
/// Cancellation token
member container.IsNotDeletedAsync
- deletedFieldName
+ (deletedFieldName : string | null)
(id : string, [] requiestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken)
=
task {
+ let isAsciiLetter c = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
+ let isAsciiDigit c = '0' <= c && c <= '9'
+
let isValidDeletedFieldName =
- not (String.IsNullOrWhiteSpace deletedFieldName)
- && (deletedFieldName[0] = '_' || Char.IsLetter deletedFieldName[0])
- && deletedFieldName |> Seq.forall (fun c -> c = '_' || Char.IsLetterOrDigit c)
+ match deletedFieldName with
+ | null -> false
+ | name when String.IsNullOrWhiteSpace name -> false
+ | name ->
+ let firstCharacter = name[0]
+ let hasValidStart = firstCharacter = '_' || isAsciiLetter firstCharacter
+ let hasValidBody = name |> Seq.forall (fun c -> c = '_' || isAsciiLetter c || isAsciiDigit c)
+ hasValidStart && hasValidBody
if not isValidDeletedFieldName then
invalidArg
From 5db799cccb4022bc62eab14094e68bc680f9ffdc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 22:19:25 +0000
Subject: [PATCH 26/33] Address review feedback for nullArg, test categories,
and scenario seeding
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/fb87fcd3-5c25-4e1f-ab2c-0f29ce614280
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
src/Cosmos/Cosmos.fs | 25 ++++---
tests/Cosmos.Tests/BuilderUnitTests.fs | 2 +-
.../Cosmos.Tests/CosmosReadExtensionsTests.fs | 51 ++++++++++++-
tests/Cosmos.Tests/CreateOperationTests.fs | 3 +-
tests/Cosmos.Tests/DeleteOperationTests.fs | 3 +-
.../FSharp.Azure.Cosmos.Tests.fsproj | 1 +
.../Cosmos.Tests/IntegrationInfrastructure.fs | 35 +++++++--
.../Cosmos.Tests/IterationExtensionsTests.fs | 21 ++++--
.../OperationTestInfrastructure.fs | 26 +++----
tests/Cosmos.Tests/PatchOperationTests.fs | 3 +-
tests/Cosmos.Tests/ReadManyOperationTests.fs | 73 ++++++++++++-------
tests/Cosmos.Tests/ReadOperationTests.fs | 44 ++++++++---
tests/Cosmos.Tests/ReplaceOperationTests.fs | 3 +-
tests/Cosmos.Tests/TestCategories.fs | 33 +++++++++
tests/Cosmos.Tests/UpsertOperationTests.fs | 3 +-
15 files changed, 244 insertions(+), 82 deletions(-)
create mode 100644 tests/Cosmos.Tests/TestCategories.fs
diff --git a/src/Cosmos/Cosmos.fs b/src/Cosmos/Cosmos.fs
index fc53b5e..c379fcc 100644
--- a/src/Cosmos/Cosmos.fs
+++ b/src/Cosmos/Cosmos.fs
@@ -219,27 +219,34 @@ module Operations =
(id : string, [] requiestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken)
=
task {
+ let deletedFieldName =
+ match deletedFieldName with
+ | null -> nullArg (nameof deletedFieldName)
+ | deletedFieldName -> deletedFieldName
+
let isAsciiLetter c = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
let isAsciiDigit c = '0' <= c && c <= '9'
let isValidDeletedFieldName =
- match deletedFieldName with
- | null -> false
- | name when String.IsNullOrWhiteSpace name -> false
- | name ->
- let firstCharacter = name[0]
+ if String.IsNullOrWhiteSpace deletedFieldName then
+ false
+ else
+ let firstCharacter = deletedFieldName[0]
let hasValidStart = firstCharacter = '_' || isAsciiLetter firstCharacter
- let hasValidBody = name |> Seq.forall (fun c -> c = '_' || isAsciiLetter c || isAsciiDigit c)
+ let hasValidBody =
+ deletedFieldName
+ |> Seq.forall (fun c -> c = '_' || isAsciiLetter c || isAsciiDigit c)
+
hasValidStart && hasValidBody
if not isValidDeletedFieldName then
invalidArg
- (nameof deletedFieldName)
- "Deleted field name must start with a letter or underscore and contain only letters, digits, or underscores."
+ (nameof deletedFieldName)
+ "Deleted field name must start with a letter or underscore and contain only letters, digits, or underscores."
let query =
QueryDefinition(
- $"""SELECT VALUE COUNT(1)
+ $"""SELECT VALUE COUNT(1)
FROM item
WHERE item.id = @Id
AND (NOT IS_DEFINED(item.{deletedFieldName}) OR IS_NULL(item.{deletedFieldName}))"""
diff --git a/tests/Cosmos.Tests/BuilderUnitTests.fs b/tests/Cosmos.Tests/BuilderUnitTests.fs
index 3eb5478..2743bf2 100644
--- a/tests/Cosmos.Tests/BuilderUnitTests.fs
+++ b/tests/Cosmos.Tests/BuilderUnitTests.fs
@@ -7,7 +7,7 @@ open Microsoft.VisualStudio.TestTools.UnitTesting
type private BuilderTestItem = { id : string; partitionKey : string; value : int }
-[]
+[]
type BuilderUnitTests () =
[]
diff --git a/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs b/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
index 487e0eb..05502d0 100644
--- a/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
+++ b/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
@@ -1,12 +1,14 @@
namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
+open System
open System.Threading.Tasks
open FSharp.Azure.Cosmos
+open FSharp.Azure.Cosmos.Tests
open Microsoft.Azure.Cosmos
open Microsoft.VisualStudio.TestTools.UnitTesting
-[]
+[]
type CosmosReadExtensionsIntegrationTests () =
inherit OperationTestBase ()
@@ -48,6 +50,13 @@ type CosmosReadExtensionsIntegrationTests () =
Assert.IsTrue (notDeletedBeforePatch, "IsNotDeletedAsync should return true before deleted marker is set.")
+ let! notDeletedWithUnderscoreFieldName = container.IsNotDeletedAsync "_deletedAt" secondItem.id
+
+ Assert.IsTrue (
+ notDeletedWithUnderscoreFieldName,
+ "IsNotDeletedAsync should support deleted field names starting with underscore."
+ )
+
let! patchResponse =
container.ExecuteOverwriteAsync (
patch {
@@ -66,3 +75,43 @@ type CosmosReadExtensionsIntegrationTests () =
Assert.IsFalse (notDeletedAfterPatch, "IsNotDeletedAsync should return false after deleted marker is set.")
}
+
+ []
+ member this.``IsNotDeletedAsync throws for invalid deleted field names`` () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.NewItem "invalid-deleted-field-name"
+ let invokeIsNotDeleted (deletedFieldName : string | null) =
+ Func (fun () ->
+ task {
+ let! _ = container.IsNotDeletedAsync deletedFieldName testItem.id
+ return ()
+ }
+ :> Task
+ )
+
+ let! _ =
+ Assert.ThrowsExactlyAsync (
+ invokeIsNotDeleted null,
+ "IsNotDeletedAsync should throw ArgumentNullException when deleted field name is null."
+ )
+
+ let! _ =
+ Assert.ThrowsExactlyAsync (
+ invokeIsNotDeleted " ",
+ "IsNotDeletedAsync should throw ArgumentException when deleted field name is whitespace."
+ )
+
+ let! _ =
+ Assert.ThrowsExactlyAsync (
+ invokeIsNotDeleted "1deletedAt",
+ "IsNotDeletedAsync should throw ArgumentException when deleted field name starts with a digit."
+ )
+
+ let! _ =
+ Assert.ThrowsExactlyAsync (
+ invokeIsNotDeleted "deleted-at",
+ "IsNotDeletedAsync should throw ArgumentException when deleted field name has unsupported characters."
+ )
+
+ return ()
+ }
diff --git a/tests/Cosmos.Tests/CreateOperationTests.fs b/tests/Cosmos.Tests/CreateOperationTests.fs
index fd0914e..143541b 100644
--- a/tests/Cosmos.Tests/CreateOperationTests.fs
+++ b/tests/Cosmos.Tests/CreateOperationTests.fs
@@ -3,9 +3,10 @@ namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
open FSharp.Azure.Cosmos
+open FSharp.Azure.Cosmos.Tests
open Microsoft.VisualStudio.TestTools.UnitTesting
-[]
+[]
type CreateOperationIntegrationTests () =
inherit OperationTestBase ()
diff --git a/tests/Cosmos.Tests/DeleteOperationTests.fs b/tests/Cosmos.Tests/DeleteOperationTests.fs
index 25518a2..ed22f8e 100644
--- a/tests/Cosmos.Tests/DeleteOperationTests.fs
+++ b/tests/Cosmos.Tests/DeleteOperationTests.fs
@@ -3,9 +3,10 @@ namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
open FSharp.Azure.Cosmos
+open FSharp.Azure.Cosmos.Tests
open Microsoft.VisualStudio.TestTools.UnitTesting
-[]
+[]
type DeleteOperationIntegrationTests () =
inherit OperationTestBase ()
diff --git a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
index e3180d3..8e3b214 100644
--- a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
+++ b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
@@ -20,6 +20,7 @@
+
diff --git a/tests/Cosmos.Tests/IntegrationInfrastructure.fs b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
index 8bc90b4..6c1d790 100644
--- a/tests/Cosmos.Tests/IntegrationInfrastructure.fs
+++ b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
@@ -97,6 +97,25 @@ type DatabaseTestApplicationFactory (testContext : TestContext) =
database <- ValueNone
}
+ member _.GetOrCreateContainerAsync
+ (containerId : string, partitionKeyPath : string, cancellationToken : CancellationToken)
+ : Task
+ =
+ task {
+ let database =
+ match database with
+ | ValueSome existingDatabase -> existingDatabase
+ | ValueNone -> invalidOp "Database is not initialized."
+
+ let! containerResponse =
+ database.CreateContainerIfNotExistsAsync (
+ ContainerProperties (containerId, partitionKeyPath),
+ cancellationToken = cancellationToken
+ )
+
+ return containerResponse.Container
+ }
+
abstract SeedDataAsync : cancellationToken : CancellationToken -> Task
default _.SeedDataAsync (cancellationToken : CancellationToken) = Task.CompletedTask
@@ -109,18 +128,19 @@ type DatabaseTestApplicationFactory (testContext : TestContext) =
|> ValueTask
[]
-type IntegrationTestBase () =
+type IntegrationTestBase<'DatabaseTestApplicationFactory when 'DatabaseTestApplicationFactory :> DatabaseTestApplicationFactory>
+ ()
+ =
inherit TestBase ()
- member val private application : DatabaseTestApplicationFactory voption = ValueNone with get, set
+ member val private application : 'DatabaseTestApplicationFactory voption = ValueNone with get, set
member this.Application =
match this.application with
- | ValueNone -> invalidOp "Application not initialized. Ensure test runs within TestInitialize/TestCleanup lifecycle."
| ValueSome application -> application
+ | ValueNone -> invalidOp "Application not initialized. Ensure test runs within TestInitialize/TestCleanup lifecycle."
- abstract CreateApplication : TestContext -> DatabaseTestApplicationFactory
- default _.CreateApplication context = DatabaseTestApplicationFactory (context)
+ abstract CreateApplication : TestContext -> 'DatabaseTestApplicationFactory
[]
member this.Initialize () : Task = task {
@@ -138,3 +158,8 @@ type IntegrationTestBase () =
do! (application :> IAsyncDisposable).DisposeAsync().AsTask ()
this.application <- ValueNone
}
+
+type IntegrationTestBase () =
+ inherit IntegrationTestBase ()
+
+ override _.CreateApplication context = DatabaseTestApplicationFactory (context)
diff --git a/tests/Cosmos.Tests/IterationExtensionsTests.fs b/tests/Cosmos.Tests/IterationExtensionsTests.fs
index f3bc66f..0dd7316 100644
--- a/tests/Cosmos.Tests/IterationExtensionsTests.fs
+++ b/tests/Cosmos.Tests/IterationExtensionsTests.fs
@@ -2,20 +2,24 @@ namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Threading.Tasks
open FSharp.Control
+open FSharp.Azure.Cosmos.Tests
open Microsoft.Azure.Cosmos
open Microsoft.Azure.Cosmos.Linq
open Microsoft.VisualStudio.TestTools.UnitTesting
-[]
+[]
type IterationExtensionsIntegrationTests () =
- inherit OperationTestBase ()
+ inherit OperationTestBase ()
+
+ override _.CreateApplication context = MultipleItemsScenario (context)
[]
member this.``FeedIterator AsAsyncEnumerable iterates seeded items`` () : Task = task {
let! container = this.GetContainer ()
- let firstItem = this.NewItem "iterator-1"
- let secondItem = this.NewItem "iterator-2"
- do! this.SeedItemsAsync (container, [ firstItem; secondItem ])
+ let firstItem, secondItem =
+ match this.Application.SeededItems with
+ | [ firstItem; secondItem ] -> firstItem, secondItem
+ | seededItems -> failwith $"Expected exactly two seeded items but got {seededItems.Length}."
let query =
QueryDefinition("SELECT * FROM c WHERE c.partitionKey = @partitionKey").WithParameter ("@partitionKey", "integration")
@@ -35,9 +39,10 @@ type IterationExtensionsIntegrationTests () =
[]
member this.``IQueryable AsAsyncEnumerable iterates seeded items`` () : Task = task {
let! container = this.GetContainer ()
- let firstItem = this.NewItem "queryable-1"
- let secondItem = this.NewItem "queryable-2"
- do! this.SeedItemsAsync (container, [ firstItem; secondItem ])
+ let firstItem, secondItem =
+ match this.Application.SeededItems with
+ | [ firstItem; secondItem ] -> firstItem, secondItem
+ | seededItems -> failwith $"Expected exactly two seeded items but got {seededItems.Length}."
let queryable =
container.GetItemLinqQueryable (
diff --git a/tests/Cosmos.Tests/OperationTestInfrastructure.fs b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
index 200dbdb..3478351 100644
--- a/tests/Cosmos.Tests/OperationTestInfrastructure.fs
+++ b/tests/Cosmos.Tests/OperationTestInfrastructure.fs
@@ -9,26 +9,14 @@ open Microsoft.Azure.Cosmos
type TestItem = { id : string; partitionKey : string; name : string; quantity : int }
[]
-type OperationTestBase () =
- inherit IntegrationTestBase ()
+type OperationTestBase<'DatabaseTestApplicationFactory when 'DatabaseTestApplicationFactory :> DatabaseTestApplicationFactory> ()
+ =
+ inherit IntegrationTestBase<'DatabaseTestApplicationFactory> ()
let containerId = "operation-tests"
- member private this.GetDatabase () =
- match this.Application.Database with
- | ValueSome database -> database
- | ValueNone -> invalidOp "Database is not initialized."
-
member this.GetContainer () : Task = task {
- let database = this.GetDatabase ()
-
- let! containerResponse =
- database.CreateContainerIfNotExistsAsync (
- ContainerProperties (containerId, "/partitionKey"),
- cancellationToken = this.CancellationToken
- )
-
- return containerResponse.Container
+ return! this.Application.GetOrCreateContainerAsync (containerId, "/partitionKey", this.CancellationToken)
}
member internal this.NewItem (suffix : string) : TestItem = {
@@ -51,3 +39,9 @@ type OperationTestBase () =
CosmosAssert.IsOk (createResponse.Result, $"Seed create should succeed for item '{seedItem.id}'.")
}
+
+[]
+type OperationTestBase () =
+ inherit OperationTestBase ()
+
+ override _.CreateApplication context = DatabaseTestApplicationFactory (context)
diff --git a/tests/Cosmos.Tests/PatchOperationTests.fs b/tests/Cosmos.Tests/PatchOperationTests.fs
index 8db5ae7..f7d907a 100644
--- a/tests/Cosmos.Tests/PatchOperationTests.fs
+++ b/tests/Cosmos.Tests/PatchOperationTests.fs
@@ -3,10 +3,11 @@ namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
open FSharp.Azure.Cosmos
+open FSharp.Azure.Cosmos.Tests
open Microsoft.Azure.Cosmos
open Microsoft.VisualStudio.TestTools.UnitTesting
-[]
+[]
type PatchOperationIntegrationTests () =
inherit OperationTestBase ()
diff --git a/tests/Cosmos.Tests/ReadManyOperationTests.fs b/tests/Cosmos.Tests/ReadManyOperationTests.fs
index ac9fec4..7646a0e 100644
--- a/tests/Cosmos.Tests/ReadManyOperationTests.fs
+++ b/tests/Cosmos.Tests/ReadManyOperationTests.fs
@@ -1,42 +1,63 @@
namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
+open System.Threading
open System.Threading.Tasks
open FSharp.Azure.Cosmos
+open FSharp.Azure.Cosmos.Tests
open Microsoft.Azure.Cosmos
open Microsoft.VisualStudio.TestTools.UnitTesting
-[]
-type ReadManyOperationIntegrationTests () =
- inherit OperationTestBase ()
+type MultipleItemsScenario (testContext : TestContext) as this =
+ inherit DatabaseTestApplicationFactory (testContext)
- []
- member this.``ReadMany execute returns matching items`` () : Task = task {
- let! container = this.GetContainer ()
- let firstItem = this.NewItem "readmany-1"
- let secondItem = this.NewItem "readmany-2"
+ let containerId = "operation-tests"
- let! firstCreateResponse =
- container.ExecuteAsync (
- create {
- item firstItem
- partitionKey firstItem.partitionKey
- },
- this.CancellationToken
- )
+ let firstSeededItem : TestItem = {
+ id = $"{testContext.TestName}-readmany-1"
+ partitionKey = "integration"
+ name = "item-readmany-1"
+ quantity = 1
+ }
- CosmosAssert.IsOk (firstCreateResponse.Result, "First seed create should succeed.")
+ let secondSeededItem : TestItem = {
+ id = $"{testContext.TestName}-readmany-2"
+ partitionKey = "integration"
+ name = "item-readmany-2"
+ quantity = 2
+ }
- let! secondCreateResponse =
- container.ExecuteAsync (
- create {
- item secondItem
- partitionKey secondItem.partitionKey
- },
- this.CancellationToken
- )
+ member _.SeededItems = [ firstSeededItem; secondSeededItem ]
+
+ override _.SeedDataAsync (cancellationToken : CancellationToken) : Task = task {
+ let! container = this.GetOrCreateContainerAsync (containerId, "/partitionKey", cancellationToken)
+
+ for seededItem in this.SeededItems do
+ let! createResponse =
+ container.ExecuteAsync (
+ create {
+ item seededItem
+ partitionKey seededItem.partitionKey
+ },
+ cancellationToken
+ )
+
+ CosmosAssert.IsOk (createResponse.Result, $"ReadMany scenario seed create should succeed for '{seededItem.id}'.")
+ }
- CosmosAssert.IsOk (secondCreateResponse.Result, "Second seed create should succeed.")
+[]
+type ReadManyOperationIntegrationTests () =
+ inherit OperationTestBase ()
+
+ override _.CreateApplication context = MultipleItemsScenario (context)
+
+ []
+ member this.``ReadMany execute returns matching items`` () : Task = task {
+ let! container = this.GetContainer ()
+ let firstItem, secondItem =
+ match this.Application.SeededItems with
+ | [ firstItem; secondItem ] -> firstItem, secondItem
+ | seededItems -> failwith $"Expected exactly two seeded items but got {seededItems.Length}."
let! readManyResponse =
container.ExecuteAsync (
diff --git a/tests/Cosmos.Tests/ReadOperationTests.fs b/tests/Cosmos.Tests/ReadOperationTests.fs
index 25f5642..a758825 100644
--- a/tests/Cosmos.Tests/ReadOperationTests.fs
+++ b/tests/Cosmos.Tests/ReadOperationTests.fs
@@ -1,29 +1,51 @@
namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
+open System.Threading
open System.Threading.Tasks
open FSharp.Azure.Cosmos
+open FSharp.Azure.Cosmos.Tests
open Microsoft.VisualStudio.TestTools.UnitTesting
-[]
-type ReadOperationIntegrationTests () =
- inherit OperationTestBase ()
+type SingleItemScenario (testContext : TestContext) as this =
+ inherit DatabaseTestApplicationFactory (testContext)
- []
- member this.``Read execute returns existing and not found states`` () : Task = task {
- let! container = this.GetContainer ()
- let testItem = this.NewItem "read"
+ let containerId = "operation-tests"
+
+ let seededItem : TestItem = {
+ id = $"{testContext.TestName}-read"
+ partitionKey = "integration"
+ name = "item-read"
+ quantity = 1
+ }
+
+ member _.SeededItem = seededItem
+
+ override _.SeedDataAsync (cancellationToken : CancellationToken) : Task = task {
+ let! container = this.GetOrCreateContainerAsync (containerId, "/partitionKey", cancellationToken)
let! createResponse =
container.ExecuteAsync (
create {
- item testItem
- partitionKey testItem.partitionKey
+ item seededItem
+ partitionKey seededItem.partitionKey
},
- this.CancellationToken
+ cancellationToken
)
- CosmosAssert.IsOk (createResponse.Result, "Seed create should succeed.")
+ CosmosAssert.IsOk (createResponse.Result, "Read scenario seed create should succeed.")
+ }
+
+[]
+type ReadOperationIntegrationTests () =
+ inherit OperationTestBase ()
+
+ override _.CreateApplication context = SingleItemScenario (context)
+
+ []
+ member this.``Read execute returns existing and not found states`` () : Task = task {
+ let! container = this.GetContainer ()
+ let testItem = this.Application.SeededItem
let! foundResponse =
container.ExecuteAsync (
diff --git a/tests/Cosmos.Tests/ReplaceOperationTests.fs b/tests/Cosmos.Tests/ReplaceOperationTests.fs
index 05c90b0..aafc6df 100644
--- a/tests/Cosmos.Tests/ReplaceOperationTests.fs
+++ b/tests/Cosmos.Tests/ReplaceOperationTests.fs
@@ -3,9 +3,10 @@ namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
open FSharp.Azure.Cosmos
+open FSharp.Azure.Cosmos.Tests
open Microsoft.VisualStudio.TestTools.UnitTesting
-[]
+[]
type ReplaceOperationIntegrationTests () =
inherit OperationTestBase ()
diff --git a/tests/Cosmos.Tests/TestCategories.fs b/tests/Cosmos.Tests/TestCategories.fs
new file mode 100644
index 0000000..6ee0c07
--- /dev/null
+++ b/tests/Cosmos.Tests/TestCategories.fs
@@ -0,0 +1,33 @@
+namespace FSharp.Azure.Cosmos.Tests
+
+[]
+module TestCategories =
+ []
+ let Builders = "Builders"
+
+ []
+ let Create = "Create"
+
+ []
+ let Read = "Read"
+
+ []
+ let ReadMany = "ReadMany"
+
+ []
+ let Upsert = "Upsert"
+
+ []
+ let Replace = "Replace"
+
+ []
+ let Patch = "Patch"
+
+ []
+ let Delete = "Delete"
+
+ []
+ let ReadExtensions = "ReadExtensions"
+
+ []
+ let IterationExtensions = "IterationExtensions"
diff --git a/tests/Cosmos.Tests/UpsertOperationTests.fs b/tests/Cosmos.Tests/UpsertOperationTests.fs
index fad153d..791f889 100644
--- a/tests/Cosmos.Tests/UpsertOperationTests.fs
+++ b/tests/Cosmos.Tests/UpsertOperationTests.fs
@@ -3,9 +3,10 @@ namespace FSharp.Azure.Cosmos.Tests.Integration
open System.Net
open System.Threading.Tasks
open FSharp.Azure.Cosmos
+open FSharp.Azure.Cosmos.Tests
open Microsoft.VisualStudio.TestTools.UnitTesting
-[]
+[]
type UpsertOperationIntegrationTests () =
inherit OperationTestBase ()
From 842d5edfd8f8f4abbce133505d0192fee646d871 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 22:20:17 +0000
Subject: [PATCH 27/33] Apply validation feedback ordering in
IntegrationTestBase
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/fb87fcd3-5c25-4e1f-ab2c-0f29ce614280
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/IntegrationInfrastructure.fs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/Cosmos.Tests/IntegrationInfrastructure.fs b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
index 6c1d790..31d1c10 100644
--- a/tests/Cosmos.Tests/IntegrationInfrastructure.fs
+++ b/tests/Cosmos.Tests/IntegrationInfrastructure.fs
@@ -137,8 +137,8 @@ type IntegrationTestBase<'DatabaseTestApplicationFactory when 'DatabaseTestAppli
member this.Application =
match this.application with
- | ValueSome application -> application
| ValueNone -> invalidOp "Application not initialized. Ensure test runs within TestInitialize/TestCleanup lifecycle."
+ | ValueSome application -> application
abstract CreateApplication : TestContext -> 'DatabaseTestApplicationFactory
From bde1f0df4f59c4244c318bb9fcf17c8993104656 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 22:21:39 +0000
Subject: [PATCH 28/33] Simplify async exception test delegate in read
extensions tests
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/fb87fcd3-5c25-4e1f-ab2c-0f29ce614280
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/CosmosReadExtensionsTests.fs | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs b/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
index 05502d0..127c132 100644
--- a/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
+++ b/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
@@ -81,13 +81,10 @@ type CosmosReadExtensionsIntegrationTests () =
let! container = this.GetContainer ()
let testItem = this.NewItem "invalid-deleted-field-name"
let invokeIsNotDeleted (deletedFieldName : string | null) =
- Func (fun () ->
- task {
- let! _ = container.IsNotDeletedAsync deletedFieldName testItem.id
- return ()
- }
- :> Task
- )
+ Func (fun () -> task {
+ let! _ = container.IsNotDeletedAsync deletedFieldName testItem.id
+ return ()
+ })
let! _ =
Assert.ThrowsExactlyAsync (
From 7badc2f3f16fa9a0088c810dfea12f3e068a9342 Mon Sep 17 00:00:00 2001
From: Andrii Chebukin
Date: Mon, 25 May 2026 01:03:10 +0200
Subject: [PATCH 29/33] fix `ReadExtensionsIntegrationTests` name
---
tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj | 2 +-
.../{CosmosReadExtensionsTests.fs => ReadExtensionsTests.fs} | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
rename tests/Cosmos.Tests/{CosmosReadExtensionsTests.fs => ReadExtensionsTests.fs} (99%)
diff --git a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
index 8e3b214..e186657 100644
--- a/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
+++ b/tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj
@@ -29,7 +29,7 @@
-
+
diff --git a/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs b/tests/Cosmos.Tests/ReadExtensionsTests.fs
similarity index 99%
rename from tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
rename to tests/Cosmos.Tests/ReadExtensionsTests.fs
index 127c132..ec3a44b 100644
--- a/tests/Cosmos.Tests/CosmosReadExtensionsTests.fs
+++ b/tests/Cosmos.Tests/ReadExtensionsTests.fs
@@ -9,7 +9,7 @@ open Microsoft.Azure.Cosmos
open Microsoft.VisualStudio.TestTools.UnitTesting
[]
-type CosmosReadExtensionsIntegrationTests () =
+type ReadExtensionsIntegrationTests () =
inherit OperationTestBase ()
[]
From 69776da917363c708a7e09d6812bb600739fb2d1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 23:11:03 +0000
Subject: [PATCH 30/33] Refine IsNotDeletedAsync docs and validation coverage
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/7c652b6b-140a-42c5-9e86-a71d0a69c5ba
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
src/Cosmos/Cosmos.fs | 32 ++++++++++++++++++-----
tests/Cosmos.Tests/ReadExtensionsTests.fs | 17 ++++++++++--
2 files changed, 40 insertions(+), 9 deletions(-)
diff --git a/src/Cosmos/Cosmos.fs b/src/Cosmos/Cosmos.fs
index c379fcc..7eb3d79 100644
--- a/src/Cosmos/Cosmos.fs
+++ b/src/Cosmos/Cosmos.fs
@@ -70,6 +70,10 @@ module Operations =
type ItemRequestOptions with
+ ///
+ /// Adds a pre-trigger to request options.
+ ///
+ /// Trigger name.
member options.AddPreTrigger (trigger : string) =
options.PreTriggers <- [|
if not <| isNull options.PreTriggers then
@@ -77,6 +81,11 @@ module Operations =
yield trigger
|]
+ ///
+ /// Adds pre-triggers to request options.
+ ///
+ /// Trigger names.
+ /// Thrown when is null.
member options.AddPreTriggers (triggers : string seq) =
if obj.ReferenceEquals (triggers, null) then
raise (ArgumentNullException (nameof triggers))
@@ -93,6 +102,11 @@ module Operations =
yield trigger
|]
+ ///
+ /// Adds post-triggers to request options.
+ ///
+ /// Trigger names.
+ /// Thrown when is null.
member options.AddPostTriggers (triggers : string seq) =
if obj.ReferenceEquals (triggers, null) then
raise (ArgumentNullException (nameof triggers))
@@ -211,19 +225,23 @@ module Operations =
///
/// Checks if an item with specified Id exists in the container partition with specified key.
///
+ /// Deleted marker field name.
/// Item Id
- /// Partition key
+ /// Request options
/// Cancellation token
+ /// Thrown when is null.
+ ///
+ /// Thrown when does not start with a letter or underscore,
+ /// or contains characters other than letters, digits, or underscores.
+ ///
member container.IsNotDeletedAsync
- (deletedFieldName : string | null)
+ (deletedFieldName : string)
(id : string, [] requiestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken)
=
- task {
- let deletedFieldName =
- match deletedFieldName with
- | null -> nullArg (nameof deletedFieldName)
- | deletedFieldName -> deletedFieldName
+ if obj.ReferenceEquals (deletedFieldName, null) then
+ nullArg (nameof deletedFieldName)
+ task {
let isAsciiLetter c = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
let isAsciiDigit c = '0' <= c && c <= '9'
diff --git a/tests/Cosmos.Tests/ReadExtensionsTests.fs b/tests/Cosmos.Tests/ReadExtensionsTests.fs
index ec3a44b..22fda15 100644
--- a/tests/Cosmos.Tests/ReadExtensionsTests.fs
+++ b/tests/Cosmos.Tests/ReadExtensionsTests.fs
@@ -57,6 +57,13 @@ type ReadExtensionsIntegrationTests () =
"IsNotDeletedAsync should support deleted field names starting with underscore."
)
+ let! notDeletedWithDigitInBodyFieldName = container.IsNotDeletedAsync "deletedAt1" secondItem.id
+
+ Assert.IsTrue (
+ notDeletedWithDigitInBodyFieldName,
+ "IsNotDeletedAsync should support deleted field names with digits after the first character."
+ )
+
let! patchResponse =
container.ExecuteOverwriteAsync (
patch {
@@ -80,7 +87,7 @@ type ReadExtensionsIntegrationTests () =
member this.``IsNotDeletedAsync throws for invalid deleted field names`` () : Task = task {
let! container = this.GetContainer ()
let testItem = this.NewItem "invalid-deleted-field-name"
- let invokeIsNotDeleted (deletedFieldName : string | null) =
+ let invokeIsNotDeleted (deletedFieldName : string) =
Func (fun () -> task {
let! _ = container.IsNotDeletedAsync deletedFieldName testItem.id
return ()
@@ -88,10 +95,16 @@ type ReadExtensionsIntegrationTests () =
let! _ =
Assert.ThrowsExactlyAsync (
- invokeIsNotDeleted null,
+ invokeIsNotDeleted Unchecked.defaultof,
"IsNotDeletedAsync should throw ArgumentNullException when deleted field name is null."
)
+ let! _ =
+ Assert.ThrowsExactlyAsync (
+ invokeIsNotDeleted "",
+ "IsNotDeletedAsync should throw ArgumentException when deleted field name is empty."
+ )
+
let! _ =
Assert.ThrowsExactlyAsync (
invokeIsNotDeleted " ",
From 1a733408cee6811f2c032cdee889a5c15fc09e6d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 23:12:06 +0000
Subject: [PATCH 31/33] Address follow-up review notes for IsNotDeletedAsync
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/7c652b6b-140a-42c5-9e86-a71d0a69c5ba
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
src/Cosmos/Cosmos.fs | 4 ++--
tests/Cosmos.Tests/ReadExtensionsTests.fs | 5 ++++-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/Cosmos/Cosmos.fs b/src/Cosmos/Cosmos.fs
index 7eb3d79..f596c26 100644
--- a/src/Cosmos/Cosmos.fs
+++ b/src/Cosmos/Cosmos.fs
@@ -236,7 +236,7 @@ module Operations =
///
member container.IsNotDeletedAsync
(deletedFieldName : string)
- (id : string, [] requiestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken)
+ (id : string, [] requestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken)
=
if obj.ReferenceEquals (deletedFieldName, null) then
nullArg (nameof deletedFieldName)
@@ -273,7 +273,7 @@ module Operations =
let! count =
container.GetItemQueryIterator (
query,
- requestOptions = getRequestOptionsWithMaxItemCount1 requiestOptions
+ requestOptions = getRequestOptionsWithMaxItemCount1 requestOptions
)
|> CancellableTaskSeq.ofFeedIterator cancellationToken
|> TaskSeq.tryHead
diff --git a/tests/Cosmos.Tests/ReadExtensionsTests.fs b/tests/Cosmos.Tests/ReadExtensionsTests.fs
index 22fda15..c6c264d 100644
--- a/tests/Cosmos.Tests/ReadExtensionsTests.fs
+++ b/tests/Cosmos.Tests/ReadExtensionsTests.fs
@@ -95,7 +95,10 @@ type ReadExtensionsIntegrationTests () =
let! _ =
Assert.ThrowsExactlyAsync (
- invokeIsNotDeleted Unchecked.defaultof,
+ Func (fun () -> task {
+ let! _ = container.IsNotDeletedAsync Unchecked.defaultof testItem.id
+ return ()
+ }),
"IsNotDeletedAsync should throw ArgumentNullException when deleted field name is null."
)
From b9c6eae87288cd28f7eb3acadcbfa43cd67b5a8c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 23:12:51 +0000
Subject: [PATCH 32/33] Tidy read extensions test variable naming
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/7c652b6b-140a-42c5-9e86-a71d0a69c5ba
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/ReadExtensionsTests.fs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/Cosmos.Tests/ReadExtensionsTests.fs b/tests/Cosmos.Tests/ReadExtensionsTests.fs
index c6c264d..47a625b 100644
--- a/tests/Cosmos.Tests/ReadExtensionsTests.fs
+++ b/tests/Cosmos.Tests/ReadExtensionsTests.fs
@@ -57,10 +57,10 @@ type ReadExtensionsIntegrationTests () =
"IsNotDeletedAsync should support deleted field names starting with underscore."
)
- let! notDeletedWithDigitInBodyFieldName = container.IsNotDeletedAsync "deletedAt1" secondItem.id
+ let! notDeletedWithDigit = container.IsNotDeletedAsync "deletedAt1" secondItem.id
Assert.IsTrue (
- notDeletedWithDigitInBodyFieldName,
+ notDeletedWithDigit,
"IsNotDeletedAsync should support deleted field names with digits after the first character."
)
From 985d3f94f9afd773b75ee2692468ebb0c2eea6b6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 24 May 2026 23:13:52 +0000
Subject: [PATCH 33/33] Expand deleted-field validation coverage in read
extension tests
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Azure.Cosmos/sessions/7c652b6b-140a-42c5-9e86-a71d0a69c5ba
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
---
tests/Cosmos.Tests/ReadExtensionsTests.fs | 34 +++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/tests/Cosmos.Tests/ReadExtensionsTests.fs b/tests/Cosmos.Tests/ReadExtensionsTests.fs
index 47a625b..829ac4a 100644
--- a/tests/Cosmos.Tests/ReadExtensionsTests.fs
+++ b/tests/Cosmos.Tests/ReadExtensionsTests.fs
@@ -64,6 +64,28 @@ type ReadExtensionsIntegrationTests () =
"IsNotDeletedAsync should support deleted field names with digits after the first character."
)
+ let! digitFieldPatchResponse =
+ container.ExecuteOverwriteAsync (
+ patch {
+ id firstItem.id
+ partitionKey firstItem.partitionKey
+ operation (PatchOperation.Set ("/deletedAt1", "2026-05-24T00:00:00Z"))
+ },
+ this.CancellationToken
+ )
+
+ match digitFieldPatchResponse.Result with
+ | PatchResult.Ok _ ->
+ Assert.IsTrue (digitFieldPatchResponse.HttpStatusCode = HttpStatusCode.OK, "Patch should return HTTP 200.")
+ | result -> Assert.Fail ($"Expected patch success for digit-field marker, got {result}.")
+
+ let! notDeletedWithDigitAfterPatch = container.IsNotDeletedAsync "deletedAt1" firstItem.id
+
+ Assert.IsFalse (
+ notDeletedWithDigitAfterPatch,
+ "IsNotDeletedAsync should return false when a digit-containing deleted marker field is set."
+ )
+
let! patchResponse =
container.ExecuteOverwriteAsync (
patch {
@@ -120,11 +142,23 @@ type ReadExtensionsIntegrationTests () =
"IsNotDeletedAsync should throw ArgumentException when deleted field name starts with a digit."
)
+ let! _ =
+ Assert.ThrowsExactlyAsync (
+ invokeIsNotDeleted "1deleted",
+ "IsNotDeletedAsync should throw ArgumentException for field names that start with a digit."
+ )
+
let! _ =
Assert.ThrowsExactlyAsync (
invokeIsNotDeleted "deleted-at",
"IsNotDeletedAsync should throw ArgumentException when deleted field name has unsupported characters."
)
+ let! _ =
+ Assert.ThrowsExactlyAsync (
+ invokeIsNotDeleted "deleted-field",
+ "IsNotDeletedAsync should throw ArgumentException for field names with hyphens."
+ )
+
return ()
}