OverviewFilesCommitsIssuesDocsReleasesPeers

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

78 lines · javascript ref:
// migrateOrgs (#51): orgs v1 single-autobase → v2 per-org + resourceKeys.
// Pure transformation, deps mocked — no live corestore.

import test from 'brittle'
import { migrateOrgs } from '../lib/services/migrate/migrate-orgs.js'

function recorder () {
  const calls = { created: [], members: [], resources: [] }
  return {
    calls,
    create: async (o) => calls.created.push(o),
    addMember: async (name, m) => calls.members.push([name, m]),
    addResource: async (name, r) => calls.resources.push([name, r])
  }
}

const OLD = [
  {
    name: 'alpha', type: 'group', displayName: 'Alpha', description: 'a', about: 'A', visibility: 'public', userId: 'owner-a',
    members: [{ userId: 'owner-a', role: 'owner' }, { userId: 'bob', role: 'contributor' }],
    resources: ['repo-1', 'repo-2']
  },
  {
    name: 'beta', type: 'group', members: [{ userId: 'owner-b', role: 'owner' }], resources: []
  }
]

test('migrates every org, replaying members + resources', async (t) => {
  const r = recorder()
  const summary = await migrateOrgs({ oldOrgs: OLD, ...r })
  t.alike(summary, { orgs: 2, members: 3, resources: 2, skipped: 0, errors: 0 })
  t.alike(r.calls.created.map(o => o.name).sort(), ['alpha', 'beta'])
  t.alike(r.calls.created.find(o => o.name === 'alpha').displayName, 'Alpha')
  t.is(r.calls.members.filter(([n]) => n === 'alpha').length, 2)
  t.alike(r.calls.resources.map(([n, res]) => n + ':' + res.name).sort(), ['alpha:repo-1', 'alpha:repo-2'])
})

test('resolves resourceKeys via the injected resolver', async (t) => {
  const r = recorder()
  await migrateOrgs({
    oldOrgs: [OLD[0]],
    ...r,
    resolveResourceKey: async (repo) => repo === 'repo-1' ? 'k1'.padEnd(64, '1') : null
  })
  const r1 = r.calls.resources.find(([, res]) => res.name === 'repo-1')[1]
  const r2 = r.calls.resources.find(([, res]) => res.name === 'repo-2')[1]
  t.is(r1.bootstrapKey, 'k1'.padEnd(64, '1'), 'repo-1 key resolved')
  t.is(r2.bootstrapKey, null, 'repo-2 had no key → null (name-only)')
})

test('idempotent: a populated `seen` set skips already-migrated orgs', async (t) => {
  const r = recorder()
  const seen = new Set(['alpha'])
  const summary = await migrateOrgs({ oldOrgs: OLD, ...r, seen })
  t.is(summary.skipped, 1, 'alpha skipped')
  t.is(summary.orgs, 1, 'only beta migrated')
  t.alike(r.calls.created.map(o => o.name), ['beta'])
  t.ok(seen.has('beta'), 'beta recorded as migrated for resumability')
})

test('one org failing does not abort the rest', async (t) => {
  const r = recorder()
  const errors = []
  const create = async (o) => { if (o.name === 'alpha') throw new Error('boom'); r.calls.created.push(o) }
  const summary = await migrateOrgs({ oldOrgs: OLD, create, addMember: r.addMember, addResource: r.addResource, onError: (n, e) => errors.push([n, e.message]) })
  t.is(summary.errors, 1)
  t.is(summary.orgs, 1, 'beta still migrated')
  t.alike(errors, [['alpha', 'boom']])
  t.alike(r.calls.created.map(o => o.name), ['beta'])
})

test('async-iterable source (a real autobase view stream)', async (t) => {
  const r = recorder()
  async function * gen () { for (const o of OLD) yield o }
  const summary = await migrateOrgs({ oldOrgs: gen(), ...r })
  t.is(summary.orgs, 2, 'consumes an async iterable, not just an array')
})