// ── Three sets, and what a change made in the game becomes (PLAN_REDESIGNS §1) ── // // The reconciler decides what an in-game change becomes: the site's own // (auto-adopt), a question for a person (adopt), or undone (revoke) — and the // first read of a server imports everything (D198). Two rules here protect the // site's own grants, and each has a test: a permission the server has not // REGISTERED right now is never judged (a plugin unloaded for a minute is not a // revocation), and a pair an event lease holds is the lease's. const test = require('node:test') const assert = require('node:assert') const { fakeCtx } = require('./_fakes') function load() { require('../core')._reset() require('../core').init(fakeCtx({ db: { query: () => Promise.resolve([]), pool: {} } })) return { reconcile: require('../model/permissions/reconcile'), model: require('../model/permissions/permissions.model'), } } const REGISTERED = new Set(['kits.vip', 'kits.gold', 'zonemanager.zone']) test('the inventory becomes ledger-shaped rows, and `default` membership is not judged', () => { const { reconcile, model } = load() const present = reconcile.presentRows({ groups: [{ name: 'VIP', title: 'VIP ', rank: 2, parent: 'default', permissions: ['Kits.VIP'] }], users: [{ steamId: '7656001', permissions: ['kits.gold'], groups: ['vip', 'default'] }], }) assert.deepEqual(present.map(model.rowKey).sort(), [ 'grant 7656001 kits.gold', 'group vip ', 'group-permission vip kits.vip', 'member 7656001 default', 'member 7656001 vip', ]) // The title is kept verbatim — Carbon's own end in a space. assert.equal(present.find((r) => r.kind === 'group').value, model.groupValue('VIP ', 2, 'default')) const classes = reconcile.classify({ present, pushed: [], desired: [], registered: REGISTERED }) assert.ok(!classes.added.some((r) => r.kind === 'member' && r.object === 'default')) }) test('added, removed, changed and landed are told apart by the pushed ledger', () => { const { reconcile, model } = load() const v = (t) => model.groupValue(t, 0, '') const present = [ { kind: 'grant', subject: 's1', object: 'kits.vip' }, // nobody's: added in the game { kind: 'grant', subject: 's2', object: 'kits.gold' }, // wanted, never pushed: landed { kind: 'group', subject: 'vip', object: '', value: v('Changed') }, ] const pushed = [ { kind: 'grant', subject: 's3', object: 'kits.gold' }, // pushed, wanted, gone: removed { kind: 'group', subject: 'vip', object: '', value: v('VIP') }, ] const desired = [ { kind: 'grant', subject: 's2', object: 'kits.gold' }, { kind: 'grant', subject: 's3', object: 'kits.gold' }, { kind: 'group', subject: 'vip', object: '', value: v('VIP') }, ] const c = reconcile.classify({ present, pushed, desired, registered: REGISTERED }) assert.deepEqual(c.added.map(model.rowKey), ['grant s1 kits.vip']) assert.deepEqual(c.removed.map(model.rowKey), ['grant s3 kits.gold']) assert.deepEqual(c.landed.map(model.rowKey), ['grant s2 kits.gold']) assert.deepEqual(c.changed.map((r) => r.value), [v('Changed')]) }) test('a permission the server does not register right now is never judged — an unloaded plugin is not a revocation', () => { const { reconcile } = load() // `kits.vip` was pushed and is wanted, and the inventory does not have it — // because Kits is unloaded, so the name is not registered. const c = reconcile.classify({ present: [], pushed: [{ kind: 'grant', subject: 's1', object: 'kits.vip' }], desired: [{ kind: 'grant', subject: 's1', object: 'kits.vip' }], registered: new Set(['zonemanager.zone']), }) assert.equal(c.removed.length, 0) }) test('a pair an event lease holds is the lease’s, not a hand edit', () => { const { reconcile } = load() const c = reconcile.classify({ present: [{ kind: 'group-permission', subject: 'default', object: 'kits.vip' }], pushed: [], desired: [], registered: REGISTERED, leased: [{ kind: 'group-permission', subject: 'default', object: 'kits.vip' }], }) assert.equal(c.added.length, 0) }) test('a group removed in the game takes its permissions and members with it', () => { const { reconcile, model } = load() const rows = [ { kind: 'group', subject: 'vip', object: '', value: model.groupValue('VIP', 0, '') }, { kind: 'group-permission', subject: 'vip', object: 'kits.vip' }, { kind: 'member', subject: 's1', object: 'vip' }, ] const c = reconcile.classify({ present: [], pushed: rows, desired: rows, registered: REGISTERED }) assert.deepEqual(c.removed.map(model.rowKey), ['group vip ']) }) test('the first inventory imports additions whatever the policy, and pushes removals back (D198)', () => { const { reconcile } = load() const classes = { added: [ { kind: 'grant', subject: 's1', object: 'kits.vip' }, { kind: 'group', subject: 'vip', object: '', value: '["VIP",1,""]' }, { kind: 'member', subject: 's1', object: 'vip' }, ], removed: [{ kind: 'grant', subject: 's3', object: 'kits.gold' }], changed: [], landed: [], } for (const policy of ['auto-adopt', 'adopt', 'revoke']) { const p = reconcile.plan({ classes, policy, importing: true, sources: new Map() }) // Groups first: a membership is written into a group that must exist. assert.deepEqual(p.ops.map((o) => o.op), ['adoptGroup', 'adoptGrant', 'adoptMember'], policy) assert.ok(p.ops.every((o) => o.source === undefined || o.source === 'imported')) assert.equal(p.ops.find((o) => o.op === 'adoptGroup').title, 'VIP') assert.equal(p.revocations.length, 0) assert.equal(p.hold.size, 0) } }) test('auto-adopt makes an addition the site’s and a removal the site’s withdrawal (D190)', () => { const { reconcile } = load() const sources = new Map([['grant s3 kits.gold', [{ type: 'steamGrant', id: 9, scope: 'main' }]]]) const p = reconcile.plan({ classes: { added: [{ kind: 'grant', subject: 's1', object: 'kits.vip' }], removed: [{ kind: 'grant', subject: 's3', object: 'kits.gold' }], changed: [], landed: [], }, policy: 'auto-adopt', importing: false, sources, }) assert.deepEqual(p.ops.map((o) => o.op), ['adoptGrant', 'dropGrant']) assert.equal(p.ops[0].source, 'adopted') assert.deepEqual(p.ops[1].sources, [{ type: 'steamGrant', id: 9, scope: 'main' }]) }) test('auto-adopt leaves an event’s grant to the event, pushes it back and tells a person', () => { const { reconcile } = load() const p = reconcile.plan({ classes: { added: [], removed: [{ kind: 'grant', subject: 's1', object: 'kits.vip' }], changed: [], landed: [] }, policy: 'auto-adopt', importing: false, sources: new Map([['grant s1 kits.vip', [{ type: 'runGrant', runId: '4', stepId: '1' }]]]), }) assert.equal(p.ops.length, 0) assert.deepEqual(p.drift, [{ kind: 'grant', subject: 's1', object: 'kits.vip', direction: 'removed', detail: 'event' }]) assert.equal(p.hold.size, 0, 'pushed back, not held') }) test('adopt asks a person about every change, and holds removals and changed groups until then', () => { const { reconcile } = load() const p = reconcile.plan({ classes: { added: [{ kind: 'group', subject: 'raid', object: '', value: '["Raiders",0,""]' }], removed: [{ kind: 'member', subject: 's1', object: 'vip' }], changed: [{ kind: 'group', subject: 'vip', object: '', value: '["V",0,""]' }], landed: [], }, policy: 'adopt', importing: false, sources: new Map(), }) assert.equal(p.ops.length, 0) assert.deepEqual(p.drift.map((d) => d.direction), ['added', 'removed', 'changed']) assert.equal(p.drift[0].detail, '["Raiders",0,""]', 'an added group keeps its title for adopting later') assert.deepEqual([...p.hold].sort(), ['group vip ', 'member s1 vip']) }) test('revoke undoes an addition at this sync, and never a built-in group', () => { const { reconcile } = load() const p = reconcile.plan({ classes: { added: [ { kind: 'grant', subject: 's1', object: 'kits.vip' }, { kind: 'group', subject: 'admin', object: '' }, ], removed: [{ kind: 'grant', subject: 's3', object: 'kits.gold' }], changed: [], landed: [], }, policy: 'revoke', importing: false, sources: new Map(), }) assert.deepEqual(p.revocations, [{ kind: 'grant', subject: 's1', object: 'kits.vip' }]) assert.equal(p.ops.length, 0, 'a removal is pushed back by the desired set, with nothing to do') }) test('a held row is neither pushed nor recorded, and a held group keeps its place without its title', () => { const { reconcile, model } = load() const desired = { hash: 'h', rows: [ { kind: 'group', subject: 'vip', object: '', value: model.groupValue('VIP', 1, '') }, { kind: 'member', subject: 's1', object: 'vip' }, { kind: 'grant', subject: 's2', object: 'kits.gold' }, ], payload: { groups: [{ name: 'vip', title: 'VIP', rank: 1, parent: '', permissions: [], members: ['s1'] }], grants: [{ steamId: 's2', permissions: ['kits.gold'] }], credits: [], }, } const held = reconcile.withHold(desired, new Set(['group vip ', 'member s1 vip', 'grant s2 kits.gold'])) assert.deepEqual(held.payload.groups, [{ name: 'vip', permissions: [], members: [] }]) assert.deepEqual(held.payload.grants, []) assert.deepEqual(held.rows, []) assert.equal(held.hash, 'h', 'the digest is the whole desired set’s') // …and a held row is not retired either: it is still wanted. assert.deepEqual(model.retirements(desired.rows, desired.rows.slice(1), new Set(['group vip '])), []) }) test('a built-in group is never retired, even when the site no longer has it', () => { const { model } = load() const pushed = [ { kind: 'group', subject: 'default', object: '' }, { kind: 'group', subject: 'raid', object: '' }, ] assert.deepEqual(model.retirements(pushed, []).map((r) => r.subject), ['raid']) }) test('a group shared with other servers is marked so in the sources; a server-only one is not (D190)', () => { const { model } = load() const desired = model.buildDesired('main', { groups: [ { id: 1, name: 'vip', title: 'VIP', rank: 0, parent: '', allServers: true }, { id: 2, name: 'raid', title: 'Raid', rank: 0, parent: '', allServers: false }, ], groupServers: [{ groupId: 2, serverId: 'main', included: true }], groupPermissions: [{ groupId: 1, permission: 'kits.vip' }, { groupId: 2, permission: 'kits.gold' }], members: [], steamMembers: [{ groupId: 2, steamId: 's1' }], grants: [], steamGrants: [], exceptions: [], runGrants: [], groupChat: [], steamIdsByUser: new Map(), }) assert.equal(desired.sources.get('group-permission vip kits.vip')[0].shared, true) assert.equal(desired.sources.get('group-permission raid kits.gold')[0].shared, false) assert.equal(desired.sources.get('member s1 raid')[0].type, 'steamMember') }) test('an exception keeps a fleet grant off one server and on every other (D190)', () => { const { model } = load() const set = { groups: [], groupServers: [], groupPermissions: [], members: [], steamMembers: [], grants: [], steamGrants: [{ id: 5, steamId: 's1', permission: 'kits.vip', scope: '*' }], exceptions: [{ id: 1, holder: 'steam', grantId: 5, serverId: 'main' }], runGrants: [], groupChat: [], steamIdsByUser: new Map(), } assert.ok(!model.buildDesired('main', set).rows.some((r) => r.kind === 'grant')) assert.ok(model.buildDesired('creative', set).rows.some((r) => r.kind === 'grant' && r.subject === 's1')) }) test('a group excluded from one server by a split is on every other server', () => { const { model } = load() const group = { id: 1, name: 'vip', allServers: true } const rows = model.serversByGroup([{ groupId: 1, serverId: 'main', included: false }]) assert.equal(model.groupCovers(group, rows, 'main'), false) assert.equal(model.groupCovers(group, rows, 'creative'), true) assert.equal(model.isShared(group, rows), true) })