diff --git a/.gitignore b/.gitignore index 9fa38aa..fa5af1f 100644 --- a/.gitignore +++ b/.gitignore @@ -349,4 +349,9 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ -**/App_Data/FShopOnWeb* +# SQLite database files +*.sqlite* + +# App_Data directory - ignore all contents except .gitkeep +**/App_Data/* +!**/App_Data/.gitkeep diff --git a/src/Microsoft.eShopWeb.Web/Basket/Basket.Component.fs b/src/Microsoft.eShopWeb.Web/Basket/Basket.Component.fs index 534858e..94f3c2b 100644 --- a/src/Microsoft.eShopWeb.Web/Basket/Basket.Component.fs +++ b/src/Microsoft.eShopWeb.Web/Basket/Basket.Component.fs @@ -29,19 +29,23 @@ module BasketComponent = section [ class' "esh-basket-item esh-basket-item--middle col" ] [ raw item.ProductName ] section [ class' "esh-basket-item esh-basket-item--middle col" ] [ raw (item.UnitPrice.ToString "C") ] section [ class' "esh-basket-item esh-basket-item--middle col" ] [ raw (item.Quantity.ToString()) ] - section [ class' "esh-basket-item esh-basket-item--middle col" ] [ raw ((decimal(item.Quantity) * item.UnitPrice).ToString "C" ) ] ] ] + section [ class' "esh-basket-item esh-basket-item--middle col" ] [ raw ((decimal(item.Quantity) * item.UnitPrice).ToString "C" ) ] + section [ class' "esh-basket-item esh-basket-item--middle col" ] + [ Elem.form + [ method "post"; action "/basket/remove" ] + [ input [ type' "hidden"; name "id"; value $"{item.CatalogItemId}" ] + input [ class' "btn btn-danger btn-sm"; type' "submit"; value "Remove" ] ] ] ] ] let itemsTmpl items = - [ Elem.form - [ method "post" ] - [ article - [ class' "esh-basket-titles row row-cols-auto justify-content-between" ] - [ section [ class' "esh-basket-title col" ] [ raw "Product" ] - section [ class' "esh-basket-title col hidden-lg-down" ] [] - section [ class' "esh-basket-title col" ] [ raw "Price" ] - section [ class' "esh-basket-title col" ] [ raw "Quantity" ] - section [ class' "esh-basket-title col" ] [ raw "Cost" ] ] - div [ class' "esh-catalog-items" ] (Seq.mapi itemTmpl items |> List.ofSeq) ] ] + [ article + [ class' "esh-basket-titles row row-cols-auto justify-content-between" ] + [ section [ class' "esh-basket-title col" ] [ raw "Product" ] + section [ class' "esh-basket-title col hidden-lg-down" ] [] + section [ class' "esh-basket-title col" ] [ raw "Price" ] + section [ class' "esh-basket-title col" ] [ raw "Quantity" ] + section [ class' "esh-basket-title col" ] [ raw "Cost" ] + section [ class' "esh-basket-title col" ] [ raw "Actions" ] ] + div [ class' "esh-catalog-items" ] (Seq.mapi itemTmpl items |> List.ofSeq) ] let noItemsTmpl = [ h3 [ class' "esh-catalog-items row" ] [ raw "Basket is empty." ] diff --git a/src/Microsoft.eShopWeb.Web/Basket/Basket.Domain.fs b/src/Microsoft.eShopWeb.Web/Basket/Basket.Domain.fs index c098fbf..1ba0b52 100644 --- a/src/Microsoft.eShopWeb.Web/Basket/Basket.Domain.fs +++ b/src/Microsoft.eShopWeb.Web/Basket/Basket.Domain.fs @@ -58,6 +58,8 @@ module BasketDomain = |> ignore basket + + let updateBasket (db: ShopContext) (quantity: int) productId = async { let! catalogItem = @@ -96,3 +98,28 @@ module BasketDomain = | None -> printfn "No product specified to be added to basket" return None } + + let removeFromBasket (db: ShopContext) catalogItemId = + async { + let! existingBasket = + (db.Baskets.Include(fun b -> b.Items).OrderBy(fun b -> b.Id)) |> tryFirstAsync + + let basket = existingBasket |> defaultValue emptyBasket + + try + // Remove the basket item from the database + let itemToRemove = + db.BasketItems.Where(fun bi -> bi.CatalogItemId = catalogItemId && bi.BasketId = basket.Id) + |> Seq.tryHead + + match itemToRemove with + | Some item -> + db.BasketItems.Remove(item) |> ignore + do! saveChangesAsync' db |> Async.Ignore + return Some catalogItemId + | None -> + return None + with exp -> + printfn $"Error removing item {catalogItemId} from basket"; printfn $"{exp}" + return None + } diff --git a/src/Microsoft.eShopWeb.Web/Basket/Basket.Page.fs b/src/Microsoft.eShopWeb.Web/Basket/Basket.Page.fs index 6735e71..ab1ef95 100644 --- a/src/Microsoft.eShopWeb.Web/Basket/Basket.Page.fs +++ b/src/Microsoft.eShopWeb.Web/Basket/Basket.Page.fs @@ -49,6 +49,21 @@ module BasketPage = | None -> Response.redirectPermanently "/basket?error=notfound" | Some q -> Response.redirectPermanently $"/basket?added={q}")) + let remove: HttpHandler = + Services.inject (fun db -> + + let mapAsync = fun (form: FormCollectionReader) -> + form.TryGetGuid "id" + |> fun catalogItemId -> + match catalogItemId with + | Some id -> BasketDomain.removeFromBasket db id |> Async.StartAsTask + | None -> async { return None } |> Async.StartAsTask + + Request.mapFormAsync mapAsync (fun result -> + match result with + | None -> Response.redirectPermanently "/basket?error=notfound" + | Some _ -> Response.redirectPermanently "/basket?removed=success")) + // This uses a more low-level approach to reading the form let postAlternate: HttpHandler = Services.inject (fun db -> fun ctx -> diff --git a/src/Microsoft.eShopWeb.Web/Program.fs b/src/Microsoft.eShopWeb.Web/Program.fs index 2eed2c8..868e953 100644 --- a/src/Microsoft.eShopWeb.Web/Program.fs +++ b/src/Microsoft.eShopWeb.Web/Program.fs @@ -67,6 +67,7 @@ module Program = get "/basket" BasketPage.get post "/basket" BasketPage.post + post "/basket/remove" BasketPage.remove get "/identity/account/login" LoginPage.handler diff --git a/src/Microsoft.eShopWeb.Web/packages.lock.json b/src/Microsoft.eShopWeb.Web/packages.lock.json index fd6be9b..b45c10f 100644 --- a/src/Microsoft.eShopWeb.Web/packages.lock.json +++ b/src/Microsoft.eShopWeb.Web/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - "net8.0": { + "net9.0": { "EntityFrameworkCore.FSharp": { "type": "Direct", "requested": "[6.0.7, )", @@ -24,18 +24,24 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.200, )", - "resolved": "8.0.200", - "contentHash": "qnxoF3Fu0HzfOeYdrwmQOsLP1v+OtOMSIYkNVUwf6nGqWzL03Hh4r6VFCvCb54jlsgtt3WADVYkKkrgdeY5kiQ==" + "requested": "[9.0.300, )", + "resolved": "9.0.300", + "contentHash": "TVt2J7RCE1KCS2IaONF+p8/KIZ1eHNbW+7qmKF6hGoD4tXl+o07ja1mPtFjMqRa5uHMFaTrGTPn/m945WnDLiQ==" }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "hd3l+6Wyo4GwFAWa8J87L1X1ypYsk3za1lIsaF3U4X/tUJof/QPkuFbdfAADhmNqvqppmUL04RbgFM2nl5A7rQ==", + "requested": "[9.0.6, )", + "resolved": "9.0.6", + "contentHash": "bVSdfFrqIo3ZeQfWYYfnVVanP1GWghkdw+MnEmZJz7jUwtdPQpBKHr0BW9dMizPamzU+SMA1Qu4nXuRTlKVAGQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "8.0.0", - "SQLitePCLRaw.bundle_e_sqlite3": "2.1.6" + "Microsoft.EntityFrameworkCore.Sqlite.Core": "9.0.6", + "Microsoft.Extensions.Caching.Memory": "9.0.6", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.6", + "Microsoft.Extensions.DependencyModel": "9.0.6", + "Microsoft.Extensions.Logging": "9.0.6", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.10", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.6" } }, "Falco.Markup": { @@ -53,32 +59,32 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "pujbzfszX7jAl7oTbHhqx7pxd9jibeyHHl8zy1gd55XMaKWjDtc5XhhNYwQnrwWYCInNdVoArbaaAvLgW7TwuA==", + "resolved": "9.0.6", + "contentHash": "3auiudiViGzj1TidUdjuDqtP3+f6PBk4xdw6r9sBaTtkYoGc3AZn0cP8LgYZaLRnJBqY5bXRLB+qhjoB+iATzA==", "dependencies": { - "SQLitePCLRaw.core": "2.1.6" + "SQLitePCLRaw.core": "2.1.10" } }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "SoODat83pGQUpWB9xULdMX6tuKpq/RTXDuJ2WeC1ldUKcKzLkaFJD1n+I0nOLY58odez/e7z8b6zdp235G/kyg==", + "resolved": "9.0.6", + "contentHash": "r5hzM6Bhw4X3z28l5vmsaCPjk9VsQP4zaaY01THh1SAYjgTMVadYIvpNkCfmrv/Klks6aIf2A9eY7cpGZab/hg==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "8.0.0", - "Microsoft.EntityFrameworkCore.Analyzers": "8.0.0", - "Microsoft.Extensions.Caching.Memory": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0" + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.6", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.6", + "Microsoft.Extensions.Caching.Memory": "9.0.6", + "Microsoft.Extensions.Logging": "9.0.6" } }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "VR22s3+zoqlVI7xauFKn1znSIFHO8xuILT+noSwS8bZCKcHz0ydkTDQMuaxSa5WBaQrZmwtTz9rmRvJ7X8mSPQ==" + "resolved": "9.0.6", + "contentHash": "7MkhPK8emb8hfOx/mFVvHuIHxQ+mH2YdlK4sFUXgsGlvR0A44vsmd2wcHavZOTTzaKhN+aFUVy3zmkztKmTo+A==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ZXxEeLs2zoZ1TA+QoMMcw4f3Tirf8PzgdDax8RoWo0dxI2KmqiEGWYjhm2B/XyWfglc6+mNRyB8rZiQSmxCpeg==" + "resolved": "9.0.6", + "contentHash": "VKggHNQC5FCn3/vooaIM/4aEjGmrmWm78IrdRLz9lLV0Rm9bVHEr/jiWApDkU0U9ec2xGAilvQqJ5mMX7QC2cw==" }, "Microsoft.EntityFrameworkCore.Design": { "type": "Transitive", @@ -91,133 +97,136 @@ }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "fFKkr24cYc7Zw5T6DC4tEyOEPgPbq23BBmym1r9kn4ET9F3HKaetpOeQtV2RryYyUxEeNkJuxgfiZHTisqZc+A==", + "resolved": "9.0.6", + "contentHash": "Ht6OT17sYnO31Dx+hX72YHrc5kZt53g5napaw0FpyIekXCvb+gUVvufEG55Fa7taFm8ccy0Vzs+JVNR9NL0JlA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + "Microsoft.EntityFrameworkCore": "9.0.6", + "Microsoft.Extensions.Caching.Memory": "9.0.6", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.6", + "Microsoft.Extensions.Logging": "9.0.6" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "Vtnf4SIenAR0fp4OGEb83Dgn37lSMQqt6952e0f/6u/HNO4KQBKYiFw9vWIW4f4nNApre39WioW+jqaIVk15Wg==", + "resolved": "9.0.6", + "contentHash": "xP+SvMDR/GZCDNXFw7z4WYbO2sYpECvht3+lqejg+Md8vLtURwTBvdsOUAnY4jBGmNFqHeh87hZSmUGmuxyqMA==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "8.0.0", - "Microsoft.EntityFrameworkCore.Relational": "8.0.0", - "Microsoft.Extensions.DependencyModel": "8.0.0" + "Microsoft.Data.Sqlite.Core": "9.0.6", + "Microsoft.EntityFrameworkCore.Relational": "9.0.6", + "Microsoft.Extensions.Caching.Memory": "9.0.6", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.6", + "Microsoft.Extensions.DependencyModel": "9.0.6", + "Microsoft.Extensions.Logging": "9.0.6", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.6" } }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", + "resolved": "9.0.6", + "contentHash": "bL/xQsVNrdVkzjP5yjX4ndkQ03H3+Bk3qPpl+AMCEJR2RkfgAYmoQ/xXffPV7is64+QHShnhA12YAaFmNbfM+A==", "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.Primitives": "9.0.6" } }, "Microsoft.Extensions.Caching.Memory": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==", + "resolved": "9.0.6", + "contentHash": "qPW2d798tBPZcRmrlaBJqyChf2+0odDdE+0Lxvrr0ywkSNl1oNMK8AKrOfDwyXyjuLCv0ua7p6nrUExCeXhCcg==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.Caching.Abstractions": "9.0.6", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6", + "Microsoft.Extensions.Logging.Abstractions": "9.0.6", + "Microsoft.Extensions.Options": "9.0.6", + "Microsoft.Extensions.Primitives": "9.0.6" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", + "resolved": "9.0.6", + "contentHash": "3GgMIi2jP8g1fBW93Z9b9Unamc0SIsgyhiCmC91gq4loTixK9vQMuxxUsfJ1kRGwn+/FqLKwOHqmn0oYWn3Fvw==", "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.Primitives": "9.0.6" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", + "resolved": "9.0.6", + "contentHash": "vS65HMo5RS10DD543fknsyVDxihMcVxVn3/hNaILgBxWYnOLxWIeCIO9X0QFuCvPRNjClvXe9Aj8KaQNx7vFkQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" + "resolved": "9.0.6", + "contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "NSmDw3K0ozNDgShSIpsZcbFIzBX4w28nDag+TfaQujkXGazBm+lid5onlWoCBy4VsLxqnnKjEBbGSJVWJMf43g==", - "dependencies": { - "System.Text.Encodings.Web": "8.0.0", - "System.Text.Json": "8.0.0" - } + "resolved": "9.0.6", + "contentHash": "grVU1ixgMHp+kuhIgvEzhE73jXRY6XmxNBPWrotmbjB9AvJvkwHnIzm1JlOsPpyixFgnzreh/bFBMJAjveX+fQ==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", + "resolved": "9.0.6", + "contentHash": "XBzjitTFaQhF8EbJ645vblZezV1p52ePTxKHoVkRidHF11Xkjxg94qr0Rvp2qyxK2vBJ4OIZ41NB15YUyxTGMQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.DependencyInjection": "9.0.6", + "Microsoft.Extensions.Logging.Abstractions": "9.0.6", + "Microsoft.Extensions.Options": "9.0.6" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "resolved": "9.0.6", + "contentHash": "LFnyBNK7WtFmKdnHu3v0HOYQ8BcjYuy0jdC9pgCJ/rbLKoJEG9/dBzSKMEeeWDbDeoWS0TIxOC8a9CM5ufca3A==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6" } }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", + "resolved": "9.0.6", + "contentHash": "wUPhNM1zsI58Dy10xRdF2+pnsisiUuETg5ZBncyAEEUm/CQ9Q1vmivyUWH8RDbAlqyixf2dJNQ2XZb7HsKUEQw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6", + "Microsoft.Extensions.Primitives": "9.0.6" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + "resolved": "9.0.6", + "contentHash": "BHniU24QV67qp1pJknqYSofAPYGmijGI8D+ci9yfw33iuFdyOeB9lWTg78ThyYLyQwZw3s0vZ36VMb0MqbUuLw==" }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", - "resolved": "2.1.6", - "contentHash": "BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==", + "resolved": "2.1.10", + "contentHash": "UxWuisvZ3uVcVOLJQv7urM/JiQH+v3TmaJc1BLKl5Dxfm/nTzTUrqswCqg/INiYLi61AXnHo1M1JPmPqqLnAdg==", "dependencies": { - "SQLitePCLRaw.lib.e_sqlite3": "2.1.6", - "SQLitePCLRaw.provider.e_sqlite3": "2.1.6" + "SQLitePCLRaw.lib.e_sqlite3": "2.1.10", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.10" } }, "SQLitePCLRaw.core": { "type": "Transitive", - "resolved": "2.1.6", - "contentHash": "wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==", + "resolved": "2.1.10", + "contentHash": "Ii8JCbC7oiVclaE/mbDEK000EFIJ+ShRPwAvvV89GOZhQ+ZLtlnSWl6ksCNMKu/VGXA4Nfi2B7LhN/QFN9oBcw==", "dependencies": { "System.Memory": "4.5.3" } }, "SQLitePCLRaw.lib.e_sqlite3": { "type": "Transitive", - "resolved": "2.1.6", - "contentHash": "2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==" + "resolved": "2.1.10", + "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" }, "SQLitePCLRaw.provider.e_sqlite3": { "type": "Transitive", - "resolved": "2.1.6", - "contentHash": "PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==", + "resolved": "2.1.10", + "contentHash": "uZVTi02C1SxqzgT0HqTWatIbWGb40iIkfc3FpFCpE/r7g6K0PqzDUeefL6P6HPhDtc6BacN3yQysfzP7ks+wSQ==", "dependencies": { - "SQLitePCLRaw.core": "2.1.6" + "SQLitePCLRaw.core": "2.1.10" } }, "System.Memory": { @@ -225,18 +234,10 @@ "resolved": "4.5.3", "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - }, "System.Text.Json": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==", - "dependencies": { - "System.Text.Encodings.Web": "8.0.0" - } + "resolved": "9.0.6", + "contentHash": "h+ZtYTyTnTh5Ju6mHCKb3FPGx4ylJZgm9W7Y2psUnkhQRPMOIxX+TCN0ZgaR/+Yea+93XHWAaMzYTar1/EHIPg==" } } } diff --git a/tests/Basket/RemoveFromBasket.fs b/tests/Basket/RemoveFromBasket.fs new file mode 100644 index 0000000..97933e3 --- /dev/null +++ b/tests/Basket/RemoveFromBasket.fs @@ -0,0 +1,167 @@ +module RemoveFromBasket + +open System +open System.Linq +open Xunit +open Microsoft.EntityFrameworkCore +open Microsoft.eShopWeb.Web +open Microsoft.eShopWeb.Web.Domain +open Microsoft.eShopWeb.Web.Persistence +open Microsoft.eShopWeb.Web.Basket.BasketDomain +open EntityFrameworkCore.FSharp.DbContextHelpers + +// Helper function to create an in-memory database context +let createInMemoryContext () = + let options = DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName = Guid.NewGuid().ToString()) + .Options + new ShopContext(options) + +// Helper function to seed test data +let seedTestData (context: ShopContext) = + let basketId = Guid.NewGuid() + let catalogItemId = Guid.NewGuid() + + // Create a basket entry first to ensure the basket exists + let basketItem = { + Id = 0 + CatalogItemId = catalogItemId + ProductName = "Test Product" + UnitPrice = 10.0M + OldUnitPrice = 10.0M + Quantity = 1 + PictureUri = "/test.png" + BasketId = basketId + } + + // Create the basket by directly executing insert (without navigation property issues) + // Since we can't use SQL with in-memory, we need to create a minimal basket + // We'll work around this by using the emptyBasket approach the function expects + + // First add the basket item with emptyBasket.Id + let basketItemWithEmptyBasketId = { basketItem with BasketId = Unchecked.defaultof } + context.BasketItems.Add(basketItemWithEmptyBasketId) |> ignore + context.SaveChanges() |> ignore + + (catalogItemId, Unchecked.defaultof) + +[] +let ``removeFromBasket should return Some catalogItemId when item exists`` () = + async { + // Arrange + use context = createInMemoryContext() + context.Database.EnsureCreated() |> ignore + let (catalogItemId, basketId) = seedTestData context + + // Act + let! result = removeFromBasket context catalogItemId + + // Assert + Assert.Equal(Some catalogItemId, result) + + // Verify item was removed from database + let remainingItems = context.BasketItems.Where(fun bi -> bi.CatalogItemId = catalogItemId && bi.BasketId = basketId) + |> Seq.toList + Assert.Empty(remainingItems) + } + +[] +let ``removeFromBasket should return None when item does not exist`` () = + async { + // Arrange + use context = createInMemoryContext() + context.Database.EnsureCreated() |> ignore + let (_, _) = seedTestData context + let nonExistentItemId = Guid.NewGuid() + + // Act + let! result = removeFromBasket context nonExistentItemId + + // Assert + Assert.Equal(None, result) + } + +[] +let ``removeFromBasket should return None when basket is empty`` () = + async { + // Arrange + use context = createInMemoryContext() + context.Database.EnsureCreated() |> ignore + let itemId = Guid.NewGuid() + + // Act + let! result = removeFromBasket context itemId + + // Assert + Assert.Equal(None, result) + } + +[] +let ``removeFromBasket should only remove specified item from basket with multiple items`` () = + async { + // Arrange + use context = createInMemoryContext() + context.Database.EnsureCreated() |> ignore + + let basketId = Guid.NewGuid() + let catalogItemId1 = Guid.NewGuid() + let catalogItemId2 = Guid.NewGuid() + + let basketItem1 = { + Id = 0 + CatalogItemId = catalogItemId1 + ProductName = "Test Product 1" + UnitPrice = 10.0M + OldUnitPrice = 10.0M + Quantity = 1 + PictureUri = "/test1.png" + BasketId = Unchecked.defaultof + } + + let basketItem2 = { + Id = 0 + CatalogItemId = catalogItemId2 + ProductName = "Test Product 2" + UnitPrice = 20.0M + OldUnitPrice = 20.0M + Quantity = 2 + PictureUri = "/test2.png" + BasketId = Unchecked.defaultof + } + + // Add a minimal basket directly without navigation properties + context.BasketItems.AddRange([basketItem1; basketItem2]) |> ignore + context.SaveChanges() |> ignore + + // Act - remove first item + let! result = removeFromBasket context catalogItemId1 + + // Assert + Assert.Equal(Some catalogItemId1, result) + + // Verify only the specified item was removed + let remainingItems = context.BasketItems.Where(fun bi -> bi.BasketId = Unchecked.defaultof) + |> Seq.toList + Assert.Single(remainingItems) |> ignore + Assert.Equal(catalogItemId2, remainingItems.[0].CatalogItemId) + } + +[] +let ``removeFromBasket should handle basket with no items gracefully`` () = + async { + // Arrange + use context = createInMemoryContext() + context.Database.EnsureCreated() |> ignore + + let basketId = Guid.NewGuid() + + let itemId = Guid.NewGuid() + + // No need to create any basket items since we're testing empty basket behavior + + // Act + let! result = removeFromBasket context itemId + + // Assert + Assert.Equal(None, result) + } diff --git a/tests/FShopOnWeb.Tests.fsproj b/tests/FShopOnWeb.Tests.fsproj index b4f1e82..88f719e 100644 --- a/tests/FShopOnWeb.Tests.fsproj +++ b/tests/FShopOnWeb.Tests.fsproj @@ -7,7 +7,7 @@ - + @@ -16,6 +16,12 @@ + + + + + + diff --git a/tests/Tests.fs b/tests/Tests.fs deleted file mode 100644 index 7f70fae..0000000 --- a/tests/Tests.fs +++ /dev/null @@ -1,8 +0,0 @@ -module Tests - -open System -open Xunit - -[] -let ``My test`` () = - Assert.True(true) diff --git a/tests/packages.lock.json b/tests/packages.lock.json index 38e5a2d..1da65ff 100644 --- a/tests/packages.lock.json +++ b/tests/packages.lock.json @@ -8,11 +8,32 @@ "resolved": "6.0.2", "contentHash": "bJShQ6uWRTQ100ZeyiMqcFlhP7WJ+bCuabUs885dJiBEzMsJMSFr7BOyeCw4rgvQokteGi5rKQTlkhfQPUXg2A==" }, + "EntityFrameworkCore.FSharp": { + "type": "Direct", + "requested": "[6.0.7, )", + "resolved": "6.0.7", + "contentHash": "TeHeXBlrVjUC55q7YnWXieBSNVoHAagykATm2By6RBgn1DNxXtU9jazP0lJ3MigOaTygto4fR2TB9YbAAUYcFg==", + "dependencies": { + "FSharp.Core": "6.0.1", + "Microsoft.EntityFrameworkCore.Design": "6.0.0" + } + }, "FSharp.Core": { "type": "Direct", - "requested": "[9.0.202, )", - "resolved": "9.0.202", - "contentHash": "p8iGT2PeRL9q0ZIQ3ZhmRMRs4+b5Xaf9SSYKOt/+tDtznMQhHovYWaD1491RvBz8rnxcesAISbrkrjI1/4TUtw==" + "requested": "[9.0.300, )", + "resolved": "9.0.300", + "contentHash": "TVt2J7RCE1KCS2IaONF+p8/KIZ1eHNbW+7qmKF6hGoD4tXl+o07ja1mPtFjMqRa5uHMFaTrGTPn/m945WnDLiQ==" + }, + "Microsoft.EntityFrameworkCore.InMemory": { + "type": "Direct", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "Pm4NBnv3aB8O5bBNwWRkL4a/H+3WdgKRKYD93FkR9TrUNb0jfns9JVN5w9WEUsQCm0C69Eg2Y85i8pdmSfaNnQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0" + } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", @@ -41,11 +62,190 @@ "resolved": "2.8.2", "contentHash": "vm1tbfXhFmjFMUmS4M0J0ASXz3/U5XvXBa6DOQUL3fEz4Vt6YPhv+ESCarx6M6D+9kJkJYZKCNvJMas1+nVfmQ==" }, + "Falco": { + "type": "Transitive", + "resolved": "4.0.6", + "contentHash": "EbXhnwdalKEGVBwYctYmh1QpgWs/0zJ37CNtSWwt5mmj1Xk6bNTJhyU55ppiF3HFnXRia3iJEc4x6okWoNnx2Q==", + "dependencies": { + "FSharp.Core": "6.0.0", + "Falco.Markup": "1.0.2" + } + }, + "Falco.Markup": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "MbAUvgmPJc+JVawHwlPf4v3ql5E7BIUBkqvSi+6U6j2PDvBWbuB76u+O7ZLN86Jp9hbtaDNizO8L0AGRbAO20Q==", + "dependencies": { + "FSharp.Core": "4.5.2" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.8.26", + "contentHash": "OiKusGL20vby4uDEswj2IgkdchC1yQ6rwbIkZDVBPIR6al2b7n3pC91elBul9q33KaBgRKhbZH3+2Ur4fnWx2A==" + }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "17.12.0", "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA==" }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "3auiudiViGzj1TidUdjuDqtP3+f6PBk4xdw6r9sBaTtkYoGc3AZn0cP8LgYZaLRnJBqY5bXRLB+qhjoB+iATzA==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "r5hzM6Bhw4X3z28l5vmsaCPjk9VsQP4zaaY01THh1SAYjgTMVadYIvpNkCfmrv/Klks6aIf2A9eY7cpGZab/hg==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.6", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.6", + "Microsoft.Extensions.Caching.Memory": "9.0.6", + "Microsoft.Extensions.Logging": "9.0.6" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "7MkhPK8emb8hfOx/mFVvHuIHxQ+mH2YdlK4sFUXgsGlvR0A44vsmd2wcHavZOTTzaKhN+aFUVy3zmkztKmTo+A==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "VKggHNQC5FCn3/vooaIM/4aEjGmrmWm78IrdRLz9lLV0Rm9bVHEr/jiWApDkU0U9ec2xGAilvQqJ5mMX7QC2cw==" + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "RFdomymyuPNffl+VPk7osdxCJQ0xlGuxr28ifdfFFNUaMK0OYiJOjr6w9z3kscOM2p2gdPWNI1IFUXllEyphow==", + "dependencies": { + "Humanizer.Core": "2.8.26", + "Microsoft.EntityFrameworkCore.Relational": "6.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "Ht6OT17sYnO31Dx+hX72YHrc5kZt53g5napaw0FpyIekXCvb+gUVvufEG55Fa7taFm8ccy0Vzs+JVNR9NL0JlA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.6", + "Microsoft.Extensions.Caching.Memory": "9.0.6", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.6", + "Microsoft.Extensions.Logging": "9.0.6" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "bVSdfFrqIo3ZeQfWYYfnVVanP1GWghkdw+MnEmZJz7jUwtdPQpBKHr0BW9dMizPamzU+SMA1Qu4nXuRTlKVAGQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "9.0.6", + "Microsoft.Extensions.Caching.Memory": "9.0.6", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.6", + "Microsoft.Extensions.DependencyModel": "9.0.6", + "Microsoft.Extensions.Logging": "9.0.6", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.10", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.6" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "xP+SvMDR/GZCDNXFw7z4WYbO2sYpECvht3+lqejg+Md8vLtURwTBvdsOUAnY4jBGmNFqHeh87hZSmUGmuxyqMA==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "9.0.6", + "Microsoft.EntityFrameworkCore.Relational": "9.0.6", + "Microsoft.Extensions.Caching.Memory": "9.0.6", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.6", + "Microsoft.Extensions.DependencyModel": "9.0.6", + "Microsoft.Extensions.Logging": "9.0.6", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.6" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "bL/xQsVNrdVkzjP5yjX4ndkQ03H3+Bk3qPpl+AMCEJR2RkfgAYmoQ/xXffPV7is64+QHShnhA12YAaFmNbfM+A==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.6" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "qPW2d798tBPZcRmrlaBJqyChf2+0odDdE+0Lxvrr0ywkSNl1oNMK8AKrOfDwyXyjuLCv0ua7p6nrUExCeXhCcg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.6", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6", + "Microsoft.Extensions.Logging.Abstractions": "9.0.6", + "Microsoft.Extensions.Options": "9.0.6", + "Microsoft.Extensions.Primitives": "9.0.6" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "3GgMIi2jP8g1fBW93Z9b9Unamc0SIsgyhiCmC91gq4loTixK9vQMuxxUsfJ1kRGwn+/FqLKwOHqmn0oYWn3Fvw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.6" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "vS65HMo5RS10DD543fknsyVDxihMcVxVn3/hNaILgBxWYnOLxWIeCIO9X0QFuCvPRNjClvXe9Aj8KaQNx7vFkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "grVU1ixgMHp+kuhIgvEzhE73jXRY6XmxNBPWrotmbjB9AvJvkwHnIzm1JlOsPpyixFgnzreh/bFBMJAjveX+fQ==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "XBzjitTFaQhF8EbJ645vblZezV1p52ePTxKHoVkRidHF11Xkjxg94qr0Rvp2qyxK2vBJ4OIZ41NB15YUyxTGMQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.6", + "Microsoft.Extensions.Logging.Abstractions": "9.0.6", + "Microsoft.Extensions.Options": "9.0.6" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "LFnyBNK7WtFmKdnHu3v0HOYQ8BcjYuy0jdC9pgCJ/rbLKoJEG9/dBzSKMEeeWDbDeoWS0TIxOC8a9CM5ufca3A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "wUPhNM1zsI58Dy10xRdF2+pnsisiUuETg5ZBncyAEEUm/CQ9Q1vmivyUWH8RDbAlqyixf2dJNQ2XZb7HsKUEQw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6", + "Microsoft.Extensions.Primitives": "9.0.6" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "BHniU24QV67qp1pJknqYSofAPYGmijGI8D+ci9yfw33iuFdyOeB9lWTg78ThyYLyQwZw3s0vZ36VMb0MqbUuLw==" + }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "17.12.0", @@ -68,11 +268,51 @@ "resolved": "13.0.1", "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "UxWuisvZ3uVcVOLJQv7urM/JiQH+v3TmaJc1BLKl5Dxfm/nTzTUrqswCqg/INiYLi61AXnHo1M1JPmPqqLnAdg==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.10", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.10" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "Ii8JCbC7oiVclaE/mbDEK000EFIJ+ShRPwAvvV89GOZhQ+ZLtlnSWl6ksCNMKu/VGXA4Nfi2B7LhN/QFN9oBcw==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "uZVTi02C1SxqzgT0HqTWatIbWGb40iIkfc3FpFCpE/r7g6K0PqzDUeefL6P6HPhDtc6BacN3yQysfzP7ks+wSQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, "System.Reflection.Metadata": { "type": "Transitive", "resolved": "1.6.0", "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "h+ZtYTyTnTh5Ju6mHCKb3FPGx4ylJZgm9W7Y2psUnkhQRPMOIxX+TCN0ZgaR/+Yea+93XHWAaMzYTar1/EHIPg==" + }, "xunit.abstractions": { "type": "Transitive", "resolved": "2.0.3", @@ -112,6 +352,15 @@ "dependencies": { "xunit.extensibility.core": "[2.9.2]" } + }, + "microsoft.eshopweb.web": { + "type": "Project", + "dependencies": { + "EntityFrameworkCore.FSharp": "[6.0.7, )", + "FSharp.Core": "[9.0.300, )", + "Falco": "[4.0.6, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[9.0.6, )" + } } } }