Commit 0857532d authored by Hannah Stepanek's avatar Hannah Stepanek

Get group for direct-linked ann in group service

- Fetches the direct-linked annotation's group when the direct-linked
  annotation's group is not fetched by the group.list, profile.group,
  or the direct-linked group.
- Simplify the logic in the filterGroups function and pass the
  direct-linked annotation's groupid to the filterGroups function to
  avoid a duplicate annotation fetch.
- Add tests.
This is a fix for hypothesis/product-backlog#986.
parent 3cb001c7
...@@ -58,14 +58,14 @@ function groups( ...@@ -58,14 +58,14 @@ function groups(
* *
* @param {Group[]} groups * @param {Group[]} groups
* @param {boolean} isLoggedIn * @param {boolean} isLoggedIn
* @param {string|null} directLinkedAnnotationId * @param {string|null} directLinkedAnnotationGroupId
* @param {string|null} directLinkedGroupId * @param {string|null} directLinkedGroupId
* @return {Promise<Group[]>} * @return {Group[]}
*/ */
function filterGroups( function filterGroups(
groups, groups,
isLoggedIn, isLoggedIn,
directLinkedAnnotationId, directLinkedAnnotationGroupId,
directLinkedGroupId directLinkedGroupId
) { ) {
// Filter the directLinkedGroup out if it is out of scope and scope is enforced. // Filter the directLinkedGroup out if it is out of scope and scope is enforced.
...@@ -87,12 +87,12 @@ function groups( ...@@ -87,12 +87,12 @@ function groups(
const focusedGroups = groups.filter( const focusedGroups = groups.filter(
g => svc.groups.includes(g.id) || svc.groups.includes(g.groupid) g => svc.groups.includes(g.id) || svc.groups.includes(g.groupid)
); );
return Promise.resolve(focusedGroups); return focusedGroups;
} }
// Logged-in users always see the "Public" group. // Logged-in users always see the "Public" group.
if (isLoggedIn) { if (isLoggedIn) {
return Promise.resolve(groups); return groups;
} }
// If the main document URL has no groups associated with it, always show // If the main document URL has no groups associated with it, always show
...@@ -101,39 +101,20 @@ function groups( ...@@ -101,39 +101,20 @@ function groups(
g => g.id !== '__world__' && g.isScopedToUri g => g.id !== '__world__' && g.isScopedToUri
); );
if (!pageHasAssociatedGroups) { if (!pageHasAssociatedGroups) {
return Promise.resolve(groups); return groups;
}
// Hide the "Public" group, unless the user specifically visited a direct-
// link to an annotation in that group.
const nonWorldGroups = groups.filter(g => g.id !== '__world__');
if (!directLinkedAnnotationId && !directLinkedGroupId) {
return Promise.resolve(nonWorldGroups);
} }
// If directLinkedGroup is the "Public" group, always return groups. // If directLinkedGroup or directLinkedAnnotationGroupId is the "Public" group,
if (directLinkedGroupId === '__world__') { // always return groups.
if (
directLinkedGroupId === '__world__' ||
directLinkedAnnotationGroupId === '__world__'
) {
return groups; return groups;
} }
// If the directLinkedAnnotationId's group is the "Public" group return groups, // Return non-world groups.
// otherwise filter out the "Public" group. return groups.filter(g => g.id !== '__world__');
// Force getAnnotation to enter the catch clause if there is no linked annotation.
const getAnnotation = directLinkedAnnotationId
? api.annotation.get({ id: directLinkedAnnotationId })
: Promise.reject();
return getAnnotation
.then(ann => {
return ann.group === '__world__' ? groups : nonWorldGroups;
})
.catch(() => {
// Annotation does not exist or we do not have permission to read it.
// Assume it is not in "Public".
return nonWorldGroups;
});
} }
/** /**
...@@ -158,6 +139,19 @@ function groups( ...@@ -158,6 +139,19 @@ function groups(
// sidebar app changes. // sidebar app changes.
let documentUri; let documentUri;
/*
* Fetch an individual group.
*
* @param {Object} requestParams
* @return {Promise<Group>|undefined}
*/
function fetchGroup(requestParams) {
return api.group.read(requestParams).catch(() => {
// If the group does not exist or the user doesn't have permission.
return null;
});
}
/** /**
* Fetch the list of applicable groups from the API. * Fetch the list of applicable groups from the API.
* *
...@@ -171,7 +165,9 @@ function groups( ...@@ -171,7 +165,9 @@ function groups(
if (isSidebar) { if (isSidebar) {
uri = getDocumentUriForGroupSearch(); uri = getDocumentUriForGroupSearch();
} }
const directLinkedGroup = settings.group; const directLinkedGroupId = settings.group;
const directLinkedAnnId = settings.annotations;
let directLinkedAnnotationGroupId = null;
return uri return uri
.then(uri => { .then(uri => {
const params = { const params = {
...@@ -195,45 +191,95 @@ function groups( ...@@ -195,45 +191,95 @@ function groups(
auth.tokenGetter(), auth.tokenGetter(),
]; ];
// If there is a directLinkedGroup, add an api request to get that // If there is a directLinkedAnnId, fetch the annotation to see if there needs
// particular group as well since it may not be in the results returned // to be a second api request to fetch its group since the group may not be in
// by group.list or profile.groups. // the results returned by group.list, profile.groups, or the direct-linked group.
if (directLinkedGroup) { let selectedAnnApi = Promise.resolve(null);
const selectedGroupApi = api.group if (directLinkedAnnId) {
.read({ selectedAnnApi = api.annotation
id: directLinkedGroup, .get({ id: directLinkedAnnId })
expand: params.expand,
})
.catch(() => { .catch(() => {
// If the group does not exist or the user doesn't have permission, // If the annotation does not exist or the user doesn't have permission.
// return undefined. return null;
return undefined;
}); });
groupApiRequests = groupApiRequests.concat(selectedGroupApi);
} }
groupApiRequests = groupApiRequests.concat(selectedAnnApi);
// If there is a directLinkedGroupId, add an api request to get that
// particular group since it may not be in the results returned by
// group.list or profile.groups.
let selectedGroupApi = Promise.resolve(null);
if (directLinkedGroupId) {
selectedGroupApi = fetchGroup({
id: directLinkedGroupId,
expand: params.expand,
});
}
groupApiRequests = groupApiRequests.concat(selectedGroupApi);
return Promise.all(groupApiRequests).then( return Promise.all(groupApiRequests).then(
([myGroups, featuredGroups, token, selectedGroup]) => [ ([myGroups, featuredGroups, token, selectedAnn, selectedGroup]) => {
// Don't add the selectedGroup if it's already in the featuredGroups. // If there is a direct-linked group, add it to the featured groups list.
combineGroups( const allFeaturedGroups =
myGroups, selectedGroup !== null &&
selectedGroup !== undefined && !featuredGroups.some(g => g.id === selectedGroup.id)
!featuredGroups.some(g => g.id === selectedGroup.id)
? featuredGroups.concat([selectedGroup]) ? featuredGroups.concat([selectedGroup])
: featuredGroups, : featuredGroups;
documentUri
), // If there's a selected annotation it may require an extra api call
token, // to fetch its group.
] if (selectedAnn) {
// Set the directLinkedAnnotationGroupId to be used later in
// the filterGroups method.
directLinkedAnnotationGroupId = selectedAnn.group;
const selectedAnnGroup = myGroups
.concat(allFeaturedGroups)
.some(g => g.id === selectedAnn.group);
// If the direct-linked annotation's group has not already been fetched,
// fetch it.
if (!selectedAnnGroup) {
return fetchGroup({
id: selectedAnn.group,
expand: params.expand,
}).then(directLinkedAnnGroup => {
// If the directLinkedAnnotation's group fetch failed, return
// the list of groups without it.
if (!directLinkedAnnGroup) {
return [
combineGroups(myGroups, allFeaturedGroups, documentUri),
token,
];
}
// If the directLinkedAnnotation's group fetch was successful,
// combine it with the other groups.
return [
combineGroups(
myGroups,
allFeaturedGroups.concat(directLinkedAnnGroup),
documentUri
),
token,
];
});
}
}
// If there is no direct-linked annotation, return the list of groups without it.
return [
combineGroups(myGroups, allFeaturedGroups, documentUri),
token,
];
}
); );
}) })
.then(([groups, token]) => { .then(([groups, token]) => {
const isLoggedIn = token !== null; const isLoggedIn = token !== null;
const directLinkedAnnotation = settings.annotations;
return filterGroups( return filterGroups(
groups, groups,
isLoggedIn, isLoggedIn,
directLinkedAnnotation, directLinkedAnnotationGroupId,
directLinkedGroup directLinkedGroupId
); );
}) })
.then(groups => { .then(groups => {
...@@ -243,8 +289,17 @@ function groups( ...@@ -243,8 +289,17 @@ function groups(
const prevFocusedGroup = localStorage.getItem(STORAGE_KEY); const prevFocusedGroup = localStorage.getItem(STORAGE_KEY);
store.loadGroups(groups); store.loadGroups(groups);
if (isFirstLoad && groups.some(g => g.id === directLinkedGroup)) {
store.focusGroup(directLinkedGroup); if (
isFirstLoad &&
groups.some(g => g.id === directLinkedAnnotationGroupId)
) {
store.focusGroup(directLinkedAnnotationGroupId);
} else if (
isFirstLoad &&
groups.some(g => g.id === directLinkedGroupId)
) {
store.focusGroup(directLinkedGroupId);
} else if (isFirstLoad && groups.some(g => g.id === prevFocusedGroup)) { } else if (isFirstLoad && groups.some(g => g.id === prevFocusedGroup)) {
store.focusGroup(prevFocusedGroup); store.focusGroup(prevFocusedGroup);
} }
......
...@@ -117,7 +117,7 @@ describe('groups', function() { ...@@ -117,7 +117,7 @@ describe('groups', function() {
member: { member: {
delete: sinon.stub().returns(Promise.resolve()), delete: sinon.stub().returns(Promise.resolve()),
}, },
read: sinon.stub().returns(Promise.resolve()), read: sinon.stub().returns(Promise.resolve(new Error('404 Error'))),
}, },
groups: { groups: {
list: sinon.stub().returns(dummyGroups), list: sinon.stub().returns(dummyGroups),
...@@ -198,14 +198,16 @@ describe('groups', function() { ...@@ -198,14 +198,16 @@ describe('groups', function() {
}); });
}); });
it('catches 404 error from api.group.read request', () => { it('catches error from api.group.read request', () => {
const svc = service(); const svc = service();
fakeLocalStorage.getItem.returns(dummyGroups[0].id); fakeLocalStorage.getItem.returns(dummyGroups[0].id);
fakeSettings.group = 'does-not-exist'; fakeSettings.group = 'does-not-exist';
fakeApi.group.read.returns( fakeApi.group.read.returns(
Promise.reject( Promise.reject(
"404 Not Found: Either the resource you requested doesn't exist, \ new Error(
"404 Not Found: Either the resource you requested doesn't exist, \
or you are not currently authorized to see it." or you are not currently authorized to see it."
)
) )
); );
return svc.load().then(() => { return svc.load().then(() => {
...@@ -327,6 +329,39 @@ describe('groups', function() { ...@@ -327,6 +329,39 @@ describe('groups', function() {
}); });
}); });
it("sets the direct-linked annotation's group to take precedence over the group saved in local storage and the direct-linked group", () => {
const svc = service();
fakeSettings.annotations = 'ann-id';
fakeSettings.group = dummyGroups[1].id;
fakeLocalStorage.getItem.returns(dummyGroups[0].id);
fakeApi.groups.list.returns(Promise.resolve(dummyGroups));
fakeApi.annotation.get.returns(
Promise.resolve({
id: 'ann-id',
group: dummyGroups[2].id,
})
);
return svc.load().then(() => {
assert.calledWith(fakeStore.focusGroup, dummyGroups[2].id);
});
});
it("sets the focused group to the direct-linked annotation's group", () => {
const svc = service();
fakeSettings.annotations = 'ann-id';
fakeApi.groups.list.returns(Promise.resolve(dummyGroups));
fakeLocalStorage.getItem.returns(dummyGroups[0].id);
fakeApi.annotation.get.returns(
Promise.resolve({
id: 'ann-id',
group: dummyGroups[1].id,
})
);
return svc.load().then(() => {
assert.calledWith(fakeStore.focusGroup, dummyGroups[1].id);
});
});
it('sets the direct-linked group to take precedence over the group saved in local storage', () => { it('sets the direct-linked group to take precedence over the group saved in local storage', () => {
const svc = service(); const svc = service();
fakeSettings.group = dummyGroups[1].id; fakeSettings.group = dummyGroups[1].id;
...@@ -410,6 +445,96 @@ describe('groups', function() { ...@@ -410,6 +445,96 @@ describe('groups', function() {
}); });
}); });
it('catches error when fetching the direct-linked annotation', () => {
const svc = service();
fakeSettings.annotations = 'ann-id';
fakeApi.profile.groups.read.returns(Promise.resolve([]));
fakeApi.groups.list.returns(
Promise.resolve([{ name: 'BioPub', id: 'biopub' }])
);
fakeApi.annotation.get.returns(
Promise.reject(
new Error(
"404 Not Found: Either the resource you requested doesn't exist, \
or you are not currently authorized to see it."
)
)
);
return svc.load().then(groups => {
const groupIds = groups.map(g => g.id);
assert.deepEqual(groupIds, ['biopub']);
});
});
it("catches error when fetching the direct-linked annotation's group", () => {
const svc = service();
fakeSettings.annotations = 'ann-id';
fakeApi.profile.groups.read.returns(Promise.resolve([]));
fakeApi.groups.list.returns(
Promise.resolve([
{ name: 'BioPub', id: 'biopub' },
{ name: 'Public', id: '__world__' },
])
);
fakeApi.group.read.returns(
Promise.reject(
new Error(
"404 Not Found: Either the resource you requested doesn't exist, \
or you are not currently authorized to see it."
)
)
);
fakeApi.annotation.get.returns(
Promise.resolve({
id: 'ann-id',
group: 'out-of-scope',
})
);
// The user is logged out.
fakeAuth.tokenGetter.returns(null);
return svc.load().then(groups => {
const groupIds = groups.map(g => g.id);
assert.deepEqual(groupIds, ['biopub']);
});
});
it("includes the direct-linked annotation's group when it is not in the normal list of groups", () => {
const svc = service();
fakeSettings.annotations = 'ann-id';
fakeApi.profile.groups.read.returns(Promise.resolve([]));
fakeApi.groups.list.returns(
Promise.resolve([
{ name: 'BioPub', id: 'biopub' },
{ name: 'Public', id: '__world__' },
])
);
fakeApi.group.read.returns(
Promise.resolve({ name: 'Restricted', id: 'out-of-scope' })
);
fakeApi.annotation.get.returns(
Promise.resolve({
id: 'ann-id',
group: 'out-of-scope',
})
);
return svc.load().then(groups => {
const directLinkedAnnGroupShown = groups.some(
g => g.id === 'out-of-scope'
);
assert.isTrue(directLinkedAnnGroupShown);
});
});
it('both groups are in the final groups list when an annotation and a group are linked to', () => { it('both groups are in the final groups list when an annotation and a group are linked to', () => {
// This can happen if the linked to annotation and group are configured by // This can happen if the linked to annotation and group are configured by
// the frame embedding the client. // the frame embedding the client.
......
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