1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Server: Fixed issue when a notebook is shared, then unshared, then shared again

This commit is contained in:
Laurent Cozic 2021-10-14 15:33:30 +01:00
parent 9bff2d1ef4
commit 47fc51ea8a
3 changed files with 59 additions and 2 deletions

View File

@ -230,6 +230,7 @@ export default class ChangeModel extends BaseModel<Change> {
// create - delete => NOOP
// update - update => update
// update - delete => delete
// delete - create => create
//
// There's one exception for changes that include a "previous_item". This is
// used to save specific properties about the previous state of the item,
@ -237,6 +238,13 @@ export default class ChangeModel extends BaseModel<Change> {
// to know if an item has been moved from one folder to another. In that
// case, we need to know about each individual change, so they are not
// compressed.
//
// The latest change, when an item goes from DELETE to CREATE seems odd but
// can happen because we are not checking for "item" changes but for
// "user_item" changes. When sharing is involved, an item can be shared
// (CREATED), then unshared (DELETED), then shared again (CREATED). When it
// happens, we want the user to get the item, thus we generate a CREATE
// event.
private compressChanges(changes: Change[]): Change[] {
const itemChanges: Record<Uuid, Change> = {};
@ -268,6 +276,10 @@ export default class ChangeModel extends BaseModel<Change> {
if (previous.type === ChangeType.Update && change.type === ChangeType.Delete) {
itemChanges[itemId] = change;
}
if (previous.type === ChangeType.Delete && change.type === ChangeType.Create) {
itemChanges[itemId] = change;
}
} else {
itemChanges[itemId] = change;
}

View File

@ -2,7 +2,7 @@ import { ChangeType, Share, ShareType, ShareUser, ShareUserStatus } from '../../
import { beforeAllDb, afterAllTests, beforeEachDb, createUserAndSession, models, createNote, createFolder, updateItem, createItemTree, makeNoteSerializedBody, updateNote, expectHttpError, createResource } from '../../utils/testing/testUtils';
import { postApi, patchApi, getApi, deleteApi } from '../../utils/testing/apiUtils';
import { PaginatedDeltaChanges } from '../../models/ChangeModel';
import { shareFolderWithUser } from '../../utils/testing/shareApiUtils';
import { inviteUserToShare, shareFolderWithUser } from '../../utils/testing/shareApiUtils';
import { msleep } from '../../utils/time';
import { ErrorForbidden } from '../../utils/errors';
import { resourceBlobPath, serializeJoplinItem, unserializeJoplinItem } from '../../utils/joplinUtils';
@ -860,4 +860,45 @@ describe('shares.folder', function() {
);
});
test('should allow sharing, unsharing and sharing again', async function() {
// - U1 share a folder that contains a note
// - U2 accept
// - U2 syncs
// - U1 remove U2
// - U1 adds back U2
// - U2 accept
//
// => Previously, the notebook would be deleted fro U2 due to a quirk in
// delta sync, that doesn't handle user_items being deleted, then
// created again. Instead U2 should end up with both the folder and the
// note.
//
// Ref: https://discourse.joplinapp.org/t/20977
const { session: session1 } = await createUserAndSession(1);
const { user: user2, session: session2 } = await createUserAndSession(2);
const { shareUser: shareUserA, share } = await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
'000000000000000000000000000000F1': {
'00000000000000000000000000000001': null,
},
});
await models().share().updateSharedItems3();
await deleteApi(session1.id, `share_users/${shareUserA.id}`);
await models().share().updateSharedItems3();
await inviteUserToShare(share, session1.id, user2.email, true);
await models().share().updateSharedItems3();
const page = await getApi<PaginatedDeltaChanges>(session2.id, 'items/root/delta', { query: { cursor: '' } });
expect(page.items.length).toBe(2);
expect(page.items.find(it => it.item_name === '000000000000000000000000000000F1.md').type).toBe(ChangeType.Create);
expect(page.items.find(it => it.item_name === '00000000000000000000000000000001.md').type).toBe(ChangeType.Create);
});
});

View File

@ -150,13 +150,17 @@ export async function shareWithUserAndAccept(sharerSessionId: string, shareeSess
shareUser = await models().shareUser().load(shareUser.id);
await patchApi(shareeSessionId, `share_users/${shareUser.id}`, { status: ShareUserStatus.Accepted });
await respondInvitation(shareeSessionId, shareUser.id, ShareUserStatus.Accepted);
await models().share().updateSharedItems3();
return { share, item, shareUser };
}
export async function respondInvitation(recipientSessionId: Uuid, shareUserId: Uuid, status: ShareUserStatus) {
await patchApi(recipientSessionId, `share_users/${shareUserId}`, { status });
}
export async function postShareContext(sessionId: string, shareType: ShareType, itemId: Uuid): Promise<AppContext> {
const context = await koaAppContext({
sessionId: sessionId,