# 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.ClientTest do
use ExUnit.Case
defmodule Httpd do
# for testing basic calls
def foo(session_id, _env, {_, input}) do
if input == 'foobar' do
:mod_esi.deliver(
session_id,
'Content-Type: application/json\r\n\r\n{"foo":"bar"}'
)
else
:mod_esi.deliver(
session_id,
'Status: 400 Bad Request\r\nContent-Type: application/json\r\n\r\n{"errcode":"M_UNKNOWN","error":"Wrong contents"}'
)
end
end
def _matrix(session_id, env, input) do
[path | _] =
Keyword.get(env, :path_info)
|> to_string()
|> String.split("?", parts: 2)
case path do
"client/r0/login" ->
handle_login(session_id, env, input)
"client/r0/logout" ->
handle_logout(session_id, env, input)
"client/r0/register" ->
handle_register(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_login(session_id, _env, {_, input}) do
identifier_type =
to_string(input) |> Jason.decode!() |> Map.get("identifier") |> Map.get("type")
:mod_esi.deliver(
session_id,
'Content-Type: application/json\r\n\r\n{"user_id":"@alice:example.org","access_token":"#{
identifier_type
}_login","device_id":"foo"}'
)
end
defp handle_logout(session_id, _env, _input) do
:mod_esi.deliver(
session_id,
'Content-Type: application/json\r\n\r\n{}'
)
end
defp handle_register(session_id, _env, {_, input}) do
inhibit_login = to_string(input) |> Jason.decode!() |> Map.get("inhibit_login")
username = to_string(input) |> Jason.decode!() |> Map.get("username")
response =
if inhibit_login == "true" do
'Content-Type: application/json\r\n\r\n{"user_id":"@#{username}:example.org"}'
else
'Content-Type: application/json\r\n\r\n{"user_id":"@#{username}:example.org","access_token":"m.id.user_login","device_id":"foo"}'
end
:mod_esi.deliver(
session_id,
response
)
end
end
defmodule Httpd.LoggedOut do
# just tells the user that they were logged out, no matter what
def _matrix(session_id, _env, _input) do
:mod_esi.deliver(
session_id,
'Status: 401 Unauthorized\r\nContent-Type: application/json\r\n\r\n{"errcode":"M_UNKNOWN_TOKEN","error":"Unknown token"}'
)
end
end
defmodule Httpd.UserAlreadyTaken do
# just tells the user that the user_id is already taken, no matter what
def _matrix(session_id, _env, _input) do
:mod_esi.deliver(
session_id,
'Status: 400 Unauthorized\r\nContent-Type: application/json\r\n\r\n{"errcode":"M_USER_IN_USE","error":"User ID already taken."}'
)
end
end
test "transaction_id is unique" do
client = %Polyjuice.Client{
base_url: "http://localhost:8008",
id: nil
}
# the best that we can do is test that two calls to transaction_id return
# different values
assert Polyjuice.Client.API.transaction_id(client) !=
Polyjuice.Client.API.transaction_id(client)
end
test "sync" do
with client = %DummyClient{
response: {
%Polyjuice.Client.Endpoint.GetSync{},
{:ok, %{}}
}
} do
{:ok, %{}} = Polyjuice.Client.sync(client)
end
with client = %DummyClient{
response: {
%Polyjuice.Client.Endpoint.GetSync{
filter: %{},
since: "token",
full_state: true,
set_presence: :offline,
timeout: 120
},
{:ok, %{}}
}
} do
{:ok, %{}} =
Polyjuice.Client.sync(
client,
filter: %{},
since: "token",
full_state: true,
set_presence: :offline,
timeout: 120
)
end
end
test "call" do
{:ok, tmpdir} = TestUtil.mktmpdir("client-call-")
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.ClientTest.Httpd]}
)
port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
{:ok, client_pid} =
Polyjuice.Client.start_link(
"http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd/",
access_token: "an_access_token",
user_id: "@alice:example.org",
sync: false,
test: true
)
client = Polyjuice.Client.get_client(client_pid)
# binary body
assert Polyjuice.Client.API.call(
client,
%DummyEndpoint{
http_spec: %Polyjuice.Client.Endpoint.HttpSpec{
method: :put,
path: "foo",
headers: [],
body: "foobar",
auth_required: false
}
}
) == {:ok, %{"foo" => "bar"}}
# iolist body
assert Polyjuice.Client.API.call(
client,
%DummyEndpoint{
http_spec: %Polyjuice.Client.Endpoint.HttpSpec{
method: :put,
path: "foo",
headers: [],
body: [?f, ["oo"], ["b", [?a | "r"]]],
auth_required: false
}
}
) == {:ok, %{"foo" => "bar"}}
Polyjuice.Client.API.stop(client)
:inets.stop(:httpd, httpd_pid)
after
File.rm_rf(tmpdir)
end
end
test "login" do
{:ok, tmpdir} = TestUtil.mktmpdir("login-")
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.ClientTest.Httpd]}
)
port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
{:ok, client_pid} =
Polyjuice.Client.start_link(
"http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd/",
access_token: nil,
sync: false,
storage: storage,
handler: self(),
test: true
)
client = Polyjuice.Client.get_client(client_pid)
Polyjuice.Client.log_in_with_password(client, "@alice:example.org", "password")
assert GenServer.call(client.pid, :get_state) == %{
access_token: "m.id.user_login",
user_id: "@alice:example.org",
device_id: "foo"
}
assert_receive({:polyjuice_client, :logged_in, {"@alice:example.org", "foo", _}})
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "access_token") ==
"m.id.user_login"
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "user_id") ==
"@alice:example.org"
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "device_id") == "foo"
Polyjuice.Client.log_out(client)
assert_receive({:polyjuice_client, :logged_out, {false}})
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "access_token") ==
nil
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "user_id") == nil
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "device_id") == nil
assert GenServer.call(client.pid, :get_state) |> Map.get(:access_token) == nil
Polyjuice.Client.log_in_with_password(
client,
{:email, "user@example.com"},
"password"
)
assert_receive({:polyjuice_client, :logged_in, {"@alice:example.org", "foo", _}})
assert GenServer.call(client.pid, :get_state) == %{
access_token: "m.id.thirdparty_login",
user_id: "@alice:example.org",
device_id: "foo"
}
Polyjuice.Client.log_in_with_password(
client,
{:phone, "CA", "1234567890"},
"password"
)
assert_receive({:polyjuice_client, :logged_in, {"@alice:example.org", "foo", _}})
assert GenServer.call(client.pid, :get_state) == %{
access_token: "m.id.phone_login",
user_id: "@alice:example.org",
device_id: "foo"
}
Polyjuice.Client.log_in_with_password(
client,
%{
"type" => "ca.uhoreg.foo"
},
"password"
)
assert_receive({:polyjuice_client, :logged_in, {"@alice:example.org", "foo", _}})
assert GenServer.call(client.pid, :get_state) == %{
access_token: "ca.uhoreg.foo_login",
user_id: "@alice:example.org",
device_id: "foo"
}
Polyjuice.Client.API.stop(client)
:inets.stop(:httpd, httpd_pid)
after
Polyjuice.Client.Storage.close(storage)
File.rm_rf(tmpdir)
end
end
test "stores and retrieves from storage on creation" do
storage = Polyjuice.Client.Storage.Ets.open()
# if we start a client and specify the access token, user ID, and device
# ID, then those values get stored
{:ok, client_pid} =
Polyjuice.Client.start_link(
"http://127.0.0.1:8008/",
access_token: "an_access_token",
user_id: "@alice:example.org",
device_id: "a_device",
sync: false,
storage: storage,
test: true
)
client = Polyjuice.Client.get_client(client_pid)
assert GenServer.call(client.pid, :get_state) == %{
access_token: "an_access_token",
user_id: "@alice:example.org",
device_id: "a_device"
}
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "access_token") ==
"an_access_token"
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "user_id") ==
"@alice:example.org"
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "device_id") ==
"a_device"
Polyjuice.Client.API.stop(client)
# if we start a client and don't specify them, but they're stored, use the
# stored values
{:ok, client2_pid} =
Polyjuice.Client.start_link(
"http://127.0.0.1:8008/",
sync: false,
storage: storage,
test: true
)
client2 = Polyjuice.Client.get_client(client2_pid)
assert GenServer.call(client2.pid, :get_state) == %{
access_token: "an_access_token",
user_id: "@alice:example.org",
device_id: "a_device"
}
end
test "invalidates token if server says we're logged out" do
{:ok, tmpdir} = TestUtil.mktmpdir("logout-")
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.ClientTest.Httpd.LoggedOut]}
)
port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
{:ok, client_pid} =
Polyjuice.Client.start_link(
"http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd.LoggedOut",
access_token: "some_token",
user_id: "@alice:example.org",
device_id: "a_device",
sync: false,
storage: storage,
test: true
)
client = Polyjuice.Client.get_client(client_pid)
Polyjuice.Client.Room.send_event(client, "!a_room:example.org", "m.room.message", %{})
assert GenServer.call(client.pid, :get_state) |> Map.get(:state) == nil
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "access_token") ==
nil
Polyjuice.Client.API.stop(client)
:inets.stop(:httpd, httpd_pid)
after
Polyjuice.Client.Storage.close(storage)
File.rm_rf(tmpdir)
end
end
test "gets the user and device IDs" do
{:ok, client_pid} =
Polyjuice.Client.start_link(
"",
access_token: nil,
sync: false,
handler: self(),
test: true,
user_id: "@alice:example.org",
device_id: "DEVICEID"
)
client = Polyjuice.Client.get_client(client_pid)
assert Polyjuice.Client.API.get_user_and_device(client) == {"@alice:example.org", "DEVICEID"}
end
test "register" do
{:ok, tmpdir} = TestUtil.mktmpdir("register-")
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.ClientTest.Httpd]}
)
port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
{:ok, client_pid} =
Polyjuice.Client.start_link(
"http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd/",
access_token: nil,
sync: false,
storage: storage,
handler: self(),
test: true
)
client = Polyjuice.Client.get_client(client_pid)
opts = [username: "alice", password: "password", device_id: "foo"]
assert Polyjuice.Client.register(client, opts) ==
{:ok,
%{
"access_token" => "m.id.user_login",
"device_id" => "foo",
"user_id" => "@alice:example.org"
}}
assert GenServer.call(client.pid, :get_state) == %{
access_token: "m.id.user_login",
user_id: "@alice:example.org",
device_id: "foo"
}
assert_receive({:polyjuice_client, :logged_in, {"@alice:example.org", "foo", _}})
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "access_token") ==
"m.id.user_login"
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "user_id") ==
"@alice:example.org"
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "device_id") == "foo"
Polyjuice.Client.log_out(client)
# Test inhibit_login: :true
opts = [
username: "bob",
password: "password",
device_id: "foo",
inhibit_login: true
]
assert Polyjuice.Client.register(client, opts) ==
{:ok,
%{
"user_id" => "@bob:example.org"
}}
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "access_token") ==
nil
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "user_id") == nil
assert Polyjuice.Client.Storage.kv_get(storage, "ca.uhoreg.polyjuice", "device_id") == nil
assert GenServer.call(client.pid, :get_state) |> Map.get(:access_token) == nil
# Test error when registering if username is taken
{: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.ClientTest.Httpd.UserAlreadyTaken]}
)
port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
{:ok, client_pid} =
Polyjuice.Client.start_link(
"http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd.UserAlreadyTaken/",
access_token: nil,
sync: false,
storage: storage,
handler: self(),
test: true
)
client = Polyjuice.Client.get_client(client_pid)
opts = [username: "alice", password: "password", device_id: "foo"]
assert {:error, 400, %{"errcode" => "M_USER_IN_USE", "error" => _}} =
Polyjuice.Client.register(client, opts)
Polyjuice.Client.API.stop(client)
:inets.stop(:httpd, httpd_pid)
after
Polyjuice.Client.Storage.close(storage)
File.rm_rf(tmpdir)
end
end
end