From 77518bab090c1c46846ccc2e194072bbbc0f0b9e Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Mon, 31 Aug 2026 20:44:14 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=85(e2e)=20cover=20the=20disconnection=20?= =?UTF-8?q?of=20a=20client=20whose=20access=20changed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check was dropped during the yhub migration, on the grounds that `reset_connections` was a no-op. It is not one any more: the backend calls yhub's reset-connections route, which re-runs the authorization of each connection and closes the sockets whose access is no longer the one they were opened with, with the code 4401. The old check cannot come back as it was. It had the author of the document change its visibility and waited for the author's own socket to close, which is what the collaboration server used to do - close every connection to the document, whatever it was that changed. yhub leaves alone the connections that are unaffected, and an owner's access is the same before and after, so the author's socket is now deliberately kept. The client to watch is therefore another one: the anonymous reader holding the public link, while the author switches that link from reading to editing. Their socket goes down and the client comes back on its own. That the new connection is live is asserted by what flows through it rather than by the socket being open - the author writes after the change and the text reaches the reader, with nothing reloaded in between. Signed-off-by: Kevin Jahns --- .../app-impress/doc-collaboration.spec.ts | 88 ++++++++++++++++++- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts index 816871ed0..b1fbaa032 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts @@ -37,11 +37,91 @@ test.describe('Doc Collaboration', () => { const framesent = await framesentPromise; expect(framesent.payload).not.toBeNull(); + }); - // TODO(yhub): re-add the close/reconnect check (the backend closed the - // connection when the doc visibility changed) once yhub exposes a kick - // API - `reset_connections` is currently a no-op so the server never - // closes the connection. + /** + * A change to the sharing settings has to reach the clients that are already connected. + * The backend asks the collaboration server to re-check them, and yhub closes the sockets + * whose access is no longer the one they connected with - code 4401 - and leaves the + * others open. The client then reconnects and resyncs at its new level. + * + * The author is not the one to watch: their access is the same before and after, and their + * socket is deliberately kept. This is the difference with what the collaboration server + * used to do, which was to close every connection to the document indiscriminately. What + * moves here is the anonymous reader holding the public link, whose access goes from read + * to read/write when the link is switched to Editing. + */ + test('closes the connection of a user whose access changed', async ({ + page, + browserName, + }) => { + const [docTitle] = await createDoc( + page, + 'doc-reset-connections', + browserName, + 1, + ); + await verifyDocName(page, docTitle); + + await page.getByRole('button', { name: 'Share' }).click(); + await updateShareLink(page, 'Public', 'Reading'); + await page.getByRole('button', { name: 'close' }).first().click(); + + const { otherPage, cleanup } = await connectOtherUserToDoc({ + browserName, + docUrl: page.url(), + withoutSignIn: true, + docTitle, + }); + + // the reader is on the document, at the level the link grants today + await expect(otherPage.locator('.ProseMirror')).toHaveAttribute( + 'contenteditable', + 'false', + ); + + // The socket the reader holds was opened by `connectOtherUserToDoc`, before this test had + // a page to listen on, so it is unreachable. Reloading opens another one under a listener. + const readerSocketPromise = otherPage.waitForEvent( + 'websocket', + (webSocket) => + webSocket.url().includes(`${process.env.COLLABORATION_WS_URL}/`), + ); + await otherPage.reload(); + const readerSocket = await readerSocketPromise; + await readerSocket.waitForEvent('framesent'); + + const readerSocketClosed = readerSocket.waitForEvent('close'); + const readerReconnected = otherPage.waitForEvent('websocket', (webSocket) => + webSocket.url().includes(`${process.env.COLLABORATION_WS_URL}/`), + ); + + // the author opens the link to editing + await page.getByRole('button', { name: 'Share' }).click(); + await page.getByTestId('doc-access-mode').click(); + await page.getByRole('menuitemradio', { name: 'Editing' }).click(); + await expect( + page.getByText('The document visibility has been updated').first(), + ).toBeVisible(); + + // the re-check reaches the reader and their socket goes down + await readerSocketClosed; + expect(readerSocket.isClosed()).toBeTruthy(); + + // and the client comes back on its own. What proves the new connection is live is not the + // socket being open - a socket that is refused is open for a moment too - but the document + // still flowing through it: nothing is reloaded here, so the only way the text below can + // reach the reader is the connection opened after the revocation. + await readerReconnected; + + await page.getByRole('button', { name: 'close' }).first().click(); + await writeInEditor({ page, text: 'Hello after the reset' }); + + await expect(otherPage.getByText('Hello after the reset')).toBeVisible({ + timeout: 15000, + }); + + await cleanup(); }); test('it cannot edit if viewer but see and can get resources', async ({