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)
})