OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / unit / linked-repo.test.js

264 lines · javascript ref:
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import Corestore from 'corestore'
import { LinkedRepo } from '../../lib/repo-link/linked-repo.js'
import { encodeOp, decodeOp, validateOp, OP_ADD_WRITER, OP_UPDATE_REF, OP_CREATE_ISSUE } from '../../lib/repo-link/operations.js'

const tmpDir = path.join(os.tmpdir(), 'pear-git-test-linked-' + Date.now())

test('setup: create temp dir', async (t) => {
  await fs.promises.mkdir(tmpDir, { recursive: true })
  t.pass('temp dir created')
})

test('operations: encode and decode roundtrip', async (t) => {
  const op = { type: 'update-ref', ref: 'refs/heads/main', hash: 'abc123', userId: 'user1' }
  const encoded = encodeOp(op)
  t.ok(Buffer.isBuffer(encoded), 'encode returns Buffer')

  const decoded = decodeOp(encoded)
  t.is(decoded.type, 'update-ref')
  t.is(decoded.ref, 'refs/heads/main')
  t.is(decoded.hash, 'abc123')
  t.is(decoded.userId, 'user1')
})

test('operations: validate catches missing fields', async (t) => {
  // Missing type
  t.is(validateOp({}).valid, false)
  t.is(validateOp({ type: 'unknown-type' }).valid, false)

  // add-writer needs userId and serverKey and role
  t.is(validateOp({ type: OP_ADD_WRITER }).valid, false)
  t.is(validateOp({ type: OP_ADD_WRITER, userId: 'u1' }).valid, false)
  t.is(validateOp({ type: OP_ADD_WRITER, userId: 'u1', serverKey: 'abc', role: 'owner' }).valid, true)

  // update-ref needs ref, hash, userId
  t.is(validateOp({ type: OP_UPDATE_REF, ref: 'r', hash: 'h' }).valid, false)
  t.is(validateOp({ type: OP_UPDATE_REF, ref: 'r', hash: 'h', userId: 'u' }).valid, true)

  // create-issue needs id, title, userId
  t.is(validateOp({ type: OP_CREATE_ISSUE, id: 1, title: 'Bug' }).valid, false)
  t.is(validateOp({ type: OP_CREATE_ISSUE, id: 1, title: 'Bug', userId: 'u1' }).valid, true)
})

test('linked-repo: create new autobase', async (t) => {
  const store = new Corestore(path.join(tmpDir, 'store-create'))
  const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'test-repo' })

  await linked.ready()

  t.ok(linked.bootstrapKey, 'has bootstrap key')
  t.ok(linked.bootstrapKeyHex, 'has bootstrap key hex')
  t.is(linked.bootstrapKeyHex.length, 64, 'key is 32 bytes hex')
  t.ok(linked.writable, 'is writable (creator)')

  await linked.close()
  await store.close()
})

test('linked-repo: append and read ref', async (t) => {
  const store = new Corestore(path.join(tmpDir, 'store-ref'))
  const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'ref-repo' })
  await linked.ready()

  await linked.updateRef('refs/heads/main', 'aabbccdd', 'user1')
  const ref = await linked.getRef('refs/heads/main')

  t.ok(ref, 'ref exists')
  t.is(ref.hash, 'aabbccdd')
  t.is(ref.updatedBy, 'user1')
  t.ok(ref.timestamp > 0, 'has timestamp')

  await linked.close()
  await store.close()
})

test('linked-repo: update and delete ref', async (t) => {
  const store = new Corestore(path.join(tmpDir, 'store-ref-ud'))
  const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'ref-ud-repo' })
  await linked.ready()

  // Create ref
  await linked.updateRef('refs/heads/feature', '111111', 'user1')

  // Update ref
  await linked.updateRef('refs/heads/feature', '222222', 'user2')
  let ref = await linked.getRef('refs/heads/feature')
  t.is(ref.hash, '222222', 'ref updated')
  t.is(ref.updatedBy, 'user2', 'updatedBy changed')

  // Delete ref
  await linked.deleteRef('refs/heads/feature', 'user1')
  ref = await linked.getRef('refs/heads/feature')
  t.is(ref, null, 'ref deleted')

  // List refs
  await linked.updateRef('refs/heads/main', 'aaa', 'user1')
  await linked.updateRef('refs/heads/dev', 'bbb', 'user1')
  const refs = await linked.listRefs()
  t.is(refs.length, 2, 'two refs listed')

  await linked.close()
  await store.close()
})

