diff --git a/src/MyWebLog.Data/Interfaces.fs b/src/MyWebLog.Data/Interfaces.fs index a05d6b5..1f6a839 100644 --- a/src/MyWebLog.Data/Interfaces.fs +++ b/src/MyWebLog.Data/Interfaces.fs @@ -54,7 +54,7 @@ type IPageData = /// Add a page abstract member Add : Page -> Task - /// Get all pages for the web log (excluding revisions) + /// Get all pages for the web log (excluding text, metadata, revisions, and prior permalinks) abstract member All : WebLogId -> Task /// Count all pages for the given web log diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs index 33fb0e4..465172b 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -35,13 +35,13 @@ type PostgresPageData(log: ILogger) = // IMPLEMENTATION FUNCTIONS - /// Get all pages for a web log (without text or revisions) + /// Get all pages for a web log (without text, metadata, revisions, or prior permalinks) let all webLogId = log.LogTrace "Page.all" Custom.list $"{selectWithCriteria Table.Page} ORDER BY LOWER(data ->> '{nameof Page.Empty.Title}')" [ webLogContains webLogId ] - fromData + (fun row -> { fromData row with Text = ""; Metadata = []; PriorPermalinks = [] }) /// Count all pages for the given web log let countAll webLogId = diff --git a/src/MyWebLog.Data/RethinkDbData.fs b/src/MyWebLog.Data/RethinkDbData.fs index 4ab4294..ce65ef3 100644 --- a/src/MyWebLog.Data/RethinkDbData.fs +++ b/src/MyWebLog.Data/RethinkDbData.fs @@ -279,7 +279,7 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger { withTable Table.Category getAll [ webLogId ] (nameof Category.Empty.WebLogId) - filter (nameof Category.Empty.ParentId) None + filter (nameof Category.Empty.ParentId) None (Default FilterDefaultHandling.Return) count result; withRetryDefault conn } @@ -361,7 +361,9 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger row[nameof Post.Empty.CategoryIds].Contains catId :> obj) update (fun row -> - {| CategoryIds = r.Array(row[nameof Post.Empty.CategoryIds]).Remove catId |} :> obj) + {| CategoryIds = + row[nameof Post.Empty.CategoryIds].CoerceTo("array") + .SetDifference(r.Array(catId)) |} :> obj) write; withRetryDefault; ignoreResult conn } // Delete the category itself @@ -408,10 +410,10 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger { withTable Table.Page getAll [ webLogId ] (nameof Page.Empty.WebLogId) - without [ nameof Page.Empty.Text - nameof Page.Empty.Metadata - nameof Page.Empty.Revisions - nameof Page.Empty.PriorPermalinks ] + merge (r.HashMap(nameof Page.Empty.Text, "") + .With(nameof Page.Empty.Metadata, [||]) + .With(nameof Page.Empty.Revisions, [||]) + .With(nameof Page.Empty.PriorPermalinks, [||])) orderByFunc (fun row -> row[nameof Page.Empty.Title].Downcase() :> obj) result; withRetryDefault conn } diff --git a/src/MyWebLog.Data/SQLite/SQLitePageData.fs b/src/MyWebLog.Data/SQLite/SQLitePageData.fs index dde7b02..738b5f4 100644 --- a/src/MyWebLog.Data/SQLite/SQLitePageData.fs +++ b/src/MyWebLog.Data/SQLite/SQLitePageData.fs @@ -36,13 +36,13 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) = // IMPLEMENTATION FUNCTIONS - /// Get all pages for a web log (without text or revisions) + /// Get all pages for a web log (without text, metadata, revisions, or prior permalinks) let all webLogId = log.LogTrace "Page.all" conn.customList $"{Query.selectFromTable Table.Page} WHERE {Document.Query.whereByWebLog} ORDER BY LOWER({titleField})" [ webLogParam webLogId ] - (fun rdr -> { fromData rdr with Text = "" }) + (fun rdr -> { fromData rdr with Text = ""; Metadata = []; PriorPermalinks = [] }) /// Count all pages for the given web log let countAll webLogId = diff --git a/src/MyWebLog.Tests/Data/PageDataTests.fs b/src/MyWebLog.Tests/Data/PageDataTests.fs index 0017066..1bece19 100644 --- a/src/MyWebLog.Tests/Data/PageDataTests.fs +++ b/src/MyWebLog.Tests/Data/PageDataTests.fs @@ -49,7 +49,20 @@ let ``Add succeeds`` (data: IData) = task { let ``All succeeds`` (data: IData) = task { let! pages = data.Page.All rootId - Expect.hasLength pages 2 "There should have been 4 pages retrieved" - Expect.isEmpty pages[0].Revisions "Page 0 should have had no revisions" - Expect.isEmpty pages[1].Revisions "Page 1 should have had no revisions" + Expect.hasLength pages 2 "There should have been 2 pages retrieved" + pages |> List.iteri (fun idx pg -> + Expect.equal pg.Text "" $"Page {idx} should have had no text" + Expect.isEmpty pg.Metadata $"Page {idx} should have had no metadata" + Expect.isEmpty pg.Revisions $"Page {idx} should have had no revisions" + Expect.isEmpty pg.PriorPermalinks $"Page {idx} should have had no prior permalinks") +} + +let ``CountAll succeeds`` (data: IData) = task { + let! pages = data.Page.CountAll rootId + Expect.equal pages 2 "There should have been 2 pages counted" +} + +let ``CountListed succeeds`` (data: IData) = task { + let! pages = data.Page.CountListed rootId + Expect.equal pages 1 "There should have been 1 page in the page list" } diff --git a/src/MyWebLog.Tests/Data/PostgresDataTests.fs b/src/MyWebLog.Tests/Data/PostgresDataTests.fs index d7c4d3b..6266bc6 100644 --- a/src/MyWebLog.Tests/Data/PostgresDataTests.fs +++ b/src/MyWebLog.Tests/Data/PostgresDataTests.fs @@ -1,5 +1,6 @@ module PostgresDataTests +open System open BitBadger.Documents open Expecto open Microsoft.Extensions.Logging.Abstractions @@ -20,10 +21,26 @@ let mutable db: ThrowawayDatabase option = None let mkData () = PostgresData(NullLogger(), ser) :> IData +/// The host for the PostgreSQL test database (defaults to localhost) +let testHost = + RethinkDbTests.env "PG_HOST" "localhost" + +/// The database name for the PostgreSQL test database (defaults to postgres) +let testDb = + RethinkDbTests.env "PG_DB" "postgres" + +/// The user ID for the PostgreSQL test database (defaults to postgres) +let testUser = + RethinkDbTests.env "PG_USER" "postgres" + +/// The password for the PostgreSQL test database (defaults to postgres) +let testPw = + RethinkDbTests.env "PG_PW" "postgres" + /// Create a fresh environment from the root backup let freshEnvironment () = task { if Option.isSome db then db.Value.Dispose() - db <- Some (ThrowawayDatabase.Create "Host=localhost;Database=postgres;User ID=postgres;Password=postgres") + db <- Some (ThrowawayDatabase.Create $"Host={testHost};Database={testDb};User ID={testUser};Password={testPw}") let source = NpgsqlDataSourceBuilder db.Value.ConnectionString let _ = source.UseNodaTime() Postgres.Configuration.useDataSource (source.Build()) @@ -108,6 +125,12 @@ let pageTests = testList "Page" [ testTask "All succeeds" { do! PageDataTests.``All succeeds`` (mkData ()) } + testTask "CountAll succeeds" { + do! PageDataTests.``CountAll succeeds`` (mkData ()) + } + testTask "CountListed succeeds" { + do! PageDataTests.``CountListed succeeds`` (mkData ()) + } ] /// Drop the throwaway PostgreSQL database diff --git a/src/MyWebLog.Tests/Data/RethinkDbTests.fs b/src/MyWebLog.Tests/Data/RethinkDbTests.fs new file mode 100644 index 0000000..d5abf93 --- /dev/null +++ b/src/MyWebLog.Tests/Data/RethinkDbTests.fs @@ -0,0 +1,147 @@ +module RethinkDbTests + +open System +open Expecto +open Microsoft.Extensions.Logging.Abstractions +open MyWebLog +open MyWebLog.Converters +open MyWebLog.Data +open RethinkDb.Driver.FSharp +open RethinkDb.Driver.Net + +/// Get an environment variable, using the given value as the default if it is not set +let env name value = + match Environment.GetEnvironmentVariable $"MWL_TEST_{name}" with + | null -> value + | it when it.Trim() = "" -> value + | it -> it + + +/// The data configuration for the test database +let dataCfg = + DataConfig.FromUri (env "RETHINK_URI" "rethinkdb://172.17.0.2/mwl_test") + +/// The active data instance to use for testing +let mutable data: IData option = None + +/// Dispose the existing data +let disposeData () = task { + if data.IsSome then + let conn = (data.Value :?> RethinkDbData).Conn + do! rethink { dbDrop dataCfg.Database; write; withRetryOnce; ignoreResult conn } + conn.Dispose() + data <- None +} + +/// Create a new data implementation instance +let newData () = + let log = NullLogger() + let conn = dataCfg.CreateConnection log + RethinkDbData(conn, dataCfg, log) + +/// Create a fresh environment from the root backup +let freshEnvironment () = task { + do! disposeData () + data <- Some (newData ()) + do! data.Value.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 data.Value +} + +/// Set up the environment for the RethinkDB tests +let environmentSetUp = testTask "creating database" { + let _ = Json.configure Converter.Serializer + do! freshEnvironment () +} + +/// Integration tests for the Category implementation in RethinkDB +let categoryTests = testList "Category" [ + testTask "Add succeeds" { + do! CategoryDataTests.``Add succeeds`` data.Value + } + testList "CountAll" [ + testTask "succeeds when categories exist" { + do! CategoryDataTests.``CountAll succeeds when categories exist`` data.Value + } + testTask "succeeds when categories do not exist" { + do! CategoryDataTests.``CountAll succeeds when categories do not exist`` data.Value + } + ] + testList "CountTopLevel" [ + testTask "succeeds when top-level categories exist" { + do! CategoryDataTests.``CountTopLevel succeeds when top-level categories exist`` data.Value + } + testTask "succeeds when no top-level categories exist" { + do! CategoryDataTests.``CountTopLevel succeeds when no top-level categories exist`` data.Value + } + ] + testTask "FindAllForView succeeds" { + do! CategoryDataTests.``FindAllForView succeeds`` data.Value + } + testList "FindById" [ + testTask "succeeds when a category is found" { + do! CategoryDataTests.``FindById succeeds when a category is found`` data.Value + } + testTask "succeeds when a category is not found" { + do! CategoryDataTests.``FindById succeeds when a category is not found`` data.Value + } + ] + testList "FindByWebLog" [ + testTask "succeeds when categories exist" { + do! CategoryDataTests.``FindByWebLog succeeds when categories exist`` data.Value + } + testTask "succeeds when no categories exist" { + do! CategoryDataTests.``FindByWebLog succeeds when no categories exist`` data.Value + } + ] + testTask "Update succeeds" { + do! CategoryDataTests.``Update succeeds`` data.Value + } + testList "Delete" [ + testTask "succeeds when the category is deleted (no posts)" { + do! CategoryDataTests.``Delete succeeds when the category is deleted (no posts)`` data.Value + } + testTask "succeeds when the category does not exist" { + do! CategoryDataTests.``Delete succeeds when the category does not exist`` data.Value + } + testTask "succeeds when reassigning parent category to None" { + do! CategoryDataTests.``Delete succeeds when reassigning parent category to None`` data.Value + } + testTask "succeeds when reassigning parent category to Some" { + do! CategoryDataTests.``Delete succeeds when reassigning parent category to Some`` data.Value + } + testTask "succeeds and removes category from posts" { + do! CategoryDataTests.``Delete succeeds and removes category from posts`` data.Value + } + ] +] + +/// Integration tests for the Page implementation in RethinkDB +let pageTests = testList "Page" [ + testTask "Add succeeds" { + do! PageDataTests.``Add succeeds`` data.Value + } + testTask "All succeeds" { + do! PageDataTests.``All succeeds`` data.Value + } + testTask "CountAll succeeds" { + do! PageDataTests.``CountAll succeeds`` data.Value + } + testTask "CountListed succeeds" { + do! PageDataTests.``CountListed succeeds`` data.Value + } +] + +/// Drop the throwaway RethinkDB database +let environmentCleanUp = testTask "Clean Up" { + do! disposeData () +} + +/// All RethinkDB data tests +let all = + testList "RethinkDbData" + [ environmentSetUp + categoryTests + pageTests + environmentCleanUp ] + |> testSequenced diff --git a/src/MyWebLog.Tests/Data/SQLiteDataTests.fs b/src/MyWebLog.Tests/Data/SQLiteDataTests.fs index 08edc8b..a6bcde2 100644 --- a/src/MyWebLog.Tests/Data/SQLiteDataTests.fs +++ b/src/MyWebLog.Tests/Data/SQLiteDataTests.fs @@ -13,7 +13,8 @@ open Newtonsoft.Json let ser = Json.configure (JsonSerializer.CreateDefault()) /// The test database name -let dbName = "test-db.db" +let dbName = + RethinkDbTests.env "SQLITE_DB" "test-db.db" /// Create a SQLiteData instance for testing let mkData () = @@ -153,6 +154,16 @@ let pageTests = testList "Page" [ try do! PageDataTests.``All succeeds`` data finally dispose data } + testTask "CountAll succeeds" { + let data = mkData () + try do! PageDataTests.``CountAll succeeds`` data + finally dispose data + } + testTask "CountListed succeeds" { + let data = mkData () + try do! PageDataTests.``CountListed succeeds`` data + finally dispose data + } ] /// Delete the SQLite database diff --git a/src/MyWebLog.Tests/MyWebLog.Tests.fsproj b/src/MyWebLog.Tests/MyWebLog.Tests.fsproj index 2066418..1ef9297 100644 --- a/src/MyWebLog.Tests/MyWebLog.Tests.fsproj +++ b/src/MyWebLog.Tests/MyWebLog.Tests.fsproj @@ -12,6 +12,7 @@ + diff --git a/src/MyWebLog.Tests/Program.fs b/src/MyWebLog.Tests/Program.fs index f360e1c..43077ee 100644 --- a/src/MyWebLog.Tests/Program.fs +++ b/src/MyWebLog.Tests/Program.fs @@ -2,7 +2,13 @@ let allTests = testList "MyWebLog" [ testList "Domain" [ SupportTypesTests.all; DataTypesTests.all; ViewModelsTests.all ] - testList "Data" [ ConvertersTests.all; UtilsTests.all; SQLiteDataTests.all; PostgresDataTests.all ] + testList "Data" [ + ConvertersTests.all + UtilsTests.all + RethinkDbTests.all + SQLiteDataTests.all + PostgresDataTests.all + ] ] []