diff options
-rw-r--r-- | lib/polyjuice/client/sync.ex | 9 | ||||
-rw-r--r-- | test/polyjuice/client/sync_test.exs | 313 | ||||
-rw-r--r-- | test/support/test_util.ex | 40 |
3 files changed, 360 insertions, 2 deletions
diff --git a/lib/polyjuice/client/sync.ex b/lib/polyjuice/client/sync.ex index 67b83fd..416c336 100644 --- a/lib/polyjuice/client/sync.ex +++ b/lib/polyjuice/client/sync.ex @@ -1,4 +1,4 @@ -# Copyright 2019 Hubert Chathi <hubert@uhoreg.ca> +# Copyright 2019-2020 Hubert Chathi <hubert@uhoreg.ca> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -58,6 +58,11 @@ defmodule Polyjuice.Client.Sync do listener, opts ) do + # Make sure the URL ends with a slash, so that URI.merge doesn't clobber + # the last path component. (URI.merge is smart enough to drop the double + # "/" if it already ends with a slash.) + homeserver_url = homeserver_url <> "/" + # Figure out how to handle the filter (if any): can we pass it in straight # to the query, or do we need to get its ID. And if we get its ID, do we # already have it, or do we need to send it to the server? @@ -301,7 +306,7 @@ defmodule Polyjuice.Client.Sync do timeline = Map.get(room, "timeline", %{}) if Map.get(timeline, "limited", false) do - with {:ok, prev_batch} <- Map.get(timeline, "prev_batch") do + with {:ok, prev_batch} <- Map.fetch(timeline, "prev_batch") do state.send.({:limited, roomname, prev_batch}) end end diff --git a/test/polyjuice/client/sync_test.exs b/test/polyjuice/client/sync_test.exs new file mode 100644 index 0000000..d301ccc --- /dev/null +++ b/test/polyjuice/client/sync_test.exs @@ -0,0 +1,313 @@ +# Copyright 2020 Hubert Chathi <hubert@uhoreg.ca> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defmodule Polyjuice.Client.SyncTest do + use ExUnit.Case + + defmodule Httpd do + def _matrix(session_id, env, input) do + # FIXME: check authorization header + # FIXME: check method + [path | _] = + Keyword.get(env, :path_info) + |> to_string() + |> String.split("?", parts: 2) + + case path do + "client/r0/user/%40alice%3Aexample.org/filter" -> + handle_filter(session_id, env, input) + + "client/r0/sync" -> + handle_sync(session_id, env, input) + + _ -> + :mod_esi.deliver( + session_id, + 'Status: 404 Not Found\r\nContent-Type: application/json\r\n\r\n{"errcode":"M_NOT_FOUND","error":"Not found"}' + ) + end + end + + defp handle_filter(session_id, _env, _input) do + :mod_esi.deliver( + session_id, + 'Content-Type: application/json\r\n\r\n{"filter_id":"1"}' + ) + end + + defp handle_sync(session_id, env, _input) do + sync = + Keyword.get(env, :path_info) + |> to_string() + |> String.split("?", parts: 2) + |> Enum.at(1) + |> String.split("&") + |> Enum.find("since=", &String.starts_with?(&1, "since=")) + |> String.replace_prefix("since=", "") + + response = + case sync do + "" -> + %{ + "next_batch" => "1", + "presence" => %{}, + "account_data" => %{}, + "rooms" => %{ + "invite" => %{ + "!room_id" => %{ + "invite_state" => %{ + "events" => [ + %{ + "content" => %{ + "membership" => "invite" + }, + "type" => "m.room.member", + "state_key" => "@alice:example.org", + "event_id" => "$invite_event", + "room_id" => "!room_id", + "sender" => "@bob:example.org" + } + ] + } + } + } + } + } + + "1" -> + %{ + "next_batch" => "2", + "presence" => %{}, + "account_data" => %{}, + "rooms" => %{ + "join" => %{ + "!room_id" => %{ + "state" => %{ + "events" => [ + %{ + "content" => %{ + "membership" => "join" + }, + "type" => "m.room.member", + "state_key" => "@alice:example.org", + "event_id" => "$join_event", + "room_id" => "!room_id", + "sender" => "@alice:example.org" + } + ] + }, + "timeline" => %{ + "limited" => true, + "prev_batch" => "p1", + "events" => [] + } + } + } + } + } + + "2" -> + %{ + "next_batch" => "3", + "presence" => %{}, + "account_data" => %{}, + "rooms" => %{ + "join" => %{ + "!room_id" => %{ + "timeline" => %{ + "limited" => false, + "prev_batch" => "p2", + "events" => [ + %{ + "content" => %{ + "msgtype" => "m.text", + "body" => "Hello World!" + }, + "type" => "m.room.message", + "event_id" => "$message_event", + "room_id" => "!room_id", + "sender" => "@alice:example.org" + } + ] + } + } + } + } + } + + "3" -> + %{ + "next_batch" => "4", + "presence" => %{}, + "account_data" => %{}, + "rooms" => %{ + "leave" => %{ + "!room_id" => %{ + "timeline" => %{ + "limited" => false, + "prev_batch" => "p2", + "events" => [ + %{ + "content" => %{ + "membership" => "leave", + "reason" => "Goodbye Cruel World!" + }, + "type" => "m.room.member", + "state_key" => "@alice:example.org", + "event_id" => "$leave_event", + "room_id" => "!room_id", + "sender" => "@alice:example.org" + } + ] + } + } + } + } + } + + _ -> + nil + end + + if response do + :mod_esi.deliver( + session_id, + 'Content-Type: application/json\r\n\r\n#{Jason.encode!(response)}' + ) + end + end + end + + test "sync" do + {:ok, tmpdir} = TestUtil.mktmpdir("sync-") + + storage = Polyjuice.Client.Storage.Ets.open() + + try do + tmpdir_charlist = to_charlist(tmpdir) + + :inets.start() + + {:ok, httpd_pid} = + :inets.start( + :httpd, + port: 0, + server_name: 'sync.test', + server_root: tmpdir_charlist, + document_root: tmpdir_charlist, + bind_address: {127, 0, 0, 1}, + modules: [:mod_esi], + erl_script_alias: {'', [Polyjuice.Client.SyncTest.Httpd]} + ) + + port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port) + + client = %Polyjuice.Client{ + base_url: "http://127.0.0.1:#{port}/Elixir.Polyjuice.Client.SyncTest.Httpd", + access_token: "an_access_token", + user_id: "@alice:example.org", + storage: storage + } + + {:ok, sync_pid} = + Polyjuice.Client.API.sync_child_spec(client, self()) + |> (fn %{start: {module, func, args}} -> apply(module, func, args) end).() + + assert_receive({:connected}) + + assert_receive( + {:invite, "!room_id", "@bob:example.org", + %{ + "m.room.member" => %{ + "@alice:example.org" => %{ + "content" => %{ + "membership" => "invite" + }, + "type" => "m.room.member", + "state_key" => "@alice:example.org", + "event_id" => "$invite_event", + "room_id" => "!room_id", + "sender" => "@bob:example.org" + } + } + }}, + 1000 + ) + + assert_receive({:initial_sync_completed}) + + assert_receive( + {:limited, "!room_id", "p1"}, + 1000 + ) + + assert_receive( + {:state, "!room_id", + %{ + "content" => %{ + "membership" => "join" + }, + "type" => "m.room.member", + "state_key" => "@alice:example.org", + "event_id" => "$join_event", + "room_id" => "!room_id", + "sender" => "@alice:example.org" + }}, + 1000 + ) + + assert_receive( + {:message, "!room_id", + %{ + "content" => %{ + "msgtype" => "m.text", + "body" => "Hello World!" + }, + "type" => "m.room.message", + "event_id" => "$message_event", + "room_id" => "!room_id", + "sender" => "@alice:example.org" + }}, + 1000 + ) + + assert_receive( + {:state, "!room_id", + %{ + "content" => %{ + "membership" => "leave", + "reason" => "Goodbye Cruel World!" + }, + "type" => "m.room.member", + "state_key" => "@alice:example.org", + "event_id" => "$leave_event", + "room_id" => "!room_id", + "sender" => "@alice:example.org" + }}, + 1000 + ) + + assert_receive( + {:left, "!room_id"}, + 1000 + ) + + Process.unlink(sync_pid) + Process.exit(sync_pid, :kill) + after + Polyjuice.Client.Storage.close(storage) + File.rm_rf(tmpdir) + end + end +end diff --git a/test/support/test_util.ex b/test/support/test_util.ex new file mode 100644 index 0000000..2724678 --- /dev/null +++ b/test/support/test_util.ex @@ -0,0 +1,40 @@ +# Copyright 2020 Hubert Chathi <hubert@uhoreg.ca> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defmodule TestUtil do + def mktmpdir(prefix) do + Enum.find_value( + 1..5, + {:error, :efail}, + fn _ -> + random = + Enum.map( + Range.new(0, 12), + fn _ -> Enum.random('abcdefghijklmnopqrstuvwxyz1234567890') end + ) + |> to_string() + + tmpdir = Path.join(System.tmp_dir!(), prefix <> random) + + case File.mkdir(tmpdir) do + :ok -> {:ok, tmpdir} + # try again if the name is already taken + {:error, :eexist} -> nil + # pass through any other errors + {:error, _} = err -> err + end + end + ) + end +end |