test('linked-repo: create and list issues', async (t) => {
  const store = new Corestore(path.join(tmpDir, 'store-issues'))
  const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'issue-repo' })
  await linked.ready()

  const id1 = await linked.createIssue('Bug report', 'Something is broken', 'user1')
  const id2 = await linked.createIssue('Feature request', 'Add dark mode', 'user2')

  t.is(id1, 1, 'first issue id is 1')
  t.is(id2, 2, 'second issue id is 2')

  const issue1 = await linked.getIssue(1)
  t.ok(issue1, 'issue 1 exists')
  t.is(issue1.title, 'Bug report')
  t.is(issue1.body, 'Something is broken')
  t.is(issue1.author, 'user1')
  t.is(issue1.status, 'open')

  // Update issue
  await linked.updateIssue(1, { status: 'closed' }, 'user1')
  const updated = await linked.getIssue(1)
  t.is(updated.status, 'closed', 'issue closed')

  // List all issues
  const issues = await linked.listIssues()
  t.is(issues.length, 2, 'two issues listed')

  await linked.close()
  await store.close()
})

test('linked-repo: create and list comments', async (t) => {
  const store = new Corestore(path.join(tmpDir, 'store-comments'))
  const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'comment-repo' })
  await linked.ready()

  await linked.createIssue('Bug', 'Details', 'user1')
  const commentId = await linked.createComment(1, 'I can reproduce this', 'user2')

  t.ok(commentId, 'comment id returned')

  const comments = await linked.getComments(1)
  t.is(comments.length, 1, 'one comment')
  t.is(comments[0].body, 'I can reproduce this')
  t.is(comments[0].author, 'user2')

  // Issue updatedAt should be touched
  const issue = await linked.getIssue(1)
  t.ok(issue.updatedAt >= issue.createdAt, 'issue updatedAt updated')

  await linked.close()
  await store.close()
})

test('linked-repo: create and delete labels', async (t) => {
  const store = new Corestore(path.join(tmpDir, 'store-labels'))
  const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'label-repo' })
  await linked.ready()

  await linked.createLabel('bug', '#d73a4a', 'Something is broken')
  await linked.createLabel('enhancement', '#a2eeef', 'New feature')

  let labels = await linked.listLabels()
  t.is(labels.length, 2, 'two labels')
  t.is(labels[0].name, 'bug')
  t.is(labels[0].color, '#d73a4a')

  await linked.deleteLabel('bug')
  labels = await linked.listLabels()
  t.is(labels.length, 1, 'one label after delete')
  t.is(labels[0].name, 'enhancement')

  await linked.close()
  await store.close()
})

test('linked-repo: set and get meta', async (t) => {
  const store = new Corestore(path.join(tmpDir, 'store-meta'))
  const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'meta-repo' })
  await linked.ready()

  await linked.setMeta('description', 'A test repository')
  await linked.setMeta('defaultBranch', 'main')

  const desc = await linked.getMeta('description')
  t.is(desc, 'A test repository')

  const branch = await linked.getMeta('defaultBranch')
  t.is(branch, 'main')

  // Overwrite
  await linked.setMeta('description', 'Updated description')
  const newDesc = await linked.getMeta('description')
  t.is(newDesc, 'Updated description')

  // Non-existent key
  const missing = await linked.getMeta('nonexistent')
  t.is(missing, null)

  await linked.close()
  await store.close()
})

test('linked-repo: list writers (owner is first)', async (t) => {
  const store = new Corestore(path.join(tmpDir, 'store-writers'))
  const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'writer-repo' })
  await linked.ready()

  // Add owner
  const ownerKey = 'a'.repeat(64)
  await linked.addWriter('owner-user', ownerKey, null, null, 'owner')

  const writers = await linked.listWriters()
  t.is(writers.length, 1, 'one writer')
  t.is(writers[0].userId, 'owner-user')
  t.is(writers[0].role, 'owner')
  t.is(writers[0].serverKey, ownerKey)

  await linked.close()
  await store.close()
})

test('linked-repo: reopen from bootstrap key', async (t) => {
  const storePath = path.join(tmpDir, 'store-reopen')
  const store1 = new Corestore(storePath)
  const linked1 = new LinkedRepo({ corestore: store1, bootstrapKey: null, repoName: 'reopen-repo' })
  await linked1.ready()

  const key = linked1.bootstrapKeyHex

  // Write some data
  await linked1.updateRef('refs/heads/main', 'abc123', 'user1')
  await linked1.setMeta('name', 'reopen-test')
  await linked1.close()
  await store1.close()

  // Reopen with same store path and bootstrap key
  const store2 = new Corestore(storePath)
  const linked2 = new LinkedRepo({ corestore: store2, bootstrapKey: key, repoName: 'reopen-repo' })
  await linked2.ready()

  const ref = await linked2.getRef('refs/heads/main')
  t.ok(ref, 'ref persisted')
  t.is(ref.hash, 'abc123')

  const meta = await linked2.getMeta('name')
  t.is(meta, 'reopen-test', 'meta persisted')

  await linked2.close()
  await store2.close()
})

test('cleanup: remove temp dir', async (t) => {
  await fs.promises.rm(tmpDir, { recursive: true, force: true })
  t.pass('cleaned up')
})