diff --git a/src/MyWebLog.Data/Interfaces.fs b/src/MyWebLog.Data/Interfaces.fs index 4c4c4c7..6fd9207 100644 --- a/src/MyWebLog.Data/Interfaces.fs +++ b/src/MyWebLog.Data/Interfaces.fs @@ -128,7 +128,7 @@ type IPostData = abstract member FindPageOfCategorizedPosts : WebLogId -> CategoryId list -> pageNbr: int -> postsPerPage: int -> Task - /// Find posts to be displayed on an admin page (excluding revisions and prior permalinks) + /// Find posts to be displayed on an admin page (excluding text, revisions, and prior permalinks) abstract member FindPageOfPosts : WebLogId -> pageNbr: int -> postsPerPage: int -> Task /// Find posts to be displayed on a page (excluding revisions and prior permalinks) diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs index c41ccee..7724ca0 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs @@ -5,7 +5,7 @@ open BitBadger.Documents.Postgres open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data -open NodaTime.Text +open NodaTime open Npgsql.FSharp /// PostgreSQL myWebLog post data implementation @@ -20,9 +20,13 @@ type PostgresPostData(log: ILogger) = return { post with Revisions = revisions } } - /// Return a post with no revisions or text + /// Return a post with no revisions or prior permalinks + let postWithoutLinks row = + { fromData row with PriorPermalinks = [] } + + /// Return a post with no revisions, prior permalinks, or text let postWithoutText row = - { fromData row with Text = "" } + { postWithoutLinks row with Text = "" } /// Update a post's revisions let updatePostRevisions (postId: PostId) oldRevs newRevs = @@ -36,6 +40,13 @@ type PostgresPostData(log: ILogger) = // IMPLEMENTATION FUNCTIONS + /// Add a post + let add (post : Post) = backgroundTask { + log.LogTrace "Post.add" + do! insert Table.Post { post with Revisions = [] } + do! updatePostRevisions post.Id [] post.Revisions + } + /// Count posts in a status for the given web log let countByStatus (status: PostStatus) webLogId = log.LogTrace "Post.countByStatus" @@ -55,7 +66,7 @@ type PostgresPostData(log: ILogger) = Custom.single (selectWithCriteria Table.Post) [ jsonParam "@criteria" {| webLogDoc webLogId with Permalink = permalink |} ] - (fun row -> { fromData row with PriorPermalinks = [] }) + postWithoutLinks /// Find a complete post by its ID for the given web log let findFullById postId webLogId = backgroundTask { @@ -118,7 +129,7 @@ type PostgresPostData(log: ILogger) = ORDER BY data ->> '{nameof Post.Empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" [ jsonParam "@criteria" {| webLogDoc webLogId with Status = Published |}; catParam ] - fromData + postWithoutLinks /// Get a page of posts for the given web log (excludes text and revisions) let findPageOfPosts webLogId pageNbr postsPerPage = @@ -139,7 +150,7 @@ type PostgresPostData(log: ILogger) = ORDER BY data ->> '{nameof Post.Empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" [ jsonParam "@criteria" {| webLogDoc webLogId with Status = Published |} ] - fromData + postWithoutLinks /// Get a page of tagged posts for the given web log (excludes revisions and prior permalinks) let findPageOfTaggedPosts webLogId (tag: string) pageNbr postsPerPage = @@ -150,40 +161,32 @@ type PostgresPostData(log: ILogger) = ORDER BY data ->> '{nameof Post.Empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" [ jsonParam "@criteria" {| webLogDoc webLogId with Status = Published |}; jsonParam "@tag" [| tag |] ] - fromData + postWithoutLinks /// Find the next newest and oldest post from a publish date for the given web log - let findSurroundingPosts webLogId publishedOn = backgroundTask { + let findSurroundingPosts webLogId (publishedOn: Instant) = backgroundTask { log.LogTrace "Post.findSurroundingPosts" let queryParams () = [ jsonParam "@criteria" {| webLogDoc webLogId with Status = Published |} - "@publishedOn", Sql.string ((InstantPattern.General.Format publishedOn)[..19]) ] - let pubField = nameof Post.Empty.PublishedOn - let! older = - Custom.list - $"{selectWithCriteria Table.Post} - AND SUBSTR(data ->> '{pubField}', 1, 19) < @publishedOn - ORDER BY data ->> '{pubField}' DESC - LIMIT 1" - (queryParams ()) - fromData - let! newer = - Custom.list - $"{selectWithCriteria Table.Post} - AND SUBSTR(data ->> '{pubField}', 1, 19) > @publishedOn - ORDER BY data ->> '{pubField}' - LIMIT 1" - (queryParams ()) - fromData + "@publishedOn", Sql.timestamptz (publishedOn.ToDateTimeOffset()) ] + let query op direction = + $"{selectWithCriteria Table.Post} + AND (data ->> '{nameof Post.Empty.PublishedOn}')::timestamp with time zone %s{op} @publishedOn + ORDER BY data ->> '{nameof Post.Empty.PublishedOn}' %s{direction} + LIMIT 1" + let! older = Custom.list (query "<" "DESC") (queryParams ()) postWithoutLinks + let! newer = Custom.list (query ">" "") (queryParams ()) postWithoutLinks return List.tryHead older, List.tryHead newer } - /// Save a post - let save (post : Post) = backgroundTask { + /// Update a post + let update (post : Post) = backgroundTask { log.LogTrace "Post.save" - let! oldPost = findFullById post.Id post.WebLogId - do! save Table.Post { post with Revisions = [] } - do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions + match! findFullById post.Id post.WebLogId with + | Some oldPost -> + do! Update.byId Table.Post post.Id { post with Revisions = [] } + do! updatePostRevisions post.Id oldPost.Revisions post.Revisions + | None -> () } /// Restore posts from a backup @@ -212,7 +215,7 @@ type PostgresPostData(log: ILogger) = } interface IPostData with - member _.Add post = save post + member _.Add post = add post member _.CountByStatus status webLogId = countByStatus status webLogId member _.Delete postId webLogId = delete postId webLogId member _.FindById postId webLogId = findById postId webLogId @@ -229,5 +232,5 @@ type PostgresPostData(log: ILogger) = findPageOfTaggedPosts webLogId tag pageNbr postsPerPage member _.FindSurroundingPosts webLogId publishedOn = findSurroundingPosts webLogId publishedOn member _.Restore posts = restore posts - member _.Update post = save post + member _.Update post = update post member _.UpdatePriorPermalinks postId webLogId permalinks = updatePriorPermalinks postId webLogId permalinks diff --git a/src/MyWebLog.Data/RethinkDbData.fs b/src/MyWebLog.Data/RethinkDbData.fs index 5a86813..fe46841 100644 --- a/src/MyWebLog.Data/RethinkDbData.fs +++ b/src/MyWebLog.Data/RethinkDbData.fs @@ -649,7 +649,8 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger obj nameof Post.Empty.Status, Published ] - without [ nameof Post.Empty.PriorPermalinks; nameof Post.Empty.Revisions ] + merge (r.HashMap(nameof Post.Empty.PriorPermalinks, [||]) + .With(nameof Post.Empty.Revisions, [||])) distinct orderByDescending (nameof Post.Empty.PublishedOn) skip ((pageNbr - 1) * postsPerPage) @@ -660,7 +661,9 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger { withTable Table.Post getAll [ webLogId ] (nameof Post.Empty.WebLogId) - without [ nameof Post.Empty.PriorPermalinks; nameof Post.Empty.Revisions ] + merge (r.HashMap(nameof Post.Empty.Text, "") + .With(nameof Post.Empty.PriorPermalinks, [||]) + .With(nameof Post.Empty.Revisions, [||])) orderByFuncDescending (fun row -> row[nameof Post.Empty.PublishedOn].Default_(nameof Post.Empty.UpdatedOn) :> obj) skip ((pageNbr - 1) * postsPerPage) @@ -672,7 +675,8 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger obj nameof Post.Empty.Status, Published ] - without [ nameof Post.Empty.PriorPermalinks; nameof Post.Empty.Revisions ] + merge (r.HashMap(nameof Post.Empty.PriorPermalinks, [||]) + .With(nameof Post.Empty.Revisions, [||])) orderByDescending (nameof Post.Empty.PublishedOn) skip ((pageNbr - 1) * postsPerPage) limit (postsPerPage + 1) @@ -697,7 +702,8 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger row[nameof Post.Empty.PublishedOn].Lt publishedOn :> obj) - without [ nameof Post.Empty.PriorPermalinks; nameof Post.Empty.Revisions ] + merge (r.HashMap(nameof Post.Empty.PriorPermalinks, [||]) + .With(nameof Post.Empty.Revisions, [||])) orderByDescending (nameof Post.Empty.PublishedOn) limit 1 result; withRetryDefault @@ -708,7 +714,8 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger row[nameof Post.Empty.PublishedOn].Gt publishedOn :> obj) - without [ nameof Post.Empty.PriorPermalinks; nameof Post.Empty.Revisions ] + merge (r.HashMap(nameof Post.Empty.PriorPermalinks, [||]) + .With(nameof Post.Empty.Revisions, [||])) orderBy (nameof Post.Empty.PublishedOn) limit 1 result; withRetryDefault @@ -726,22 +733,20 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger + do! rethink { + withTable Table.Post + get post.Id + replace post + write; withRetryDefault; ignoreResult conn + } + | None -> () } - member _.UpdatePriorPermalinks postId webLogId permalinks = backgroundTask { - match! ( - rethink { - withTable Table.Post - get postId - without [ nameof Post.Empty.Revisions; nameof Post.Empty.PriorPermalinks ] - resultOption; withRetryOptionDefault - } - |> verifyWebLog webLogId (_.WebLogId)) conn with + member this.UpdatePriorPermalinks postId webLogId permalinks = backgroundTask { + match! this.FindById postId webLogId with | Some _ -> do! rethink { withTable Table.Post diff --git a/src/MyWebLog.Data/SQLite/SQLitePostData.fs b/src/MyWebLog.Data/SQLite/SQLitePostData.fs index 62e8826..cc2063e 100644 --- a/src/MyWebLog.Data/SQLite/SQLitePostData.fs +++ b/src/MyWebLog.Data/SQLite/SQLitePostData.fs @@ -33,6 +33,14 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = /// The SELECT statement to retrieve posts with a web log ID parameter let postByWebLog = Document.Query.selectByWebLog Table.Post + /// Return a post with no revisions or prior permalinks + let postWithoutLinks rdr = + { fromData rdr with PriorPermalinks = [] } + + /// Return a post with no revisions, prior permalinks, or text + let postWithoutText rdr = + { postWithoutLinks rdr with Text = "" } + /// The SELECT statement to retrieve published posts with a web log ID parameter let publishedPostByWebLog = $"""{postByWebLog} AND {Query.whereByField (Field.EQ statName "") $"'{string Published}'"}""" @@ -44,6 +52,13 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = // IMPLEMENTATION FUNCTIONS + /// Add a post + let add (post: Post) = backgroundTask { + log.LogTrace "Post.add" + do! conn.insert Table.Post { post with Revisions = [] } + do! updatePostRevisions post.Id [] post.Revisions + } + /// Count posts in a status for the given web log let countByStatus (status: PostStatus) webLogId = log.LogTrace "Post.countByStatus" @@ -68,7 +83,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = conn.customSingle $"""{Document.Query.selectByWebLog Table.Post} AND {Query.whereByField linkParam "@link"}""" (addFieldParam "@link" linkParam [ webLogParam webLogId ]) - (fun rdr -> { fromData rdr with PriorPermalinks = [] }) + postWithoutLinks /// Find a complete post by its ID for the given web log let findFullById postId webLogId = backgroundTask { @@ -123,7 +138,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = ORDER BY {publishField} DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" (webLogParam webLogId :: catParams) - fromData + postWithoutLinks /// Get a page of posts for the given web log (excludes text and revisions) let findPageOfPosts webLogId pageNbr postsPerPage = @@ -133,7 +148,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = ORDER BY {publishField} DESC NULLS FIRST, data ->> '{nameof Post.Empty.UpdatedOn}' LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" [ webLogParam webLogId ] - (fun rdr -> { fromData rdr with Text = "" }) + postWithoutText /// Get a page of published posts for the given web log (excludes revisions) let findPageOfPublishedPosts webLogId pageNbr postsPerPage = @@ -143,7 +158,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = ORDER BY {publishField} DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" [ webLogParam webLogId ] - fromData + postWithoutLinks /// Get a page of tagged posts for the given web log (excludes revisions) let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage = @@ -154,7 +169,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = ORDER BY {publishField} DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" (webLogParam webLogId :: tagParams) - fromData + postWithoutLinks /// Find the next newest and oldest post from a publish date for the given web log let findSurroundingPosts webLogId (publishedOn : Instant) = backgroundTask { @@ -163,27 +178,29 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = conn.customSingle $"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1" [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ] - fromData + postWithoutLinks let! newer = conn.customSingle $"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1" [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ] - fromData + postWithoutLinks return older, newer } - /// Save a post - let save (post: Post) = backgroundTask { - log.LogTrace "Post.save" - let! oldPost = findFullById post.Id post.WebLogId - do! conn.save Table.Post { post with Revisions = [] } - do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions + /// Update a post + let update (post: Post) = backgroundTask { + log.LogTrace "Post.update" + match! findFullById post.Id post.WebLogId with + | Some oldPost -> + do! conn.updateById Table.Post post.Id { post with Revisions = [] } + do! updatePostRevisions post.Id oldPost.Revisions post.Revisions + | None -> () } /// Restore posts from a backup let restore posts = backgroundTask { log.LogTrace "Post.restore" - for post in posts do do! save post + for post in posts do do! add post } /// Update prior permalinks for a post @@ -196,7 +213,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = } interface IPostData with - member _.Add post = save post + member _.Add post = add post member _.CountByStatus status webLogId = countByStatus status webLogId member _.Delete postId webLogId = delete postId webLogId member _.FindById postId webLogId = findById postId webLogId @@ -213,5 +230,5 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) = findPageOfTaggedPosts webLogId tag pageNbr postsPerPage member _.FindSurroundingPosts webLogId publishedOn = findSurroundingPosts webLogId publishedOn member _.Restore posts = restore posts - member _.Update post = save post + member _.Update post = update post member _.UpdatePriorPermalinks postId webLogId permalinks = updatePriorPermalinks postId webLogId permalinks diff --git a/src/MyWebLog.Tests/Data/PostDataTests.fs b/src/MyWebLog.Tests/Data/PostDataTests.fs index e6e4dde..ecf63c8 100644 --- a/src/MyWebLog.Tests/Data/PostDataTests.fs +++ b/src/MyWebLog.Tests/Data/PostDataTests.fs @@ -24,9 +24,31 @@ let episode2 = PostId "l4_Eh4aFO06SqqJjOymNzA" /// The ID of "Something May Happen" post let something = PostId "QweKbWQiOkqqrjEdgP9wwg" +/// The published instant for "Something May Happen" post +let somethingPublished = Instant.FromDateTimeOffset(DateTimeOffset.Parse "2024-01-20T22:32:59Z") + +/// The ID of "An Incomplete Thought" post +let incomplete = PostId "VweKbWQiOkqqrjEdgP9wwg" + /// The ID of "Test Post 1" post let testPost1 = PostId "RCsCU2puYEmkpzotoi8p4g" +/// The published instant for "Test Post 1" post +let testPost1Published = Instant.FromDateTimeOffset(DateTimeOffset.Parse "2024-01-20T22:17:29Z") + +/// The category IDs for "Spitball" (parent) and "Moonshot" +let testCatIds = [ CategoryId "jw6N69YtTEWVHAO33jHU-w"; CategoryId "ScVpyu1e7UiP7bDdge3ZEw" ] + +/// Ensure that a list of posts has text for each post +let ensureHasText (posts: Post list) = + for post in posts do Expect.isNotEmpty post.Text $"Text should not be blank (post ID {post.Id})" + +/// Ensure that a list of posts has no revisions or prior permalinks +let ensureEmpty posts = + for post in posts do + Expect.isEmpty post.Revisions $"There should have been no revisions (post ID {post.Id})" + Expect.isEmpty post.PriorPermalinks $"There should have been no prior permalinks (post ID {post.Id})" + let ``Add succeeds`` (data: IData) = task { let post = { Id = PostId "a-new-post" @@ -109,8 +131,7 @@ let ``FindById succeeds when a post is found`` (data: IData) = task { it.Metadata [ { Name = "Density"; Value = "Non-existent" }; { Name = "Intensity"; Value = "Low" } ] "Metadata is incorrect" - Expect.isEmpty it.PriorPermalinks "Prior permalinks should have been empty" - Expect.isEmpty it.Revisions "Revisions should have been empty" + ensureEmpty [ it ] } let ``FindById succeeds when a post is not found (incorrect weblog)`` (data: IData) = task { @@ -128,8 +149,7 @@ let ``FindByPermalink succeeds when a post is found`` (data: IData) = task { Expect.isSome post "A post should have been returned" let it = post.Value Expect.equal it.Id episode1 "The wrong post was retrieved" - Expect.isEmpty it.PriorPermalinks "Prior permalinks should have been empty" - Expect.isEmpty it.Revisions "Revisions should have been empty" + ensureEmpty [ it ] } let ``FindByPermalink succeeds when a post is not found (incorrect weblog)`` (data: IData) = task { @@ -173,8 +193,8 @@ let ``FindFullById succeeds when a post is not found`` (data: IData) = task { let ``FindFullByWebLog succeeds when posts are found`` (data: IData) = task { let! posts = data.Post.FindFullByWebLog rootId - Expect.hasLength posts 4 "There should have been 4 posts returned" - let allPosts = [ testPost1; episode1; episode2; something ] + Expect.hasLength posts 5 "There should have been 5 posts returned" + let allPosts = [ testPost1; episode1; episode2; something; incomplete ] posts |> List.iter (fun it -> Expect.contains allPosts it.Id $"Post ID {it.Id} unexpected" if it.Id = episode1 then @@ -187,3 +207,225 @@ let ``FindFullByWebLog succeeds when posts are not found`` (data: IData) = task let! posts = data.Post.FindFullByWebLog (WebLogId "nonexistent") Expect.isEmpty posts "No posts should have been retrieved" } + +let ``FindPageOfCategorizedPosts succeeds when posts are found`` (data: IData) = task { + let! posts = data.Post.FindPageOfCategorizedPosts rootId testCatIds 1 1 + Expect.hasLength posts 2 "There should be 2 posts returned" + Expect.equal posts[0].Id something "The wrong post was returned for page 1" + ensureEmpty posts + let! posts = data.Post.FindPageOfCategorizedPosts rootId testCatIds 2 1 + Expect.hasLength posts 1 "There should be 1 post returned" + Expect.equal posts[0].Id testPost1 "The wrong post was returned for page 2" + ensureEmpty posts +} + +let ``FindPageOfCategorizedPosts succeeds when finding a too-high page number`` (data: IData) = task { + let! posts = data.Post.FindPageOfCategorizedPosts rootId testCatIds 17 2 + Expect.hasLength posts 0 "There should have been no posts returned (not enough posts)" +} + +let ``FindPageOfCategorizedPosts succeeds when a category has no posts`` (data: IData) = task { + let! posts = data.Post.FindPageOfCategorizedPosts rootId [ CategoryId "nope" ] 1 1 + Expect.hasLength posts 0 "There should have been no posts returned (none match)" +} + +let ``FindPageOfPosts succeeds when posts are found`` (data: IData) = task { + let ensureNoText (posts: Post list) = + for post in posts do Expect.equal post.Text "" $"There should be no text (post ID {post.Id})" + let! posts = data.Post.FindPageOfPosts rootId 1 2 + Expect.hasLength posts 3 "There should have been 3 posts returned for page 1" + Expect.equal posts[0].Id incomplete "Page 1, post 1 is incorrect" + Expect.equal posts[1].Id something "Page 1, post 2 is incorrect" + Expect.equal posts[2].Id episode2 "Page 1, post 3 is incorrect" + ensureNoText posts + ensureEmpty posts + let! posts = data.Post.FindPageOfPosts rootId 2 2 + Expect.hasLength posts 3 "There should have been 3 posts returned for page 2" + Expect.equal posts[0].Id episode2 "Page 2, post 1 is incorrect" + Expect.equal posts[1].Id episode1 "Page 2, post 2 is incorrect" + Expect.equal posts[2].Id testPost1 "Page 2, post 3 is incorrect" + ensureNoText posts + ensureEmpty posts + let! posts = data.Post.FindPageOfPosts rootId 3 2 + Expect.hasLength posts 1 "There should have been 1 post returned for page 3" + Expect.equal posts[0].Id testPost1 "Page 3, post 1 is incorrect" + ensureNoText posts + ensureEmpty posts +} + +let ``FindPageOfPosts succeeds when finding a too-high page number`` (data: IData) = task { + let! posts = data.Post.FindPageOfPosts rootId 88 3 + Expect.isEmpty posts "There should have been no posts returned (not enough posts)" +} + +let ``FindPageOfPosts succeeds when there are no posts`` (data: IData) = task { + let! posts = data.Post.FindPageOfPosts (WebLogId "no-posts") 1 25 + Expect.isEmpty posts "There should have been no posts returned (no posts)" +} + +let ``FindPageOfPublishedPosts succeeds when posts are found`` (data: IData) = task { + let! posts = data.Post.FindPageOfPublishedPosts rootId 1 3 + Expect.hasLength posts 4 "There should have been 4 posts returned for page 1" + Expect.equal posts[0].Id something "Page 1, post 1 is incorrect" + Expect.equal posts[1].Id episode2 "Page 1, post 2 is incorrect" + Expect.equal posts[2].Id episode1 "Page 1, post 3 is incorrect" + Expect.equal posts[3].Id testPost1 "Page 1, post 4 is incorrect" + ensureHasText posts + ensureEmpty posts + let! posts = data.Post.FindPageOfPublishedPosts rootId 2 2 + Expect.hasLength posts 2 "There should have been 2 posts returned for page 2" + Expect.equal posts[0].Id episode1 "Page 2, post 1 is incorrect" + Expect.equal posts[1].Id testPost1 "Page 2, post 2 is incorrect" + ensureHasText posts + ensureEmpty posts +} + +let ``FindPageOfPublishedPosts succeeds when finding a too-high page number`` (data: IData) = task { + let! posts = data.Post.FindPageOfPublishedPosts rootId 7 22 + Expect.isEmpty posts "There should have been no posts returned (not enough posts)" +} + +let ``FindPageOfPublishedPosts succeeds when there are no posts`` (data: IData) = task { + let! posts = data.Post.FindPageOfPublishedPosts (WebLogId "empty") 1 8 + Expect.isEmpty posts "There should have been no posts returned (no posts)" +} + +let ``FindPageOfTaggedPosts succeeds when posts are found`` (data: IData) = task { + let! posts = data.Post.FindPageOfTaggedPosts rootId "f#" 1 1 + Expect.hasLength posts 2 "There should have been 2 posts returned" + Expect.equal posts[0].Id something "Page 1, post 1 is incorrect" + Expect.equal posts[1].Id testPost1 "Page 1, post 2 is incorrect" + ensureHasText posts + ensureEmpty posts + let! posts = data.Post.FindPageOfTaggedPosts rootId "f#" 2 1 + Expect.hasLength posts 1 "There should have been 1 posts returned" + Expect.equal posts[0].Id testPost1 "Page 2, post 1 is incorrect" + ensureHasText posts + ensureEmpty posts +} + +let ``FindPageOfTaggedPosts succeeds when posts are found (excluding drafts)`` (data: IData) = task { + let! posts = data.Post.FindPageOfTaggedPosts rootId "speculation" 1 10 + Expect.hasLength posts 1 "There should have been 1 post returned" + Expect.equal posts[0].Id something "Post 1 is incorrect" + ensureHasText posts + ensureEmpty posts +} + +let ``FindPageOfTaggedPosts succeeds when finding a too-high page number`` (data: IData) = task { + let! posts = data.Post.FindPageOfTaggedPosts rootId "f#" 436 18 + Expect.isEmpty posts "There should have been no posts returned (not enough posts)" +} + +let ``FindPageOfTaggedPosts succeeds when there are no posts`` (data: IData) = task { + let! posts = data.Post.FindPageOfTaggedPosts rootId "non-existent-tag" 1 8 + Expect.isEmpty posts "There should have been no posts returned (no posts)" +} + +let ``FindSurroundingPosts succeeds when there is no next newer post`` (data: IData) = task { + let! older, newer = data.Post.FindSurroundingPosts rootId somethingPublished + Expect.isSome older "There should have been an older post" + Expect.equal older.Value.Id episode2 "The next older post is incorrect" + ensureHasText [ older.Value ] + ensureEmpty [ older.Value ] + Expect.isNone newer "There should not have been a newer post" +} + +let ``FindSurroundingPosts succeeds when there is no next older post`` (data: IData) = task { + let! older, newer = data.Post.FindSurroundingPosts rootId testPost1Published + Expect.isNone older "There should not have been an older post" + Expect.isSome newer "There should have been a newer post" + Expect.equal newer.Value.Id episode1 "The next newer post is incorrect" + ensureHasText [ newer.Value ] + ensureEmpty [ newer.Value ] +} + +let ``FindSurroundingPosts succeeds when older and newer exist`` (data: IData) = task { + let! older, newer = data.Post.FindSurroundingPosts rootId episode1Published + Expect.isSome older "There should have been an older post" + Expect.equal older.Value.Id testPost1 "The next older post is incorrect" + Expect.isSome newer "There should have been a newer post" + Expect.equal newer.Value.Id episode2 "The next newer post is incorrect" + ensureHasText [ older.Value; newer.Value ] + ensureEmpty [ older.Value; newer.Value ] +} + +let ``Update succeeds when the post exists`` (data: IData) = task { + let! before = data.Post.FindFullById (PostId "a-new-post") (WebLogId "test") + Expect.isSome before "The post to be updated should have been found" + do! data.Post.Update + { before.Value with + AuthorId = WebLogUserId "someone-else" + Status = Draft + Title = "An Updated Test Post" + Permalink = Permalink "2021/updated-post.html" + PublishedOn = None + UpdatedOn = Noda.epoch + Duration.FromDays 4 + Template = Some "other" + Text = "

