OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-orgs / test / basic.test.js

445 lines · javascript ref:
import test from 'brittle'
import Corestore from 'corestore'
import { mkdtempSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import { replicateAndSync } from 'autobase-test-helpers'
import { HyperOrgs } from '../index.js'

function tmpStore () {
  const dir = mkdtempSync(join(tmpdir(), 'hyper-orgs-test-'))
  return new Corestore(dir)
}

async function makeOrgs (t, namespace = 'test') {
  const store = tmpStore()
  const orgs = new HyperOrgs(store, { namespace })
  await orgs.ready()
  t.teardown(async () => {
    try { await orgs.close() } catch {}
    try { await store.close() } catch {}
  })
  return orgs
}

// ── Org CRUD ──────────────────────────────────────────────────

test('create and get organization', async (t) => {
  const orgs = await makeOrgs(t, 'test-1')

  const org = await orgs.create('my-team', 'group', {
    displayName: 'My Team',
    description: 'A test team'
  })

  t.is(org.name, 'my-team')
  t.is(org.displayName, 'My Team')
  t.is(org.description, 'A test team')
  t.is(org.type, 'group')
  t.is(org.visibility, 'public')
  t.alike(org.resources, [])
})

test('update organization', async (t) => {
  const orgs = await makeOrgs(t, 'test-2')

  await orgs.create('team', 'group')
  const updated = await orgs.update('team', {
    displayName: 'Team Alpha',
    description: 'Updated'
  })

  t.is(updated.displayName, 'Team Alpha')
  t.is(updated.description, 'Updated')
})

test('delete empty org', async (t) => {
  const orgs = await makeOrgs(t, 'test-3')

  await orgs.create('empty-team', 'group')
  await orgs.delete('empty-team')

  const deleted = await orgs.get('empty-team')
  t.is(deleted, null)
})

test('cannot delete org with resources', async (t) => {
  const orgs = await makeOrgs(t, 'test-4')

  await orgs.create('team', 'group')
  await orgs.addResource('team', 'project')

  try {
    await orgs.delete('team')
    t.fail('should have thrown')
  } catch (err) {
    t.ok(err.message.includes('still has'))
  }
})

test('reserved name rejected', async (t) => {
  const orgs = await makeOrgs(t, 'test-5')

  try {
    await orgs.create('admin', 'group')
    t.fail('should have thrown')
  } catch (err) {
    t.ok(err.message.includes('reserved'))
  }
})

test('invalid name rejected', async (t) => {
  const orgs = await makeOrgs(t, 'test-6')

  try {
    await orgs.create('-bad-', 'group')
    t.fail('should have thrown')
  } catch (err) {
    t.ok(err.message.toLowerCase().includes('name'))
  }
})

test('list orgs with filters', async (t) => {
  const orgs = await makeOrgs(t, 'test-7')

  await orgs.create('team-a', 'group')
  await orgs.create('team-b', 'group', { visibility: 'private' })
  await orgs.create('alice', 'user', { userId: 'alice-key' })

  const all = await orgs.list()
  t.is(all.length, 3)

  const groups = await orgs.list({ type: 'group' })
  t.is(groups.length, 2)

  const users = await orgs.list({ type: 'user' })
  t.is(users.length, 1)

  const priv = await orgs.list({ visibility: 'private' })
  t.is(priv.length, 1)
})

// ── Members ───────────────────────────────────────────────────

test('add and remove members', async (t) => {
  const orgs = await makeOrgs(t, 'test-8')

  await orgs.create('team', 'group')
  await orgs.addMember('team', 'alice', 'admin')
  await orgs.addMember('team', 'bob', 'member')

  let org = await orgs.get('team')
  t.is(org.members.length, 2)
  t.is(org.members[0].userId, 'alice')
  t.is(org.members[0].role, 'admin')

  await orgs.removeMember('team', 'bob')
  org = await orgs.get('team')
  t.is(org.members.length, 1)
})

test('update member role', async (t) => {
  const orgs = await makeOrgs(t, 'test-9')

  await orgs.create('team', 'group')
  await orgs.addMember('team', 'alice', 'member')
  await orgs.addMember('team', 'alice', 'admin') // upgrade

  const org = await orgs.get('team')
  t.is(org.members.length, 1)
  t.is(org.members[0].role, 'admin')
})

test('cannot add members to user org', async (t) => {
  const orgs = await makeOrgs(t, 'test-10')

  await orgs.create('alice', 'user', { userId: 'alice-key' })
  try {
    await orgs.addMember('alice', 'bob', 'member')
    t.fail('should have thrown')
  } catch (err) {
    t.ok(err.message.includes('group'))
  }
})

test('getUserOrgs', async (t) => {
  const orgs = await makeOrgs(t, 'test-11')

  await orgs.create('alice-ns', 'user', { userId: 'alice' })
  await orgs.create('team', 'group')
  await orgs.create('other', 'group')
  await orgs.addMember('team', 'alice', 'admin')

  const aliceOrgs = await orgs.getUserOrgs('alice')
  t.is(aliceOrgs.length, 2) // her personal ns + team
})

// ── Resources ─────────────────────────────────────────────────

test('add and remove resources', async (t) => {
  const orgs = await makeOrgs(t, 'test-12')

  await orgs.create('team', 'group')
  await orgs.addResource('team', 'project-a')
  await orgs.addResource('team', 'project-b')

  let org = await orgs.get('team')
  t.is(org.resources.length, 2)
  t.ok(org.resources.includes('project-a'))

  await orgs.removeResource('team', 'project-a')
  org = await orgs.get('team')
  t.is(org.resources.length, 1)
  t.is(org.resources[0], 'project-b')
})

test('move resource between orgs', async (t) => {
  const orgs = await makeOrgs(t, 'test-13')

  await orgs.create('team-a', 'group')
  await orgs.create('team-b', 'group')
  await orgs.addResource('team-a', 'project')

  await orgs.moveResource('project', 'team-a', 'team-b')

  const a = await orgs.get('team-a')
  const b = await orgs.get('team-b')
  t.is(a.resources.length, 0)
  t.is(b.resources.length, 1)
  t.is(b.resources[0], 'project')
})


// ── Grants ────────────────────────────────────────────────────

test('grants', async (t) => {
  const orgs = await makeOrgs(t, 'test-15')

  await orgs.create('team', 'group')

  const grant = await orgs.createGrant({
    subjectType: 'user',
    subjectId: 'alice',
    grantType: 'group',
    groupName: 'team',
    permission: 'write'
  })

  t.ok(grant.grantId)
  t.is(grant.permission, 'write')
  t.is(grant.revokedAt, null)

  const grants = await orgs.listGrants({ subjectType: 'user', subjectId: 'alice' })
  t.is(grants.length, 1)

  await orgs.revokeGrant(grant.grantId, 'admin')
  const revoked = await orgs.getGrant(grant.grantId)
  t.ok(revoked.revokedAt)
  t.is(revoked.revokedBy, 'admin')
})

test('listGrants filters revoked by default', async (t) => {
  const orgs = await makeOrgs(t, 'test-16')

  await orgs.create('team', 'group')
  const g1 = await orgs.createGrant({
    subjectType: 'user', subjectId: 'alice',
    grantType: 'group', groupName: 'team', permission: 'write'
  })
  await orgs.createGrant({
    subjectType: 'user', subjectId: 'alice',
    grantType: 'group', groupName: 'team', permission: 'moderator'
  })
  await orgs.revokeGrant(g1.grantId)

  const active = await orgs.listGrants({ subjectType: 'user', subjectId: 'alice' })
  t.is(active.length, 1)

  const all = await orgs.listGrants({
    subjectType: 'user', subjectId: 'alice', activeOnly: false
  })
  t.is(all.length, 2)
})

// ── Access Checking ───────────────────────────────────────────

test('checkAccess — denied when no grants', async (t) => {
  const orgs = await makeOrgs(t, 'test-22')

  await orgs.create('team', 'group')
  await orgs.addResource('team', 'repo-a')

  const denied = await orgs.checkAccess({
    userId: 'alice',
    resourceName: 'repo-a',
    requiredPermission: 'write'
  })
  t.is(denied, false)
})

// `read` is the lowest real level. Before it existed, checkAccess with
// requiredPermission:'read' hit `default: 0` and returned false no matter what
// was granted — the model could say who may WRITE but not who may READ, which
// is why repo visibility was unbuildable.

test('checkAccess — a read grant satisfies a read requirement', async (t) => {
  const orgs = await makeOrgs(t, 'test-22r')
  await orgs.create('team', 'group')
  await orgs.addResource('team', 'repo-a')

  await orgs.createGrant({
    subjectType: 'user',
    subjectId: 'reader',
    grantType: 'resource',
    resourceName: 'repo-a',
    permission: 'read'
  })

  t.is(await orgs.checkAccess({
    userId: 'reader', resourceName: 'repo-a', requiredPermission: 'read'
  }), true, 'read grant satisfies read')

  t.is(await orgs.checkAccess({
    userId: 'reader', resourceName: 'repo-a', requiredPermission: 'write'
  }), false, 'read does NOT imply write')
})

test('checkAccess — higher permissions imply read', async (t) => {
  const orgs = await makeOrgs(t, 'test-22h')
  await orgs.create('team', 'group')
  await orgs.addResource('team', 'repo-a')

  for (const [who, perm] of [['w', 'write'], ['m', 'moderator'], ['o', 'owner']]) {
    await orgs.createGrant({
      subjectType: 'user',
      subjectId: who,
      grantType: 'resource',
      resourceName: 'repo-a',
      permission: perm
    })
    t.is(await orgs.checkAccess({
      userId: who, resourceName: 'repo-a', requiredPermission: 'read'
    }), true, perm + ' implies read')
  }
})

test('checkAccess — a read grant does NOT satisfy moderator or owner', async (t) => {
  const orgs = await makeOrgs(t, 'test-22n')
  await orgs.create('team', 'group')
  await orgs.addResource('team', 'repo-a')

  await orgs.createGrant({
    subjectType: 'user',
    subjectId: 'reader',
    grantType: 'resource',
    resourceName: 'repo-a',
    permission: 'read'
  })

  t.is(await orgs.checkAccess({ userId: 'reader', resourceName: 'repo-a', requiredPermission: 'moderator' }), false)
  t.is(await orgs.checkAccess({ userId: 'reader', resourceName: 'repo-a', requiredPermission: 'owner' }), false)
})

test('checkAccess — an UNKNOWN permission still grants nothing', async (t) => {
  const orgs = await makeOrgs(t, 'test-22u')
  await orgs.create('team', 'group')
  await orgs.addResource('team', 'repo-a')

  await orgs.createGrant({
    subjectType: 'user',
    subjectId: 'alice',
    grantType: 'resource',
    resourceName: 'repo-a',
    permission: 'owner'
  })

  // A typo'd requirement must not fall through to "allowed".
  t.is(await orgs.checkAccess({
    userId: 'alice', resourceName: 'repo-a', requiredPermission: 'reed'
  }), false, 'unknown requirement rejected even for an owner')
})

test('checkAccess — a read grant works via a GROUP grant too', async (t) => {
  const orgs = await makeOrgs(t, 'test-22g')
  await orgs.create('team', 'group')
  await orgs.addResource('team', 'repo-a')

  await orgs.createGrant({
    subjectType: 'user',
    subjectId: 'alice',
    grantType: 'group',
    groupName: 'team',
    permission: 'read'
  })

  t.is(await orgs.checkAccess({
    userId: 'alice', resourceName: 'repo-a', requiredPermission: 'read'
  }), true, 'org-wide read reaches the org\'s resources')
})

test('checkAccess — group grant allows resource access', async (t) => {
  const orgs = await makeOrgs(t, 'test-23')

  await orgs.create('team', 'group')
  await orgs.addResource('team', 'repo-a')

  await orgs.createGrant({
    subjectType: 'user',
    subjectId: 'alice',
    grantType: 'group',
    groupName: 'team',
    permission: 'write'
  })

  const allowed = await orgs.checkAccess({
    userId: 'alice',
    resourceName: 'repo-a',
    requiredPermission: 'write'
  })
  t.is(allowed, true)
})

test('checkAccess — direct resource grant', async (t) => {
  const orgs = await makeOrgs(t, 'test-24')

  await orgs.create('team', 'group')
  await orgs.addResource('team', 'repo-a')

  await orgs.createGrant({
    subjectType: 'user',
    subjectId: 'alice',
    grantType: 'resource',
    resourceName: 'repo-a',
    permission: 'owner'
  })

  const allowed = await orgs.checkAccess({
    userId: 'alice',
    resourceName: 'repo-a',
    requiredPermission: 'moderator'
  })
  t.is(allowed, true)
})

test('checkAccess — server grants', async (t) => {
  const orgs = await makeOrgs(t, 'test-25')

  await orgs.create('team', 'group')
  await orgs.addResource('team', 'repo-a')

  await orgs.createGrant({
    subjectType: 'server',
    subjectId: 'serverkey123',
    grantType: 'resource',
    resourceName: 'repo-a',
    permission: 'write'
  })

  const allowed = await orgs.checkAccess({
    serverKey: 'serverkey123',
    resourceName: 'repo-a',
    requiredPermission: 'write'
  })
  t.is(allowed, true)
})