diff --git a/src/components/App.tsx b/src/components/App.tsx index 25d25a36..e927c7b1 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -654,8 +654,10 @@ class App extends React.Component { if (isCollaborationScene) { // when joining a room we don't want user's local scene data to be merged - // into the remote scene, so set `clearScene` - this.initializeSocketClient({ showLoadingState: true, clearScene: true }); + // into the remote scene + this.resetScene(); + + this.initializeSocketClient({ showLoadingState: true }); } else if (scene) { if (scene.appState) { scene.appState = { @@ -1256,6 +1258,14 @@ class App extends React.Component { "Excalidraw", await generateCollaborationLink(), ); + // remove deleted elements from elements array & history to ensure we don't + // expose potentially sensitive user data in case user manually deletes + // existing elements (or clears scene), which would otherwise be persisted + // to database even if deleted before creating the room. + history.clear(); + history.resumeRecording(); + this.scene.replaceAllElements(this.scene.getElements()); + this.initializeSocketClient({ showLoadingState: false }); }; @@ -1365,14 +1375,11 @@ class App extends React.Component { private initializeSocketClient = async (opts: { showLoadingState: boolean; - clearScene?: boolean; }) => { if (this.portal.socket) { return; } - if (opts.clearScene) { - this.resetScene(); - } + const roomMatch = getCollaborationLinkData(window.location.href); if (roomMatch) { const roomID = roomMatch[1]; diff --git a/src/tests/collab.test.tsx b/src/tests/collab.test.tsx new file mode 100644 index 00000000..ea952b55 --- /dev/null +++ b/src/tests/collab.test.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { render, waitFor } from "./test-utils"; +import App from "../components/App"; +import { API } from "./helpers/api"; +import { createUndoAction } from "../actions/actionHistory"; + +const { h } = window; + +Object.defineProperty(window, "crypto", { + value: { + getRandomValues: (arr: number[]) => + arr.forEach((v, i) => (arr[i] = Math.floor(Math.random() * 256))), + subtle: { + generateKey: () => {}, + exportKey: () => ({ k: "sTdLvMC_M3V8_vGa3UVRDg" }), + }, + }, +}); + +jest.mock("../data/firebase.ts", () => { + const loadFromFirebase = async () => null; + const saveToFirebase = () => {}; + const isSavedToFirebase = () => true; + + return { + loadFromFirebase, + saveToFirebase, + isSavedToFirebase, + }; +}); + +describe("collaboration", () => { + it("creating room should reset deleted elements", async () => { + render( + , + ); + + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A" }), + expect.objectContaining({ id: "B", isDeleted: true }), + ]); + expect(API.getStateHistory().length).toBe(1); + }); + + h.app.openPortal(); + await waitFor(() => { + expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]); + expect(API.getStateHistory().length).toBe(1); + }); + + const undoAction = createUndoAction(h.history); + // noop + h.app.actionManager.executeAction(undoAction); + await waitFor(() => { + expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]); + expect(API.getStateHistory().length).toBe(1); + }); + }); +});