Updated text here" + CategoryIds = [ CategoryId "c"; CategoryId "d"; CategoryId "e" ] + Tags = [ "alpha"; "beta"; "nu"; "zeta" ] + Episode = None + Metadata = [ { Name = "Howdy"; Value = "Pardner" } ] + PriorPermalinks = Permalink "2020/test-post.html" :: before.Value.PriorPermalinks + Revisions = + { AsOf = Noda.epoch + Duration.FromDays 4; Text = Html "

Updated text here" } + :: before.Value.Revisions } + let! after = data.Post.FindFullById (PostId "a-new-post") (WebLogId "test") + Expect.isSome after "The updated post should have been found" + let post = after.Value + Expect.equal post.AuthorId (WebLogUserId "someone-else") "Updated author is incorrect" + Expect.equal post.Status Draft "Updated status is incorrect" + Expect.equal post.Title "An Updated Test Post" "Updated title is incorrect" + Expect.equal post.Permalink (Permalink "2021/updated-post.html") "Updated permalink is incorrect" + Expect.isNone post.PublishedOn "Updated post should not have had a published-on date/time" + Expect.equal post.UpdatedOn (Noda.epoch + Duration.FromDays 4) "Updated updated-on date/time is incorrect" + Expect.equal post.Template (Some "other") "Updated template is incorrect" + Expect.equal post.Text "

