OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / migrate-runner.test.js

85 lines · javascript ref:
// Generic migration runner (#51): version-gated, idempotent, resumable,
// fail-stop. Mocked version store + migrations — no live corestore.

import test from 'brittle'
import { runMigrations } from '../lib/services/migrate/runner.js'

// A fake persisted version + a migration set that records what ran.
function harness (start = 0) {
  let version = start
  const order = []
  const mk = (v, name, fn) => ({ version: v, name, up: async (ctx) => { order.push(v); if (fn) await fn(ctx) } })
  return {
    order,
    getVersion: async () => version,
    setVersion: async (v) => { version = v },
    currentVersion: () => version,
    mk
  }
}

test('runs only pending migrations, in order, bumping the version', async (t) => {
  const h = harness(0)
  const r = await runMigrations({
    getVersion: h.getVersion, setVersion: h.setVersion,
    migrations: [h.mk(2, 'b'), h.mk(1, 'a'), h.mk(3, 'c')]  // unsorted on purpose
  })
  t.alike(h.order, [1, 2, 3], 'ran in version order')
  t.alike(r, { startVersion: 0, version: 3, ran: [1, 2, 3] })
  t.is(h.currentVersion(), 3, 'version persisted to latest')
})

test('version-gated: skips migrations <= current version', async (t) => {
  const h = harness(2)  // store already at v2
  const r = await runMigrations({
    getVersion: h.getVersion, setVersion: h.setVersion,
    migrations: [h.mk(1, 'a'), h.mk(2, 'b'), h.mk(3, 'c')]
  })
  t.alike(h.order, [3], 'only v3 ran (1 and 2 already applied)')
  t.alike(r.ran, [3])
})

test('idempotent: re-running at the latest version does nothing', async (t) => {
  const h = harness(0)
  const migs = [h.mk(1, 'a'), h.mk(2, 'b')]
  await runMigrations({ getVersion: h.getVersion, setVersion: h.setVersion, migrations: migs })
  h.order.length = 0
  const r = await runMigrations({ getVersion: h.getVersion, setVersion: h.setVersion, migrations: migs })
  t.alike(h.order, [], 'nothing ran on the second pass')
  t.alike(r.ran, [])
  t.is(r.version, 2)
})

test('fail-stop + resumable: a throwing up aborts the chain, version stays put', async (t) => {
  const h = harness(0)
  const boom = { version: 2, name: 'boom', up: async () => { h.order.push(2); throw new Error('kaboom') } }
  const migs = [h.mk(1, 'a'), boom, h.mk(3, 'c')]
  await t.exception(() => runMigrations({ getVersion: h.getVersion, setVersion: h.setVersion, migrations: migs }), /kaboom/)
  t.is(h.currentVersion(), 1, 'stopped at v1 (v2 failed, v3 never ran)')
  t.alike(h.order, [1, 2], 'v1 ran, v2 attempted, v3 not reached')

  // Resume: fix v2, re-run → resumes from v2 (v1 skipped as applied).
  h.order.length = 0
  const fixed = [h.mk(1, 'a'), h.mk(2, 'boom-fixed'), h.mk(3, 'c')]
  const r = await runMigrations({ getVersion: h.getVersion, setVersion: h.setVersion, migrations: fixed })
  t.alike(h.order, [2, 3], 'resumed from v2; v1 not re-run')
  t.is(r.version, 3)
})

test('rejects duplicate target versions + malformed migrations', async (t) => {
  const h = harness(0)
  await t.exception(() => runMigrations({ getVersion: h.getVersion, setVersion: h.setVersion, migrations: [h.mk(1, 'a'), h.mk(1, 'dup')] }), /duplicate/)
  await t.exception(() => runMigrations({ getVersion: h.getVersion, setVersion: h.setVersion, migrations: [{ version: 1 }] }), /needs/)
})

test('passes ctx to each migration', async (t) => {
  const h = harness(0)
  let seen = null
  await runMigrations({
    getVersion: h.getVersion, setVersion: h.setVersion,
    ctx: { corestore: 'CS', orgs: 'ORGS' },
    migrations: [{ version: 1, name: 'x', up: async (ctx) => { seen = ctx } }]
  })
  t.alike(seen, { corestore: 'CS', orgs: 'ORGS' })
})