summaryrefslogblamecommitdiff
path: root/test/polyjuice/client_test.exs
blob: a87fa4a5d958e09dc67027bbca968c4c069ba494 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
















                                                                          




























                                                                                                                             


                                                 

























                                                                                                                          




































                                                                                                                                           

     

                                    
                                        
             







                                                                             






























                                                  

       
 
                
                                                     



















                                                              
                         
                                    


                                                                        
                      

                    
 

                                                      





                                                                
                               












                                                                
                               






                                                         
                                       







                                    
                                               
 

                                                 


















                                                              
                         
                                    

                                                                        
                      
                           
                          


                    

                                                      

                                                                                     
                                                         



                                               
 

                                                                                       







                                                                                                  

                                      

                                                               





                                                                                                
                                                                                    






                                            

                                                                                       
                                                         



                                                     






                                            

                                                                                       
                                                         



                                                








                                            

                                                                                       
                                                         



                                                   
 
                                       
 

                                    
                                             


                        





                                                                            
                       









                                        

                                                    
                                                       



                                             









                                                                                             
                                     



                                                                              
                        






                                  

                                                      
                                                        



                                             
     
























                                                                        
                         









                                                                                 

                                                      

                                                                                            
                                                                             



                                                                                               
                                       






                                             
















                                                                                                 






























































































































                                                                                                  
   
# 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