migrations_runner_extra_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. package repository
  2. import (
  3. "context"
  4. "crypto/sha256"
  5. "encoding/hex"
  6. "errors"
  7. "io/fs"
  8. "strings"
  9. "testing"
  10. "testing/fstest"
  11. "time"
  12. sqlmock "github.com/DATA-DOG/go-sqlmock"
  13. "github.com/stretchr/testify/require"
  14. )
  15. func TestApplyMigrations_NilDB(t *testing.T) {
  16. err := ApplyMigrations(context.Background(), nil)
  17. require.Error(t, err)
  18. require.Contains(t, err.Error(), "nil sql db")
  19. }
  20. func TestApplyMigrations_DelegatesToApplyMigrationsFS(t *testing.T) {
  21. db, mock, err := sqlmock.New()
  22. require.NoError(t, err)
  23. defer func() { _ = db.Close() }()
  24. mock.ExpectQuery("SELECT pg_try_advisory_lock\\(\\$1\\)").
  25. WithArgs(migrationsAdvisoryLockID).
  26. WillReturnError(errors.New("lock failed"))
  27. err = ApplyMigrations(context.Background(), db)
  28. require.Error(t, err)
  29. require.Contains(t, err.Error(), "acquire migrations lock")
  30. require.NoError(t, mock.ExpectationsWereMet())
  31. }
  32. func TestLatestMigrationBaseline(t *testing.T) {
  33. t.Run("empty_fs_returns_baseline", func(t *testing.T) {
  34. version, description, hash, err := latestMigrationBaseline(fstest.MapFS{})
  35. require.NoError(t, err)
  36. require.Equal(t, "baseline", version)
  37. require.Equal(t, "baseline", description)
  38. require.Equal(t, "", hash)
  39. })
  40. t.Run("uses_latest_sorted_sql_file", func(t *testing.T) {
  41. fsys := fstest.MapFS{
  42. "001_init.sql": &fstest.MapFile{Data: []byte("CREATE TABLE t1(id int);")},
  43. "010_final.sql": &fstest.MapFile{
  44. Data: []byte("CREATE TABLE t2(id int);"),
  45. },
  46. }
  47. version, description, hash, err := latestMigrationBaseline(fsys)
  48. require.NoError(t, err)
  49. require.Equal(t, "010_final", version)
  50. require.Equal(t, "010_final", description)
  51. require.Len(t, hash, 64)
  52. })
  53. t.Run("read_file_error", func(t *testing.T) {
  54. fsys := fstest.MapFS{
  55. "010_bad.sql": &fstest.MapFile{Mode: fs.ModeDir},
  56. }
  57. _, _, _, err := latestMigrationBaseline(fsys)
  58. require.Error(t, err)
  59. })
  60. }
  61. func TestIsMigrationChecksumCompatible_AdditionalCases(t *testing.T) {
  62. require.False(t, isMigrationChecksumCompatible("unknown.sql", "db", "file"))
  63. var (
  64. name string
  65. rule migrationChecksumCompatibilityRule
  66. )
  67. for n, r := range migrationChecksumCompatibilityRules {
  68. name = n
  69. rule = r
  70. break
  71. }
  72. require.NotEmpty(t, name)
  73. require.False(t, isMigrationChecksumCompatible(name, "db-not-accepted", "file-not-match"))
  74. require.False(t, isMigrationChecksumCompatible(name, "db-not-accepted", rule.fileChecksum))
  75. var accepted string
  76. for checksum := range rule.acceptedDBChecksum {
  77. accepted = checksum
  78. break
  79. }
  80. require.NotEmpty(t, accepted)
  81. require.True(t, isMigrationChecksumCompatible(name, accepted, rule.fileChecksum))
  82. }
  83. func TestMigrationChecksumCompatibilityRules_CoverEditedUpgradeCompatibilityMigrations(t *testing.T) {
  84. for _, name := range []string{
  85. "109_auth_identity_compat_backfill.sql",
  86. "110_pending_auth_and_provider_default_grants.sql",
  87. "112_add_payment_order_provider_key_snapshot.sql",
  88. "115_auth_identity_legacy_external_backfill.sql",
  89. "116_auth_identity_legacy_external_safety_reports.sql",
  90. "118_wechat_dual_mode_and_auth_source_defaults.sql",
  91. "120_enforce_payment_orders_out_trade_no_unique_notx.sql",
  92. "123_fix_legacy_auth_source_grant_on_signup_defaults.sql",
  93. } {
  94. rule, ok := migrationChecksumCompatibilityRules[name]
  95. require.Truef(t, ok, "missing compatibility rule for %s", name)
  96. require.NotEmpty(t, rule.fileChecksum)
  97. require.NotEmpty(t, rule.acceptedDBChecksum)
  98. }
  99. }
  100. func TestEnsureAtlasBaselineAligned(t *testing.T) {
  101. t.Run("skip_when_no_legacy_table", func(t *testing.T) {
  102. db, mock, err := sqlmock.New()
  103. require.NoError(t, err)
  104. defer func() { _ = db.Close() }()
  105. mock.ExpectQuery("SELECT EXISTS \\(").
  106. WithArgs("schema_migrations").
  107. WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false))
  108. err = ensureAtlasBaselineAligned(context.Background(), db, fstest.MapFS{})
  109. require.NoError(t, err)
  110. require.NoError(t, mock.ExpectationsWereMet())
  111. })
  112. t.Run("create_atlas_and_insert_baseline_when_empty", func(t *testing.T) {
  113. db, mock, err := sqlmock.New()
  114. require.NoError(t, err)
  115. defer func() { _ = db.Close() }()
  116. mock.ExpectQuery("SELECT EXISTS \\(").
  117. WithArgs("schema_migrations").
  118. WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
  119. mock.ExpectQuery("SELECT EXISTS \\(").
  120. WithArgs("atlas_schema_revisions").
  121. WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false))
  122. mock.ExpectExec("CREATE TABLE IF NOT EXISTS atlas_schema_revisions").
  123. WillReturnResult(sqlmock.NewResult(0, 0))
  124. mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM atlas_schema_revisions").
  125. WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
  126. mock.ExpectExec("INSERT INTO atlas_schema_revisions").
  127. WithArgs("002_next", "002_next", 1, sqlmock.AnyArg()).
  128. WillReturnResult(sqlmock.NewResult(1, 1))
  129. fsys := fstest.MapFS{
  130. "001_init.sql": &fstest.MapFile{Data: []byte("CREATE TABLE t1(id int);")},
  131. "002_next.sql": &fstest.MapFile{Data: []byte("CREATE TABLE t2(id int);")},
  132. }
  133. err = ensureAtlasBaselineAligned(context.Background(), db, fsys)
  134. require.NoError(t, err)
  135. require.NoError(t, mock.ExpectationsWereMet())
  136. })
  137. t.Run("error_when_checking_legacy_table", func(t *testing.T) {
  138. db, mock, err := sqlmock.New()
  139. require.NoError(t, err)
  140. defer func() { _ = db.Close() }()
  141. mock.ExpectQuery("SELECT EXISTS \\(").
  142. WithArgs("schema_migrations").
  143. WillReturnError(errors.New("exists failed"))
  144. err = ensureAtlasBaselineAligned(context.Background(), db, fstest.MapFS{})
  145. require.Error(t, err)
  146. require.Contains(t, err.Error(), "check schema_migrations")
  147. require.NoError(t, mock.ExpectationsWereMet())
  148. })
  149. t.Run("error_when_counting_atlas_rows", func(t *testing.T) {
  150. db, mock, err := sqlmock.New()
  151. require.NoError(t, err)
  152. defer func() { _ = db.Close() }()
  153. mock.ExpectQuery("SELECT EXISTS \\(").
  154. WithArgs("schema_migrations").
  155. WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
  156. mock.ExpectQuery("SELECT EXISTS \\(").
  157. WithArgs("atlas_schema_revisions").
  158. WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
  159. mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM atlas_schema_revisions").
  160. WillReturnError(errors.New("count failed"))
  161. err = ensureAtlasBaselineAligned(context.Background(), db, fstest.MapFS{})
  162. require.Error(t, err)
  163. require.Contains(t, err.Error(), "count atlas_schema_revisions")
  164. require.NoError(t, mock.ExpectationsWereMet())
  165. })
  166. t.Run("error_when_creating_atlas_table", func(t *testing.T) {
  167. db, mock, err := sqlmock.New()
  168. require.NoError(t, err)
  169. defer func() { _ = db.Close() }()
  170. mock.ExpectQuery("SELECT EXISTS \\(").
  171. WithArgs("schema_migrations").
  172. WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
  173. mock.ExpectQuery("SELECT EXISTS \\(").
  174. WithArgs("atlas_schema_revisions").
  175. WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false))
  176. mock.ExpectExec("CREATE TABLE IF NOT EXISTS atlas_schema_revisions").
  177. WillReturnError(errors.New("create failed"))
  178. err = ensureAtlasBaselineAligned(context.Background(), db, fstest.MapFS{})
  179. require.Error(t, err)
  180. require.Contains(t, err.Error(), "create atlas_schema_revisions")
  181. require.NoError(t, mock.ExpectationsWereMet())
  182. })
  183. t.Run("error_when_inserting_baseline", func(t *testing.T) {
  184. db, mock, err := sqlmock.New()
  185. require.NoError(t, err)
  186. defer func() { _ = db.Close() }()
  187. mock.ExpectQuery("SELECT EXISTS \\(").
  188. WithArgs("schema_migrations").
  189. WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
  190. mock.ExpectQuery("SELECT EXISTS \\(").
  191. WithArgs("atlas_schema_revisions").
  192. WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
  193. mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM atlas_schema_revisions").
  194. WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
  195. mock.ExpectExec("INSERT INTO atlas_schema_revisions").
  196. WithArgs("001_init", "001_init", 1, sqlmock.AnyArg()).
  197. WillReturnError(errors.New("insert failed"))
  198. fsys := fstest.MapFS{
  199. "001_init.sql": &fstest.MapFile{Data: []byte("CREATE TABLE t(id int);")},
  200. }
  201. err = ensureAtlasBaselineAligned(context.Background(), db, fsys)
  202. require.Error(t, err)
  203. require.Contains(t, err.Error(), "insert atlas baseline")
  204. require.NoError(t, mock.ExpectationsWereMet())
  205. })
  206. }
  207. func TestApplyMigrationsFS_ChecksumMismatchRejected(t *testing.T) {
  208. db, mock, err := sqlmock.New()
  209. require.NoError(t, err)
  210. defer func() { _ = db.Close() }()
  211. prepareMigrationsBootstrapExpectations(mock)
  212. mock.ExpectQuery("SELECT checksum FROM schema_migrations WHERE filename = \\$1").
  213. WithArgs("001_init.sql").
  214. WillReturnRows(sqlmock.NewRows([]string{"checksum"}).AddRow("mismatched-checksum"))
  215. mock.ExpectExec("SELECT pg_advisory_unlock\\(\\$1\\)").
  216. WithArgs(migrationsAdvisoryLockID).
  217. WillReturnResult(sqlmock.NewResult(0, 1))
  218. fsys := fstest.MapFS{
  219. "001_init.sql": &fstest.MapFile{Data: []byte("CREATE TABLE t(id int);")},
  220. }
  221. err = applyMigrationsFS(context.Background(), db, fsys)
  222. require.Error(t, err)
  223. require.Contains(t, err.Error(), "checksum mismatch")
  224. require.NoError(t, mock.ExpectationsWereMet())
  225. }
  226. func TestApplyMigrationsFS_CheckMigrationQueryError(t *testing.T) {
  227. db, mock, err := sqlmock.New()
  228. require.NoError(t, err)
  229. defer func() { _ = db.Close() }()
  230. prepareMigrationsBootstrapExpectations(mock)
  231. mock.ExpectQuery("SELECT checksum FROM schema_migrations WHERE filename = \\$1").
  232. WithArgs("001_err.sql").
  233. WillReturnError(errors.New("query failed"))
  234. mock.ExpectExec("SELECT pg_advisory_unlock\\(\\$1\\)").
  235. WithArgs(migrationsAdvisoryLockID).
  236. WillReturnResult(sqlmock.NewResult(0, 1))
  237. fsys := fstest.MapFS{
  238. "001_err.sql": &fstest.MapFile{Data: []byte("SELECT 1;")},
  239. }
  240. err = applyMigrationsFS(context.Background(), db, fsys)
  241. require.Error(t, err)
  242. require.Contains(t, err.Error(), "check migration 001_err.sql")
  243. require.NoError(t, mock.ExpectationsWereMet())
  244. }
  245. func TestApplyMigrationsFS_SkipEmptyAndAlreadyApplied(t *testing.T) {
  246. db, mock, err := sqlmock.New()
  247. require.NoError(t, err)
  248. defer func() { _ = db.Close() }()
  249. prepareMigrationsBootstrapExpectations(mock)
  250. alreadySQL := "CREATE TABLE t(id int);"
  251. checksum := migrationChecksum(alreadySQL)
  252. mock.ExpectQuery("SELECT checksum FROM schema_migrations WHERE filename = \\$1").
  253. WithArgs("001_already.sql").
  254. WillReturnRows(sqlmock.NewRows([]string{"checksum"}).AddRow(checksum))
  255. mock.ExpectExec("SELECT pg_advisory_unlock\\(\\$1\\)").
  256. WithArgs(migrationsAdvisoryLockID).
  257. WillReturnResult(sqlmock.NewResult(0, 1))
  258. fsys := fstest.MapFS{
  259. "000_empty.sql": &fstest.MapFile{Data: []byte(" \n\t ")},
  260. "001_already.sql": &fstest.MapFile{Data: []byte(alreadySQL)},
  261. }
  262. err = applyMigrationsFS(context.Background(), db, fsys)
  263. require.NoError(t, err)
  264. require.NoError(t, mock.ExpectationsWereMet())
  265. }
  266. func TestApplyMigrationsFS_ReadMigrationError(t *testing.T) {
  267. db, mock, err := sqlmock.New()
  268. require.NoError(t, err)
  269. defer func() { _ = db.Close() }()
  270. prepareMigrationsBootstrapExpectations(mock)
  271. mock.ExpectExec("SELECT pg_advisory_unlock\\(\\$1\\)").
  272. WithArgs(migrationsAdvisoryLockID).
  273. WillReturnResult(sqlmock.NewResult(0, 1))
  274. fsys := fstest.MapFS{
  275. "001_bad.sql": &fstest.MapFile{Mode: fs.ModeDir},
  276. }
  277. err = applyMigrationsFS(context.Background(), db, fsys)
  278. require.Error(t, err)
  279. require.Contains(t, err.Error(), "read migration 001_bad.sql")
  280. require.NoError(t, mock.ExpectationsWereMet())
  281. }
  282. func TestPgAdvisoryLockAndUnlock_ErrorBranches(t *testing.T) {
  283. t.Run("context_cancelled_while_not_locked", func(t *testing.T) {
  284. db, mock, err := sqlmock.New()
  285. require.NoError(t, err)
  286. defer func() { _ = db.Close() }()
  287. mock.ExpectQuery("SELECT pg_try_advisory_lock\\(\\$1\\)").
  288. WithArgs(migrationsAdvisoryLockID).
  289. WillReturnRows(sqlmock.NewRows([]string{"pg_try_advisory_lock"}).AddRow(false))
  290. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
  291. defer cancel()
  292. err = pgAdvisoryLock(ctx, db)
  293. require.Error(t, err)
  294. require.Contains(t, err.Error(), "acquire migrations lock")
  295. require.NoError(t, mock.ExpectationsWereMet())
  296. })
  297. t.Run("unlock_exec_error", func(t *testing.T) {
  298. db, mock, err := sqlmock.New()
  299. require.NoError(t, err)
  300. defer func() { _ = db.Close() }()
  301. mock.ExpectExec("SELECT pg_advisory_unlock\\(\\$1\\)").
  302. WithArgs(migrationsAdvisoryLockID).
  303. WillReturnError(errors.New("unlock failed"))
  304. err = pgAdvisoryUnlock(context.Background(), db)
  305. require.Error(t, err)
  306. require.Contains(t, err.Error(), "release migrations lock")
  307. require.NoError(t, mock.ExpectationsWereMet())
  308. })
  309. t.Run("acquire_lock_after_retry", func(t *testing.T) {
  310. db, mock, err := sqlmock.New()
  311. require.NoError(t, err)
  312. defer func() { _ = db.Close() }()
  313. mock.ExpectQuery("SELECT pg_try_advisory_lock\\(\\$1\\)").
  314. WithArgs(migrationsAdvisoryLockID).
  315. WillReturnRows(sqlmock.NewRows([]string{"pg_try_advisory_lock"}).AddRow(false))
  316. mock.ExpectQuery("SELECT pg_try_advisory_lock\\(\\$1\\)").
  317. WithArgs(migrationsAdvisoryLockID).
  318. WillReturnRows(sqlmock.NewRows([]string{"pg_try_advisory_lock"}).AddRow(true))
  319. ctx, cancel := context.WithTimeout(context.Background(), migrationsLockRetryInterval*3)
  320. defer cancel()
  321. start := time.Now()
  322. err = pgAdvisoryLock(ctx, db)
  323. require.NoError(t, err)
  324. require.GreaterOrEqual(t, time.Since(start), migrationsLockRetryInterval)
  325. require.NoError(t, mock.ExpectationsWereMet())
  326. })
  327. }
  328. func migrationChecksum(content string) string {
  329. sum := sha256.Sum256([]byte(strings.TrimSpace(content)))
  330. return hex.EncodeToString(sum[:])
  331. }