Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c327a5d
Initial plan
Copilot May 23, 2026
d921eca
Add Cosmos emulator integration test infrastructure and scenario plan
Copilot May 23, 2026
527ea36
Refine Cosmos test infrastructure and planned scenario list
Copilot May 23, 2026
f98ebe5
Remove DB name normalization and format integration files
Copilot May 23, 2026
67c9905
Use TestContext-based database identifier for integration tests
Copilot May 24, 2026
44baaef
Harden test data hash conversion for database identifier
Copilot May 24, 2026
e177988
Add integration tests for create/read/upsert/replace/patch/delete/rea…
Copilot May 24, 2026
7d70871
Address integration test review feedback in patch scenario
Copilot May 24, 2026
660a0cc
Add CosmosAssert helper and simplify integration test assertions
Copilot May 24, 2026
771f88d
Refine CosmosAssert failure handling consistency
Copilot May 24, 2026
f9d3251
Plan split operation tests into separate files with AndRead coverage
Copilot May 24, 2026
6d5d59f
Split operation integration tests into per-operation files with AndRe…
Copilot May 24, 2026
4a47672
Adjust shared operation test fixture visibility
Copilot May 24, 2026
0bf340c
Remove class-scope literal from operation test infrastructure
Copilot May 24, 2026
03795eb
Rename test methods add read extension coverage and builder tests
Copilot May 24, 2026
31d54c0
Add Assert extensions and align test namespaces
Copilot May 24, 2026
d63fbff
Fix Assert extension signatures and keep test helper usage
Copilot May 24, 2026
60e97b6
fixup! Split operation integration tests into per-operation files wit…
xperiandri May 24, 2026
e1305d3
Delete IntegrationTestPlan file per PR feedback
Copilot May 24, 2026
5d79634
Apply suggestions from code review
xperiandri May 24, 2026
f62634d
fixup! ci(cosmos): use separate common action to check Azure Cosmos E…
xperiandri May 24, 2026
cd491da
Fix failing Cosmos emulator tests
Copilot May 24, 2026
d53c998
Handle undefined deleted marker in IsNotDeletedAsync
Copilot May 24, 2026
829a053
Validate deleted field name in IsNotDeletedAsync query
Copilot May 24, 2026
a06c43a
Harden IsNotDeletedAsync field-name validation
Copilot May 24, 2026
5db799c
Address review feedback for nullArg, test categories, and scenario se…
Copilot May 24, 2026
842d5ed
Apply validation feedback ordering in IntegrationTestBase
Copilot May 24, 2026
bde1f0d
Simplify async exception test delegate in read extensions tests
Copilot May 24, 2026
7badc2f
fix `ReadExtensionsIntegrationTests` name
xperiandri May 24, 2026
69776da
Refine IsNotDeletedAsync docs and validation coverage
Copilot May 24, 2026
1a73340
Address follow-up review notes for IsNotDeletedAsync
Copilot May 24, 2026
b9c6eae
Tidy read extensions test variable naming
Copilot May 24, 2026
985d3f9
Expand deleted-field validation coverage in read extension tests
Copilot May 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions src/Cosmos/Cosmos.fs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,22 @@ module Operations =

type ItemRequestOptions with

/// <summary>
/// Adds a pre-trigger to request options.
/// </summary>
/// <param name="trigger">Trigger name.</param>
member options.AddPreTrigger (trigger : string) =
options.PreTriggers <- [|
if not <| isNull options.PreTriggers then
yield! options.PreTriggers
yield trigger
|]

/// <summary>
/// Adds pre-triggers to request options.
/// </summary>
/// <param name="triggers">Trigger names.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="triggers"/> is <c>null</c>.</exception>
member options.AddPreTriggers (triggers : string seq) =
if obj.ReferenceEquals (triggers, null) then
raise (ArgumentNullException (nameof triggers))
Expand All @@ -93,6 +102,11 @@ module Operations =
yield trigger
|]

