Unverified Commit b76a94a8 authored by Kyle Keating's avatar Kyle Keating Committed by GitHub

Call requestGroups to get groups filter

Allow `groups` filter to resolve from an RCP ancestor frame
- If the groups array exists at load time, leave it in place.
- If the groups array value is equal to '$rcp:requestGroups' then fetch that value with a second RCP request to the ancestor frame.
parent b1895465
...@@ -25,6 +25,7 @@ export default function groups( ...@@ -25,6 +25,7 @@ export default function groups(
serviceUrl, serviceUrl,
session, session,
settings, settings,
toastMessenger,
auth auth
) { ) {
const svc = serviceConfig(settings); const svc = serviceConfig(settings);
...@@ -87,7 +88,7 @@ export default function groups( ...@@ -87,7 +88,7 @@ export default function groups(
* @param {string|null} directLinkedGroupId * @param {string|null} directLinkedGroupId
* @return {Group[]} * @return {Group[]}
*/ */
function filterGroups( async function filterGroups(
groups, groups,
isLoggedIn, isLoggedIn,
directLinkedAnnotationGroupId, directLinkedAnnotationGroupId,
...@@ -106,14 +107,21 @@ export default function groups( ...@@ -106,14 +107,21 @@ export default function groups(
directLinkedGroupId = null; directLinkedGroupId = null;
} }
} }
// If service groups are specified only return those. // If service groups are specified only return those.
// If a service group doesn't exist in the list of groups don't return it. // If a service group doesn't exist in the list of groups don't return it.
if (svc && svc.groups) { if (svc && svc.groups) {
const focusedGroups = groups.filter( try {
g => svc.groups.includes(g.id) || svc.groups.includes(g.groupid) // The groups may not be immediately available if they are being fetched via RPC.
const focusedGroups = await svc.groups;
const filteredGroups = groups.filter(
g => focusedGroups.includes(g.id) || focusedGroups.includes(g.groupid)
); );
return focusedGroups; return filteredGroups;
} catch (e) {
toastMessenger.error(e.message);
// Don't show any groups if we catch an error
return [];
}
} }
// Logged-in users always see the "Public" group. // Logged-in users always see the "Public" group.
...@@ -295,7 +303,7 @@ export default function groups( ...@@ -295,7 +303,7 @@ export default function groups(
// Step 4. Combine all the groups into a single list and set additional // Step 4. Combine all the groups into a single list and set additional
// metadata on them that will be used elsewhere in the app. // metadata on them that will be used elsewhere in the app.
const isLoggedIn = token !== null; const isLoggedIn = token !== null;
const groups = filterGroups( const groups = await filterGroups(
combineGroups(myGroups, featuredGroups, documentUri), combineGroups(myGroups, featuredGroups, documentUri),
isLoggedIn, isLoggedIn,
directLinkedAnnotationGroupId, directLinkedAnnotationGroupId,
......
...@@ -47,6 +47,7 @@ describe('groups', function () { ...@@ -47,6 +47,7 @@ describe('groups', function () {
let fakeRootScope; let fakeRootScope;
let fakeServiceUrl; let fakeServiceUrl;
let fakeMetadata; let fakeMetadata;
let fakeToastMessenger;
beforeEach(function () { beforeEach(function () {
fakeAuth = { fakeAuth = {
...@@ -57,6 +58,10 @@ describe('groups', function () { ...@@ -57,6 +58,10 @@ describe('groups', function () {
isReply: sinon.stub(), isReply: sinon.stub(),
}; };
fakeToastMessenger = {
error: sinon.stub(),
};
fakeStore = fakeReduxStore( fakeStore = fakeReduxStore(
{ {
frames: [{ uri: 'http://example.org' }], frames: [{ uri: 'http://example.org' }],
...@@ -151,6 +156,7 @@ describe('groups', function () { ...@@ -151,6 +156,7 @@ describe('groups', function () {
fakeServiceUrl, fakeServiceUrl,
fakeSession, fakeSession,
fakeSettings, fakeSettings,
fakeToastMessenger,
fakeAuth fakeAuth
); );
} }
...@@ -746,7 +752,16 @@ describe('groups', function () { ...@@ -746,7 +752,16 @@ describe('groups', function () {
services: [{}], services: [{}],
expected: ['__world__', 'abc123', 'def456'], expected: ['__world__', 'abc123', 'def456'],
}, },
].forEach(({ description, services, expected }) => { {
description: 'does not show any groups if the groups promise rejects',
services: [
{ groups: Promise.reject(new Error('something went wrong')) },
],
toastMessageError: 'something went wrong',
expected: [],
},
].forEach(
({ description, services, expected, toastMessageError = null }) => {
it(description, () => { it(description, () => {
fakeSettings.services = services; fakeSettings.services = services;
const svc = service(); const svc = service();
...@@ -764,9 +779,13 @@ describe('groups', function () { ...@@ -764,9 +779,13 @@ describe('groups', function () {
return svc.load().then(groups => { return svc.load().then(groups => {
let displayedGroups = groups.map(g => g.id); let displayedGroups = groups.map(g => g.id);
assert.deepEqual(displayedGroups, expected); assert.deepEqual(displayedGroups, expected);
if (toastMessageError) {
assert.calledWith(fakeToastMessenger.error, toastMessageError);
}
}); });
}); });
}); }
);
}); });
describe('#get', function () { describe('#get', function () {
......
...@@ -10,7 +10,7 @@ class FakeWindow { ...@@ -10,7 +10,7 @@ class FakeWindow {
} }
} }
describe('sidebar/cross-origin-rcp', function () { describe('sidebar/cross-origin-rpc', function () {
let fakeStore; let fakeStore;
let fakeWarnOnce; let fakeWarnOnce;
let fakeWindow; let fakeWindow;
......
...@@ -132,9 +132,43 @@ async function fetchConfigRpc(appConfig, parentFrame, origin) { ...@@ -132,9 +132,43 @@ async function fetchConfigRpc(appConfig, parentFrame, origin) {
[], [],
3000 3000
); );
return fetchConfigEmbed(appConfig, remoteConfig); // Closure for the RPC call to scope parentFrame and origin variables.
const rpcCall = (method, args = [], timeout = 3000) =>
postMessageJsonRpc.call(parentFrame, origin, method, args, timeout);
const mergedConfig = fetchConfigEmbed(appConfig, remoteConfig);
return fetchGroupsAsync(mergedConfig, rpcCall);
} }
/**
* Potentially mutates the `groups` config object(s) to be a promise that
* resolves right away if the `groups` value exists in the original
* config, or returns a promise that resolves with a secondary RPC
* call to `requestGroups` when the `groups` value is '$rpc:requestGroups'
* If the `groups` value is missing or falsely then it won't be mutated.
*
* This allows the app to start with an incomplete config and then lazily
* fill in the `groups` value(s) later when its ready. This helps speed
* up the loading process.
*
* @param {Object} config - The configuration object to mutate. This should
* already have the `services` value
* @param {function} rpcCall - RPC method
* (method, args, timeout) => Promise
* @return {Object} - The mutated settings
*/
async function fetchGroupsAsync(config, rpcCall) {
if (Array.isArray(config.services)) {
config.services.forEach((service, index) => {
if (service.groups === '$rpc:requestGroups') {
// The `groups` need to be fetched from a secondary RPC call
service.groups = rpcCall('requestGroups', [index], 5000).catch(() => {
throw new Error('Unable to fetch groups');
});
}
});
}
return config;
}
/** /**
* Fetch the host configuration and merge it with the app configuration from h. * Fetch the host configuration and merge it with the app configuration from h.
* *
......
...@@ -178,12 +178,14 @@ describe('sidebar.util.fetch-config', () => { ...@@ -178,12 +178,14 @@ describe('sidebar.util.fetch-config', () => {
it('makes an RPC request to `requestConfig` ', async () => { it('makes an RPC request to `requestConfig` ', async () => {
await fetchConfig({}, fakeWindow); await fetchConfig({}, fakeWindow);
assert.isTrue(
fakeJsonRpc.call.calledWithExactly( fakeJsonRpc.call.calledWithExactly(
fakeTopWindow, fakeTopWindow,
'https://embedder.com', 'https://embedder.com',
'requestConfig', 'requestConfig',
[], [],
3000 3000
)
); );
}); });
...@@ -219,7 +221,7 @@ describe('sidebar.util.fetch-config', () => { ...@@ -219,7 +221,7 @@ describe('sidebar.util.fetch-config', () => {
} }
}); });
it('creates a merged config when the rpc requests returns the host config` ', async () => { it('creates a merged config when the RPC requests returns the host config', async () => {
const appConfig = { foo: 'bar', appType: 'via' }; const appConfig = { foo: 'bar', appType: 'via' };
fakeJsonRpc.call.resolves({ foo: 'baz' }); // host config fakeJsonRpc.call.resolves({ foo: 'baz' }); // host config
const result = await fetchConfig(appConfig, fakeWindow); const result = await fetchConfig(appConfig, fakeWindow);
...@@ -237,6 +239,52 @@ describe('sidebar.util.fetch-config', () => { ...@@ -237,6 +239,52 @@ describe('sidebar.util.fetch-config', () => {
const result = fetchConfig(appConfig, fakeWindow); const result = fetchConfig(appConfig, fakeWindow);
return assertPromiseIsRejected(result, 'Nope'); return assertPromiseIsRejected(result, 'Nope');
}); });
it('returns the `groups` array with the initial host config request', async () => {
const appConfig = {
services: [{ groups: ['group1', 'group2'] }],
appType: 'via',
};
fakeJsonRpc.call.onFirstCall().resolves({ foo: 'baz' }); // host config
const result = await fetchConfig(appConfig, fakeWindow);
assert.deepEqual(result.services[0].groups, ['group1', 'group2']);
});
it("creates a merged config where `groups` is a promise when its initial value is '$rpc:requestGroups'", async () => {
const appConfig = {
services: [{ groups: '$rpc:requestGroups' }],
appType: 'via',
};
fakeJsonRpc.call.onFirstCall().resolves({ foo: 'baz' }); // host config
fakeJsonRpc.call.onSecondCall().resolves(['group1', 'group2']); // requestGroups
const result = await fetchConfig(appConfig, fakeWindow);
assert.deepEqual(await result.services[0].groups, ['group1', 'group2']);
assert.isTrue(
fakeJsonRpc.call.getCall(1).calledWithExactly(
fakeTopWindow,
'https://embedder.com',
'requestGroups',
[0], // passes service index to requestGroups
5000
)
);
});
it('throws an error when the RPC call to `requestGroups` fails', async () => {
const appConfig = {
services: [{ groups: '$rpc:requestGroups' }],
appType: 'via',
};
fakeJsonRpc.call.onFirstCall().resolves({ foo: 'baz' }); // host config
fakeJsonRpc.call.onSecondCall().rejects(); // requestGroups
const result = await fetchConfig(appConfig, fakeWindow);
try {
await result.services[0].groups;
throw new Error('Failed to catch error');
} catch (e) {
assert.equal(e.message, 'Unable to fetch groups');
}
});
}); });
context('incorrect requestConfigFromFrame object', () => { context('incorrect requestConfigFromFrame object', () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment