From e26ff99d4f47f3a77cd002df75ef2b31d655e987 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Tue, 5 Sep 2023 10:38:54 -0400 Subject: [PATCH] add capabilities to implementation --- src/member-api.js | 20 +++++++-- tests/member-api.js | 100 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/src/member-api.js b/src/member-api.js index 8a4f7df3a..292ccde3f 100644 --- a/src/member-api.js +++ b/src/member-api.js @@ -1,6 +1,8 @@ import { TypedEmitter } from 'tiny-typed-emitter' +import { InviteResponse_Decision } from './generated/rpc.js' export class MemberApi extends TypedEmitter { + #capabilities #encryptionKeys #getProjectInfo #projectKey @@ -8,29 +10,37 @@ export class MemberApi extends TypedEmitter { /** * @param {Object} opts + * @param {import('./capabilities.js').Capabilities} opts.capabilities * @param {import('./generated/keys.js').EncryptionKeys} opts.encryptionKeys * @param {() => Promise} opts.getProjectInfo * @param {Buffer} opts.projectKey * @param {import('./rpc/index.js').MapeoRPC} opts.rpc */ - constructor({ encryptionKeys, getProjectInfo, projectKey, rpc }) { + constructor({ + capabilities, + encryptionKeys, + getProjectInfo, + projectKey, + rpc, + }) { super() this.#encryptionKeys = encryptionKeys this.#getProjectInfo = getProjectInfo this.#projectKey = projectKey this.#rpc = rpc + this.#capabilities = capabilities } /** * @param {string} deviceId * * @param {Object} opts - * @param {string} opts.roleId + * @param {import('./capabilities.js').RoleId} opts.roleId * @param {number} [opts.timeout] * * @returns {Promise} */ - async invite(deviceId, { timeout }) { + async invite(deviceId, { roleId, timeout }) { const projectInfo = await this.#getProjectInfo() const response = await this.#rpc.invite(deviceId, { @@ -40,7 +50,9 @@ export class MemberApi extends TypedEmitter { timeout, }) - // TODO: If response is ACCEPT, write to capabilities + if (response === InviteResponse_Decision.ACCEPT) { + await this.#capabilities.assignRole(deviceId, roleId) + } return response } diff --git a/tests/member-api.js b/tests/member-api.js index 096275505..9f5da451c 100644 --- a/tests/member-api.js +++ b/tests/member-api.js @@ -7,7 +7,7 @@ import { MemberApi } from '../src/member-api.js' import { InviteResponse_Decision } from '../src/generated/rpc.js' import { replicate } from './helpers/rpc.js' -test('Invite sends expected project-related details', async (t) => { +test('invite() sends expected project-related details', async (t) => { t.plan(4) const projectKey = KeyManager.generateProjectKeypair().publicKey @@ -18,6 +18,7 @@ test('Invite sends expected project-related details', async (t) => { const r2 = new MapeoRPC() const memberApi = new MemberApi({ + capabilities: { async assignRole() {} }, encryptionKeys, getProjectInfo: async () => projectInfo, projectKey, @@ -32,7 +33,7 @@ test('Invite sends expected project-related details', async (t) => { t.is(response, InviteResponse_Decision.ACCEPT) }) - r2.on('invite', async (peerId, invite) => { + r2.on('invite', (peerId, invite) => { t.alike(invite.projectKey, projectKey) t.alike(invite.encryptionKeys, encryptionKeys) t.alike(invite.projectInfo, projectInfo) @@ -45,3 +46,98 @@ test('Invite sends expected project-related details', async (t) => { replicate(r1, r2) }) + +test('invite() assigns role to invited device after invite accepted', async (t) => { + t.plan(4) + + const r1 = new MapeoRPC() + const r2 = new MapeoRPC() + + const expectedRoleId = randomBytes(8).toString('hex') + let expectedDeviceId = null + + // We're only testing that this gets called with the expected arguments + const capabilities = { + async assignRole(deviceId, roleId) { + t.ok(expectedDeviceId) + t.is(deviceId, expectedDeviceId) + t.is(roleId, expectedRoleId) + }, + } + + const memberApi = new MemberApi({ + capabilities, + encryptionKeys: { auth: randomBytes(32) }, + getProjectInfo: async () => {}, + projectKey: KeyManager.generateProjectKeypair().publicKey, + rpc: r1, + }) + + r1.on('peers', async (peers) => { + expectedDeviceId = peers[0].id + + const response = await memberApi.invite(expectedDeviceId, { + roleId: expectedRoleId, + }) + + t.is(response, InviteResponse_Decision.ACCEPT) + }) + + r2.on('invite', (peerId, invite) => { + r2.inviteResponse(peerId, { + projectKey: invite.projectKey, + decision: InviteResponse_Decision.ACCEPT, + }) + }) + + replicate(r1, r2) +}) + +test('invite() does not assign role to invited device if invite is not accepted', async (t) => { + const nonAcceptInviteDecisions = Object.values( + InviteResponse_Decision + ).filter((d) => d !== InviteResponse_Decision.ACCEPT) + + for (const decision of nonAcceptInviteDecisions) { + t.test(`${decision}`, (t) => { + t.plan(1) + + const r1 = new MapeoRPC() + const r2 = new MapeoRPC() + + const capabilities = { + // This should not be called at any point in this test + async assignRole() { + t.fail( + 'Attempted to assign role despite decision being non-acceptance' + ) + }, + } + + const memberApi = new MemberApi({ + capabilities, + encryptionKeys: { auth: randomBytes(32) }, + getProjectInfo: async () => {}, + projectKey: KeyManager.generateProjectKeypair().publicKey, + rpc: r1, + }) + + r1.on('peers', async (peers) => { + const response = await memberApi.invite(peers[0].id, { + roleId: randomBytes(8).toString('hex'), + }) + + t.is(response, decision) + }) + + r2.on('invite', (peerId, invite) => { + r2.inviteResponse(peerId, { + projectKey: invite.projectKey, + decision, + }) + }) + + replicate(r1, r2) + }) + } +})