Version 2.1 #41

Merged
danieljsummers merged 123 commits from version-2.1 into main 2024-03-27 00:13:28 +00:00
4 changed files with 266 additions and 26 deletions
Showing only changes of commit 182b33ae79 - Show all commits

View File

@ -80,13 +80,21 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
| Some cat ->
// Reassign any children to the category's parent category
let! children = conn.countByField Table.Category parentIdField EQ (string catId)
if children > 0 then
if children > 0L then
match cat.ParentId with
| Some _ ->
do! conn.patchByField Table.Category parentIdField EQ (string catId) {| ParentId = cat.ParentId |}
| None ->
do! conn.customNonQuery
$"""UPDATE {Table.Category}
SET data = json_remove(data, '$.ParentId')
WHERE {Query.whereByField parentIdField EQ "@field"}"""
[ fieldParam (string catId) ]
// Delete the category off all posts where it is assigned, and the category itself
let catIdField = nameof Post.Empty.CategoryIds
let! posts =
conn.customList
$"SELECT data ->> '{Post.Empty.Id}', data -> '{catIdField}'
$"SELECT data ->> '{nameof Post.Empty.Id}', data -> '{catIdField}'
FROM {Table.Post}
WHERE {Document.Query.whereByWebLog}
AND EXISTS

View File

