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 -> | Some cat ->
// Reassign any children to the category's parent category // Reassign any children to the category's parent category
let! children = conn.countByField Table.Category parentIdField EQ (string catId) 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 |} 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 // Delete the category off all posts where it is assigned, and the category itself
let catIdField = nameof Post.Empty.CategoryIds let catIdField = nameof Post.Empty.CategoryIds
let! posts = let! posts =
conn.customList conn.customList
$"SELECT data ->> '{Post.Empty.Id}', data -> '{catIdField}' $"SELECT data ->> '{nameof Post.Empty.Id}', data -> '{catIdField}'
FROM {Table.Post} FROM {Table.Post}
WHERE {Document.Query.whereByWebLog} WHERE {Document.Query.whereByWebLog}
AND EXISTS AND EXISTS

View File

@ -4,8 +4,14 @@ open Expecto
open MyWebLog open MyWebLog
open MyWebLog.Data 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 /// Tests for the Add method
let addTests (data: IData) = task { let ``Add succeeds`` (data: IData) = task {
let category = let category =
{ Category.Empty with Id = CategoryId "added-cat"; WebLogId = WebLogId "test"; Name = "Added"; Slug = "added" } { Category.Empty with Id = CategoryId "added-cat"; WebLogId = WebLogId "test"; Name = "Added"; Slug = "added" }
do! data.Category.Add category do! data.Category.Add category
@ -13,3 +19,130 @@ let addTests (data: IData) = task {
Expect.isSome stored "The category should have been added" 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 module SQLiteDataTests
open System.IO
open BitBadger.Documents open BitBadger.Documents
open Expecto open Expecto
open Microsoft.Extensions.Logging.Abstractions open Microsoft.Extensions.Logging.Abstractions
@ -11,9 +12,12 @@ open Newtonsoft.Json
/// JSON serializer /// JSON serializer
let ser = Json.configure (JsonSerializer.CreateDefault()) let ser = Json.configure (JsonSerializer.CreateDefault())
/// The test database name
let dbName = "test-db.db"
/// Create a SQLiteData instance for testing /// Create a SQLiteData instance for testing
let mkData () = let mkData () =
Sqlite.Configuration.useConnectionString "Data Source=./test-db.db" Sqlite.Configuration.useConnectionString $"Data Source=./{dbName}"
let conn = Sqlite.Configuration.dbConn () let conn = Sqlite.Configuration.dbConn ()
SQLiteData(conn, NullLogger<SQLiteData>(), ser) :> IData SQLiteData(conn, NullLogger<SQLiteData>(), ser) :> IData
@ -21,13 +25,24 @@ let mkData () =
let dispose (data: IData) = let dispose (data: IData) =
(data :?> SQLiteData).Conn.Dispose() (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 /// Set up the environment for the SQLite tests
let environmentSetUp = testList "Environment" [ let environmentSetUp = testList "Environment" [
testTask "creating database" { testTask "creating database" {
let data = mkData () let data = mkData ()
try try do! freshEnvironment (Some data)
do! data.StartUp()
do! Maintenance.Backup.restoreBackup "root-weblog.json" None false data
finally dispose data finally dispose data
} }
] ]
@ -36,18 +51,101 @@ let environmentSetUp = testList "Environment" [
let categoryTests = testList "Category" [ let categoryTests = testList "Category" [
testTask "Add succeeds" { testTask "Add succeeds" {
let data = mkData () 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 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 /// Delete the SQLite database
let environmentCleanUp = test "Clean Up" { let environmentCleanUp = test "Clean Up" {
File.Delete "test-db.db" File.Delete dbName
Expect.isFalse (File.Exists "test-db.db") "The test SQLite database should have been deleted" Expect.isFalse (File.Exists dbName) "The test SQLite database should have been deleted"
} }
/// All SQLite data tests /// All SQLite data tests

View File

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