{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module AgentTests.MigrationTests (migrationTests) where import Control.Monad import Data.Maybe (fromJust) import Data.Word (Word32) import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction) import Simplex.Messaging.Agent.Store.Interface import Simplex.Messaging.Agent.Store.Migrations (migrationsToRun) import Simplex.Messaging.Agent.Store.Shared import System.Random (randomIO) import Test.Hspec hiding (fit, it) import Util #if defined(dbPostgres) import qualified Data.ByteString.Char8 as B import Database.PostgreSQL.Simple (fromOnly) import Fixtures import Simplex.Messaging.Agent.Store.Postgres.Util (dropSchema) import qualified Simplex.Messaging.Agent.Store.Postgres.DB as DB #else import Database.SQLite.Simple (fromOnly) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import System.Directory (removeFile) #endif migrationTests :: Spec migrationTests = do it "should determine migrations to run" testMigrationsToRun describe "run migrations" $ do -- (init migrs, tables) -- (final migrs, confirm modes, final tables or error) it "up 1-2 tables (yes)" $ testMigration ([m1], [t1]) ([m1, m2], [MCYesUp, MCYesUpDown], Right [t1, t2]) it "up 1-2 tables (error)" $ testMigration ([m1], [t1]) ([m1, m2], [MCError], Left $ MEUpgrade [upMigration m2]) it "up 1-2 tables (error, down)" $ testMigration ([m1], [t1]) ([m1, m2'], [MCError], Left $ MEUpgrade [upMigration m2']) it "1-2 (different)" $ testMigration ([m2], [t2]) ([m1, m2], [MCYesUp, MCYesUpDown, MCError], Left $ MigrationError $ MTREDifferent (name m1) (name m2)) it "up 2-4 tables (yes)" $ testMigration ([m1, m2], [t1, t2]) ([m1, m2, m3, m4], [MCYesUp, MCYesUpDown], Right [t1, t2, t3, t4]) it "up 2-4 tables (error)" $ testMigration ([m1, m2], [t1, t2]) ([m1, m2, m3, m4], [MCError], Left $ MEUpgrade [upMigration m3, upMigration m4]) it "up 2-4 tables (error, down)" $ testMigration ([m1, m2], [t1, t2]) ([m1, m2, m3', m4'], [MCError], Left $ MEUpgrade [upMigration m3', upMigration m4']) it "no change 2 tables" $ testMigration ([m1, m2], [t1, t2]) ([m1, m2], [MCYesUp, MCYesUpDown, MCError], Right [t1, t2]) it "2 tables (different order)" $ testMigration ([m1, m2], [t1, t2]) ([m2, m1], [MCYesUp, MCYesUpDown, MCError], Left $ MigrationError $ MTREDifferent (name m2) (name m1)) it "down 2-1 tables (no down)" $ testMigration ([m1, m2], [t1, t2]) ([m1], [MCYesUp, MCYesUpDown, MCError], Left . MigrationError $ MTRENoDown [name m2]) it "down 2-1 tables (error)" $ testMigration ([m1, m2'], [t1, t2]) ([m1], [MCYesUp, MCError], Left $ MEDowngrade [name m2]) it "down 2-1 tables (yes)" $ testMigration ([m1, m2'], [t1, t2]) ([m1], [MCYesUpDown], Right [t1]) it "down 4-2 tables (no down)" $ testMigration ([m1, m2, m3, m4], [t1, t2, t3, t4]) ([m1, m2], [MCYesUp, MCYesUpDown, MCError], Left . MigrationError $ MTRENoDown [name m3, name m4]) it "down 4-2 tables (partial down)" $ do testMigration ([m1, m2, m3, m4'], [t1, t2, t3, t4]) ([m1, m2], [MCYesUp, MCYesUpDown, MCError], Left . MigrationError $ MTRENoDown [name m3]) testMigration ([m1, m2, m3', m4], [t1, t2, t3, t4]) ([m1, m2], [MCYesUp, MCYesUpDown, MCError], Left . MigrationError $ MTRENoDown [name m4]) it "down 4-2 tables (error)" $ testMigration ([m1, m2, m3', m4'], [t1, t2, t3, t4]) ([m1, m2], [MCYesUp, MCError], Left $ MEDowngrade [name m3, name m4]) it "down 4-2 tables (yes)" $ testMigration ([m1, m2, m3', m4'], [t1, t2, t3, t4]) ([m1, m2], [MCYesUpDown], Right [t1, t2]) it "4-2 tables (different)" $ testMigration ([m1, m2, m3', m4'], [t1, t2, t3, t4]) ([m1, m3], [MCYesUp, MCYesUpDown, MCError], Left . MigrationError $ MTREDifferent (name m3) (name m2)) it "4-3 tables (different)" $ testMigration ([m1, m2, m3, m4], [t1, t2, t3, t4]) ([m1, m2, m4], [MCYesUp, MCYesUpDown, MCError], Left . MigrationError $ MTREDifferent (name m4) (name m3)) m1 :: Migration m1 = Migration "20230301-migration1" "create table test1 (id1 integer primary key);" Nothing m1' :: Migration m1' = Migration "20230301-migration1" "create table test1 (id1 integer primary key);" (Just "drop table test1;") t1 :: String t1 = "test1" m2 :: Migration m2 = Migration "20230302-migration2" "create table test2 (id2 integer primary key);" Nothing m2' :: Migration m2' = Migration "20230302-migration2" "create table test2 (id2 integer primary key);" (Just "drop table test2;") t2 :: String t2 = "test2" m3 :: Migration m3 = Migration "20230303-migration3" "create table test3 (id3 integer primary key);" Nothing m3' :: Migration m3' = Migration "20230303-migration3" "create table test3 (id3 integer primary key);" (Just "drop table test3;") t3 :: String t3 = "test3" m4 :: Migration m4 = Migration "20230304-migration4" "create table test4 (id4 integer primary key);" Nothing m4' :: Migration m4' = Migration "20230304-migration4" "create table test4 (id4 integer primary key);" (Just "drop table test4;") t4 :: String t4 = "test4" downMigration :: Migration -> DownMigration downMigration = fromJust . toDownMigration testMigrationsToRun :: IO () testMigrationsToRun = do migrationsToRun [] [] `shouldBe` Right MTRNone migrationsToRun [m1] [] `shouldBe` Right (MTRUp [m1]) migrationsToRun [] [m1] `shouldBe` Left (MTRENoDown ["20230301-migration1"]) migrationsToRun [] [m1'] `shouldBe` Right (MTRDown [downMigration m1']) migrationsToRun [m1, m2] [] `shouldBe` Right (MTRUp [m1, m2]) migrationsToRun [] [m1, m2] `shouldBe` Left (MTRENoDown ["20230301-migration1", "20230302-migration2"]) migrationsToRun [] [m1', m2] `shouldBe` Left (MTRENoDown ["20230302-migration2"]) migrationsToRun [] [m1, m2'] `shouldBe` Left (MTRENoDown ["20230301-migration1"]) migrationsToRun [] [m1', m2'] `shouldBe` Right (MTRDown [downMigration m1', downMigration m2']) migrationsToRun [m1] [m1] `shouldBe` Right MTRNone migrationsToRun [m1] [m1'] `shouldBe` Right MTRNone migrationsToRun [m1'] [m1] `shouldBe` Right MTRNone migrationsToRun [m1] [m2] `shouldBe` Left (MTREDifferent "20230301-migration1" "20230302-migration2") migrationsToRun [m1, m2] [m1] `shouldBe` Right (MTRUp [m2]) migrationsToRun [m1] [m1, m2] `shouldBe` Left (MTRENoDown ["20230302-migration2"]) migrationsToRun [m1] [m1, m2'] `shouldBe` Right (MTRDown [downMigration m2']) migrationsToRun [m1, m2, m3] [m1] `shouldBe` Right (MTRUp [m2, m3]) migrationsToRun [m1] [m1, m2, m3] `shouldBe` Left (MTRENoDown ["20230302-migration2", "20230303-migration3"]) migrationsToRun [m1] [m1, m2', m3] `shouldBe` Left (MTRENoDown ["20230303-migration3"]) migrationsToRun [m1] [m1, m2, m3'] `shouldBe` Left (MTRENoDown ["20230302-migration2"]) migrationsToRun [m1] [m1, m2', m3'] `shouldBe` Right (MTRDown [downMigration m2', downMigration m3']) migrationsToRun [m1, m2] [m1, m2] `shouldBe` Right MTRNone migrationsToRun [m1, m2] [m2] `shouldBe` Left (MTREDifferent "20230301-migration1" "20230302-migration2") migrationsToRun [m1', m2'] [m1, m2] `shouldBe` Right MTRNone migrationsToRun [m1, m2] [m1, m2'] `shouldBe` Right MTRNone migrationsToRun [m1, m2, m3] [m1, m2] `shouldBe` Right (MTRUp [m3]) migrationsToRun [m1, m2, m3, m4] [m1, m2] `shouldBe` Right (MTRUp [m3, m4]) migrationsToRun [m1, m2] [m1, m2, m3, m4] `shouldBe` Left (MTRENoDown ["20230303-migration3", "20230304-migration4"]) migrationsToRun [m1, m2] [m1, m2, m3', m4] `shouldBe` Left (MTRENoDown ["20230304-migration4"]) migrationsToRun [m1, m2] [m1, m2, m3, m4'] `shouldBe` Left (MTRENoDown ["20230303-migration3"]) migrationsToRun [m1, m2] [m1, m2, m3', m4'] `shouldBe` Right (MTRDown [downMigration m3', downMigration m4']) testMigration :: ([Migration], [String]) -> ([Migration], [MigrationConfirmation], Either MigrationError [String]) -> IO () testMigration (initMs, initTables) (finalMs, confirmModes, tablesOrError) = forM_ confirmModes $ \confirmMode -> do r <- randomIO :: IO Word32 Right st <- createStore r initMs MCError st `shouldHaveTables` initTables closeDBStore st case tablesOrError of Right tables -> do Right st' <- createStore r finalMs confirmMode st' `shouldHaveTables` tables closeDBStore st' Left e -> do Left e' <- createStore r finalMs confirmMode e `shouldBe` e' cleanup r #if defined(dbPostgres) testSchema :: Word32 -> String testSchema randSuffix = "test_migrations_schema" <> show randSuffix createStore :: Word32 -> [Migration] -> MigrationConfirmation -> IO (Either MigrationError DBStore) createStore randSuffix migrations confirmMigrations = do let dbOpts = DBOpts { connstr = testDBConnstr, schema = B.pack $ testSchema randSuffix, poolSize = 1, createSchema = True } createDBStore dbOpts migrations (MigrationConfig confirmMigrations Nothing) cleanup :: Word32 -> IO () cleanup randSuffix = dropSchema testDBConnectInfo (testSchema randSuffix) shouldHaveTables :: DBStore -> [String] -> IO () st `shouldHaveTables` expected = do tables <- map fromOnly <$> withTransaction st (`DB.query_` "SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema() AND table_type = 'BASE TABLE' ORDER BY 1") tables `shouldBe` "migrations" : expected #else testDB :: Word32 -> FilePath testDB randSuffix = "tests/tmp/test_migrations.db" <> show randSuffix createStore :: Word32 -> [Migration] -> MigrationConfirmation -> IO (Either MigrationError DBStore) createStore randSuffix migrations confirmMigrations = do let dbOpts = DBOpts { dbFilePath = testDB randSuffix, dbKey = "", keepKey = False, vacuum = True, track = DB.TQOff } createDBStore dbOpts migrations (MigrationConfig confirmMigrations Nothing) cleanup :: Word32 -> IO () cleanup randSuffix = removeFile (testDB randSuffix) shouldHaveTables :: DBStore -> [String] -> IO () st `shouldHaveTables` expected = do tables <- map fromOnly <$> withTransaction st (`DB.query_` "SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY 1") tables `shouldBe` "migrations" : expected #endif