Updated text here" "Updated text is incorrect" + Expect.equal + post.CategoryIds [ CategoryId "c"; CategoryId "d"; CategoryId "e" ] "Updated category IDs are incorrect" + Expect.equal post.Tags [ "alpha"; "beta"; "nu"; "zeta" ] "Updated tags are incorrect" + Expect.isNone post.Episode "Update episode is incorrect" + Expect.equal post.Metadata [ { Name = "Howdy"; Value = "Pardner" } ] "Updated metadata is incorrect" + Expect.equal + post.PriorPermalinks + [ Permalink "2020/test-post.html"; Permalink "2020/test-post-a.html" ] + "Updated prior permalinks are incorrect" + Expect.equal + post.Revisions + [ { AsOf = Noda.epoch + Duration.FromDays 4; Text = Html "

Updated text here" } + { AsOf = Noda.epoch + Duration.FromMinutes 1L; Text = Html "

Test text here" } ] + "Updated revisions are incorrect" +} + +let ``Update succeeds when the post does not exist`` (data: IData) = task { + let postId = PostId "lost-post" + do! data.Post.Update { Post.Empty with Id = postId; WebLogId = rootId } + let! post = data.Post.FindById postId rootId + Expect.isNone post "A post should not have been retrieved" +} + +let ``UpdatePriorPermalinks succeeds when the post exists`` (data: IData) = task { + let links = [ Permalink "2024/ep-1.html"; Permalink "2023/ep-1.html" ] + let! found = data.Post.UpdatePriorPermalinks episode1 rootId links + Expect.isTrue found "The permalinks should have been updated" + let! post = data.Post.FindFullById episode1 rootId + Expect.isSome post "The post should have been found" + Expect.equal post.Value.PriorPermalinks links "The prior permalinks were not correct" +} + +let ``UpdatePriorPermalinks succeeds when the post does not exist`` (data: IData) = task { + let! found = + data.Post.UpdatePriorPermalinks (PostId "silence") WebLogId.Empty [ Permalink "a.html"; Permalink "b.html" ] + Expect.isFalse found "The permalinks should not have been updated" +} + +let ``Delete succeeds when a post is deleted`` (data: IData) = task { + let! deleted = data.Post.Delete episode2 rootId + Expect.isTrue deleted "The post should have been deleted" +} + +let ``Delete succeeds when a post is not deleted`` (data: IData) = task { + let! deleted = data.Post.Delete episode2 rootId // this was deleted above + Expect.isFalse deleted "A post should not have been deleted" +} diff --git a/src/MyWebLog.Tests/Data/PostgresDataTests.fs b/src/MyWebLog.Tests/Data/PostgresDataTests.fs index 48e8594..9c9b0ae 100644 --- a/src/MyWebLog.Tests/Data/PostgresDataTests.fs +++ b/src/MyWebLog.Tests/Data/PostgresDataTests.fs @@ -1,6 +1,5 @@ module PostgresDataTests -open System open BitBadger.Documents open Expecto open Microsoft.Extensions.Logging.Abstractions @@ -275,6 +274,88 @@ let postTests = testList "Post" [ do! PostDataTests.``FindFullByWebLog succeeds when posts are not found`` (mkData ()) } ] + testList "FindPageOfCategorizedPosts" [ + testTask "succeeds when posts are found" { + do! PostDataTests.``FindPageOfCategorizedPosts succeeds when posts are found`` (mkData ()) + } + testTask "succeeds when finding a too-high page number" { + do! PostDataTests.``FindPageOfCategorizedPosts succeeds when finding a too-high page number`` (mkData ()) + } + testTask "succeeds when a category has no posts" { + do! PostDataTests.``FindPageOfCategorizedPosts succeeds when a category has no posts`` (mkData ()) + } + ] + testList "FindPageOfPosts" [ + testTask "succeeds when posts are found" { + do! PostDataTests.``FindPageOfPosts succeeds when posts are found`` (mkData ()) + } + testTask "succeeds when finding a too-high page number" { + do! PostDataTests.``FindPageOfPosts succeeds when finding a too-high page number`` (mkData ()) + } + testTask "succeeds when there are no posts" { + do! PostDataTests.``FindPageOfPosts succeeds when there are no posts`` (mkData ()) + } + ] + testList "FindPageOfPublishedPosts" [ + testTask "succeeds when posts are found" { + do! PostDataTests.``FindPageOfPublishedPosts succeeds when posts are found`` (mkData ()) + } + testTask "succeeds when finding a too-high page number" { + do! PostDataTests.``FindPageOfPublishedPosts succeeds when finding a too-high page number`` (mkData ()) + } + testTask "succeeds when there are no posts" { + do! PostDataTests.``FindPageOfPublishedPosts succeeds when there are no posts`` (mkData ()) + } + ] + testList "FindPageOfTaggedPosts" [ + testTask "succeeds when posts are found" { + do! PostDataTests.``FindPageOfTaggedPosts succeeds when posts are found`` (mkData ()) + } + testTask "succeeds when posts are found (excluding drafts)" { + do! PostDataTests.``FindPageOfTaggedPosts succeeds when posts are found (excluding drafts)`` (mkData ()) + } + testTask "succeeds when finding a too-high page number" { + do! PostDataTests.``FindPageOfTaggedPosts succeeds when finding a too-high page number`` (mkData ()) + } + testTask "succeeds when there are no posts" { + do! PostDataTests.``FindPageOfTaggedPosts succeeds when there are no posts`` (mkData ()) + } + ] + testList "FindSurroundingPosts" [ + testTask "succeeds when there is no next newer post" { + do! PostDataTests.``FindSurroundingPosts succeeds when there is no next newer post`` (mkData ()) + } + testTask "succeeds when there is no next older post" { + do! PostDataTests.``FindSurroundingPosts succeeds when there is no next older post`` (mkData ()) + } + testTask "succeeds when older and newer exist" { + do! PostDataTests.``FindSurroundingPosts succeeds when older and newer exist`` (mkData ()) + } + ] + testList "Update" [ + testTask "succeeds when the post exists" { + do! PostDataTests.``Update succeeds when the post exists`` (mkData ()) + } + testTask "succeeds when the post does not exist" { + do! PostDataTests.``Update succeeds when the post does not exist`` (mkData ()) + } + ] + testList "UpdatePriorPermalinks" [ + testTask "succeeds when the post exists" { + do! PostDataTests.``UpdatePriorPermalinks succeeds when the post exists`` (mkData ()) + } + testTask "succeeds when the post does not exist" { + do! PostDataTests.``UpdatePriorPermalinks succeeds when the post does not exist`` (mkData ()) + } + ] + testList "Delete" [ + testTask "succeeds when a post is deleted" { + do! PostDataTests.``Delete succeeds when a post is deleted`` (mkData ()) + } + testTask "succeeds when a post is not deleted" { + do! PostDataTests.``Delete succeeds when a post is not deleted`` (mkData ()) + } + ] ] /// Drop the throwaway PostgreSQL database diff --git a/src/MyWebLog.Tests/Data/RethinkDbDataTests.fs b/src/MyWebLog.Tests/Data/RethinkDbDataTests.fs index 2765617..d33fca5 100644 --- a/src/MyWebLog.Tests/Data/RethinkDbDataTests.fs +++ b/src/MyWebLog.Tests/Data/RethinkDbDataTests.fs @@ -274,6 +274,88 @@ let postTests = testList "Post" [ do! PostDataTests.``FindFullByWebLog succeeds when posts are not found`` data.Value } ] + testList "FindPageOfCategorizedPosts" [ + testTask "succeeds when posts are found" { + do! PostDataTests.``FindPageOfCategorizedPosts succeeds when posts are found`` data.Value + } + testTask "succeeds when finding a too-high page number" { + do! PostDataTests.``FindPageOfCategorizedPosts succeeds when finding a too-high page number`` data.Value + } + testTask "succeeds when a category has no posts" { + do! PostDataTests.``FindPageOfCategorizedPosts succeeds when a category has no posts`` data.Value + } + ] + testList "FindPageOfPosts" [ + testTask "succeeds when posts are found" { + do! PostDataTests.``FindPageOfPosts succeeds when posts are found`` data.Value + } + testTask "succeeds when finding a too-high page number" { + do! PostDataTests.``FindPageOfPosts succeeds when finding a too-high page number`` data.Value + } + testTask "succeeds when there are no posts" { + do! PostDataTests.``FindPageOfPosts succeeds when there are no posts`` data.Value + } + ] + testList "FindPageOfPublishedPosts" [ + testTask "succeeds when posts are found" { + do! PostDataTests.``FindPageOfPublishedPosts succeeds when posts are found`` data.Value + } + testTask "succeeds when finding a too-high page number" { + do! PostDataTests.``FindPageOfPublishedPosts succeeds when finding a too-high page number`` data.Value + } + testTask "succeeds when there are no posts" { + do! PostDataTests.``FindPageOfPublishedPosts succeeds when there are no posts`` data.Value + } + ] + testList "FindPageOfTaggedPosts" [ + testTask "succeeds when posts are found" { + do! PostDataTests.``FindPageOfTaggedPosts succeeds when posts are found`` data.Value + } + testTask "succeeds when posts are found (excluding drafts)" { + do! PostDataTests.``FindPageOfTaggedPosts succeeds when posts are found (excluding drafts)`` data.Value + } + testTask "succeeds when finding a too-high page number" { + do! PostDataTests.``FindPageOfTaggedPosts succeeds when finding a too-high page number`` data.Value + } + testTask "succeeds when there are no posts" { + do! PostDataTests.``FindPageOfTaggedPosts succeeds when there are no posts`` data.Value + } + ] + testList "FindSurroundingPosts" [ + testTask "succeeds when there is no next newer post" { + do! PostDataTests.``FindSurroundingPosts succeeds when there is no next newer post`` data.Value + } + testTask "succeeds when there is no next older post" { + do! PostDataTests.``FindSurroundingPosts succeeds when there is no next older post`` data.Value + } + testTask "succeeds when older and newer exist" { + do! PostDataTests.``FindSurroundingPosts succeeds when older and newer exist`` data.Value + } + ] + testList "Update" [ + testTask "succeeds when the post exists" { + do! PostDataTests.``Update succeeds when the post exists`` data.Value + } + testTask "succeeds when the post does not exist" { + do! PostDataTests.``Update succeeds when the post does not exist`` data.Value + } + ] + testList "UpdatePriorPermalinks" [ + testTask "succeeds when the post exists" { + do! PostDataTests.``UpdatePriorPermalinks succeeds when the post exists`` data.Value + } + testTask "succeeds when the post does not exist" { + do! PostDataTests.``UpdatePriorPermalinks succeeds when the post does not exist`` data.Value + } + ] + testList "Delete" [ + testTask "succeeds when a post is deleted" { + do! PostDataTests.``Delete succeeds when a post is deleted`` data.Value + } + testTask "succeeds when a post is not deleted" { + do! PostDataTests.``Delete succeeds when a post is not deleted`` data.Value + } + ] ] /// Drop the throwaway RethinkDB database diff --git a/src/MyWebLog.Tests/Data/SQLiteDataTests.fs b/src/MyWebLog.Tests/Data/SQLiteDataTests.fs index 484e1a8..2b735c9 100644 --- a/src/MyWebLog.Tests/Data/SQLiteDataTests.fs +++ b/src/MyWebLog.Tests/Data/SQLiteDataTests.fs @@ -390,6 +390,132 @@ let postTests = testList "Post" [ finally dispose data } ] + testList "FindPageOfCategorizedPosts" [ + testTask "succeeds when posts are found" { + let data = mkData () + try do! PostDataTests.``FindPageOfCategorizedPosts succeeds when posts are found`` data + finally dispose data + } + testTask "succeeds when finding a too-high page number" { + let data = mkData () + try do! PostDataTests.``FindPageOfCategorizedPosts succeeds when finding a too-high page number`` data + finally dispose data + } + testTask "succeeds when a category has no posts" { + let data = mkData () + try do! PostDataTests.``FindPageOfCategorizedPosts succeeds when a category has no posts`` data + finally dispose data + } + ] + testList "FindPageOfPosts" [ + testTask "succeeds when posts are found" { + let data = mkData () + try do! PostDataTests.``FindPageOfPosts succeeds when posts are found`` data + finally dispose data + } + testTask "succeeds when finding a too-high page number" { + let data = mkData () + try do! PostDataTests.``FindPageOfPosts succeeds when finding a too-high page number`` data + finally dispose data + } + testTask "succeeds when there are no posts" { + let data = mkData () + try do! PostDataTests.``FindPageOfPosts succeeds when there are no posts`` data + finally dispose data + } + ] + testList "FindPageOfPublishedPosts" [ + testTask "succeeds when posts are found" { + let data = mkData () + try do! PostDataTests.``FindPageOfPublishedPosts succeeds when posts are found`` data + finally dispose data + } + testTask "succeeds when finding a too-high page number" { + let data = mkData () + try do! PostDataTests.``FindPageOfPublishedPosts succeeds when finding a too-high page number`` data + finally dispose data + } + testTask "succeeds when there are no posts" { + let data = mkData () + try do! PostDataTests.``FindPageOfPublishedPosts succeeds when there are no posts`` data + finally dispose data + } + ] + testList "FindPageOfTaggedPosts" [ + testTask "succeeds when posts are found" { + let data = mkData () + try do! PostDataTests.``FindPageOfTaggedPosts succeeds when posts are found`` data + finally dispose data + } + testTask "succeeds when posts are found (excluding drafts)" { + let data = mkData () + try do! PostDataTests.``FindPageOfTaggedPosts succeeds when posts are found (excluding drafts)`` data + finally dispose data + } + testTask "succeeds when finding a too-high page number" { + let data = mkData () + try do! PostDataTests.``FindPageOfTaggedPosts succeeds when finding a too-high page number`` data + finally dispose data + } + testTask "succeeds when there are no posts" { + let data = mkData () + try do! PostDataTests.``FindPageOfTaggedPosts succeeds when there are no posts`` data + finally dispose data + } + ] + testList "FindSurroundingPosts" [ + testTask "succeeds when there is no next newer post" { + let data = mkData () + try do! PostDataTests.``FindSurroundingPosts succeeds when there is no next newer post`` data + finally dispose data + } + testTask "succeeds when there is no next older post" { + let data = mkData () + try do! PostDataTests.``FindSurroundingPosts succeeds when there is no next older post`` data + finally dispose data + } + testTask "succeeds when older and newer exist" { + let data = mkData () + try do! PostDataTests.``FindSurroundingPosts succeeds when older and newer exist`` data + finally dispose data + } + ] + testList "Update" [ + testTask "succeeds when the post exists" { + let data = mkData () + try do! PostDataTests.``Update succeeds when the post exists`` data + finally dispose data + } + testTask "succeeds when the post does not exist" { + let data = mkData () + try do! PostDataTests.``Update succeeds when the post does not exist`` data + finally dispose data + } + ] + testList "UpdatePriorPermalinks" [ + testTask "succeeds when the post exists" { + let data = mkData () + try do! PostDataTests.``UpdatePriorPermalinks succeeds when the post exists`` data + finally dispose data + } + testTask "succeeds when the post does not exist" { + let data = mkData () + try do! PostDataTests.``UpdatePriorPermalinks succeeds when the post does not exist`` data + finally dispose data + } + ] + testList "Delete" [ + testTask "succeeds when a post is deleted" { + let data = mkData () + try do! PostDataTests.``Delete succeeds when a post is deleted`` data + finally dispose data + } + testTask "succeeds when a post is not deleted" { + let data = mkData () + try do! PostDataTests.``Delete succeeds when a post is not deleted`` data + finally dispose data + } + ] ] /// Delete the SQLite database diff --git a/src/MyWebLog.Tests/root-weblog.json b/src/MyWebLog.Tests/root-weblog.json index a4b45a0..2b86eda 100644 --- a/src/MyWebLog.Tests/root-weblog.json +++ b/src/MyWebLog.Tests/root-weblog.json @@ -333,13 +333,39 @@ "speculation" ], "Metadata": [], - "PriorPermalinks": [], + "PriorPermalinks": [ + "2024/some-thing.html" + ], "Revisions": [ { "AsOf": "2024-01-20T22:32:59Z", "Text": "HTML:

Hmm

" } ] + }, + { + "Id": "VweKbWQiOkqqrjEdgP9wwg", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "AuthorId": "5EM2rimH9kONpmd2zQkiVA", + "Status": "Draft", + "Title": "An Incomplete Thought", + "Permalink": "2024/still-cooking.html", + "UpdatedOn": "2024-01-24T22:35:00Z", + "Text": "

Think think think", + "CategoryIds": [ + "jw6N69YtTEWVHAO33jHU-w" + ], + "Tags": [ + "speculation" + ], + "Metadata": [], + "PriorPermalinks": [], + "Revisions": [ + { + "AsOf": "2024-01-24T22:35:00Z", + "Text": "HTML:

Think think think" + } + ] } ], "Uploads": []