/// <summary>
/// Adds post-triggers to request options.
/// </summary>
/// <param name="triggers">Trigger names.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="triggers"/> is <c>null</c>.</exception>
member options.AddPostTriggers (triggers : string seq) =
if obj.ReferenceEquals (triggers, null) then
raise (ArgumentNullException (nameof triggers))
Expand Down Expand Up @@ -211,25 +225,55 @@ module Operations =
/// <summary>
/// Checks if an item with specified Id exists in the container partition with specified key.
/// </summary>
/// <param name="deletedFieldName">Deleted marker field name.</param>
/// <param name="id">Item Id</param>
/// <param name="partitionKey">Partition key</param>
/// <param name="requestOptions">Request options</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="deletedFieldName"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="deletedFieldName"/> does not start with a letter or underscore,
/// or contains characters other than letters, digits, or underscores.
/// </exception>
member container.IsNotDeletedAsync
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot <exception XML комментарии добавь на все методы где они вылетают

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Добавил XML <exception> в 69776da для методов, где выбрасываются исключения: IsNotDeletedAsync, AddPreTriggers, AddPostTriggers.

deletedFieldName
(id : string, [<Optional>] requiestOptions : QueryRequestOptions, [<Optional>] cancellationToken : CancellationToken)
(deletedFieldName : string)
(id : string, [<Optional>] requestOptions : QueryRequestOptions, [<Optional>] cancellationToken : CancellationToken)
=
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'

let isValidDeletedFieldName =
if String.IsNullOrWhiteSpace deletedFieldName then
false
else
let firstCharacter = deletedFieldName[0]
let hasValidStart = firstCharacter = '_' || isAsciiLetter firstCharacter
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."

let query =
QueryDefinition(
$"SELECT VALUE COUNT(1) \
FROM item \
WHERE item.id = @Id AND 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}))"""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot ты все кейсы этого условия тестами покрыл?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered in 64dd97d: added tests for null, whitespace, invalid first character, invalid body character, and valid underscore-prefixed field name paths for IsNotDeletedAsync.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot так что с покрытием всех этих кейсов тестами?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Покрытие расширил в 985d3f9: добавлены проверки для valid-поля с цифрой в теле (до/после установки marker), а также дополнительные invalid-кейсы (1deleted, deleted-field) вместе с уже существующими null/empty/whitespace/invalid-start/invalid-char кейсами.

)
.WithParameter ("@Id", id)
let! count =
container.GetItemQueryIterator<int> (
query,
requestOptions = getRequestOptionsWithMaxItemCount1 requiestOptions
requestOptions = getRequestOptionsWithMaxItemCount1 requestOptions
)
|> CancellableTaskSeq.ofFeedIterator cancellationToken
|> TaskSeq.tryHead
Expand Down
72 changes: 72 additions & 0 deletions tests/Cosmos.Tests/Assert.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
namespace FSharp.Azure.Cosmos.Tests

open System.Runtime.InteropServices
open Microsoft.VisualStudio.TestTools.UnitTesting

[<AutoOpen>]
module AssertExtensions =

type Assert with

static member WantSome (value, [<Optional>] message : string | null) =
match value with
| Some some -> some
| None ->
Assert.Fail (message)
Unchecked.defaultof<_>

static member IsSome (value, [<Optional>] message : string | null) = Assert.WantSome (value, message) |> ignore

static member IsNone (value, [<Optional>] message : string | null) =
match value with
| Some _ -> Assert.Fail (message)
| None -> ()

static member WantValueSome (value, [<Optional>] message : string | null) =
match value with
| ValueSome some -> some
| ValueNone ->
Assert.Fail (message)
Unchecked.defaultof<_>

static member IsValueSome (value, [<Optional>] message : string | null) = Assert.WantValueSome (value, message) |> ignore

static member IsValueNone (value, [<Optional>] message : string | null) =
match value with
| ValueSome _ -> Assert.Fail (message)
| ValueNone -> ()

static member WantOk (value, [<Optional>] 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, [<Optional>] message : string | null) = Assert.WantOk (value, message) |> ignore

static member WantError (value, [<Optional>] 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, [<Optional>] message : string | null) = Assert.WantError (value, message) |> ignore

static member inline IsDefaultOf< ^T> (value : ^T, [<Optional>] message : string) =
Assert.AreEqual (box value, box Unchecked.defaultof< ^T>, message)

static member inline OkEquals< ^R, 'E> (expected : ^R, actual : Result< ^R, 'E >, [<Optional>] 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>, [<Optional>] message : string | null) =
Assert.AreEqual (box expected, box (Assert.WantError (actual, message)), message)

static member FailWithData<'T> ([<Optional>] message : string | null) =
Assert.Fail (message)
Unchecked.defaultof<'T>
Loading