From 91eca10809937b5169a867fc9bfac6343d8573aa Mon Sep 17 00:00:00 2001 From: unenglishable Date: Wed, 20 Sep 2023 11:53:36 -0700 Subject: [PATCH 01/68] refactor(session): ? for boolean function is_active_for_user_id --- lib/epochtalk_server/session.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index a6905609..87570d5f 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -80,7 +80,7 @@ defmodule EpochtalkServer.Session do | {:error, reason :: String.t() | Redix.Error.t() | Redix.ConnectionError.t()} def get_resource(user_id, session_id) do # check if session is active in redis - is_active_for_user_id(session_id, user_id) + is_active_for_user_id?(session_id, user_id) |> case do {:error, error} -> {:error, "Error finding resource from claims #{inspect(error)}"} @@ -146,7 +146,7 @@ defmodule EpochtalkServer.Session do Redix.command(:redix, ["ZRANGE", session_key, 0, -1]) end - defp is_active_for_user_id(session_id, user_id) do + defp is_active_for_user_id?(session_id, user_id) do session_key = generate_key(user_id, "sessions") # check ZSCORE for user_id:session_id pair # returns score if it is a member of the set From 974859a8ffbd67446aaffcd75c5b1c11dec00837 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Wed, 20 Sep 2023 12:40:58 -0700 Subject: [PATCH 02/68] refactor(session): return errors on update() function either a Postgrex error (:user_not_found) or :no_sessions --- lib/epochtalk_server/session.ex | 42 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 87570d5f..9f29077d 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -45,30 +45,38 @@ defmodule EpochtalkServer.Session do @doc """ Update session performs the following actions for active session: * Saves `User` session info to redis (avatar, roles, moderating, ban info, etc) - * returns {:ok, user} + * returns {:ok, user} on success + * returns {:error, reason} on failure DOES NOT change: * User's session id, timestamp, ttl * Guardian token """ @spec update(user_id :: non_neg_integer) :: {:ok, user :: User.t()} + | {:error, reason :: String.t() | Postgrex.Error.t()} def update(user_id) do - {:ok, user} = User.by_id(user_id) - avatar = if is_nil(user.profile), do: nil, else: user.profile.avatar - update_user_info(user.id, user.username, avatar: avatar) - update_roles(user.id, user.roles) - - ban_info = - if is_nil(user.ban_info), do: %{}, else: %{ban_expiration: user.ban_info.expiration} - - ban_info = - if !is_nil(user.malicious_score) && user.malicious_score >= 1, - do: Map.put(ban_info, :malicious_score, user.malicious_score), - else: ban_info - - update_ban_info(user.id, ban_info) - update_moderating(user.id, user.moderating) - {:ok, user} + with {:ok, user} <- User.by_id(user_id), + true <- has_sessions?(user_id) + do + avatar = if is_nil(user.profile), do: nil, else: user.profile.avatar + update_user_info(user.id, user.username, avatar: avatar) + update_roles(user.id, user.roles) + + ban_info = + if is_nil(user.ban_info), do: %{}, else: %{ban_expiration: user.ban_info.expiration} + + ban_info = + if !is_nil(user.malicious_score) && user.malicious_score >= 1, + do: Map.put(ban_info, :malicious_score, user.malicious_score), + else: ban_info + + update_ban_info(user.id, ban_info) + update_moderating(user.id, user.moderating) + {:ok, user} + else + {:error, error} -> {:error, error} + false -> {:error, :no_sessions} + end end @doc """ From cca252a15e13ce9e99746dcd1970880cae2a1c5a Mon Sep 17 00:00:00 2001 From: unenglishable Date: Wed, 20 Sep 2023 12:42:28 -0700 Subject: [PATCH 03/68] feat(session): implement has_sessions checks if there are any active sessions for user_id --- lib/epochtalk_server/session.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 9f29077d..433894b6 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -154,6 +154,18 @@ defmodule EpochtalkServer.Session do Redix.command(:redix, ["ZRANGE", session_key, 0, -1]) end + defp has_sessions?(user_id) do + session_key = generate_key(user_id, "sessions") + # check ZCARD for key + # returns number of sorted set elements at key + # returns 0 if key does not exist (set is empty) + case Redix.command(:redix, ["ZCARD", session_key]) do + {:error, error} -> {:error, error} + {:ok, 0} -> false + {:ok, _score} -> true + end + end + defp is_active_for_user_id?(session_id, user_id) do session_key = generate_key(user_id, "sessions") # check ZSCORE for user_id:session_id pair From 930f3b5897b5e570fd509824449aa38cbb647b0c Mon Sep 17 00:00:00 2001 From: unenglishable Date: Wed, 20 Sep 2023 12:43:23 -0700 Subject: [PATCH 04/68] test(seed/users): seed no_login user for tests requiring a user that is not logged in --- test/seed/users.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/seed/users.exs b/test/seed/users.exs index 85afe68a..88aa2f65 100644 --- a/test/seed/users.exs +++ b/test/seed/users.exs @@ -12,6 +12,10 @@ test_user_username = "user" test_user_email = "user@test.com" test_user_password = "password" +test_no_login_user_username = "no_login" +test_no_login_user_email = "no_login@test.com" +test_no_login_user_password = "password" + build(:user, username: test_super_admin_user_username, email: test_super_admin_user_email, @@ -32,4 +36,10 @@ build(:user, password: test_user_password ) +build(:user, + username: test_no_login_user_username, + email: test_no_login_user_email, + password: test_no_login_user_password +) + IO.puts("Successfully seeded test users") From 8c331f7b31ce9c0e0096a8e1233298318496ef9e Mon Sep 17 00:00:00 2001 From: unenglishable Date: Wed, 20 Sep 2023 12:44:17 -0700 Subject: [PATCH 05/68] test(support/conn_case): export no_login_user for testing --- test/support/conn_case.ex | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 8bb47d61..fc4b098d 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -15,6 +15,16 @@ defmodule Test.Support.ConnCase do this option is not recommended for other databases. """ + # no_login username/email/password from user seed in `mix test` (see mix.exs) + @test_no_login_username "no_login" + @test_no_login_email "no_login@test.com" + @test_no_login_password "password" + @test_no_login_user_attrs %{ + username: @test_no_login_username, + email: @test_no_login_email, + password: @test_no_login_password + } + # username/email/password from user seed in `mix test` (see mix.exs) @test_username "user" @test_email "user@test.com" @@ -75,6 +85,7 @@ defmodule Test.Support.ConnCase do Ecto.Adapters.SQL.Sandbox.mode(EpochtalkServer.Repo, {:shared, self()}) end + {:ok, no_login_user} = User.by_username(@test_no_login_username) {:ok, user} = User.by_username(@test_username) {:ok, admin_user} = User.by_username(@test_admin_username) {:ok, super_admin_user} = User.by_username(@test_super_admin_username) @@ -84,11 +95,13 @@ defmodule Test.Support.ConnCase do {:ok, [ users: %{ + no_login_user: no_login_user, user: user, admin_user: admin_user, super_admin_user: super_admin_user }, user_attrs: %{ + no_login_user: @test_no_login_user_attrs, user: @test_user_attrs, admin_user: @test_admin_user_attrs, super_admin_user: @test_super_admin_user_attrs From 7b2b0e8f458784e196185540006f3617e8877bb2 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Wed, 20 Sep 2023 12:45:02 -0700 Subject: [PATCH 06/68] test(session): test session update failure cases :user_not_found error :no_sessions error --- test/epochtalk_server/session_test.exs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 1544fb7b..1c2efe29 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -360,6 +360,15 @@ defmodule Test.EpochtalkServer.Session do end end + describe "update/1" do + test "given invalid user id, errors with :user_not_found", %{conn: conn} do + assert Session.update(0) == {:error, :user_not_found} + end + test "given user id without sessions, errors with :no_sessions", %{conn: conn, users: %{no_login_user: user}} do + assert Session.update(user.id) == {:error, :no_sessions} + end + end + defp flush_redis(_) do Redix.command!(:redix, ["FLUSHALL"]) :ok From dbb913e361f556ee20311cbe85d5b54d9290b939 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 10:01:49 -0700 Subject: [PATCH 07/68] test(models/profile): implement fail case test for create --- test/epochtalk_server/models/profile_test.exs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 test/epochtalk_server/models/profile_test.exs diff --git a/test/epochtalk_server/models/profile_test.exs b/test/epochtalk_server/models/profile_test.exs new file mode 100644 index 00000000..c9aa164a --- /dev/null +++ b/test/epochtalk_server/models/profile_test.exs @@ -0,0 +1,15 @@ +defmodule Test.EpochtalkServer.Models.Profile do + use Test.Support.DataCase, async: true + alias EpochtalkServer.Models.Profile + + describe "create/2" do + test "given invalid user_id, errors with Ecto.ConstraintError" do + invalid_user_id = 0 + assert_raise Ecto.ConstraintError, + ~r/profiles_user_id_fkey/, + fn -> + Profile.create(invalid_user_id) + end + end + end +end From 414f8ad57616b0ce986736e28248f552168f0ca6 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 11:23:06 -0700 Subject: [PATCH 08/68] test(support/data_case): add users/attrs to data case for testing --- test/support/data_case.ex | 63 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/test/support/data_case.ex b/test/support/data_case.ex index c7486c74..61e147f7 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -14,6 +14,46 @@ defmodule Test.Support.DataCase do this option is not recommended for other databases. """ + # no_login username/email/password from user seed in `mix test` (see mix.exs) + @test_no_login_username "no_login" + @test_no_login_email "no_login@test.com" + @test_no_login_password "password" + @test_no_login_user_attrs %{ + username: @test_no_login_username, + email: @test_no_login_email, + password: @test_no_login_password + } + + # username/email/password from user seed in `mix test` (see mix.exs) + @test_username "user" + @test_email "user@test.com" + @test_password "password" + @test_user_attrs %{ + username: @test_username, + email: @test_email, + password: @test_password + } + + # admin username/email/password from user seed in `mix test` (see mix.exs) + @test_admin_username "admin" + @test_admin_email "admin@test.com" + @test_admin_password "password" + @test_admin_user_attrs %{ + username: @test_admin_username, + email: @test_admin_email, + password: @test_admin_password + } + + # super admin username/email/password from user seed in `mix test` (see mix.exs) + @test_super_admin_username "superadmin" + @test_super_admin_email "superadmin@test.com" + @test_super_admin_password "password" + @test_super_admin_user_attrs %{ + username: @test_super_admin_username, + email: @test_super_admin_email, + password: @test_super_admin_password + } + use ExUnit.CaseTemplate using do @@ -28,8 +68,29 @@ defmodule Test.Support.DataCase do end setup tags do + alias EpochtalkServer.Models.User Test.Support.DataCase.setup_sandbox(tags) - :ok + {:ok, no_login_user} = User.by_username(@test_no_login_username) + {:ok, user} = User.by_username(@test_username) + {:ok, admin_user} = User.by_username(@test_admin_username) + {:ok, super_admin_user} = User.by_username(@test_super_admin_username) + { + :ok, + [ + users: %{ + no_login_user: no_login_user, + user: user, + admin_user: admin_user, + super_admin_user: super_admin_user + }, + user_attrs: %{ + no_login_user: @test_no_login_user_attrs, + user: @test_user_attrs, + admin_user: @test_admin_user_attrs, + super_admin_user: @test_super_admin_user_attrs + } + ] + } end @doc """ From a99a66f6e60e9835aeb1b05102fadbc0904783b4 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 11:24:14 -0700 Subject: [PATCH 09/68] test(models/profile): implement fail case test for existing user_id --- test/epochtalk_server/models/profile_test.exs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/epochtalk_server/models/profile_test.exs b/test/epochtalk_server/models/profile_test.exs index c9aa164a..a21b4522 100644 --- a/test/epochtalk_server/models/profile_test.exs +++ b/test/epochtalk_server/models/profile_test.exs @@ -11,5 +11,12 @@ defmodule Test.EpochtalkServer.Models.Profile do Profile.create(invalid_user_id) end end + test "given an existing user_id, errors with Ecto.ConstraintError", %{users: %{user: user}} do + assert_raise Ecto.ConstraintError, + ~r/profiles_user_id_index/, + fn -> + Profile.create(user.id) + end + end end end From e93741ce76c01449254f0ae450014c10aa52167e Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 11:30:02 -0700 Subject: [PATCH 10/68] test(models/profile): test upsert for avatar update --- test/epochtalk_server/models/profile_test.exs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/epochtalk_server/models/profile_test.exs b/test/epochtalk_server/models/profile_test.exs index a21b4522..d865b532 100644 --- a/test/epochtalk_server/models/profile_test.exs +++ b/test/epochtalk_server/models/profile_test.exs @@ -1,7 +1,16 @@ defmodule Test.EpochtalkServer.Models.Profile do use Test.Support.DataCase, async: true alias EpochtalkServer.Models.Profile + alias EpochtalkServer.Models.User + describe "upsert/2" do + test "given valid user_id, updates user's avatar", %{users: %{user: user}} do + assert user.profile.avatar == "" + Profile.upsert(user.id, %{avatar: "image.png"}) + {:ok, updated_user} = User.by_id(user.id) + assert updated_user.profile.avatar == "image.png" + end + end describe "create/2" do test "given invalid user_id, errors with Ecto.ConstraintError" do invalid_user_id = 0 From 7686fdcebfd4f0325bdd58f8720fedb5651e658f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 11:41:07 -0700 Subject: [PATCH 11/68] test(models/profile): check profile default fields --- test/epochtalk_server/models/profile_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/epochtalk_server/models/profile_test.exs b/test/epochtalk_server/models/profile_test.exs index d865b532..ec30386a 100644 --- a/test/epochtalk_server/models/profile_test.exs +++ b/test/epochtalk_server/models/profile_test.exs @@ -5,7 +5,15 @@ defmodule Test.EpochtalkServer.Models.Profile do describe "upsert/2" do test "given valid user_id, updates user's avatar", %{users: %{user: user}} do + # check default fields assert user.profile.avatar == "" + assert user.profile.position == nil + assert user.profile.signature == nil + assert user.profile.post_count == 0 + assert user.profile.fields == nil + assert user.profile.raw_signature == nil + assert user.profile.last_active == nil + Profile.upsert(user.id, %{avatar: "image.png"}) {:ok, updated_user} = User.by_id(user.id) assert updated_user.profile.avatar == "image.png" From 38e069e7added11864efc1ea51c37c25335ec47f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 11:41:54 -0700 Subject: [PATCH 12/68] test(models/profile): test upsert for all string fields --- test/epochtalk_server/models/profile_test.exs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/epochtalk_server/models/profile_test.exs b/test/epochtalk_server/models/profile_test.exs index ec30386a..cd7227eb 100644 --- a/test/epochtalk_server/models/profile_test.exs +++ b/test/epochtalk_server/models/profile_test.exs @@ -4,7 +4,7 @@ defmodule Test.EpochtalkServer.Models.Profile do alias EpochtalkServer.Models.User describe "upsert/2" do - test "given valid user_id, updates user's avatar", %{users: %{user: user}} do + test "given valid user_id, updates user's profile", %{users: %{user: user}} do # check default fields assert user.profile.avatar == "" assert user.profile.position == nil @@ -14,9 +14,18 @@ defmodule Test.EpochtalkServer.Models.Profile do assert user.profile.raw_signature == nil assert user.profile.last_active == nil - Profile.upsert(user.id, %{avatar: "image.png"}) + updated_attrs = %{ + avatar: "image.png", + position: "position", + signature: "signature", + raw_signature: "raw_signature" + } + Profile.upsert(user.id, updated_attrs) {:ok, updated_user} = User.by_id(user.id) - assert updated_user.profile.avatar == "image.png" + assert updated_user.profile.avatar == updated_attrs.avatar + assert updated_user.profile.position == updated_attrs.position + assert updated_user.profile.signature == updated_attrs.signature + assert updated_user.profile.raw_signature == updated_attrs.raw_signature end end describe "create/2" do From 0df42a582bd3d69c4a01d8df02d72cd84f3ecf85 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 12:05:51 -0700 Subject: [PATCH 13/68] test(session): check avatar gets updated in session after profile update --- test/epochtalk_server/session_test.exs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 1c2efe29..9374e8d3 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -9,6 +9,7 @@ defmodule Test.EpochtalkServer.Session do @four_weeks_in_seconds 4 * 7 * @one_day_in_seconds @almost_four_weeks_in_seconds @four_weeks_in_seconds - 100 alias EpochtalkServer.Session + alias EpochtalkServer.Models.Profile describe "get_resource/2" do test "when session_id is invalid, errors", %{users: %{user: user}} do @@ -367,6 +368,16 @@ defmodule Test.EpochtalkServer.Session do test "given user id without sessions, errors with :no_sessions", %{conn: conn, users: %{no_login_user: user}} do assert Session.update(user.id) == {:error, :no_sessions} end + @tag :authenticated + test "given a valid user id, updates avatar", %{conn: conn, authed_user: authed_user} do + updated_attrs = %{avatar: "image.png"} + Profile.upsert(authed_user.id, updated_attrs) + Session.update(authed_user.id) + # get session_id (jti) from conn + session_id = conn.private.guardian_default_claims["jti"] + {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) + assert resource_user.avatar == updated_attrs.avatar + end end defp flush_redis(_) do From 7c9ac0152401bc2b17011a54b4f5ed6f87734bc6 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 12:30:22 -0700 Subject: [PATCH 14/68] test(session): check that user role gets updated to admin --- test/epochtalk_server/session_test.exs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 9374e8d3..d285f1f2 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -10,6 +10,7 @@ defmodule Test.EpochtalkServer.Session do @almost_four_weeks_in_seconds @four_weeks_in_seconds - 100 alias EpochtalkServer.Session alias EpochtalkServer.Models.Profile + alias EpochtalkServer.Models.RoleUser describe "get_resource/2" do test "when session_id is invalid, errors", %{users: %{user: user}} do @@ -378,6 +379,18 @@ defmodule Test.EpochtalkServer.Session do {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) assert resource_user.avatar == updated_attrs.avatar end + @tag :authenticated + test "given a valid user id, updates role to admin", %{conn: conn, authed_user: authed_user} do + authed_user_role = List.first(authed_user.roles) + assert authed_user_role.lookup == "user" + RoleUser.set_admin(authed_user.id) + Session.update(authed_user.id) + # get session_id (jti) from conn + session_id = conn.private.guardian_default_claims["jti"] + {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) + resource_user_role = List.first(resource_user.roles) + assert resource_user_role.lookup == "superAdministrator" + end end defp flush_redis(_) do From 9ff5c0c69e6faeceffea4a459fe9ccf8f1a0e05a Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 13:50:49 -0700 Subject: [PATCH 15/68] test(session): use role cache to check that session roles were updated --- test/epochtalk_server/session_test.exs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index d285f1f2..659142bd 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -11,6 +11,7 @@ defmodule Test.EpochtalkServer.Session do alias EpochtalkServer.Session alias EpochtalkServer.Models.Profile alias EpochtalkServer.Models.RoleUser + alias EpochtalkServer.Cache.Role, as: RoleCache describe "get_resource/2" do test "when session_id is invalid, errors", %{users: %{user: user}} do @@ -382,14 +383,14 @@ defmodule Test.EpochtalkServer.Session do @tag :authenticated test "given a valid user id, updates role to admin", %{conn: conn, authed_user: authed_user} do authed_user_role = List.first(authed_user.roles) - assert authed_user_role.lookup == "user" + assert authed_user_role == RoleCache.by_lookup("user") RoleUser.set_admin(authed_user.id) Session.update(authed_user.id) # get session_id (jti) from conn session_id = conn.private.guardian_default_claims["jti"] {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) resource_user_role = List.first(resource_user.roles) - assert resource_user_role.lookup == "superAdministrator" + assert resource_user_role == RoleCache.by_lookup("superAdministrator") end end From 011aa93c5f4e3daf4ca87a8a13f3746b8a374e04 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 13:52:59 -0700 Subject: [PATCH 16/68] test(session): check that user role can be added to superadmin --- test/epochtalk_server/session_test.exs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 659142bd..62751e2a 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -392,6 +392,19 @@ defmodule Test.EpochtalkServer.Session do resource_user_role = List.first(resource_user.roles) assert resource_user_role == RoleCache.by_lookup("superAdministrator") end + @tag authenticated: :super_admin + test "given a valid superAdministrator id, updates role to user", %{conn: conn, authed_user: authed_user} do + authed_user_role = List.first(authed_user.roles) + assert authed_user_role == RoleCache.by_lookup("superAdministrator") + user_role = RoleCache.by_lookup("user") + RoleUser.set_user_role(user_role.id, authed_user.id) + Session.update(authed_user.id) + # get session_id (jti) from conn + session_id = conn.private.guardian_default_claims["jti"] + {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) + resource_user_role = List.first(resource_user.roles) + assert resource_user_role == user_role + end end defp flush_redis(_) do From a339bf6c1fec04fac04309ef64ca062f8d38a4a5 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 22 Sep 2023 13:57:46 -0700 Subject: [PATCH 17/68] test(session): fix super admin test, delete admin role, check user role default --- test/epochtalk_server/session_test.exs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 62751e2a..38959b6d 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -393,16 +393,17 @@ defmodule Test.EpochtalkServer.Session do assert resource_user_role == RoleCache.by_lookup("superAdministrator") end @tag authenticated: :super_admin - test "given a valid superAdministrator id, updates role to user", %{conn: conn, authed_user: authed_user} do + test "given a valid superAdministrator id, updates role to delete admin + add user", %{conn: conn, authed_user: authed_user} do authed_user_role = List.first(authed_user.roles) - assert authed_user_role == RoleCache.by_lookup("superAdministrator") - user_role = RoleCache.by_lookup("user") - RoleUser.set_user_role(user_role.id, authed_user.id) + super_administrator_role = RoleCache.by_lookup("superAdministrator") + assert authed_user_role == super_administrator_role + RoleUser.delete(super_administrator_role.id, authed_user.id) Session.update(authed_user.id) # get session_id (jti) from conn session_id = conn.private.guardian_default_claims["jti"] {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) resource_user_role = List.first(resource_user.roles) + user_role = RoleCache.by_lookup("user") assert resource_user_role == user_role end end From 6442d8e5fa64c44a9eb9359e5a01550d576549f6 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 28 Sep 2023 11:37:06 -0700 Subject: [PATCH 18/68] test(session): check update ban_info after banning user --- test/epochtalk_server/session_test.exs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 38959b6d..41ee7e29 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -12,6 +12,7 @@ defmodule Test.EpochtalkServer.Session do alias EpochtalkServer.Models.Profile alias EpochtalkServer.Models.RoleUser alias EpochtalkServer.Cache.Role, as: RoleCache + alias EpochtalkServer.Models.Ban describe "get_resource/2" do test "when session_id is invalid, errors", %{users: %{user: user}} do @@ -406,6 +407,23 @@ defmodule Test.EpochtalkServer.Session do user_role = RoleCache.by_lookup("user") assert resource_user_role == user_role end + + @tag :authenticated + test "given a not banned user's id, when user is banned, updates role to add ban info", %{conn: conn, authed_user: authed_user} do + # get session_id (jti) from conn + session_id = conn.private.guardian_default_claims["jti"] + # check that session user does not have banned role + {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) + assert Enum.any?(resource_user.roles, &(&1.lookup == "banned")) == false + + # ban user + Ban.ban(authed_user) + + # check that session user has banned role + Session.update(authed_user.id) + {:ok, banned_resource_user} = Session.get_resource(authed_user.id, session_id) + assert Enum.any?(banned_resource_user.roles, &(&1.lookup == "banned")) == true + end end defp flush_redis(_) do From 096b8d5e4b79b27ea783204e38810f65cc3c07fa Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 28 Sep 2023 11:37:41 -0700 Subject: [PATCH 19/68] test(session): update ban_info after unbanning user --- test/epochtalk_server/session_test.exs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 41ee7e29..ac9082ff 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -424,6 +424,23 @@ defmodule Test.EpochtalkServer.Session do {:ok, banned_resource_user} = Session.get_resource(authed_user.id, session_id) assert Enum.any?(banned_resource_user.roles, &(&1.lookup == "banned")) == true end + @tag [authenticated: true, banned: true] + test "given a banned user's id, when user is unbanned, updates role to delete ban info", %{conn: conn, authed_user: authed_user} do + # get session_id (jti) from conn + session_id = conn.private.guardian_default_claims["jti"] + # check that session user has banned role + Session.update(authed_user.id) + {:ok, banned_resource_user} = Session.get_resource(authed_user.id, session_id) + assert Enum.any?(banned_resource_user.roles, &(&1.lookup == "banned")) == true + + # unban user + Ban.unban_by_user_id(authed_user.id) + + # check that session user does not have banned role + Session.update(authed_user.id) + {:ok, unbanned_resource_user} = Session.get_resource(authed_user.id, session_id) + assert Enum.any?(unbanned_resource_user.roles, &(&1.lookup == "banned")) == false + end end defp flush_redis(_) do From 4620660a877267311f1fc0a13eb6eb96db2d62e4 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 28 Sep 2023 11:45:13 -0700 Subject: [PATCH 20/68] test(session): update session in conn_case after banning --- test/epochtalk_server/session_test.exs | 1 - test/support/conn_case.ex | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index ac9082ff..6648e31f 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -429,7 +429,6 @@ defmodule Test.EpochtalkServer.Session do # get session_id (jti) from conn session_id = conn.private.guardian_default_claims["jti"] # check that session user has banned role - Session.update(authed_user.id) {:ok, banned_resource_user} = Session.get_resource(authed_user.id, session_id) assert Enum.any?(banned_resource_user.roles, &(&1.lookup == "banned")) == true diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index fc4b098d..26cff653 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -157,6 +157,8 @@ defmodule Test.Support.ConnCase do context_updates = if context[:banned] do {:ok, banned_user_changeset} = Ban.ban(user) + # update ban info in session + Session.update(user.id) {:ok, k_list} = context_updates {:ok, [banned_user_changeset: banned_user_changeset] ++ k_list} else From 2d13f2d2b319d7823a837b91816dfbc2b2b1f925 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 28 Sep 2023 11:47:27 -0700 Subject: [PATCH 21/68] test(session): update descriptions --- test/epochtalk_server/session_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 6648e31f..6d87dc9c 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -409,7 +409,7 @@ defmodule Test.EpochtalkServer.Session do end @tag :authenticated - test "given a not banned user's id, when user is banned, updates role to add ban info", %{conn: conn, authed_user: authed_user} do + test "given a not banned user's id, when user is banned, adds banned role", %{conn: conn, authed_user: authed_user} do # get session_id (jti) from conn session_id = conn.private.guardian_default_claims["jti"] # check that session user does not have banned role @@ -425,7 +425,7 @@ defmodule Test.EpochtalkServer.Session do assert Enum.any?(banned_resource_user.roles, &(&1.lookup == "banned")) == true end @tag [authenticated: true, banned: true] - test "given a banned user's id, when user is unbanned, updates role to delete ban info", %{conn: conn, authed_user: authed_user} do + test "given a banned user's id, when user is unbanned, deletes banned role", %{conn: conn, authed_user: authed_user} do # get session_id (jti) from conn session_id = conn.private.guardian_default_claims["jti"] # check that session user has banned role From e898798f27c052b468e0fb52bb01101aba916c7a Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 10 Oct 2023 11:51:54 -0700 Subject: [PATCH 22/68] fix(session): use catch-all for setting ttl in the case that old and new ttl's are invalid, set to max expiry this ensures keys are all eventually expired regardless of given ttl validity --- lib/epochtalk_server/session.ex | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 433894b6..2125f678 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -366,13 +366,20 @@ defmodule EpochtalkServer.Session do maybe_extend_ttl(key, ttl, ttl) end + # set ttl to max of old_ttl, new_ttl, and existing ttl + # - or @four_weeks_in_seconds if old and new ttl's are invalid defp maybe_extend_ttl(key, new_ttl, old_ttl) do - if old_ttl > -1 do - # re-set old expiry only if old expiry was valid and key has no expiry - Redix.command(:redix, ["EXPIRE", key, old_ttl, "NX"]) - else - # if old expiry was invalid, set new expiry only if key has no expiry - Redix.command(:redix, ["EXPIRE", key, new_ttl, "NX"]) + cond do + old_ttl > -1 -> + # re-set old expiry only if old expiry was valid and key has no expiry + Redix.command(:redix, ["EXPIRE", key, old_ttl, "NX"]) + new_ttl > -1 -> + # if old expiry was invalid, set new expiry only if new expiry is valid and key has no expiry + Redix.command(:redix, ["EXPIRE", key, new_ttl, "NX"]) + true -> + # catch-all, in case both given expiries are invalid + # if neither expiry is valid, set ttl to max + Redix.command(:redix, ["EXPIRE", key, @four_weeks_in_seconds]) end # set expiry only if new expiry is greater than current (GT) From de30a451873e5a4cfb2b0d009f8db4c50fe845da Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 10 Oct 2023 11:52:20 -0700 Subject: [PATCH 23/68] refactor(session): log a warning in the event of session TTL catch-all --- lib/epochtalk_server/session.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 2125f678..b9398233 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -5,6 +5,7 @@ defmodule EpochtalkServer.Session do @moduledoc """ Manages `User` sessions in Redis. Used by Auth related `User` actions. """ + require Logger alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role @@ -378,6 +379,10 @@ defmodule EpochtalkServer.Session do Redix.command(:redix, ["EXPIRE", key, new_ttl, "NX"]) true -> # catch-all, in case both given expiries are invalid + Logger.warning("Invalid TTL's, setting max expiry", %{ + module: __MODULE__, + parameters: "[old_ttl: #{old_ttl}, new_ttl: #{new_ttl}, key: #{key}]" + }) # if neither expiry is valid, set ttl to max Redix.command(:redix, ["EXPIRE", key, @four_weeks_in_seconds]) end From bde1918957f1181a0a075c44871578993f4ec4af Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 10 Oct 2023 11:53:35 -0700 Subject: [PATCH 24/68] test(config): configure logger output with module and parameters --- config/test.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index 09c6b477..8146257e 100644 --- a/config/test.exs +++ b/config/test.exs @@ -24,7 +24,10 @@ config :epochtalk_server, EpochtalkServerWeb.Endpoint, config :epochtalk_server, EpochtalkServer.Mailer, adapter: Swoosh.Adapters.Test # Print only warnings and errors during test -config :logger, level: :warning +# Accept module and parameters +config :logger, :console, level: :warning, + format: "[$level] $message $metadata\n", + metadata: [:module, :parameters] # Initialize plugs at runtime for faster test compilation config :phoenix, :plug_init_mode, :runtime From dab43c7374a4ce47530f0a40e8331e6e2ecea5f7 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 10 Oct 2023 12:17:10 -0700 Subject: [PATCH 25/68] docs(contributions): add note for using Logger update writing code section later with doc refactor --- CONTRIBUTIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md index fe374806..aa711c30 100644 --- a/CONTRIBUTIONS.md +++ b/CONTRIBUTIONS.md @@ -13,7 +13,7 @@ conventions. Please refer to these guidelines! * Write [code](https://en.wikipedia.org/wiki/Computer_programming) in [Elixir](https://elixir-lang.org/) to implement your feature - (Instructions for your computer to do things) +* Use Logger instead of IO.puts/inspect * Commit code using [Angular Commit Message](https://gist.github.com/brianclements/841ea7bffdb01346392c) conventions. From f47eca16d38e7a173b6785b4b7b4b336cf763405 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 13 Oct 2023 14:40:29 -0700 Subject: [PATCH 26/68] style(): mix format --- config/test.exs | 3 +- test/epochtalk_server/models/profile_test.exs | 4 +++ test/epochtalk_server/session_test.exs | 25 +++++++++++++--- test/support/data_case.ex | 29 ++++++++++--------- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/config/test.exs b/config/test.exs index 8146257e..99e67462 100644 --- a/config/test.exs +++ b/config/test.exs @@ -25,7 +25,8 @@ config :epochtalk_server, EpochtalkServer.Mailer, adapter: Swoosh.Adapters.Test # Print only warnings and errors during test # Accept module and parameters -config :logger, :console, level: :warning, +config :logger, :console, + level: :warning, format: "[$level] $message $metadata\n", metadata: [:module, :parameters] diff --git a/test/epochtalk_server/models/profile_test.exs b/test/epochtalk_server/models/profile_test.exs index cd7227eb..8f0c3e88 100644 --- a/test/epochtalk_server/models/profile_test.exs +++ b/test/epochtalk_server/models/profile_test.exs @@ -20,6 +20,7 @@ defmodule Test.EpochtalkServer.Models.Profile do signature: "signature", raw_signature: "raw_signature" } + Profile.upsert(user.id, updated_attrs) {:ok, updated_user} = User.by_id(user.id) assert updated_user.profile.avatar == updated_attrs.avatar @@ -28,15 +29,18 @@ defmodule Test.EpochtalkServer.Models.Profile do assert updated_user.profile.raw_signature == updated_attrs.raw_signature end end + describe "create/2" do test "given invalid user_id, errors with Ecto.ConstraintError" do invalid_user_id = 0 + assert_raise Ecto.ConstraintError, ~r/profiles_user_id_fkey/, fn -> Profile.create(invalid_user_id) end end + test "given an existing user_id, errors with Ecto.ConstraintError", %{users: %{user: user}} do assert_raise Ecto.ConstraintError, ~r/profiles_user_id_index/, diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 6d87dc9c..7e66df57 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -368,9 +368,14 @@ defmodule Test.EpochtalkServer.Session do test "given invalid user id, errors with :user_not_found", %{conn: conn} do assert Session.update(0) == {:error, :user_not_found} end - test "given user id without sessions, errors with :no_sessions", %{conn: conn, users: %{no_login_user: user}} do + + test "given user id without sessions, errors with :no_sessions", %{ + conn: conn, + users: %{no_login_user: user} + } do assert Session.update(user.id) == {:error, :no_sessions} end + @tag :authenticated test "given a valid user id, updates avatar", %{conn: conn, authed_user: authed_user} do updated_attrs = %{avatar: "image.png"} @@ -381,6 +386,7 @@ defmodule Test.EpochtalkServer.Session do {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) assert resource_user.avatar == updated_attrs.avatar end + @tag :authenticated test "given a valid user id, updates role to admin", %{conn: conn, authed_user: authed_user} do authed_user_role = List.first(authed_user.roles) @@ -393,8 +399,12 @@ defmodule Test.EpochtalkServer.Session do resource_user_role = List.first(resource_user.roles) assert resource_user_role == RoleCache.by_lookup("superAdministrator") end + @tag authenticated: :super_admin - test "given a valid superAdministrator id, updates role to delete admin + add user", %{conn: conn, authed_user: authed_user} do + test "given a valid superAdministrator id, updates role to delete admin + add user", %{ + conn: conn, + authed_user: authed_user + } do authed_user_role = List.first(authed_user.roles) super_administrator_role = RoleCache.by_lookup("superAdministrator") assert authed_user_role == super_administrator_role @@ -409,7 +419,10 @@ defmodule Test.EpochtalkServer.Session do end @tag :authenticated - test "given a not banned user's id, when user is banned, adds banned role", %{conn: conn, authed_user: authed_user} do + test "given a not banned user's id, when user is banned, adds banned role", %{ + conn: conn, + authed_user: authed_user + } do # get session_id (jti) from conn session_id = conn.private.guardian_default_claims["jti"] # check that session user does not have banned role @@ -424,8 +437,12 @@ defmodule Test.EpochtalkServer.Session do {:ok, banned_resource_user} = Session.get_resource(authed_user.id, session_id) assert Enum.any?(banned_resource_user.roles, &(&1.lookup == "banned")) == true end + @tag [authenticated: true, banned: true] - test "given a banned user's id, when user is unbanned, deletes banned role", %{conn: conn, authed_user: authed_user} do + test "given a banned user's id, when user is unbanned, deletes banned role", %{ + conn: conn, + authed_user: authed_user + } do # get session_id (jti) from conn session_id = conn.private.guardian_default_claims["jti"] # check that session user has banned role diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 61e147f7..9695629e 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -74,22 +74,23 @@ defmodule Test.Support.DataCase do {:ok, user} = User.by_username(@test_username) {:ok, admin_user} = User.by_username(@test_admin_username) {:ok, super_admin_user} = User.by_username(@test_super_admin_username) + { :ok, - [ - users: %{ - no_login_user: no_login_user, - user: user, - admin_user: admin_user, - super_admin_user: super_admin_user - }, - user_attrs: %{ - no_login_user: @test_no_login_user_attrs, - user: @test_user_attrs, - admin_user: @test_admin_user_attrs, - super_admin_user: @test_super_admin_user_attrs - } - ] + [ + users: %{ + no_login_user: no_login_user, + user: user, + admin_user: admin_user, + super_admin_user: super_admin_user + }, + user_attrs: %{ + no_login_user: @test_no_login_user_attrs, + user: @test_user_attrs, + admin_user: @test_admin_user_attrs, + super_admin_user: @test_super_admin_user_attrs + } + ] } end From 13c5988da36d7c2d902c3ad3ad3e78ed2db2640b Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 13 Oct 2023 14:43:10 -0700 Subject: [PATCH 27/68] style(session): mix format --- lib/epochtalk_server/session.ex | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index b9398233..acb91f8e 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -53,12 +53,12 @@ defmodule EpochtalkServer.Session do * User's session id, timestamp, ttl * Guardian token """ - @spec update(user_id :: non_neg_integer) :: {:ok, user :: User.t()} + @spec update(user_id :: non_neg_integer) :: + {:ok, user :: User.t()} | {:error, reason :: String.t() | Postgrex.Error.t()} def update(user_id) do with {:ok, user} <- User.by_id(user_id), - true <- has_sessions?(user_id) - do + true <- has_sessions?(user_id) do avatar = if is_nil(user.profile), do: nil, else: user.profile.avatar update_user_info(user.id, user.username, avatar: avatar) update_roles(user.id, user.roles) @@ -310,8 +310,9 @@ defmodule EpochtalkServer.Session do {:ok, old_ttl} = Redix.command(:redix, ["TTL", ban_key]) Redix.command(:redix, ["HDEL", ban_key, "ban_expiration", "malicious_score"]) - if ban_exp = Map.get(ban_info, :ban_expiration), - do: Redix.command(:redix, ["HSET", ban_key, "ban_expiration", ban_exp]) + if ban_exp = Map.get(ban_info, :ban_expiration) do + Redix.command(:redix, ["HSET", ban_key, "ban_expiration", ban_exp]) + end if malicious_score = Map.get(ban_info, :malicious_score), do: Redix.command(:redix, ["HSET", ban_key, "malicious_score", malicious_score]) @@ -374,15 +375,18 @@ defmodule EpochtalkServer.Session do old_ttl > -1 -> # re-set old expiry only if old expiry was valid and key has no expiry Redix.command(:redix, ["EXPIRE", key, old_ttl, "NX"]) + new_ttl > -1 -> # if old expiry was invalid, set new expiry only if new expiry is valid and key has no expiry Redix.command(:redix, ["EXPIRE", key, new_ttl, "NX"]) + true -> # catch-all, in case both given expiries are invalid Logger.warning("Invalid TTL's, setting max expiry", %{ module: __MODULE__, parameters: "[old_ttl: #{old_ttl}, new_ttl: #{new_ttl}, key: #{key}]" }) + # if neither expiry is valid, set ttl to max Redix.command(:redix, ["EXPIRE", key, @four_weeks_in_seconds]) end From d239b00fc573165a3597942f2a3abb5b8c068217 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 26 Oct 2023 13:52:44 -0700 Subject: [PATCH 28/68] feat(session): populate session ban info if it exists --- lib/epochtalk_server/session.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index acb91f8e..e62b3c41 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -124,10 +124,11 @@ defmodule EpochtalkServer.Session do else: resource ban_key = generate_key(user_id, "baninfo") - ban_expiration = Redix.command!(:redix, ["HEXISTS", ban_key, "ban_expiration"]) + ban_expiration_exists = Redix.command!(:redix, ["HEXISTS", ban_key, "ban_expiration"]) + ban_expiration = Redix.command!(:redix, ["HGET", ban_key, "ban_expiration"]) resource = - if ban_expiration != 0, + if ban_expiration_exists != 0, do: Map.put(resource, :ban_expiration, ban_expiration), else: resource From fd7d3a268573a46fd8d426f8ebacf0c30c01185c Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 26 Oct 2023 13:53:44 -0700 Subject: [PATCH 29/68] fix(session): refer to user_key for ban_key base TTL since ban key can be empty, it is possible that old TTL is -2 which is a valid number, but will expire the ban key immediately when set --- lib/epochtalk_server/session.ex | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index e62b3c41..2fa1de3c 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -307,8 +307,10 @@ defmodule EpochtalkServer.Session do defp update_ban_info(user_id, ban_info, ttl \\ nil) do # save/replace ban_expiration to redis under "user:{user_id}:baninfo" ban_key = generate_key(user_id, "baninfo") - # get current TTL - {:ok, old_ttl} = Redix.command(:redix, ["TTL", ban_key]) + # use user_key for base ttl, since baninfo may not be populated (TTL == -2) + user_key = generate_key(user_id, "user") + # get current TTL of session + {:ok, session_ttl} = Redix.command(:redix, ["TTL", user_key]) Redix.command(:redix, ["HDEL", ban_key, "ban_expiration", "malicious_score"]) if ban_exp = Map.get(ban_info, :ban_expiration) do @@ -321,10 +323,10 @@ defmodule EpochtalkServer.Session do # if ttl is provided if ttl do # maybe extend ttl - maybe_extend_ttl(ban_key, ttl, old_ttl) + maybe_extend_ttl(ban_key, ttl, session_ttl) else - # otherwise, re-set old_ttl - maybe_extend_ttl(ban_key, old_ttl) + # otherwise, re-set session_ttl from user_key + maybe_extend_ttl(ban_key, session_ttl) end end From 5845b336d13bf9d2ef3c1aeaf1910af86bc22ccd Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 26 Oct 2023 14:02:12 -0700 Subject: [PATCH 30/68] fix(session): refer to user_key for moderating_key base TTL since moderating key can be empty, it is possible that old TTL is -2 which is a valid number, but will expire the moderating key immediately when set --- lib/epochtalk_server/session.ex | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 2fa1de3c..269fdc00 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -262,8 +262,10 @@ defmodule EpochtalkServer.Session do moderating = moderating |> Enum.map(& &1.board_id) # save/replace moderating boards to redis under "user:{user_id}:moderating" moderating_key = generate_key(user_id, "moderating") - # get current TTL - {:ok, old_ttl} = Redix.command(:redix, ["TTL", moderating_key]) + # use user_key for base ttl, since moderating may not be populated (TTL == -2) + user_key = generate_key(user_id, "user") + # get current TTL of session + {:ok, session_ttl} = Redix.command(:redix, ["TTL", user_key]) Redix.command(:redix, ["DEL", moderating_key]) unless moderating == [], @@ -272,10 +274,10 @@ defmodule EpochtalkServer.Session do # if ttl is provided if ttl do # maybe extend ttl - maybe_extend_ttl(moderating_key, ttl, old_ttl) + maybe_extend_ttl(moderating_key, ttl, session_ttl) else - # otherwise, re-set old_ttl - maybe_extend_ttl(moderating_key, old_ttl) + # otherwise, re-set session_ttl from user_key + maybe_extend_ttl(moderating_key, session_ttl) end end From 0448df2e44937171bb0b353cf16a0d42e58ba447 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 26 Oct 2023 14:26:04 -0700 Subject: [PATCH 31/68] test(session): remove unused conn's --- test/epochtalk_server/session_test.exs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 7e66df57..7f6da8db 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -365,12 +365,11 @@ defmodule Test.EpochtalkServer.Session do end describe "update/1" do - test "given invalid user id, errors with :user_not_found", %{conn: conn} do + test "given invalid user id, errors with :user_not_found" do assert Session.update(0) == {:error, :user_not_found} end test "given user id without sessions, errors with :no_sessions", %{ - conn: conn, users: %{no_login_user: user} } do assert Session.update(user.id) == {:error, :no_sessions} From e7fcfcec85a2cd75eabd9c909debc513ee550713 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 26 Oct 2023 14:43:01 -0700 Subject: [PATCH 32/68] test(session): set attribute for max date --- test/epochtalk_server/session_test.exs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 7f6da8db..65307873 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -8,6 +8,7 @@ defmodule Test.EpochtalkServer.Session do @almost_one_day_in_seconds @one_day_in_seconds - 100 @four_weeks_in_seconds 4 * 7 * @one_day_in_seconds @almost_four_weeks_in_seconds @four_weeks_in_seconds - 100 + @max_date "9999-12-31 00:00:00" alias EpochtalkServer.Session alias EpochtalkServer.Models.Profile alias EpochtalkServer.Models.RoleUser @@ -271,7 +272,7 @@ defmodule Test.EpochtalkServer.Session do Redix.command!(:redix, ["HGET", "user:#{user.id}:baninfo", "ban_expiration"]) assert pre_ban_ban_expiration == nil - assert ban_expiration == "9999-12-31 00:00:00" + assert ban_expiration == @max_date assert pre_ban_baninfo_ttl == -2 assert baninfo_ttl <= @one_day_in_seconds assert baninfo_ttl > @almost_one_day_in_seconds @@ -303,7 +304,7 @@ defmodule Test.EpochtalkServer.Session do Redix.command!(:redix, ["HGET", "user:#{user.id}:baninfo", "ban_expiration"]) assert pre_ban_ban_expiration == nil - assert ban_expiration == "9999-12-31 00:00:00" + assert ban_expiration == @max_date assert pre_ban_baninfo_ttl == -2 assert baninfo_ttl <= @four_weeks_in_seconds assert baninfo_ttl > @almost_four_weeks_in_seconds From f7ad8eb11f67e0010ce3f0f55cff458634d5fbd3 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 26 Oct 2023 14:43:32 -0700 Subject: [PATCH 33/68] test(session): check for ban_expiration on update --- test/epochtalk_server/session_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 65307873..bb38caba 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -436,6 +436,8 @@ defmodule Test.EpochtalkServer.Session do Session.update(authed_user.id) {:ok, banned_resource_user} = Session.get_resource(authed_user.id, session_id) assert Enum.any?(banned_resource_user.roles, &(&1.lookup == "banned")) == true + # check ban is active + assert banned_resource_user.ban_expiration == @max_date end @tag [authenticated: true, banned: true] From 5bcfdf52c16f722b7fc91f67c9adee2566ae5041 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 26 Oct 2023 15:01:49 -0700 Subject: [PATCH 34/68] test(session): check ban is expired on update --- test/epochtalk_server/session_test.exs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index bb38caba..fcdc3d93 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -458,6 +458,10 @@ defmodule Test.EpochtalkServer.Session do Session.update(authed_user.id) {:ok, unbanned_resource_user} = Session.get_resource(authed_user.id, session_id) assert Enum.any?(unbanned_resource_user.roles, &(&1.lookup == "banned")) == false + # check ban is expired + now = NaiveDateTime.utc_now() + {:ok, ban_expiration} = unbanned_resource_user.ban_expiration |> NaiveDateTime.from_iso8601() + assert NaiveDateTime.compare(ban_expiration, now) == :lt end end From 0a600a3b73e84c9c72d47bdb01a9997c49f9f2c5 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 26 Oct 2023 15:56:43 -0700 Subject: [PATCH 35/68] feat(session): populate malicious score if it exists --- lib/epochtalk_server/session.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 269fdc00..62314de8 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -132,10 +132,11 @@ defmodule EpochtalkServer.Session do do: Map.put(resource, :ban_expiration, ban_expiration), else: resource - malicious_score = Redix.command!(:redix, ["HEXISTS", ban_key, "malicious_score"]) + malicious_score_exists = Redix.command!(:redix, ["HEXISTS", ban_key, "malicious_score"]) + malicious_score = Redix.command!(:redix, ["HGET", ban_key, "malicious_score"]) resource = - if malicious_score != 0, + if malicious_score_exists != 0, do: Map.put(resource, :malicious_score, malicious_score), else: resource From 3f466b92c935690cecffcf1ae5794b58f6b816b7 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 26 Oct 2023 15:57:17 -0700 Subject: [PATCH 36/68] test(session): check malicious score on update --- test/epochtalk_server/session_test.exs | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index fcdc3d93..55b49968 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -4,6 +4,7 @@ defmodule Test.EpochtalkServer.Session do run into concurrency issues when run alongside other tests """ use Test.Support.ConnCase, async: false + import Test.Support.Factory @one_day_in_seconds 1 * 24 * 60 * 60 @almost_one_day_in_seconds @one_day_in_seconds - 100 @four_weeks_in_seconds 4 * 7 * @one_day_in_seconds @@ -11,6 +12,7 @@ defmodule Test.EpochtalkServer.Session do @max_date "9999-12-31 00:00:00" alias EpochtalkServer.Session alias EpochtalkServer.Models.Profile + alias EpochtalkServer.Models.User alias EpochtalkServer.Models.RoleUser alias EpochtalkServer.Cache.Role, as: RoleCache alias EpochtalkServer.Models.Ban @@ -463,6 +465,34 @@ defmodule Test.EpochtalkServer.Session do {:ok, ban_expiration} = unbanned_resource_user.ban_expiration |> NaiveDateTime.from_iso8601() assert NaiveDateTime.compare(ban_expiration, now) == :lt end + + @tag :authenticated + test "given a not malicious user's id, when user is marked malicious, adds malicious score", %{ + conn: conn, + authed_user: authed_user + } do + # get session_id (jti) from conn + session_id = conn.private.guardian_default_claims["jti"] + # check that session user does not have banned role + {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) + assert Enum.any?(resource_user.roles, &(&1.lookup == "banned")) == false + assert Map.get(resource_user, :malicious_score) == nil + assert Map.get(resource_user, :ban_expiration) == nil + + # update user malicious + build(:banned_address, ip: "127.0.0.1", weight: 1.0) + build(:banned_address, hostname: "localhost", weight: 1.0) + User.handle_malicious_user(authed_user, conn.remote_ip) + + # check that session user has banned role + Session.update(authed_user.id) + {:ok, malicious_resource_user} = Session.get_resource(authed_user.id, session_id) + assert Enum.any?(malicious_resource_user.roles, &(&1.lookup == "banned")) == true + # check ban is active + assert malicious_resource_user.ban_expiration == @max_date + # check malicious score is active + assert malicious_resource_user.malicious_score != nil + end end defp flush_redis(_) do From fa165e70a3fe6fdd1ab3a6a6a8b7a5329fbc75c1 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 27 Oct 2023 13:05:21 -0700 Subject: [PATCH 37/68] test(session): check malicious score value > 1, + documentation --- test/epochtalk_server/session_test.exs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 55b49968..46f58f44 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -476,6 +476,7 @@ defmodule Test.EpochtalkServer.Session do # check that session user does not have banned role {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) assert Enum.any?(resource_user.roles, &(&1.lookup == "banned")) == false + # check user does not have malicious score or ban expiration assert Map.get(resource_user, :malicious_score) == nil assert Map.get(resource_user, :ban_expiration) == nil @@ -488,10 +489,11 @@ defmodule Test.EpochtalkServer.Session do Session.update(authed_user.id) {:ok, malicious_resource_user} = Session.get_resource(authed_user.id, session_id) assert Enum.any?(malicious_resource_user.roles, &(&1.lookup == "banned")) == true - # check ban is active - assert malicious_resource_user.ban_expiration == @max_date - # check malicious score is active + # check malicious score and ban are active assert malicious_resource_user.malicious_score != nil + assert malicious_resource_user.malicious_score > 1 + assert malicious_resource_user.ban_expiration == @max_date + end end end From 32d67fd02b7f47c579e4d4cebfb21723c7569dd4 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 27 Oct 2023 13:07:26 -0700 Subject: [PATCH 38/68] test(factory/banned_address): use upsert no longer need to check for :hostname or :ip, banned_address upsert handles those, along with decay --- test/support/factories/banned_address.ex | 25 +----------------------- 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/test/support/factories/banned_address.ex b/test/support/factories/banned_address.ex index 75247ab4..beea7350 100644 --- a/test/support/factories/banned_address.ex +++ b/test/support/factories/banned_address.ex @@ -7,34 +7,11 @@ defmodule Test.Support.Factories.BannedAddress do OR build(:banned_address, hostname: hostname, weight: weight) """ - alias EpochtalkServer.Repo alias EpochtalkServer.Models.BannedAddress defmacro __using__(_opts) do quote do - def banned_address_attributes_factory(%{ip: ip, weight: weight}) do - %{ - ip: ip, - weight: weight, - created_at: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second), - imported_at: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) - } - end - - def banned_address_attributes_factory(%{hostname: hostname, weight: weight}) do - %{ - hostname: hostname, - weight: weight, - created_at: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second), - imported_at: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) - } - end - - def banned_address_factory(attributes) do - %BannedAddress{} - |> BannedAddress.upsert_changeset(build(:banned_address_attributes, attributes)) - |> Repo.insert() - end + def banned_address_factory(attributes), do: BannedAddress.upsert(attributes) end end end From 7b3d380a379dfa6e21de3dda3d9487339d084f37 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 27 Oct 2023 13:31:02 -0700 Subject: [PATCH 39/68] refactor(models/banned_address): extract magic numbers from decay_for_time update variable names to reflect their corresponding information --- lib/epochtalk_server/models/banned_address.ex | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index 686667d5..61182fc4 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -6,6 +6,10 @@ defmodule EpochtalkServer.Models.BannedAddress do alias EpochtalkServer.Repo alias EpochtalkServer.Models.BannedAddress + @one_week_in_ms 1000 * 60 * 60 * 24 * 7 + @inital_amount 0.8897 + @rate_of_decay 0.9644 + @moduledoc """ `BannedAddress` model, for performing actions relating to banning by ip/hostname """ @@ -310,11 +314,8 @@ defmodule EpochtalkServer.Models.BannedAddress do # in ms since the weight was last updated defp decay_for_time(time, weight) do weight = Decimal.to_float(weight) - one_week = 1000 * 60 * 60 * 24 * 7 - weeks = time / one_week - a = 0.8897 - r = 0.9644 - a ** ((r ** weeks - 1) / (r - 1)) * weight ** (r ** weeks) + weeks = time / @one_week_in_ms + @inital_amount ** ((@rate_of_decay ** weeks - 1) / (@rate_of_decay - 1)) * weight ** (@rate_of_decay ** weeks) end # returns the decayed score given a banned address From 3cce8b25e1790953dbaa50e43cc318444ea96714 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 27 Oct 2023 13:32:29 -0700 Subject: [PATCH 40/68] refactor(banned_address): extract magic numbers from calculate_malicious_score_from_ip update variable names to reflect corresponding data (ip segment weights) --- lib/epochtalk_server/models/banned_address.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index 61182fc4..0bbb234b 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -9,6 +9,9 @@ defmodule EpochtalkServer.Models.BannedAddress do @one_week_in_ms 1000 * 60 * 60 * 24 * 7 @inital_amount 0.8897 @rate_of_decay 0.9644 + @ip32_weight 1 + @ip24_weight 0.04 + @ip16_weight 0.0016 @moduledoc """ `BannedAddress` model, for performing actions relating to banning by ip/hostname @@ -230,7 +233,7 @@ defmodule EpochtalkServer.Models.BannedAddress do ip24_score = calculate_ip24_score(ip) ip16_score = calculate_ip16_score(ip) # calculate malicious score using all scores - malicious_score = hostname_score + ip32_score + 0.04 + ip24_score + 0.0016 + ip16_score + malicious_score = hostname_score + (@ip32_weight * ip32_score) + (@ip24_weight * ip24_score) + (@ip16_weight * ip16_score) if malicious_score < 1, do: nil, else: malicious_score # invalid ip address, return nil for malicious score From 5696b94496845d2f272386b07c2b63f1845df2db Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 27 Oct 2023 13:40:19 -0700 Subject: [PATCH 41/68] test(session+user): update expected malicious scores --- test/epochtalk_server/session_test.exs | 4 ++-- test/epochtalk_server_web/controllers/user_test.exs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 46f58f44..7b8e9577 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -333,7 +333,7 @@ defmodule Test.EpochtalkServer.Session do Redix.command!(:redix, ["HGET", "user:#{authed_user.id}:baninfo", "malicious_score"]) assert pre_malicious_malicious_score == nil - assert malicious_score == "4.0416" + assert malicious_score == "2.0416" assert pre_malicious_baninfo_ttl == -2 assert malicious_score_ttl <= @one_day_in_seconds assert malicious_score_ttl > @almost_one_day_in_seconds @@ -360,7 +360,7 @@ defmodule Test.EpochtalkServer.Session do Redix.command!(:redix, ["HGET", "user:#{authed_user.id}:baninfo", "malicious_score"]) assert pre_malicious_malicious_score == nil - assert malicious_score == "4.0416" + assert malicious_score == "2.0416" assert pre_malicious_baninfo_ttl == -2 assert malicious_score_ttl <= @four_weeks_in_seconds assert malicious_score_ttl > @almost_four_weeks_in_seconds diff --git a/test/epochtalk_server_web/controllers/user_test.exs b/test/epochtalk_server_web/controllers/user_test.exs index c7eafd78..f3327b06 100644 --- a/test/epochtalk_server_web/controllers/user_test.exs +++ b/test/epochtalk_server_web/controllers/user_test.exs @@ -72,7 +72,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.User do } do assert malicious_user_changeset.ban_info.user_id == user.id # check that ip and hostname were banned - assert malicious_user_changeset.malicious_score == 4.0416 + assert malicious_user_changeset.malicious_score == 2.0416 end end From c810e85cbcde9238707725ba596b3b7c49961471 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 30 Oct 2023 10:38:21 -1000 Subject: [PATCH 42/68] feat(errors/custom_errors): allow custom message for invalid payload error --- lib/epochtalk_server_web/errors/custom_errors.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server_web/errors/custom_errors.ex b/lib/epochtalk_server_web/errors/custom_errors.ex index 6ae8d6d1..6c3f5f84 100644 --- a/lib/epochtalk_server_web/errors/custom_errors.ex +++ b/lib/epochtalk_server_web/errors/custom_errors.ex @@ -49,6 +49,7 @@ defmodule EpochtalkServerWeb.CustomErrors do def exception(value) do case value do [] -> %InvalidPayload{} + [message: message] -> %InvalidPayload{message: message} _ -> %InvalidPayload{message: gen_message(Enum.into(value, %{required: false}))} end end From 4ebf2a53b3379a0bfa1562dcae3a46791d8c59cd Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 30 Oct 2023 21:14:06 -1000 Subject: [PATCH 43/68] feat(helpers/validate): add function to check mutual exclusivity of body parameters --- lib/epochtalk_server_web/controllers/post.ex | 1 + lib/epochtalk_server_web/helpers/validate.ex | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/epochtalk_server_web/controllers/post.ex b/lib/epochtalk_server_web/controllers/post.ex index bb2db91f..a2b52be3 100644 --- a/lib/epochtalk_server_web/controllers/post.ex +++ b/lib/epochtalk_server_web/controllers/post.ex @@ -159,6 +159,7 @@ defmodule EpochtalkServerWeb.Controllers.Post do start <- Validate.cast(attrs, "start", :integer, min: 1), limit <- Validate.cast(attrs, "limit", :integer, default: 25, min: 1, max: 100), desc <- Validate.cast(attrs, "desc", :boolean, default: true), + :ok <- Validate.mutually_exclusive!(attrs, ["page", "start"]), user <- Guardian.Plug.current_resource(conn), user_priority <- ACL.get_user_priority(conn), diff --git a/lib/epochtalk_server_web/helpers/validate.ex b/lib/epochtalk_server_web/helpers/validate.ex index a9ee6f02..efddb588 100644 --- a/lib/epochtalk_server_web/helpers/validate.ex +++ b/lib/epochtalk_server_web/helpers/validate.ex @@ -6,6 +6,23 @@ defmodule EpochtalkServerWeb.Helpers.Validate do """ alias EpochtalkServerWeb.CustomErrors.InvalidPayload + @doc """ + Ensure that `keys` provided in list are mutually exlusive withing `attrs` map. + """ + @spec mutually_exclusive!(attrs :: map, keys :: [String.t()]) :: :ok | no_return + def mutually_exclusive!(attrs, keys) when is_map(attrs) and is_list(keys) do + contains_all_keys = + Enum.reduce(keys, true, fn key, acc -> acc && Map.has_key?(attrs, key) end) + + if contains_all_keys && length(keys) > 1, + do: + raise(InvalidPayload, + message: + "The following payload parameters cannot be passed at the same time: #{Enum.join(keys, ", ")}" + ), + else: :ok + end + @doc """ Helper used to validate and cast request parameters directly out of the incoming paylod map (usually a controller function's `attrs` parameter) to the specified type. From fc5e82807428dd62f6288810383b97f4171e72cc Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 30 Oct 2023 21:14:46 -1000 Subject: [PATCH 44/68] refactor(controller/post): update comment todos --- lib/epochtalk_server_web/controllers/post.ex | 1 - lib/epochtalk_server_web/controllers/thread.ex | 2 -- 2 files changed, 3 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/post.ex b/lib/epochtalk_server_web/controllers/post.ex index a2b52be3..11b9f8ba 100644 --- a/lib/epochtalk_server_web/controllers/post.ex +++ b/lib/epochtalk_server_web/controllers/post.ex @@ -151,7 +151,6 @@ defmodule EpochtalkServerWeb.Controllers.Post do """ # TODO(akinsey): Implement for completion # - parser - # - Implement guard in Validate that prevents passing in page and start at the same time def by_thread(conn, attrs) do # Parameter Validation with thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true), diff --git a/lib/epochtalk_server_web/controllers/thread.ex b/lib/epochtalk_server_web/controllers/thread.ex index 7e6c0110..be42f8cc 100644 --- a/lib/epochtalk_server_web/controllers/thread.ex +++ b/lib/epochtalk_server_web/controllers/thread.ex @@ -43,9 +43,7 @@ defmodule EpochtalkServerWeb.Controllers.Thread do # TODO(akinsey): Post pre processing, authorizations, image processing, hooks. Things to consider: # - does html sanitizer need to run on the front end too # - how do we run the same parser/sanitizer on the elixir back end as the node frontend - # - processing mentions # - does createImageReferences need to be called here? was this missing from the old code? - # - does updateUserActivity need to be called here? was this missing from the old code? def create(conn, attrs) do # authorization checks with :ok <- ACL.allow!(conn, "threads.create"), From e0ebdfd33cf59f4ee4c276b7c1813ae57e858c4f Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 30 Oct 2023 21:15:21 -1000 Subject: [PATCH 45/68] test(helper/validate): add doctest for mutually_exclusive! function --- lib/epochtalk_server_web/helpers/validate.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/epochtalk_server_web/helpers/validate.ex b/lib/epochtalk_server_web/helpers/validate.ex index efddb588..0906aac8 100644 --- a/lib/epochtalk_server_web/helpers/validate.ex +++ b/lib/epochtalk_server_web/helpers/validate.ex @@ -8,6 +8,20 @@ defmodule EpochtalkServerWeb.Helpers.Validate do @doc """ Ensure that `keys` provided in list are mutually exlusive withing `attrs` map. + + ## Example + iex> alias EpochtalkServerWeb.Helpers.Validate + iex> attrs = %{"page" => 1} + iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) + :ok + iex> attrs = %{"start" => 1} + iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) + :ok + iex> Validate.mutually_exclusive!(attrs, ["start"]) + :ok + iex> attrs = %{"page" => 1, "start" => 1} + iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) + ** (EpochtalkServerWeb.CustomErrors.InvalidPayload) The following payload parameters cannot be passed at the same time: page, start """ @spec mutually_exclusive!(attrs :: map, keys :: [String.t()]) :: :ok | no_return def mutually_exclusive!(attrs, keys) when is_map(attrs) and is_list(keys) do From 4b4f48abef505c3dcc27c4f04e6cf630442a0559 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 11:46:54 -0700 Subject: [PATCH 46/68] docs(board_moderator): fix spelling --- lib/epochtalk_server/models/board_moderator.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index 0abbc836..06c0d4ec 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -60,7 +60,7 @@ defmodule EpochtalkServer.Models.BoardModerator do do: Repo.all(from(b in BoardModerator, select: b.board_id, where: b.user_id == ^user_id)) @doc """ - Check if a specific `User` is moderater of a `Board` using a `Board` ID + Check if a specific `User` is moderator of a `Board` using a `Board` ID """ @spec user_is_moderator( board_id :: non_neg_integer, @@ -77,7 +77,7 @@ defmodule EpochtalkServer.Models.BoardModerator do end @doc """ - Check if a specific `User` is moderater of a `Board` using a `Thread` ID + Check if a specific `User` is moderator of a `Board` using a `Thread` ID """ @spec user_is_moderator_with_thread_id(thread_id :: non_neg_integer, user_id :: non_neg_integer) :: boolean @@ -95,7 +95,7 @@ defmodule EpochtalkServer.Models.BoardModerator do end @doc """ - Check if a specific `User` is moderater of a `Board` using a `Post` ID + Check if a specific `User` is moderator of a `Board` using a `Post` ID """ @spec user_is_moderator_with_post_id( post_id :: non_neg_integer, From d08d1a6327f31f42bf738a257fe0ab9cd1a4ad64 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 12:07:30 -0700 Subject: [PATCH 47/68] test(session): separate role/baninfo test blocks --- test/epochtalk_server/session_test.exs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 7b8e9577..79db9e7c 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -388,7 +388,9 @@ defmodule Test.EpochtalkServer.Session do {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) assert resource_user.avatar == updated_attrs.avatar end + end + describe "update/1 role" do @tag :authenticated test "given a valid user id, updates role to admin", %{conn: conn, authed_user: authed_user} do authed_user_role = List.first(authed_user.roles) @@ -419,7 +421,9 @@ defmodule Test.EpochtalkServer.Session do user_role = RoleCache.by_lookup("user") assert resource_user_role == user_role end + end + describe "update/1 baninfo" do @tag :authenticated test "given a not banned user's id, when user is banned, adds banned role", %{ conn: conn, From 51d37d1eafad5f01553966e6e9fcd8c957bd9a20 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 12:23:36 -0700 Subject: [PATCH 48/68] test(session): check moderating updates --- test/epochtalk_server/session_test.exs | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 79db9e7c..6b515068 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -16,6 +16,7 @@ defmodule Test.EpochtalkServer.Session do alias EpochtalkServer.Models.RoleUser alias EpochtalkServer.Cache.Role, as: RoleCache alias EpochtalkServer.Models.Ban + alias EpochtalkServer.Models.BoardModerator describe "get_resource/2" do test "when session_id is invalid, errors", %{users: %{user: user}} do @@ -498,6 +499,37 @@ defmodule Test.EpochtalkServer.Session do assert malicious_resource_user.malicious_score > 1 assert malicious_resource_user.ban_expiration == @max_date end + end + describe "update/1 moderating" do + @tag :authenticated + test "when a user is added/removed as moderator, updates moderating", %{ + conn: conn, + authed_user: authed_user + } do + # get session_id (jti) from conn + session_id = conn.private.guardian_default_claims["jti"] + # check that session user's moderating list is empty + {:ok, resource_user} = Session.get_resource(authed_user.id, session_id) + assert Map.get(resource_user, :moderating) == nil + + # create board and add user as moderator + board = insert(:board) + BoardModerator.add_moderators_by_username(board.id, [authed_user.username]) + + # check session user updates with moderating + Session.update(authed_user.id) + {:ok, moderator_resource_user} = Session.get_resource(authed_user.id, session_id) + # check moderating list contains new board + assert Map.get(moderator_resource_user, :moderating) == [Integer.to_string(board.id)] + + # remove user as moderator + BoardModerator.remove_moderators_by_username(board.id, [authed_user.username]) + + # check session user updates with moderating + Session.update(authed_user.id) + {:ok, moderator_resource_user} = Session.get_resource(authed_user.id, session_id) + # check moderating list contains new board + assert Map.get(moderator_resource_user, :moderating) == nil end end From 8a13d992d572ddfa43de8a414eb5912a7ef67f3c Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 12:34:47 -0700 Subject: [PATCH 49/68] style(banned_address): mix format --- lib/epochtalk_server/models/banned_address.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index 0bbb234b..95b11ff0 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -233,7 +233,10 @@ defmodule EpochtalkServer.Models.BannedAddress do ip24_score = calculate_ip24_score(ip) ip16_score = calculate_ip16_score(ip) # calculate malicious score using all scores - malicious_score = hostname_score + (@ip32_weight * ip32_score) + (@ip24_weight * ip24_score) + (@ip16_weight * ip16_score) + malicious_score = + hostname_score + @ip32_weight * ip32_score + @ip24_weight * ip24_score + + @ip16_weight * ip16_score + if malicious_score < 1, do: nil, else: malicious_score # invalid ip address, return nil for malicious score @@ -318,7 +321,9 @@ defmodule EpochtalkServer.Models.BannedAddress do defp decay_for_time(time, weight) do weight = Decimal.to_float(weight) weeks = time / @one_week_in_ms - @inital_amount ** ((@rate_of_decay ** weeks - 1) / (@rate_of_decay - 1)) * weight ** (@rate_of_decay ** weeks) + + @inital_amount ** ((@rate_of_decay ** weeks - 1) / (@rate_of_decay - 1)) * + weight ** (@rate_of_decay ** weeks) end # returns the decayed score given a banned address From 922dbc9397b2d4a53bc15557990d031bf352eba5 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 12:35:20 -0700 Subject: [PATCH 50/68] refactor(banned_address): move ip weight to variable set cleans up implementation, add documentation comment --- lib/epochtalk_server/models/banned_address.ex | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index 95b11ff0..e11b5ffb 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -229,13 +229,12 @@ defmodule EpochtalkServer.Models.BannedAddress do 0 end - ip32_score = calculate_ip32_score(ip) - ip24_score = calculate_ip24_score(ip) - ip16_score = calculate_ip16_score(ip) + # calculate ip scores with weight constants + ip32_score = calculate_ip32_score(ip) * @ip32_weight + ip24_score = calculate_ip24_score(ip) * @ip24_weight + ip16_score = calculate_ip16_score(ip) * @ip16_weight # calculate malicious score using all scores - malicious_score = - hostname_score + @ip32_weight * ip32_score + @ip24_weight * ip24_score + - @ip16_weight * ip16_score + malicious_score = hostname_score + ip32_score + ip24_score + ip16_score if malicious_score < 1, do: nil, else: malicious_score From e750f253d857c51b5e45008a3e7657f62422bb4f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 12:46:58 -0700 Subject: [PATCH 51/68] test(session): mix format --- test/epochtalk_server/session_test.exs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 6b515068..e8467489 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -467,15 +467,19 @@ defmodule Test.EpochtalkServer.Session do assert Enum.any?(unbanned_resource_user.roles, &(&1.lookup == "banned")) == false # check ban is expired now = NaiveDateTime.utc_now() - {:ok, ban_expiration} = unbanned_resource_user.ban_expiration |> NaiveDateTime.from_iso8601() + + {:ok, ban_expiration} = + unbanned_resource_user.ban_expiration |> NaiveDateTime.from_iso8601() + assert NaiveDateTime.compare(ban_expiration, now) == :lt end @tag :authenticated - test "given a not malicious user's id, when user is marked malicious, adds malicious score", %{ - conn: conn, - authed_user: authed_user - } do + test "given a not malicious user's id, when user is marked malicious, adds malicious score", + %{ + conn: conn, + authed_user: authed_user + } do # get session_id (jti) from conn session_id = conn.private.guardian_default_claims["jti"] # check that session user does not have banned role @@ -500,6 +504,7 @@ defmodule Test.EpochtalkServer.Session do assert malicious_resource_user.ban_expiration == @max_date end end + describe "update/1 moderating" do @tag :authenticated test "when a user is added/removed as moderator, updates moderating", %{ From 7e71ca6ffc38ca9d7b8f5a3683d927b5926a5f0d Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 31 Oct 2023 09:56:21 -1000 Subject: [PATCH 52/68] docs(helpers/validate): fix spelling errors in docs --- lib/epochtalk_server_web/helpers/validate.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/helpers/validate.ex b/lib/epochtalk_server_web/helpers/validate.ex index 0906aac8..36729f77 100644 --- a/lib/epochtalk_server_web/helpers/validate.ex +++ b/lib/epochtalk_server_web/helpers/validate.ex @@ -7,7 +7,7 @@ defmodule EpochtalkServerWeb.Helpers.Validate do alias EpochtalkServerWeb.CustomErrors.InvalidPayload @doc """ - Ensure that `keys` provided in list are mutually exlusive withing `attrs` map. + Ensure that `keys` provided in list are mutually exclusive within `attrs` map. ## Example iex> alias EpochtalkServerWeb.Helpers.Validate From da3ed39f5a8c7b28de6ec9bd92a9cdcd5911fce5 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 13:17:59 -0700 Subject: [PATCH 53/68] test(helpers/validate): move docs to test (for testing later) --- lib/epochtalk_server_web/helpers/validate.ex | 14 -------------- .../helpers/validate_test.exs | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/epochtalk_server_web/helpers/validate.ex b/lib/epochtalk_server_web/helpers/validate.ex index 36729f77..42d3321e 100644 --- a/lib/epochtalk_server_web/helpers/validate.ex +++ b/lib/epochtalk_server_web/helpers/validate.ex @@ -8,20 +8,6 @@ defmodule EpochtalkServerWeb.Helpers.Validate do @doc """ Ensure that `keys` provided in list are mutually exclusive within `attrs` map. - - ## Example - iex> alias EpochtalkServerWeb.Helpers.Validate - iex> attrs = %{"page" => 1} - iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) - :ok - iex> attrs = %{"start" => 1} - iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) - :ok - iex> Validate.mutually_exclusive!(attrs, ["start"]) - :ok - iex> attrs = %{"page" => 1, "start" => 1} - iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) - ** (EpochtalkServerWeb.CustomErrors.InvalidPayload) The following payload parameters cannot be passed at the same time: page, start """ @spec mutually_exclusive!(attrs :: map, keys :: [String.t()]) :: :ok | no_return def mutually_exclusive!(attrs, keys) when is_map(attrs) and is_list(keys) do diff --git a/test/epochtalk_server_web/helpers/validate_test.exs b/test/epochtalk_server_web/helpers/validate_test.exs index 6276141f..b8f0dd89 100644 --- a/test/epochtalk_server_web/helpers/validate_test.exs +++ b/test/epochtalk_server_web/helpers/validate_test.exs @@ -2,4 +2,20 @@ defmodule Test.EpochtalkServerWeb.Helpers.Validate do use Test.Support.ConnCase, async: true alias EpochtalkServerWeb.Helpers.Validate doctest Validate + + test "mutually_exclusive/2" do + ## Example + iex> alias EpochtalkServerWeb.Helpers.Validate + iex> attrs = %{"page" => 1} + iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) + :ok + iex> attrs = %{"start" => 1} + iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) + :ok + iex> Validate.mutually_exclusive!(attrs, ["start"]) + :ok + iex> attrs = %{"page" => 1, "start" => 1} + iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) + ** (EpochtalkServerWeb.CustomErrors.InvalidPayload) The following payload parameters cannot be passed at the same time: page, start + end end From 6ed9ede4f30ec4c951daf00155a1305c219c5526 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 14:23:52 -0700 Subject: [PATCH 54/68] test(helpers/validate): test mutually exclusive with several cases --- .../helpers/validate_test.exs | 62 ++++++++++++++----- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/test/epochtalk_server_web/helpers/validate_test.exs b/test/epochtalk_server_web/helpers/validate_test.exs index b8f0dd89..a8c7c786 100644 --- a/test/epochtalk_server_web/helpers/validate_test.exs +++ b/test/epochtalk_server_web/helpers/validate_test.exs @@ -1,21 +1,55 @@ defmodule Test.EpochtalkServerWeb.Helpers.Validate do use Test.Support.ConnCase, async: true alias EpochtalkServerWeb.Helpers.Validate + alias EpochtalkServerWeb.CustomErrors.InvalidPayload doctest Validate - test "mutually_exclusive/2" do - ## Example - iex> alias EpochtalkServerWeb.Helpers.Validate - iex> attrs = %{"page" => 1} - iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) - :ok - iex> attrs = %{"start" => 1} - iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) - :ok - iex> Validate.mutually_exclusive!(attrs, ["start"]) - :ok - iex> attrs = %{"page" => 1, "start" => 1} - iex> Validate.mutually_exclusive!(attrs, ["page", "start"]) - ** (EpochtalkServerWeb.CustomErrors.InvalidPayload) The following payload parameters cannot be passed at the same time: page, start + describe "mutually_exclusive/2" do + test "given cases, returns correct result" do + cases = + [ + %{ + attrs: %{"page" => 1}, + keys: ["page", "start"], + result: :ok + }, + %{ + attrs: %{"start" => 1}, + keys: ["page", "start"], + result: :ok + }, + %{ + attrs: %{"start" => 1}, + keys: ["start"], + result: :ok + }, + %{ + attrs: %{"page" => 1, "start" => 1}, + keys: ["page", "start"], + result: :error + }, + %{ + attrs: %{"page" => 1, "start" => 1, "stop" => 1}, + keys: ["page", "start"], + result: :error + }, + %{ + attrs: %{"page" => 1, "stop" => 1}, + keys: ["page", "start", "stop", "enter"], + result: :error + } + ] + + cases |> Enum.each(fn + %{attrs: attrs, keys: keys, result: :error} -> + assert_raise InvalidPayload, + ~r/^The following payload parameters cannot be passed at the same time:/, + fn -> + Validate.mutually_exclusive!(attrs, keys) + end + %{attrs: attrs, keys: keys, result: :ok} -> + assert Validate.mutually_exclusive!(attrs, keys) == :ok + end) + end end end From 3913b9a21bf2271f8361288357c17efdde3dd9e0 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 14:24:32 -0700 Subject: [PATCH 55/68] fix(helpers/validate): correctly implement mutually_exclusive needs to check if attrs contains any two keys, not all keys implement map_contains_any_two_keys_in_list uses guard clause for checking empty map (a match on %{} matches any map) use map_contains_any_two_keys_in_list --- lib/epochtalk_server_web/helpers/validate.ex | 24 ++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server_web/helpers/validate.ex b/lib/epochtalk_server_web/helpers/validate.ex index 42d3321e..b4bc0e13 100644 --- a/lib/epochtalk_server_web/helpers/validate.ex +++ b/lib/epochtalk_server_web/helpers/validate.ex @@ -11,10 +11,7 @@ defmodule EpochtalkServerWeb.Helpers.Validate do """ @spec mutually_exclusive!(attrs :: map, keys :: [String.t()]) :: :ok | no_return def mutually_exclusive!(attrs, keys) when is_map(attrs) and is_list(keys) do - contains_all_keys = - Enum.reduce(keys, true, fn key, acc -> acc && Map.has_key?(attrs, key) end) - - if contains_all_keys && length(keys) > 1, + if map_contains_any_two_keys_in_list?(attrs, keys), do: raise(InvalidPayload, message: @@ -138,6 +135,25 @@ defmodule EpochtalkServerWeb.Helpers.Validate do end end + # entrypoint + defp map_contains_any_two_keys_in_list?(map, list), do: map_contains_any_two_keys_in_list?(map, list, false) + # if map is empty, return false + defp map_contains_any_two_keys_in_list?(map, _list, _key_found?) when map == %{}, do: false + # if list is empty, return false + defp map_contains_any_two_keys_in_list?(_map, [], _key_found?), do: false + # if key_found? is false + defp map_contains_any_two_keys_in_list?(map, [key | keys] = _list, false = _key_found?) do + # check next key with updated key_found? + map_contains_any_two_keys_in_list?(map, keys, Map.has_key?(map, key)) + end + # if key_found? is true + defp map_contains_any_two_keys_in_list?(map, [key | keys] = _list, true = key_found?) do + # if current key is in map, return true + if Map.has_key?(map, key), do: true, + # otherwise, check next key + else: map_contains_any_two_keys_in_list?(map, keys, key_found?) + end + defp to_bool(str, opts) do case str do "true" -> true From b75855beabd2a11e4bd3e512c69ec696286c1007 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 14:27:16 -0700 Subject: [PATCH 56/68] style(helpers/validate): mix format --- lib/epochtalk_server_web/helpers/validate.ex | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server_web/helpers/validate.ex b/lib/epochtalk_server_web/helpers/validate.ex index b4bc0e13..e9ecfb5e 100644 --- a/lib/epochtalk_server_web/helpers/validate.ex +++ b/lib/epochtalk_server_web/helpers/validate.ex @@ -136,7 +136,9 @@ defmodule EpochtalkServerWeb.Helpers.Validate do end # entrypoint - defp map_contains_any_two_keys_in_list?(map, list), do: map_contains_any_two_keys_in_list?(map, list, false) + defp map_contains_any_two_keys_in_list?(map, list), + do: map_contains_any_two_keys_in_list?(map, list, false) + # if map is empty, return false defp map_contains_any_two_keys_in_list?(map, _list, _key_found?) when map == %{}, do: false # if list is empty, return false @@ -146,12 +148,14 @@ defmodule EpochtalkServerWeb.Helpers.Validate do # check next key with updated key_found? map_contains_any_two_keys_in_list?(map, keys, Map.has_key?(map, key)) end + # if key_found? is true defp map_contains_any_two_keys_in_list?(map, [key | keys] = _list, true = key_found?) do # if current key is in map, return true - if Map.has_key?(map, key), do: true, - # otherwise, check next key - else: map_contains_any_two_keys_in_list?(map, keys, key_found?) + if Map.has_key?(map, key), + do: true, + # otherwise, check next key + else: map_contains_any_two_keys_in_list?(map, keys, key_found?) end defp to_bool(str, opts) do From 02ec3ed90a4ba6444b2e8b2c449d93b11751b84f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 31 Oct 2023 14:27:30 -0700 Subject: [PATCH 57/68] test(helpers/validate): mix format --- .../helpers/validate_test.exs | 77 ++++++++++--------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/test/epochtalk_server_web/helpers/validate_test.exs b/test/epochtalk_server_web/helpers/validate_test.exs index a8c7c786..cf02f7ee 100644 --- a/test/epochtalk_server_web/helpers/validate_test.exs +++ b/test/epochtalk_server_web/helpers/validate_test.exs @@ -6,47 +6,48 @@ defmodule Test.EpochtalkServerWeb.Helpers.Validate do describe "mutually_exclusive/2" do test "given cases, returns correct result" do - cases = - [ - %{ - attrs: %{"page" => 1}, - keys: ["page", "start"], - result: :ok - }, - %{ - attrs: %{"start" => 1}, - keys: ["page", "start"], - result: :ok - }, - %{ - attrs: %{"start" => 1}, - keys: ["start"], - result: :ok - }, - %{ - attrs: %{"page" => 1, "start" => 1}, - keys: ["page", "start"], - result: :error - }, - %{ - attrs: %{"page" => 1, "start" => 1, "stop" => 1}, - keys: ["page", "start"], - result: :error - }, - %{ - attrs: %{"page" => 1, "stop" => 1}, - keys: ["page", "start", "stop", "enter"], - result: :error - } - ] + cases = [ + %{ + attrs: %{"page" => 1}, + keys: ["page", "start"], + result: :ok + }, + %{ + attrs: %{"start" => 1}, + keys: ["page", "start"], + result: :ok + }, + %{ + attrs: %{"start" => 1}, + keys: ["start"], + result: :ok + }, + %{ + attrs: %{"page" => 1, "start" => 1}, + keys: ["page", "start"], + result: :error + }, + %{ + attrs: %{"page" => 1, "start" => 1, "stop" => 1}, + keys: ["page", "start"], + result: :error + }, + %{ + attrs: %{"page" => 1, "stop" => 1}, + keys: ["page", "start", "stop", "enter"], + result: :error + } + ] - cases |> Enum.each(fn + cases + |> Enum.each(fn %{attrs: attrs, keys: keys, result: :error} -> assert_raise InvalidPayload, - ~r/^The following payload parameters cannot be passed at the same time:/, - fn -> - Validate.mutually_exclusive!(attrs, keys) - end + ~r/^The following payload parameters cannot be passed at the same time:/, + fn -> + Validate.mutually_exclusive!(attrs, keys) + end + %{attrs: attrs, keys: keys, result: :ok} -> assert Validate.mutually_exclusive!(attrs, keys) == :ok end) From 54ee77475f437ae717c3dc64ca65edafc3dfe192 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 2 Nov 2023 14:24:27 -0700 Subject: [PATCH 58/68] test(seed+conn_case): seed private user and prepare in conn_case can be used for checking acl's (private user can only access motd) --- test/seed/users.exs | 11 +++++++++++ test/support/conn_case.ex | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/test/seed/users.exs b/test/seed/users.exs index 88aa2f65..45c96449 100644 --- a/test/seed/users.exs +++ b/test/seed/users.exs @@ -12,6 +12,10 @@ test_user_username = "user" test_user_email = "user@test.com" test_user_password = "password" +test_private_user_username = "private" +test_private_user_email = "private@test.com" +test_private_user_password = "password" + test_no_login_user_username = "no_login" test_no_login_user_email = "no_login@test.com" test_no_login_user_password = "password" @@ -36,6 +40,13 @@ build(:user, password: test_user_password ) +build(:user, + username: test_private_user_username, + email: test_private_user_email, + password: test_private_user_password +) +|> with_role_id(10) + build(:user, username: test_no_login_user_username, email: test_no_login_user_email, diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 26cff653..8c1372e1 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -25,6 +25,16 @@ defmodule Test.Support.ConnCase do password: @test_no_login_password } + # private username/email/password from user seed in `mix test` (see mix.exs) + @test_private_username "private" + @test_private_email "private@test.com" + @test_private_password "password" + @test_private_user_attrs %{ + username: @test_private_username, + email: @test_private_email, + password: @test_private_password + } + # username/email/password from user seed in `mix test` (see mix.exs) @test_username "user" @test_email "user@test.com" @@ -86,6 +96,7 @@ defmodule Test.Support.ConnCase do end {:ok, no_login_user} = User.by_username(@test_no_login_username) + {:ok, private_user} = User.by_username(@test_private_username) {:ok, user} = User.by_username(@test_username) {:ok, admin_user} = User.by_username(@test_admin_username) {:ok, super_admin_user} = User.by_username(@test_super_admin_username) @@ -96,12 +107,14 @@ defmodule Test.Support.ConnCase do [ users: %{ no_login_user: no_login_user, + private_user: private_user, user: user, admin_user: admin_user, super_admin_user: super_admin_user }, user_attrs: %{ no_login_user: @test_no_login_user_attrs, + private_user: @test_private_user_attrs, user: @test_user_attrs, admin_user: @test_admin_user_attrs, super_admin_user: @test_super_admin_user_attrs @@ -122,6 +135,17 @@ defmodule Test.Support.ConnCase do k_list = [authed_user_attrs: @test_user_attrs] ++ k_list {:ok, k_list} + :private -> + remember_me = false + {:ok, private_user, token, authed_conn} = Session.create(private_user, remember_me, conn) + + {:ok, k_list} = context_updates + k_list = [conn: authed_conn] ++ k_list + k_list = [authed_user: private_user] ++ k_list + k_list = [token: token] ++ k_list + k_list = [authed_user_attrs: @test_private_user_attrs] ++ k_list + {:ok, k_list} + :admin -> remember_me = false {:ok, admin_user, token, authed_conn} = Session.create(admin_user, remember_me, conn) From f2364d59a251bf3e6282af001a9cef76010d7e59 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 2 Nov 2023 14:27:47 -0700 Subject: [PATCH 59/68] test(controllers/board): check by_category errors on invalid permissions --- test/epochtalk_server_web/controllers/board_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/epochtalk_server_web/controllers/board_test.exs b/test/epochtalk_server_web/controllers/board_test.exs index 66a8878f..64d5430b 100644 --- a/test/epochtalk_server_web/controllers/board_test.exs +++ b/test/epochtalk_server_web/controllers/board_test.exs @@ -30,6 +30,14 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do end describe "by_category/2" do + @tag authenticated: :private + test "when authenticated with invalid permissions, raises InvalidPermission error", %{conn: conn} do + assert_raise InvalidPermission, + ~r/^Forbidden, invalid permissions to perform this action/, + fn -> + get(conn, Routes.board_path(conn, :by_category)) + end + end test "finds all active boards", %{ conn: conn, category: category, From 12f4d80fa8118c52a37fb2b12834a4407f93ce73 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 2 Nov 2023 14:34:05 -0700 Subject: [PATCH 60/68] test(controllers/board): check invalid board read access for unauthenticated user --- test/epochtalk_server_web/controllers/board_test.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/epochtalk_server_web/controllers/board_test.exs b/test/epochtalk_server_web/controllers/board_test.exs index 64d5430b..965b42a0 100644 --- a/test/epochtalk_server_web/controllers/board_test.exs +++ b/test/epochtalk_server_web/controllers/board_test.exs @@ -138,6 +138,16 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do assert response["message"] == "Error, board does not exist" end + test "when unauthenticated, given an existing id above read access, errors", %{conn: conn, admin_board: admin_board} do + response = + conn + |> get(Routes.board_path(conn, :find, admin_board.id)) + |> json_response(404) + + assert response["error"] == "Not Found" + assert response["message"] == "Board not found" + end + test "given an existing id, finds a board", %{conn: conn, parent_board: board} do response = conn From 241e51046fe55d7bad5a37ba2eb9b66350b8b201 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 2 Nov 2023 15:23:03 -0700 Subject: [PATCH 61/68] test(controllers/board): update unauthenticated test description --- test/epochtalk_server_web/controllers/board_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/epochtalk_server_web/controllers/board_test.exs b/test/epochtalk_server_web/controllers/board_test.exs index 965b42a0..db374aa7 100644 --- a/test/epochtalk_server_web/controllers/board_test.exs +++ b/test/epochtalk_server_web/controllers/board_test.exs @@ -148,7 +148,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do assert response["message"] == "Board not found" end - test "given an existing id, finds a board", %{conn: conn, parent_board: board} do + test "when unauthenticated, given an existing id within read access, finds a board", %{conn: conn, parent_board: board} do response = conn |> get(Routes.board_path(conn, :find, board.id)) From 46e87d05d61b1d0daf88f14de282c587bf88131e Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 2 Nov 2023 15:49:07 -0700 Subject: [PATCH 62/68] test(controllers/board): check board find when authenticated --- .../controllers/board_test.exs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/epochtalk_server_web/controllers/board_test.exs b/test/epochtalk_server_web/controllers/board_test.exs index db374aa7..4fa00b2d 100644 --- a/test/epochtalk_server_web/controllers/board_test.exs +++ b/test/epochtalk_server_web/controllers/board_test.exs @@ -164,6 +164,27 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do assert response["disable_post_edit"] == board.meta["disable_post_edit"] assert response["disable_signature"] == board.meta["disable_signature"] end + + @tag :authenticated + test "when authenticated, given an existing id above read access, errors", %{conn: conn, admin_board: admin_board} do + response = + conn + |> get(Routes.board_path(conn, :find, admin_board.id)) + |> json_response(404) + + assert response["error"] == "Not Found" + assert response["message"] == "Board not found" + end + + @tag :authenticated + test "when authenticated, given an existing id at read access, finds board", %{conn: conn, parent_board: board} do + response = + conn + |> get(Routes.board_path(conn, :find, board.id)) + |> json_response(200) + + assert response["name"] == board.name + end end describe "slug_to_id/2" do From 95ed14eef5bef5348210507fe3748d99762bdd30 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 2 Nov 2023 15:49:44 -0700 Subject: [PATCH 63/68] test(controllers/board): check board find when authenticated as admin --- .../controllers/board_test.exs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/epochtalk_server_web/controllers/board_test.exs b/test/epochtalk_server_web/controllers/board_test.exs index 4fa00b2d..df83902c 100644 --- a/test/epochtalk_server_web/controllers/board_test.exs +++ b/test/epochtalk_server_web/controllers/board_test.exs @@ -185,6 +185,27 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do assert response["name"] == board.name end + + @tag authenticated: :admin + test "when authenticated as admin, given an existing id at read access, finds board", %{conn: conn, admin_board: admin_board} do + response = + conn + |> get(Routes.board_path(conn, :find, admin_board.id)) + |> json_response(200) + + assert response["name"] == admin_board.name + end + + @tag authenticated: :admin + test "when authenticated as admin, given an existing id above read access, errors", %{conn: conn, super_admin_board: super_admin_board} do + response = + conn + |> get(Routes.board_path(conn, :find, super_admin_board.id)) + |> json_response(404) + + assert response["error"] == "Not Found" + assert response["message"] == "Board not found" + end end describe "slug_to_id/2" do From 55a1e6c5c488ba5a5ebf8042d96516e1d34d44e7 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 2 Nov 2023 15:52:01 -0700 Subject: [PATCH 64/68] test(controllers/board): test super admin board find --- test/epochtalk_server_web/controllers/board_test.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/epochtalk_server_web/controllers/board_test.exs b/test/epochtalk_server_web/controllers/board_test.exs index df83902c..122f3273 100644 --- a/test/epochtalk_server_web/controllers/board_test.exs +++ b/test/epochtalk_server_web/controllers/board_test.exs @@ -206,6 +206,16 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do assert response["error"] == "Not Found" assert response["message"] == "Board not found" end + + @tag authenticated: :super_admin + test "when authenticated as super admin, given an existing id at read access, finds board", %{conn: conn, super_admin_board: super_admin_board} do + response = + conn + |> get(Routes.board_path(conn, :find, super_admin_board.id)) + |> json_response(200) + + assert response["name"] == super_admin_board.name + end end describe "slug_to_id/2" do From 3791a0961c76e565bf2b6a9e8eccfc01378bf160 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 3 Nov 2023 13:50:05 -0700 Subject: [PATCH 65/68] refactor(session): explain redis TTL special values, -1, -2 --- lib/epochtalk_server/session.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index d66feb35..26272e4f 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,6 +1,9 @@ defmodule EpochtalkServer.Session do @one_day_in_seconds 1 * 24 * 60 * 60 @four_weeks_in_seconds 4 * 7 * @one_day_in_seconds + # in redis, if a key exists and has no expiration, its TTL value is -1 + # if the key does not exist, its TTL value is -2 + @redis_ttl_no_expire_with_key -1 @moduledoc """ Manages `User` sessions in Redis. Used by Auth related `User` actions. @@ -385,11 +388,11 @@ defmodule EpochtalkServer.Session do # - or @four_weeks_in_seconds if old and new ttl's are invalid defp maybe_extend_ttl(key, new_ttl, old_ttl) do cond do - old_ttl > -1 -> + old_ttl > @redis_ttl_no_expire_with_key -> # re-set old expiry only if old expiry was valid and key has no expiry Redix.command(:redix, ["EXPIRE", key, old_ttl, "NX"]) - new_ttl > -1 -> + new_ttl > @redis_ttl_no_expire_with_key -> # if old expiry was invalid, set new expiry only if new expiry is valid and key has no expiry Redix.command(:redix, ["EXPIRE", key, new_ttl, "NX"]) From 57bc64d085bd80faf291f8ebb8076ba18468e682 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 6 Nov 2023 12:27:11 -0800 Subject: [PATCH 66/68] test(session): use new banned role in test --- test/epochtalk_server/session_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 2ab6b650..48130c9a 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -447,7 +447,7 @@ defmodule Test.EpochtalkServer.Session do assert banned_resource_user.ban_expiration == @max_date end - @tag [authenticated: true, banned: true] + @tag [authenticated: :banned, banned: true] test "given a banned user's id, when user is unbanned, deletes banned role", %{ conn: conn, authed_user: authed_user From 5855fd9f6f93dc2722a43e05540e1067e8a49b23 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 6 Nov 2023 12:40:56 -0800 Subject: [PATCH 67/68] test(controllers/board): mix format --- .../controllers/board_test.exs | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/test/epochtalk_server_web/controllers/board_test.exs b/test/epochtalk_server_web/controllers/board_test.exs index 122f3273..7e403c06 100644 --- a/test/epochtalk_server_web/controllers/board_test.exs +++ b/test/epochtalk_server_web/controllers/board_test.exs @@ -31,13 +31,16 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do describe "by_category/2" do @tag authenticated: :private - test "when authenticated with invalid permissions, raises InvalidPermission error", %{conn: conn} do + test "when authenticated with invalid permissions, raises InvalidPermission error", %{ + conn: conn + } do assert_raise InvalidPermission, ~r/^Forbidden, invalid permissions to perform this action/, fn -> get(conn, Routes.board_path(conn, :by_category)) end end + test "finds all active boards", %{ conn: conn, category: category, @@ -138,7 +141,10 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do assert response["message"] == "Error, board does not exist" end - test "when unauthenticated, given an existing id above read access, errors", %{conn: conn, admin_board: admin_board} do + test "when unauthenticated, given an existing id above read access, errors", %{ + conn: conn, + admin_board: admin_board + } do response = conn |> get(Routes.board_path(conn, :find, admin_board.id)) @@ -148,7 +154,10 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do assert response["message"] == "Board not found" end - test "when unauthenticated, given an existing id within read access, finds a board", %{conn: conn, parent_board: board} do + test "when unauthenticated, given an existing id within read access, finds a board", %{ + conn: conn, + parent_board: board + } do response = conn |> get(Routes.board_path(conn, :find, board.id)) @@ -166,7 +175,10 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do end @tag :authenticated - test "when authenticated, given an existing id above read access, errors", %{conn: conn, admin_board: admin_board} do + test "when authenticated, given an existing id above read access, errors", %{ + conn: conn, + admin_board: admin_board + } do response = conn |> get(Routes.board_path(conn, :find, admin_board.id)) @@ -177,7 +189,10 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do end @tag :authenticated - test "when authenticated, given an existing id at read access, finds board", %{conn: conn, parent_board: board} do + test "when authenticated, given an existing id at read access, finds board", %{ + conn: conn, + parent_board: board + } do response = conn |> get(Routes.board_path(conn, :find, board.id)) @@ -187,7 +202,10 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do end @tag authenticated: :admin - test "when authenticated as admin, given an existing id at read access, finds board", %{conn: conn, admin_board: admin_board} do + test "when authenticated as admin, given an existing id at read access, finds board", %{ + conn: conn, + admin_board: admin_board + } do response = conn |> get(Routes.board_path(conn, :find, admin_board.id)) @@ -197,7 +215,10 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do end @tag authenticated: :admin - test "when authenticated as admin, given an existing id above read access, errors", %{conn: conn, super_admin_board: super_admin_board} do + test "when authenticated as admin, given an existing id above read access, errors", %{ + conn: conn, + super_admin_board: super_admin_board + } do response = conn |> get(Routes.board_path(conn, :find, super_admin_board.id)) @@ -208,7 +229,10 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do end @tag authenticated: :super_admin - test "when authenticated as super admin, given an existing id at read access, finds board", %{conn: conn, super_admin_board: super_admin_board} do + test "when authenticated as super admin, given an existing id at read access, finds board", %{ + conn: conn, + super_admin_board: super_admin_board + } do response = conn |> get(Routes.board_path(conn, :find, super_admin_board.id)) From fe38cd625221082623b4156b46027a692b5ed01b Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 6 Nov 2023 15:17:40 -0800 Subject: [PATCH 68/68] test(support/conn_case): fix merge --- test/support/conn_case.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 90dbdedf..7afb0368 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -155,8 +155,6 @@ defmodule Test.Support.ConnCase do Ecto.Adapters.SQL.Sandbox.mode(EpochtalkServer.Repo, {:shared, self()}) end - {:ok, user} = User.by_username(@test_username) - {:ok, admin_user} = User.by_username(@test_admin_username) {:ok, super_admin_user} = User.by_username(@test_super_admin_username) {:ok, admin_user} = User.by_username(@test_admin_username) {:ok, global_mod_user} = User.by_username(@test_global_mod_username)