@ -4,8 +4,14 @@ open Expecto
open MyWebLog
open MyWebLog.Data
/// The ID of the root web log
let rootId = WebLogId "uSitJEuD3UyzWC9jgOHc8g"
/// The ID of the Favorites category
let favoritesId = CategoryId "S5JflPsJ9EG7gA2LD4m92A"
/// Tests for the Add method
let addTests (data: IData) = task {
let ``Add succeeds`` (data: IData) = task {
let category =
{ Category.Empty with Id = CategoryId "added-cat"; WebLogId = WebLogId "test"; Name = "Added"; Slug = "added" }
do! data.Category.Add category
@ -13,3 +19,130 @@ let addTests (data: IData) = task {
Expect.isSome stored "The category should have been added"
}
let ``CountAll succeeds when categories exist`` (data: IData) = task {
let! count = data.Category.CountAll rootId
Expect.equal count 3 "There should have been 3 categories"
}
let ``CountAll succeeds when categories do not exist`` (data: IData) = task {
let! count = data.Category.CountAll WebLogId.Empty
Expect.equal count 0 "There should have been no categories"
}
let ``CountTopLevel succeeds when top-level categories exist`` (data: IData) = task {
let! count = data.Category.CountTopLevel rootId
Expect.equal count 2 "There should have been 2 top-level categories"
}
let ``CountTopLevel succeeds when no top-level categories exist`` (data: IData) = task {
let! count = data.Category.CountTopLevel WebLogId.Empty
Expect.equal count 0 "There should have been no top-level categories"
}
let ``FindAllForView succeeds`` (data: IData) = task {
let! all = data.Category.FindAllForView rootId
Expect.equal all.Length 3 "There should have been 3 categories returned"
Expect.equal all[0].Name "Favorites" "The first category is incorrect"
Expect.equal all[0].PostCount 1 "There should be one post in this category"
Expect.equal all[1].Name "Spitball" "The second category is incorrect"
Expect.equal all[1].PostCount 2 "There should be two posts in this category"
Expect.equal all[2].Name "Moonshot" "The third category is incorrect"
Expect.equal all[2].PostCount 1 "There should be one post in this category"
}
let ``FindById succeeds when a category is found`` (data: IData) = task {
let! cat = data.Category.FindById favoritesId rootId
Expect.isSome cat "There should have been a category returned"
Expect.equal cat.Value.Name "Favorites" "The category retrieved is incorrect"
Expect.equal cat.Value.Slug "favorites" "The slug is incorrect"
Expect.equal cat.Value.Description (Some "Favorite posts") "The description is incorrect"
Expect.isNone cat.Value.ParentId "There should have been no parent ID"
}
let ``FindById succeeds when a category is not found`` (data: IData) = task {
let! cat = data.Category.FindById CategoryId.Empty rootId
Expect.isNone cat "There should not have been a category returned"
}
let ``FindByWebLog succeeds when categories exist`` (data: IData) = task {
let! cats = data.Category.FindByWebLog rootId
Expect.equal cats.Length 3 "There should be 3 categories"
Expect.exists cats (fun it -> it.Name = "Favorites") "Favorites category not found"
Expect.exists cats (fun it -> it.Name = "Spitball") "Spitball category not found"
Expect.exists cats (fun it -> it.Name = "Moonshot") "Moonshot category not found"
}
let ``FindByWebLog succeeds when no categories exist`` (data: IData) = task {
let! cats = data.Category.FindByWebLog WebLogId.Empty
Expect.isEmpty cats "There should have been no categories returned"
}
let ``Update succeeds`` (data: IData) = task {
match! data.Category.FindById favoritesId rootId with
| Some cat ->
do! data.Category.Update { cat with Name = "My Favorites"; Slug = "my-favorites"; Description = None }
match! data.Category.FindById favoritesId rootId with
| Some updated ->
Expect.equal updated.Name "My Favorites" "Name not updated properly"
Expect.equal updated.Slug "my-favorites" "Slug not updated properly"
Expect.isNone updated.Description "Description should have been removed"
| None -> Expect.isTrue false "The updated favorites category could not be retrieved"
| None -> Expect.isTrue false "The favorites category could not be retrieved"
}
let ``Delete succeeds when the category is deleted (no posts)`` (data: IData) = task {
let! result = data.Category.Delete (CategoryId "added-cat") (WebLogId "test")
Expect.equal result CategoryDeleted "The category should have been deleted"
let! cat = data.Category.FindById (CategoryId "added-cat") (WebLogId "test")
Expect.isNone cat "The deleted category should not still exist"
}
let ``Delete succeeds when the category does not exist`` (data: IData) = task {
let! result = data.Category.Delete CategoryId.Empty (WebLogId "none")
Expect.equal result CategoryNotFound "The category should not have been found"
}
let ``Delete succeeds when reassigning parent category to None`` (data: IData) = task {
let moonshotId = CategoryId "ScVpyu1e7UiP7bDdge3ZEw"
let spitballId = CategoryId "jw6N69YtTEWVHAO33jHU-w"
let! result = data.Category.Delete spitballId rootId
Expect.equal result ReassignedChildCategories "Child categories should have been reassigned"
match! data.Category.FindById moonshotId rootId with
| Some cat -> Expect.isNone cat.ParentId "Parent ID should have been cleared"
| None -> Expect.isTrue false "Unable to find former child category"
}
let ``Delete succeeds when reassigning parent category to Some`` (data: IData) = task {
do! data.Category.Add { Category.Empty with Id = CategoryId "a"; WebLogId = WebLogId "test"; Name = "A" }
do! data.Category.Add
{ Category.Empty with
Id = CategoryId "b"
WebLogId = WebLogId "test"
Name = "B"
ParentId = Some (CategoryId "a") }
do! data.Category.Add
{ Category.Empty with
Id = CategoryId "c"
WebLogId = WebLogId "test"
Name = "C"
ParentId = Some (CategoryId "b") }
let! result = data.Category.Delete (CategoryId "b") (WebLogId "test")
Expect.equal result ReassignedChildCategories "Child categories should have been reassigned"
match! data.Category.FindById (CategoryId "c") (WebLogId "test") with
| Some cat -> Expect.equal cat.ParentId (Some (CategoryId "a")) "Parent category ID not reassigned properly"
| None -> Expect.isTrue false "Expected former child category not found"
}
let ``Delete succeeds and removes category from posts`` (data: IData) = task {
let moonshotId = CategoryId "ScVpyu1e7UiP7bDdge3ZEw"
let postId = PostId "RCsCU2puYEmkpzotoi8p4g"
match! data.Post.FindById postId rootId with
| Some post ->
Expect.equal post.CategoryIds [ moonshotId ] "Post category IDs are not as expected"
let! result = data.Category.Delete moonshotId rootId
Expect.equal result CategoryDeleted "The category should have been deleted (no children)"
match! data.Post.FindById postId rootId with
| Some p -> Expect.isEmpty p.CategoryIds "Category ID was not removed"
| None -> Expect.isTrue false "The expected updated post was not found"
| None -> Expect.isTrue false "The expected test post was not found"
}

View File

@ -1,5 +1,6 @@
module SQLiteDataTests
open System.IO
open BitBadger.Documents
open Expecto
open Microsoft.Extensions.Logging.Abstractions
@ -11,9 +12,12 @@ open Newtonsoft.Json
/// JSON serializer
let ser = Json.configure (JsonSerializer.CreateDefault())
/// The test database name
let dbName = "test-db.db"
/// Create a SQLiteData instance for testing
let mkData () =
Sqlite.Configuration.useConnectionString "Data Source=./test-db.db"
Sqlite.Configuration.useConnectionString $"Data Source=./{dbName}"
let conn = Sqlite.Configuration.dbConn ()
SQLiteData(conn, NullLogger<SQLiteData>(), ser) :> IData
@ -21,13 +25,24 @@ let mkData () =
let dispose (data: IData) =
(data :?> SQLiteData).Conn.Dispose()
/// Create a fresh environment from the root backup
let freshEnvironment (data: IData option) = task {
let env =
match data with
| Some d -> d
| None ->
File.Delete dbName
mkData ()
do! env.StartUp()
// This exercises Restore for all implementations; all tests are dependent on it working as expected
do! Maintenance.Backup.restoreBackup "root-weblog.json" None false false env
}
/// Set up the environment for the SQLite tests
let environmentSetUp = testList "Environment" [
testTask "creating database" {
let data = mkData ()
try
do! data.StartUp()
do! Maintenance.Backup.restoreBackup "root-weblog.json" None false data
try do! freshEnvironment (Some data)
finally dispose data
}
]
@ -36,18 +51,101 @@ let environmentSetUp = testList "Environment" [
let categoryTests = testList "Category" [
testTask "Add succeeds" {
let data = mkData ()
try do! CategoryDataTests.addTests data
try do! CategoryDataTests.``Add succeeds`` data
finally dispose data
}
testList "CountAll" [
testTask "succeeds when categories exist" {
let data = mkData ()
try do! CategoryDataTests.``CountAll succeeds when categories exist`` data
finally dispose data
}
testTask "succeeds when categories do not exist" {
let data = mkData ()
try do! CategoryDataTests.``CountAll succeeds when categories do not exist`` data
finally dispose data
}
]
testList "CountTopLevel" [
testTask "succeeds when top-level categories exist" {
let data = mkData ()
try do! CategoryDataTests.``CountTopLevel succeeds when top-level categories exist`` data
finally dispose data
}
testTask "succeeds when no top-level categories exist" {
let data = mkData ()
try do! CategoryDataTests.``CountTopLevel succeeds when no top-level categories exist`` data
finally dispose data
}
]
testTask "FindAllForView succeeds" {
let data = mkData ()
try do! CategoryDataTests.``FindAllForView succeeds`` data
finally dispose data
}
testList "FindById" [
testTask "succeeds when a category is found" {
let data = mkData ()
try do! CategoryDataTests.``FindById succeeds when a category is found`` data
finally dispose data
}
testTask "succeeds when a category is not found" {
let data = mkData ()
try do! CategoryDataTests.``FindById succeeds when a category is not found`` data
finally dispose data
}
]
testList "FindByWebLog" [
testTask "succeeds when categories exist" {
let data = mkData ()
try do! CategoryDataTests.``FindByWebLog succeeds when categories exist`` data
finally dispose data
}
testTask "succeeds when no categories exist" {
let data = mkData ()
try do! CategoryDataTests.``FindByWebLog succeeds when no categories exist`` data
finally dispose data
}
]
testTask "Update succeeds" {
let data = mkData ()
try do! CategoryDataTests.``Update succeeds`` data
finally dispose data
}
testList "Delete" [
testTask "succeeds when the category is deleted (no posts)" {
let data = mkData ()
try do! CategoryDataTests.``Delete succeeds when the category is deleted (no posts)`` data
finally dispose data
}
testTask "succeeds when the category does not exist" {
let data = mkData ()
try do! CategoryDataTests.``Delete succeeds when the category does not exist`` data
finally dispose data
}
testTask "succeeds when reassigning parent category to None" {
let data = mkData ()
try do! CategoryDataTests.``Delete succeeds when reassigning parent category to None`` data
finally dispose data
}
testTask "succeeds when reassigning parent category to Some" {
let data = mkData ()
try do! CategoryDataTests.``Delete succeeds when reassigning parent category to Some`` data
finally dispose data
}
testTask "succeeds and removes category from posts" {
let data = mkData ()
try do! CategoryDataTests.``Delete succeeds and removes category from posts`` data
finally dispose data
}
]
]
open System.IO
/// Delete the SQLite database
let environmentCleanUp = test "Clean Up" {
File.Delete "test-db.db"
Expect.isFalse (File.Exists "test-db.db") "The test SQLite database should have been deleted"
File.Delete dbName
Expect.isFalse (File.Exists dbName) "The test SQLite database should have been deleted"
}
/// All SQLite data tests

View File

@ -313,7 +313,7 @@ module Backup =
displayStats $"{fileName} (for <>NAME<>) contains:" webLog archive
}
let private doRestore archive newUrlBase (data: IData) = task {
let private doRestore archive newUrlBase isInteractive (data: IData) = task {
let! restore = task {
match! data.WebLog.FindById archive.WebLog.Id with
| Some webLog when defaultArg newUrlBase webLog.UrlBase = webLog.UrlBase ->
@ -357,6 +357,7 @@ module Backup =
}
// Restore theme and assets (one at a time, as assets can be large)
if isInteractive then
printfn ""
printfn "- Importing theme..."
do! data.Theme.Save restore.Theme
@ -365,37 +366,37 @@ module Backup =
// Restore web log data
printfn "- Restoring web log..."
if isInteractive then printfn "- Restoring web log..."
// v2.0 backups will not have redirect rules; fix that if restoring to v2.1 or later
let webLog =
if isNull (box restore.WebLog.RedirectRules) then { restore.WebLog with RedirectRules = [] }
else restore.WebLog
do! data.WebLog.Add webLog
printfn "- Restoring users..."
if isInteractive then printfn "- Restoring users..."
do! data.WebLogUser.Restore restore.Users
printfn "- Restoring categories and tag mappings..."
if isInteractive then printfn "- Restoring categories and tag mappings..."
if not (List.isEmpty restore.TagMappings) then do! data.TagMap.Restore restore.TagMappings
if not (List.isEmpty restore.Categories) then do! data.Category.Restore restore.Categories
printfn "- Restoring pages..."
if isInteractive then printfn "- Restoring pages..."
if not (List.isEmpty restore.Pages) then do! data.Page.Restore restore.Pages
printfn "- Restoring posts..."
if isInteractive then printfn "- Restoring posts..."
if not (List.isEmpty restore.Posts) then do! data.Post.Restore restore.Posts
// TODO: comments not yet implemented
printfn "- Restoring uploads..."
if isInteractive then printfn "- Restoring uploads..."
if not (List.isEmpty restore.Uploads) then
do! data.Upload.Restore (restore.Uploads |> List.map EncodedUpload.toUpload)
displayStats "Restored for <>NAME<>:" restore.WebLog restore
if isInteractive then displayStats "Restored for <>NAME<>:" restore.WebLog restore
}
/// Decide whether to restore a backup
let internal restoreBackup fileName newUrlBase promptForOverwrite data = task {
let internal restoreBackup fileName newUrlBase promptForOverwrite isInteractive data = task {
let serializer = getSerializer false
use stream = new FileStream(fileName, FileMode.Open)
@ -413,7 +414,7 @@ module Backup =
doOverwrite <- not (Console.ReadKey().Key = ConsoleKey.N)
if doOverwrite then
do! doRestore archive newUrlBase data
do! doRestore archive newUrlBase isInteractive data
else
printfn $"{archive.WebLog.Name} backup restoration canceled"
}
@ -445,7 +446,7 @@ module Backup =
if args.Length = 2 || args.Length = 3 then
let data = sp.GetRequiredService<IData>()
let newUrlBase = if args.Length = 3 then Some args[2] else None
do! restoreBackup args[1] newUrlBase (args[0] <> "do-restore") data
do! restoreBackup args[1] newUrlBase (args[0] <> "do-restore") true data
else
eprintfn "Usage: myWebLog restore [backup-file-name] [*url-base]"
eprintfn " * optional - will restore to original URL base if omitted"