6.8 KiB · text History dc2edab
defmodule DiogramosWeb.CanvasChannelTest do
  use DiogramosWeb.ChannelCase, async: true

  import Diogramos.AccountsFixtures
  import Diogramos.DiagramsFixtures
  import Diogramos.DataCase, only: [allow_authority: 1]

  alias Diogramos.Accounts
  alias Diogramos.Accounts.Scope
  alias Diogramos.Diagrams
  alias DiogramosWeb.UserSocket

  @rect %{
    "type" => "insert_element",
    "element" => %{"id" => "e1", "type" => "rect", "x" => 0, "y" => 0, "w" => 10, "h" => 10}
  }

  defp connect_as(user) do
    token = Accounts.create_api_token(user)

    {:ok, socket} =
      Phoenix.ChannelTest.connect(UserSocket, %{"token" => token}, connect_info: %{})

    socket
  end

  setup do
    user = user_fixture()
    scope = Scope.for_user(user)
    canvas = canvas_fixture(scope)
    allow_authority(canvas.id)

    %{user: user, scope: scope, canvas: canvas}
  end

  describe "socket connect" do
    test "accepts a valid api token", %{user: user} do
      token = Accounts.create_api_token(user)
      assert {:ok, socket} = connect(UserSocket, %{"token" => token}, connect_info: %{})
      assert socket.assigns.user_id == user.id
      assert UserSocket.id(socket) == "user_socket:#{user.id}"
    end

    test "rejects a bogus or missing token" do
      assert :error = connect(UserSocket, %{"token" => "nope"}, connect_info: %{})
      assert :error = connect(UserSocket, %{}, connect_info: %{})
    end

    test "rejects a revoked token", %{user: user} do
      token = Accounts.create_api_token(user)
      {_user, token_id} = Accounts.fetch_user_by_api_token(token)
      :ok = Accounts.delete_api_token(user, token_id)

      assert :error = connect(UserSocket, %{"token" => token}, connect_info: %{})
    end
  end

  describe "join" do
    test "replies with the snapshot", %{user: user, canvas: canvas} do
      socket = connect_as(user)

      assert {:ok, reply, _socket} = subscribe_and_join(socket, "canvas:#{canvas.id}", %{})
      assert reply.version == 0
      assert reply.role == "owner"
      assert reply.document == %{"elements" => %{}, "order" => [], "connectors" => %{}}
      assert reply.canvas.slug == canvas.slug
      assert is_binary(reply.self_key)
    end

    test "a viewer joins and sees their role", %{canvas: canvas} do
      viewer = user_fixture()
      {:ok, _} = Diagrams.grant_permission("canvas", canvas.id, "user", viewer.id, "viewer")

      assert {:ok, reply, _socket} =
               subscribe_and_join(connect_as(viewer), "canvas:#{canvas.id}", %{})

      assert reply.role == "viewer"
    end

    test "refuses a canvas the user cannot read", %{canvas: canvas} do
      stranger = user_fixture()

      assert {:error, %{code: "not_found"}} =
               subscribe_and_join(connect_as(stranger), "canvas:#{canvas.id}", %{})
    end

    test "refuses a missing canvas and a malformed topic", %{user: user} do
      socket = connect_as(user)

      assert {:error, %{code: "not_found"}} = subscribe_and_join(socket, "canvas:999999", %{})
      assert {:error, %{code: "not_found"}} = subscribe_and_join(socket, "canvas:abc", %{})
    end
  end

  describe "ops" do
    setup %{user: user, canvas: canvas} do
      {:ok, _reply, socket} = subscribe_and_join(connect_as(user), "canvas:#{canvas.id}", %{})
      %{socket: socket}
    end

    test "applies an op and replies with the new version", %{socket: socket} do
      ref = push(socket, "op", %{"op" => @rect, "actor_ref" => "abc"})
      assert_reply ref, :ok, %{version: 1}
    end

    test "echoes the op back with the actor_ref so the sender can drop it", %{socket: socket} do
      push(socket, "op", %{"op" => @rect, "actor_ref" => "abc"})

      assert_push "op", %{op: op, version: 1, actor_ref: "abc"}
      assert op["type"] == "insert_element"
    end

    test "pushes a peer's op to everyone joined", %{canvas: canvas, socket: socket} do
      editor = user_fixture()
      {:ok, _} = Diagrams.grant_permission("canvas", canvas.id, "user", editor.id, "editor")

      {:ok, _reply, other} = subscribe_and_join(connect_as(editor), "canvas:#{canvas.id}", %{})

      ref = push(other, "op", %{"op" => @rect, "actor_ref" => "theirs"})
      assert_reply ref, :ok, %{version: 1}

      # Arrives on the first socket, tagged with the *other* actor_ref.
      assert_push "op", %{version: 1, actor_ref: "theirs"}
      refute socket == other
    end

    test "rejects an invalid op without advancing the version", %{socket: socket} do
      bad = %{"type" => "insert_element", "element" => %{"id" => "x", "type" => "rect"}}

      ref = push(socket, "op", %{"op" => bad, "actor_ref" => "a"})
      assert_reply ref, :error, %{code: "invalid_element_geometry"}

      ref = push(socket, "op", %{"op" => @rect, "actor_ref" => "b"})
      assert_reply ref, :ok, %{version: 1}
    end

    test "rejects a malformed payload", %{socket: socket} do
      ref = push(socket, "op", %{"actor_ref" => "a"})
      assert_reply ref, :error, %{code: "invalid_op"}
    end

    test "a viewer's op is refused", %{canvas: canvas} do
      viewer = user_fixture()
      {:ok, _} = Diagrams.grant_permission("canvas", canvas.id, "user", viewer.id, "viewer")

      {:ok, _reply, socket} = subscribe_and_join(connect_as(viewer), "canvas:#{canvas.id}", %{})

      ref = push(socket, "op", %{"op" => @rect, "actor_ref" => "a"})
      assert_reply ref, :error, %{code: "forbidden"}
    end

    test "versions advance monotonically across ops", %{socket: socket} do
      for {id, expected} <- Enum.with_index(~w(a b c), 1) do
        element = %{"id" => id, "type" => "circle", "cx" => 1, "cy" => 1, "r" => 5}
        ref = push(socket, "op", %{"op" => %{"type" => "insert_element", "element" => element}})
        assert_reply ref, :ok, %{version: ^expected}
      end
    end
  end

  describe "presence" do
    test "a joining peer is announced to the others", %{user: user, canvas: canvas} do
      {:ok, _reply, _socket} = subscribe_and_join(connect_as(user), "canvas:#{canvas.id}", %{})

      # Own presence is filtered out of the peer list.
      assert_push "peers", %{peers: []}

      editor = user_fixture()
      {:ok, _} = Diagrams.grant_permission("canvas", canvas.id, "user", editor.id, "editor")
      {:ok, _reply, _other} = subscribe_and_join(connect_as(editor), "canvas:#{canvas.id}", %{})

      assert_push "peers", %{peers: [%{source: "android"}]}, 1000
    end

    test "a cursor update reaches the other peer", %{user: user, canvas: canvas} do
      {:ok, _reply, socket} = subscribe_and_join(connect_as(user), "canvas:#{canvas.id}", %{})

      editor = user_fixture()
      {:ok, _} = Diagrams.grant_permission("canvas", canvas.id, "user", editor.id, "editor")
      {:ok, _reply, other} = subscribe_and_join(connect_as(editor), "canvas:#{canvas.id}", %{})

      push(other, "cursor", %{"x" => 12, "y" => 34, "lock" => "e1"})

      assert_push "peers", %{peers: [%{cursor: %{x: 12, y: 34}, lock: "e1"}]}, 1000
      refute socket == other
    end
  end
end