From ae0513c0ff8b549f381da84fc670cd44989522ad Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 26 Mar 2020 11:04:13 +0000 Subject: [PATCH 1/3] Revise Cart Command handling semantics --- CHANGELOG.md | 1 + samples/Store/Domain.Tests/CartTests.fs | 96 ++++++++++++------- samples/Store/Domain/Cart.fs | 77 ++++++++------- samples/Store/Integration/CartIntegration.fs | 4 +- .../CosmosIntegration.fs | 8 +- .../StoreIntegration.fs | 6 +- .../MemoryStoreIntegration.fs | 8 +- 7 files changed, 117 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa1bf952d..82007882f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The `Unreleased` section name is replaced by the expected version of next releas - Target `FSharp.Control.AsyncSeq` v `2.0.23` - Updated AzDO CI/CD to use `windows-latest` - Remove `module Commands` convention from in examples +- Revise semantics of Cart Sample Command handling ### Removed ### Fixed diff --git a/samples/Store/Domain.Tests/CartTests.fs b/samples/Store/Domain.Tests/CartTests.fs index 9f87585d6..710be2ee4 100644 --- a/samples/Store/Domain.Tests/CartTests.fs +++ b/samples/Store/Domain.Tests/CartTests.fs @@ -1,73 +1,92 @@ module Samples.Store.Domain.Tests.CartTests +open Domain open Domain.Cart -open Domain.Cart.Events open Domain.Cart.Fold open Swensen.Unquote open TypeShape.Empty -let mkAddQty skuId qty = ItemAdded { empty with skuId = skuId; quantity = qty } -let mkAdd skuId = mkAddQty skuId 1 -let mkRemove skuId = ItemRemoved { empty with skuId = skuId } -let mkChangeWaived skuId value = ItemWaiveReturnsChanged { empty with skuId = skuId; waived = value } +let mkAddQty skuId qty waive = Events.ItemAdded { empty with skuId = skuId; quantity = qty; waived = waive } +let mkAdd skuId = mkAddQty skuId 1 None +let mkRemove skuId = Events.ItemRemoved { empty with skuId = skuId } +let mkChangeWaived skuId value = Events.ItemPropertiesChanged { empty with skuId = skuId; waived = value } + +/// Represents the high level primitives that can be expressed in a SyncItem Command +type Command = + | AddItem of Context * SkuId * quantity : int * waiveStatus : bool option + | PatchItem of Context * SkuId * quantity : int option * waiveStatus : bool option + | RemoveItem of Context * SkuId + +let interpret = function + | AddItem (c, s, q, w) -> SyncItem (c, s, Some q, w) |> interpret + | PatchItem (c, s, q, w) -> SyncItem (c, s, q, w) |> interpret + | RemoveItem (c, s) -> SyncItem (c, s, Some 0, None) |> interpret /// As a basic sanity check, verify the basic properties we'd expect per command if we were to run it on an empty stream // Note validating basics like this is not normally that useful a property; in this instance (I think) it takes some // cases/logic out of the main property and is hence worth doing for this aggregate -let verifyCanProcessInInitialState cmd (originState: State) = +let verifyCanProcessInOriginState cmd (originState: State) = let events = interpret cmd originState match cmd with - | AddItem _ -> - test <@ (not << List.isEmpty) events @> - | PatchItem _ + | PatchItem (_, _, Some 0, _) | RemoveItem _ -> test <@ List.isEmpty events @> + | _ -> + test <@ (not << List.isEmpty) events @> /// Put the aggregate into the state where the command should trigger an event; verify correct events are yielded let verifyCorrectEventGenerationWhenAppropriate command (originState: State) = let initialEvents = command |> function - | AddItem _ -> [] + | AddItem _ + | PatchItem (_, _, None, _) -> [] | RemoveItem (_, skuId) - | PatchItem (_, skuId, Some 0, _) -> [ mkAdd skuId ] - | PatchItem (_, skuId, quantity, None) -> [ mkAddQty skuId (defaultArg quantity 0+1) ] - | PatchItem (_, skuId, quantity, Some waive) -> - [ mkAddQty skuId (defaultArg quantity 0+1) - mkChangeWaived skuId (not waive)] + | PatchItem (_, skuId, Some 0, _) -> [ mkAdd skuId ] + | PatchItem (_, skuId, Some quantity, Some waive) -> + [ mkAddQty skuId (quantity+1) (Some (not waive)) ] + | PatchItem (_, skuId, Some quantity, waive) -> [ mkAddQty skuId (quantity+1) waive] let state = fold originState initialEvents let events = interpret command state let state' = fold state events - let find skuId = state'.items |> List.find (fun x -> x.skuId = skuId) + let find skuId = state'.items |> List.find (fun x -> x.skuId = skuId) + match command, events with - | AddItem (_, csku, quantity), [ ItemAdded e ] -> - test <@ { ItemAddInfo.context = e.context; skuId = csku; quantity = quantity } = e + | AddItem (_, csku, quantity, waive), [ Events.ItemAdded e ] -> + test <@ e = { context = e.context; skuId = csku; quantity = quantity; waived = waive } && quantity = (find csku).quantity @> - | PatchItem (_, csku, Some 0, _), [ ItemRemoved e ] - | RemoveItem (_, csku), [ ItemRemoved e ] -> - test <@ { ItemRemoveInfo.context = e.context; skuId = csku } = e + | PatchItem (_, csku, Some 0, _), [ Events.ItemRemoved e ] + | RemoveItem (_, csku), [ Events.ItemRemoved e ] -> + test <@ e = { Events.ItemRemovedInfo.context = e.context; skuId = csku } && not (state'.items |> List.exists (fun x -> x.skuId = csku)) @> | PatchItem (_, csku, quantity, waive), es -> match quantity with | Some value -> - test <@ es |> List.exists (function ItemQuantityChanged e -> e = { context = e.context; skuId = csku; quantity = value } | _ -> false) + test <@ es + |> List.exists (function + | Events.ItemQuantityChanged e -> e = { context = e.context; skuId = csku; quantity = value } + | _ -> false) && value = (find csku).quantity @> | None -> () match waive with | None -> () | Some value -> - test <@ es |> List.exists (function ItemWaiveReturnsChanged e -> e = { context = e.context; skuId = csku; waived = value } | _ -> false) - && value = (find csku).returnsWaived @> + test <@ es + |> List.exists (function + | Events.ItemPropertiesChanged e -> e = { context = e.context; skuId = csku; waived = value } + | _ -> false) + && value = (find csku).returnsWaived.Value @> | c,e -> failwithf "Invalid result - Command %A yielded Events %A in State %A" c e state -/// Processing should allow for any given Command to be retried at will +/// Processing should allow for any given Command to be retried at will, without inducing redundant +/// (and hence potentially-conflicting) changes let verifyIdempotency (cmd: Command) (originState: State) = // Put the aggregate into the state where the command should not trigger an event - let establish: Event list = cmd |> function - | AddItem (_, skuId, qty) -> [ mkAddQty skuId qty] + let establish: Events.Event list = cmd |> function + | AddItem (_, skuId, qty, waive) -> [ mkAddQty skuId qty waive] | RemoveItem _ - | PatchItem (_, _, Some 0, _) -> [] - | PatchItem (_, skuId, quantity, waived) -> [ mkAddQty skuId (defaultArg quantity 1) - mkChangeWaived skuId (defaultArg waived false) ] + | PatchItem (_, _, Some 0, _) + | PatchItem (_, _, None, _) -> [] + | PatchItem (_, skuId, Some quantity, waived) ->[ mkAddQty skuId quantity waived ] let state = fold originState establish let events = interpret cmd state @@ -76,13 +95,22 @@ let verifyIdempotency (cmd: Command) (originState: State) = /// These cases are assumed to be covered by external validation, so logic can treat them as hypotheticals rather than have to reject let isValid = function - | PatchItem (_, _, Some quantity, _) - | AddItem (_, _, quantity) -> quantity >= 0 + // One can generate a null request consisting of quantity = None, waived = None, which has no concievable outcome + // we don't guard or special case this condition + | PatchItem (_, _, None, _) -> false + | AddItem (_, _, quantity, _) + | PatchItem (_, _, Some quantity, _) -> quantity > 0 | _ -> true +// For the origin state, we only do basic filtering, which can provide good fuzz testing even if our implementation +// might not happen to ever trigger such a state (as opposed to neutering an entire scenario as we do with isValue) +let (|ValidOriginState|) : Fold.State -> Fold.State = + let updateItems f = function { items = i } -> { items = f i } + updateItems (List.choose (function { quantity = q } as x when q > 0 -> Some x | _ -> None)) + [] -let ``interpret yields correct events, idempotently`` (cmd: Command) (originState: State) = +let ``interpret yields correct events, idempotently`` (cmd: Command) (ValidOriginState originState) = if not (isValid cmd) then () else - verifyCanProcessInInitialState cmd originState + verifyCanProcessInOriginState cmd originState verifyCorrectEventGenerationWhenAppropriate cmd originState verifyIdempotency cmd originState diff --git a/samples/Store/Domain/Cart.fs b/samples/Store/Domain/Cart.fs index 6103fb78c..342361113 100644 --- a/samples/Store/Domain/Cart.fs +++ b/samples/Store/Domain/Cart.fs @@ -3,32 +3,32 @@ let streamName (id: CartId) = FsCodec.StreamName.create "Cart" (CartId.toString id) // NOTE - these types and the union case names reflect the actual storage formats and hence need to be versioned with care +[] module Events = type ContextInfo = { time: System.DateTime; requestId: RequestId } - type ItemInfo = { context: ContextInfo; item: ItemInfo } - type ItemAddInfo = { context: ContextInfo; skuId: SkuId; quantity: int } - type ItemRemoveInfo = { context: ContextInfo; skuId: SkuId } - type ItemQuantityChangeInfo = { context: ContextInfo; skuId: SkuId; quantity: int } - type ItemWaiveReturnsInfo = { context: ContextInfo; skuId: SkuId; waived: bool } + type ItemAddedInfo = { context: ContextInfo; skuId: SkuId; quantity: int; waived: bool option } + type ItemRemovedInfo = { context: ContextInfo; skuId: SkuId } + type ItemQuantityChangedInfo = { context: ContextInfo; skuId: SkuId; quantity: int } + type ItemPropertiesChangedInfo ={ context: ContextInfo; skuId: SkuId; waived: bool } module Compaction = - type StateItemInfo = { skuId: SkuId; quantity: int; returnsWaived: bool } + type StateItemInfo = { skuId: SkuId; quantity: int; returnsWaived: bool option } type State = { items: StateItemInfo[] } type Event = | Snapshotted of Compaction.State - | ItemAdded of ItemAddInfo - | ItemRemoved of ItemRemoveInfo - | ItemQuantityChanged of ItemQuantityChangeInfo - | ItemWaiveReturnsChanged of ItemWaiveReturnsInfo + | ItemAdded of ItemAddedInfo + | ItemRemoved of ItemRemovedInfo + | ItemQuantityChanged of ItemQuantityChangedInfo + | ItemPropertiesChanged of ItemPropertiesChangedInfo interface TypeShape.UnionContract.IUnionContract let codec = FsCodec.NewtonsoftJson.Codec.Create() module Fold = - type ItemInfo = { skuId: SkuId; quantity: int; returnsWaived: bool } + type ItemInfo = { skuId: SkuId; quantity: int; returnsWaived: bool option } type State = { items: ItemInfo list } module State = let toSnapshot (s: State) : Events.Compaction.State = @@ -40,43 +40,48 @@ module Fold = let updateItems f = { state with items = f state.items } match event with | Events.Snapshotted s -> State.ofSnapshot s - | Events.ItemAdded e -> updateItems (fun current -> { skuId = e.skuId; quantity = e.quantity; returnsWaived = false } :: current) + | Events.ItemAdded e -> + updateItems (fun current -> + { skuId = e.skuId; quantity = e.quantity; returnsWaived = e.waived } + :: current) | Events.ItemRemoved e -> updateItems (List.filter (fun x -> x.skuId <> e.skuId)) | Events.ItemQuantityChanged e -> updateItems (List.map (function i when i.skuId = e.skuId -> { i with quantity = e.quantity } | i -> i)) - | Events.ItemWaiveReturnsChanged e -> updateItems (List.map (function i when i.skuId = e.skuId -> { i with returnsWaived = e.waived } | i -> i)) + | Events.ItemPropertiesChanged e -> + updateItems (List.map (function + | i when i.skuId = e.skuId -> { i with returnsWaived = Some e.waived } + | i -> i)) let fold : State -> Events.Event seq -> State = Seq.fold evolve let isOrigin = function Events.Snapshotted _ -> true | _ -> false let snapshot = State.toSnapshot >> Events.Snapshotted + type Context = { time: System.DateTime; requestId : RequestId } type Command = - | AddItem of Context * SkuId * quantity: int - | PatchItem of Context * SkuId * quantity: int option * waived: bool option - | RemoveItem of Context * SkuId + | SyncItem of Context * SkuId * quantity: int option * waived: bool option let interpret command (state : Fold.State) = let itemExists f = state.items |> List.exists f - let itemExistsWithDifferentWaiveStatus skuId waive = itemExists (fun x -> x.skuId = skuId && x.returnsWaived <> waive) + let itemExistsWithDifferentWaiveStatus skuId waive = itemExists (fun x -> x.skuId = skuId && x.returnsWaived <> Some waive) let itemExistsWithDifferentQuantity skuId quantity = itemExists (fun x -> x.skuId = skuId && x.quantity <> quantity) - let itemExistsWithSameQuantity skuId quantity = itemExists (fun x -> x.skuId = skuId && x.quantity = quantity) let itemExistsWithSkuId skuId = itemExists (fun x -> x.skuId = skuId && x.quantity <> 0) let toEventContext (reqContext: Context) = { requestId = reqContext.requestId; time = reqContext.time } : Events.ContextInfo let (|Context|) (context : Context) = toEventContext context + let maybePropChanges c skuId = function + | None -> [] + | Some waived -> + if not (itemExistsWithDifferentWaiveStatus skuId waived) then [] + else [ Events.ItemPropertiesChanged { context = c; skuId = skuId; waived = waived } ] + let maybeQuantityChanges c skuId quantity = + if not (itemExistsWithDifferentQuantity skuId quantity) then [] else + [ Events.ItemQuantityChanged { context = c; skuId = skuId; quantity = quantity } ] match command with - | AddItem (Context c, skuId, quantity) -> - if itemExistsWithSameQuantity skuId quantity then [] else - [ Events.ItemAdded { context = c; skuId = skuId; quantity = quantity } ] - | RemoveItem (Context c, skuId) - | PatchItem (Context c, skuId, Some 0, _) -> - if not (itemExistsWithSkuId skuId) then [] else - [ Events.ItemRemoved { context = c; skuId = skuId } ] - | PatchItem (_, skuId, _, _) when not (itemExistsWithSkuId skuId) -> - [] - | PatchItem (Context c, skuId, quantity, waived) -> - [ match quantity with - | Some quantity when itemExistsWithDifferentQuantity skuId quantity -> - yield Events.ItemQuantityChanged { context = c; skuId = skuId; quantity = quantity } - | _ -> () - match waived with - | Some waived when itemExistsWithDifferentWaiveStatus skuId waived -> - yield Events.ItemWaiveReturnsChanged { context = c; skuId = skuId; waived = waived } - | _ -> () ] + // a request to set quantity of `0` represents a removal request + | SyncItem (Context c, skuId, Some 0, _) -> + if itemExistsWithSkuId skuId then [ Events.ItemRemoved { context = c; skuId = skuId } ] + else [] + // Add/quantity change with potential waive change at same time + | SyncItem (Context c, skuId, Some q, w) -> + if itemExistsWithSkuId skuId then maybeQuantityChanges c skuId q @ maybePropChanges c skuId w + else [ Events.ItemAdded { context = c; skuId = skuId; quantity = q; waived = w } ] + // Waive return status change only + | SyncItem (Context c, skuId, None, w) -> + maybePropChanges c skuId w diff --git a/samples/Store/Integration/CartIntegration.fs b/samples/Store/Integration/CartIntegration.fs index bc3e49129..6c3cd8f1f 100644 --- a/samples/Store/Integration/CartIntegration.fs +++ b/samples/Store/Integration/CartIntegration.fs @@ -32,9 +32,9 @@ let resolveCosmosStreamWithoutCustomAccessStrategy gateway = let addAndThenRemoveItemsManyTimesExceptTheLastOne context cartId skuId (service: Backend.Cart.Service) count = service.ExecuteManyAsync(cartId, false, seq { for i in 1..count do - yield Domain.Cart.AddItem (context, skuId, i) + yield Domain.Cart.SyncItem (context, skuId, Some i, None) if i <> count then - yield Domain.Cart.RemoveItem (context, skuId) }) + yield Domain.Cart.SyncItem (context, skuId, Some 0, None) }) type Tests(testOutputHelper) = let testOutput = TestOutputAdapter testOutputHelper diff --git a/tests/Equinox.Cosmos.Integration/CosmosIntegration.fs b/tests/Equinox.Cosmos.Integration/CosmosIntegration.fs index 5c60ad020..7825795eb 100644 --- a/tests/Equinox.Cosmos.Integration/CosmosIntegration.fs +++ b/tests/Equinox.Cosmos.Integration/CosmosIntegration.fs @@ -61,9 +61,9 @@ type Tests(testOutputHelper) = let addAndThenRemoveItems optimistic exceptTheLastOne context cartId skuId (service: Backend.Cart.Service) count = service.ExecuteManyAsync(cartId, optimistic, seq { for i in 1..count do - yield Domain.Cart.AddItem (context, skuId, i) + yield Domain.Cart.SyncItem (context, skuId, Some i, None) if not exceptTheLastOne || i <> count then - yield Domain.Cart.RemoveItem (context, skuId) }) + yield Domain.Cart.SyncItem (context, skuId, Some 0, None) }) let addAndThenRemoveItemsManyTimes context cartId skuId service count = addAndThenRemoveItems false false context cartId skuId service count let addAndThenRemoveItemsManyTimesExceptTheLastOne context cartId skuId service count = @@ -129,7 +129,7 @@ type Tests(testOutputHelper) = return Some (skuId, addRemoveCount) } let act prepare (service : Backend.Cart.Service) skuId count = - service.ExecuteManyAsync(cartId, false, prepare = prepare, commands = [Domain.Cart.AddItem (context, skuId, count)]) + service.ExecuteManyAsync(cartId, false, prepare = prepare, commands = [Domain.Cart.SyncItem (context, skuId, Some count, None)]) let eventWaitSet () = let e = new ManualResetEvent(false) in (Async.AwaitWaitHandle e |> Async.Ignore), async { e.Set() |> ignore } let w0, s0 = eventWaitSet () @@ -258,7 +258,7 @@ type Tests(testOutputHelper) = return Some (skuId, addRemoveCount) } let act prepare (service : Backend.Cart.Service) skuId count = - service.ExecuteManyAsync(cartId, false, prepare = prepare, commands = [Domain.Cart.AddItem (context, skuId, count)]) + service.ExecuteManyAsync(cartId, false, prepare = prepare, commands = [Domain.Cart.SyncItem (context, skuId, Some count, None)]) let eventWaitSet () = let e = new ManualResetEvent(false) in (Async.AwaitWaitHandle e |> Async.Ignore), async { e.Set() |> ignore } let w0, s0 = eventWaitSet () diff --git a/tests/Equinox.EventStore.Integration/StoreIntegration.fs b/tests/Equinox.EventStore.Integration/StoreIntegration.fs index 3d6ff231a..0259c3042 100644 --- a/tests/Equinox.EventStore.Integration/StoreIntegration.fs +++ b/tests/Equinox.EventStore.Integration/StoreIntegration.fs @@ -81,9 +81,9 @@ type Tests(testOutputHelper) = let addAndThenRemoveItems optimistic exceptTheLastOne context cartId skuId (service: Backend.Cart.Service) count = service.ExecuteManyAsync(cartId, optimistic, seq { for i in 1..count do - yield Domain.Cart.AddItem (context, skuId, i) + yield Domain.Cart.SyncItem (context, skuId, Some i, None) if not exceptTheLastOne || i <> count then - yield Domain.Cart.RemoveItem (context, skuId) }) + yield Domain.Cart.SyncItem (context, skuId, Some 0, None) }) let addAndThenRemoveItemsManyTimes context cartId skuId service count = addAndThenRemoveItems false false context cartId skuId service count let addAndThenRemoveItemsManyTimesExceptTheLastOne context cartId skuId service count = @@ -158,7 +158,7 @@ type Tests(testOutputHelper) = return Some (skuId, addRemoveCount) } let act prepare (service : Backend.Cart.Service) log skuId count = - service.ExecuteManyAsync(cartId, false, prepare = prepare, commands = [Domain.Cart.AddItem (context, skuId, count)]) + service.ExecuteManyAsync(cartId, false, prepare = prepare, commands = [Domain.Cart.SyncItem (context, skuId, Some count, None)]) let eventWaitSet () = let e = new ManualResetEvent(false) in (Async.AwaitWaitHandle e |> Async.Ignore), async { e.Set() |> ignore } let w0, s0 = eventWaitSet () diff --git a/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs b/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs index e367101d1..836124092 100644 --- a/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs +++ b/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs @@ -16,13 +16,13 @@ let createServiceMemory log store = type Tests(testOutputHelper) = let testOutput = TestOutputAdapter testOutputHelper let createLog () = createLogger testOutput - + let (|NonZero|) = function None -> Some 1 | Some c when c <= 0 -> Some 1 | Some c -> Some c [] let ``Basic tracer bullet, sending a command and verifying the folded result directly and via a reload`` - cartId1 cartId2 ((_,skuId,quantity) as args) = Async.RunSynchronously <| async { + cartId1 cartId2 ((_,skuId,NonZero quantity,waive) as args) = Async.RunSynchronously <| async { let store = createMemoryStore () let service = let log = createLog () in createServiceMemory log store - let command = Domain.Cart.AddItem args + let command = Domain.Cart.SyncItem args // Act: Run the decision twice... let actTrappingStateAsSaved cartId = @@ -40,7 +40,7 @@ type Tests(testOutputHelper) = // Assert 2. Verify that the Command got correctly reflected in the state, with no extraneous effects let verifyFoldedStateReflectsCommand = function | { Domain.Cart.Fold.State.items = [ item ] } -> - let expectedItem : Domain.Cart.Fold.ItemInfo = { skuId = skuId; quantity = quantity; returnsWaived = false } + let expectedItem : Domain.Cart.Fold.ItemInfo = { skuId = skuId; quantity = quantity.Value; returnsWaived = waive } test <@ expectedItem = item @> | x -> x |> failwithf "Expected to find item, got %A" verifyFoldedStateReflectsCommand expected From 66861deca4593d5dde3357779731f79bdc625537 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 26 Mar 2020 11:51:52 +0000 Subject: [PATCH 2/3] Fix incorrect test impl --- samples/Store/Domain/Cart.fs | 13 +++++++++---- .../MemoryStoreIntegration.fs | 10 ++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/samples/Store/Domain/Cart.fs b/samples/Store/Domain/Cart.fs index 342361113..5f2d057c9 100644 --- a/samples/Store/Domain/Cart.fs +++ b/samples/Store/Domain/Cart.fs @@ -39,13 +39,18 @@ module Fold = let evolve (state : State) event = let updateItems f = { state with items = f state.items } match event with - | Events.Snapshotted s -> State.ofSnapshot s + | Events.Snapshotted s -> + State.ofSnapshot s | Events.ItemAdded e -> updateItems (fun current -> { skuId = e.skuId; quantity = e.quantity; returnsWaived = e.waived } :: current) - | Events.ItemRemoved e -> updateItems (List.filter (fun x -> x.skuId <> e.skuId)) - | Events.ItemQuantityChanged e -> updateItems (List.map (function i when i.skuId = e.skuId -> { i with quantity = e.quantity } | i -> i)) + | Events.ItemRemoved e -> + updateItems (List.filter (fun x -> x.skuId <> e.skuId)) + | Events.ItemQuantityChanged e -> + updateItems (List.map (function + | i when i.skuId = e.skuId -> { i with quantity = e.quantity } + | i -> i)) | Events.ItemPropertiesChanged e -> updateItems (List.map (function | i when i.skuId = e.skuId -> { i with returnsWaived = Some e.waived } @@ -62,7 +67,7 @@ let interpret command (state : Fold.State) = let itemExists f = state.items |> List.exists f let itemExistsWithDifferentWaiveStatus skuId waive = itemExists (fun x -> x.skuId = skuId && x.returnsWaived <> Some waive) let itemExistsWithDifferentQuantity skuId quantity = itemExists (fun x -> x.skuId = skuId && x.quantity <> quantity) - let itemExistsWithSkuId skuId = itemExists (fun x -> x.skuId = skuId && x.quantity <> 0) + let itemExistsWithSkuId skuId = itemExists (fun x -> x.skuId = skuId) let toEventContext (reqContext: Context) = { requestId = reqContext.requestId; time = reqContext.time } : Events.ContextInfo let (|Context|) (context : Context) = toEventContext context let maybePropChanges c skuId = function diff --git a/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs b/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs index 836124092..a4f9bea87 100644 --- a/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs +++ b/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs @@ -16,13 +16,15 @@ let createServiceMemory log store = type Tests(testOutputHelper) = let testOutput = TestOutputAdapter testOutputHelper let createLog () = createLogger testOutput - let (|NonZero|) = function None -> Some 1 | Some c when c <= 0 -> Some 1 | Some c -> Some c - [] + let (|NonZero|) = function + | None -> Some 1 + | Some c -> Some (max 1 c) + [] let ``Basic tracer bullet, sending a command and verifying the folded result directly and via a reload`` - cartId1 cartId2 ((_,skuId,NonZero quantity,waive) as args) = Async.RunSynchronously <| async { + cartId1 cartId2 (ctx,skuId,NonZero quantity,waive) = Async.RunSynchronously <| async { let store = createMemoryStore () let service = let log = createLog () in createServiceMemory log store - let command = Domain.Cart.SyncItem args + let command = Domain.Cart.SyncItem (ctx,skuId,quantity,waive) // Act: Run the decision twice... let actTrappingStateAsSaved cartId = From b40d0a13fbfb9721d79af1d3fb1b0dc2bc48b395 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 26 Mar 2020 11:53:41 +0000 Subject: [PATCH 3/3] Remove elevated test count --- tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs b/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs index a4f9bea87..5a02dc9be 100644 --- a/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs +++ b/tests/Equinox.MemoryStore.Integration/MemoryStoreIntegration.fs @@ -19,7 +19,7 @@ type Tests(testOutputHelper) = let (|NonZero|) = function | None -> Some 1 | Some c -> Some (max 1 c) - [] + [] let ``Basic tracer bullet, sending a command and verifying the folded result directly and via a reload`` cartId1 cartId2 (ctx,skuId,NonZero quantity,waive) = Async.RunSynchronously <| async { let store = createMemoryStore ()