more tests
This commit is contained in:
parent
ecf6edaf85
commit
50257dbe68
4 changed files with 369 additions and 3 deletions
|
|
@ -72,10 +72,10 @@ defmodule Towerops.Result do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> Result.and_then({:ok, user}, fn u -> {:ok, u.email} end)
|
||||
{:ok, "user@example.com"}
|
||||
iex> Result.and_then({:ok, 10}, fn x -> {:ok, x * 2} end)
|
||||
{:ok, 20}
|
||||
|
||||
iex> Result.and_then({:error, :not_found}, fn u -> {:ok, u.email} end)
|
||||
iex> Result.and_then({:error, :not_found}, fn x -> {:ok, x * 2} end)
|
||||
{:error, :not_found}
|
||||
"""
|
||||
@spec and_then(t(a, e), (a -> t(b, e))) :: t(b, e) when a: var, b: var, e: var
|
||||
|
|
|
|||
172
test/towerops/result_test.exs
Normal file
172
test/towerops/result_test.exs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
defmodule Towerops.ResultTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Towerops.Result
|
||||
|
||||
doctest Result
|
||||
|
||||
describe "map/2" do
|
||||
test "applies function to success value" do
|
||||
assert Result.map({:ok, 5}, fn x -> x * 2 end) == {:ok, 10}
|
||||
assert Result.map({:ok, "hello"}, &String.upcase/1) == {:ok, "HELLO"}
|
||||
end
|
||||
|
||||
test "passes through error unchanged" do
|
||||
assert Result.map({:error, :not_found}, fn x -> x * 2 end) == {:error, :not_found}
|
||||
assert Result.map({:error, "failure"}, &String.upcase/1) == {:error, "failure"}
|
||||
end
|
||||
|
||||
test "works with complex transformations" do
|
||||
result = {:ok, %{name: "John", age: 30}}
|
||||
assert Result.map(result, fn user -> user.name end) == {:ok, "John"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "map_error/2" do
|
||||
test "applies function to error value" do
|
||||
assert Result.map_error({:error, :timeout}, fn _ -> :network_error end) ==
|
||||
{:error, :network_error}
|
||||
|
||||
assert Result.map_error({:error, "db_error"}, &String.upcase/1) == {:error, "DB_ERROR"}
|
||||
end
|
||||
|
||||
test "passes through success unchanged" do
|
||||
assert Result.map_error({:ok, 42}, fn _ -> :network_error end) == {:ok, 42}
|
||||
assert Result.map_error({:ok, "success"}, &String.upcase/1) == {:ok, "success"}
|
||||
end
|
||||
|
||||
test "works with complex error transformations" do
|
||||
result = {:error, %{code: 500, message: "Internal Error"}}
|
||||
|
||||
assert Result.map_error(result, fn err -> err.code end) == {:error, 500}
|
||||
end
|
||||
end
|
||||
|
||||
describe "and_then/2" do
|
||||
test "chains successful operations" do
|
||||
result = {:ok, 5}
|
||||
|
||||
assert Result.and_then(result, fn x -> {:ok, x * 2} end) == {:ok, 10}
|
||||
end
|
||||
|
||||
test "stops on first error" do
|
||||
result = {:error, :not_found}
|
||||
|
||||
assert Result.and_then(result, fn x -> {:ok, x * 2} end) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "propagates error from chained function" do
|
||||
result = {:ok, 5}
|
||||
|
||||
assert Result.and_then(result, fn _ -> {:error, :invalid} end) == {:error, :invalid}
|
||||
end
|
||||
|
||||
test "works with nested operations" do
|
||||
result = {:ok, 10}
|
||||
|
||||
final_result =
|
||||
result
|
||||
|> Result.and_then(fn x -> {:ok, x + 5} end)
|
||||
|> Result.and_then(fn x -> {:ok, x * 2} end)
|
||||
|> Result.and_then(fn x -> {:ok, "Result: #{x}"} end)
|
||||
|
||||
assert final_result == {:ok, "Result: 30"}
|
||||
end
|
||||
|
||||
test "short-circuits on error in chain" do
|
||||
result = {:ok, 10}
|
||||
|
||||
final_result =
|
||||
result
|
||||
|> Result.and_then(fn x -> {:ok, x + 5} end)
|
||||
|> Result.and_then(fn _ -> {:error, :failed} end)
|
||||
|> Result.and_then(fn x -> {:ok, x * 2} end)
|
||||
|
||||
assert final_result == {:error, :failed}
|
||||
end
|
||||
end
|
||||
|
||||
describe "unwrap_or/2" do
|
||||
test "returns success value when ok" do
|
||||
assert Result.unwrap_or({:ok, 42}, 0) == 42
|
||||
assert Result.unwrap_or({:ok, "hello"}, "default") == "hello"
|
||||
end
|
||||
|
||||
test "returns default value on error" do
|
||||
assert Result.unwrap_or({:error, :not_found}, 0) == 0
|
||||
assert Result.unwrap_or({:error, "failure"}, "default") == "default"
|
||||
end
|
||||
|
||||
test "works with nil values" do
|
||||
assert Result.unwrap_or({:ok, nil}, "default") == nil
|
||||
assert Result.unwrap_or({:error, :not_found}, nil) == nil
|
||||
end
|
||||
|
||||
test "works with complex default values" do
|
||||
default = %{name: "Unknown", age: 0}
|
||||
assert Result.unwrap_or({:error, :not_found}, default) == default
|
||||
end
|
||||
end
|
||||
|
||||
describe "ok?/1" do
|
||||
test "returns true for success" do
|
||||
assert Result.ok?({:ok, 42}) == true
|
||||
assert Result.ok?({:ok, nil}) == true
|
||||
assert Result.ok?({:ok, []}) == true
|
||||
end
|
||||
|
||||
test "returns false for error" do
|
||||
assert Result.ok?({:error, :not_found}) == false
|
||||
assert Result.ok?({:error, "failure"}) == false
|
||||
assert Result.ok?({:error, nil}) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "error?/1" do
|
||||
test "returns true for error" do
|
||||
assert Result.error?({:error, :not_found}) == true
|
||||
assert Result.error?({:error, "failure"}) == true
|
||||
assert Result.error?({:error, nil}) == true
|
||||
end
|
||||
|
||||
test "returns false for success" do
|
||||
assert Result.error?({:ok, 42}) == false
|
||||
assert Result.error?({:ok, nil}) == false
|
||||
assert Result.error?({:ok, []}) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "combined operations" do
|
||||
test "map and and_then together" do
|
||||
result = {:ok, "5"}
|
||||
|
||||
final_result =
|
||||
result
|
||||
|> Result.map(&String.to_integer/1)
|
||||
|> Result.and_then(fn x -> {:ok, x * 2} end)
|
||||
|> Result.map(fn x -> x + 3 end)
|
||||
|
||||
assert final_result == {:ok, 13}
|
||||
end
|
||||
|
||||
test "map_error after failed operation" do
|
||||
result = {:error, :db_timeout}
|
||||
|
||||
final_result =
|
||||
result
|
||||
|> Result.map(fn x -> x * 2 end)
|
||||
|> Result.map_error(fn _ -> :service_unavailable end)
|
||||
|
||||
assert final_result == {:error, :service_unavailable}
|
||||
end
|
||||
|
||||
test "conditional unwrap based on ok?" do
|
||||
success = {:ok, 42}
|
||||
error = {:error, :not_found}
|
||||
|
||||
assert Result.ok?(success) && Result.unwrap_or(success, 0) == 42
|
||||
refute Result.ok?(error)
|
||||
assert Result.unwrap_or(error, 0) == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
83
test/towerops_web/plugs/remote_ip_logger_test.exs
Normal file
83
test/towerops_web/plugs/remote_ip_logger_test.exs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
defmodule ToweropsWeb.Plugs.RemoteIpLoggerTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
alias ToweropsWeb.Plugs.RemoteIpLogger
|
||||
|
||||
require Logger
|
||||
|
||||
describe "init/1" do
|
||||
test "returns options unchanged" do
|
||||
assert RemoteIpLogger.init([]) == []
|
||||
assert RemoteIpLogger.init(some: :option) == [some: :option]
|
||||
end
|
||||
end
|
||||
|
||||
describe "call/2" do
|
||||
test "extracts IP from X-Forwarded-For header" do
|
||||
build_conn()
|
||||
|> put_req_header("x-forwarded-for", "203.0.113.195")
|
||||
|> RemoteIpLogger.call([])
|
||||
|
||||
assert Logger.metadata()[:remote_ip] == "203.0.113.195"
|
||||
end
|
||||
|
||||
test "extracts first IP from comma-separated X-Forwarded-For" do
|
||||
build_conn()
|
||||
|> put_req_header("x-forwarded-for", "203.0.113.195, 10.0.0.1, 192.168.1.1")
|
||||
|> RemoteIpLogger.call([])
|
||||
|
||||
assert Logger.metadata()[:remote_ip] == "203.0.113.195"
|
||||
end
|
||||
|
||||
test "trims whitespace from X-Forwarded-For IP" do
|
||||
build_conn()
|
||||
|> put_req_header("x-forwarded-for", " 203.0.113.195 , 10.0.0.1")
|
||||
|> RemoteIpLogger.call([])
|
||||
|
||||
assert Logger.metadata()[:remote_ip] == "203.0.113.195"
|
||||
end
|
||||
|
||||
test "falls back to X-Real-IP when X-Forwarded-For is not present" do
|
||||
build_conn()
|
||||
|> put_req_header("x-real-ip", "198.51.100.42")
|
||||
|> RemoteIpLogger.call([])
|
||||
|
||||
assert Logger.metadata()[:remote_ip] == "198.51.100.42"
|
||||
end
|
||||
|
||||
test "formats IPv4 from conn.remote_ip when no proxy headers present" do
|
||||
# ConnCase builds a conn with remote_ip {127, 0, 0, 1}
|
||||
RemoteIpLogger.call(build_conn(), [])
|
||||
|
||||
assert Logger.metadata()[:remote_ip] == "127.0.0.1"
|
||||
end
|
||||
|
||||
test "formats IPv6 from conn.remote_ip" do
|
||||
build_conn()
|
||||
|> Map.put(:remote_ip, {8193, 3512, 0, 0, 0, 0, 0, 1})
|
||||
|> RemoteIpLogger.call([])
|
||||
|
||||
# IPv6 formatted as hex segments separated by colons (uppercase)
|
||||
assert Logger.metadata()[:remote_ip] == "2001:DB8:0:0:0:0:0:1"
|
||||
end
|
||||
|
||||
test "prefers X-Forwarded-For over X-Real-IP" do
|
||||
build_conn()
|
||||
|> put_req_header("x-forwarded-for", "203.0.113.195")
|
||||
|> put_req_header("x-real-ip", "198.51.100.42")
|
||||
|> RemoteIpLogger.call([])
|
||||
|
||||
assert Logger.metadata()[:remote_ip] == "203.0.113.195"
|
||||
end
|
||||
|
||||
test "returns conn unchanged" do
|
||||
original_conn = build_conn()
|
||||
result_conn = RemoteIpLogger.call(original_conn, [])
|
||||
|
||||
# Conn should pass through unchanged (only metadata is modified)
|
||||
assert result_conn.adapter == original_conn.adapter
|
||||
assert result_conn.method == original_conn.method
|
||||
assert result_conn.path_info == original_conn.path_info
|
||||
end
|
||||
end
|
||||
end
|
||||
111
test/towerops_web/plugs/update_session_activity_test.exs
Normal file
111
test/towerops_web/plugs/update_session_activity_test.exs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
defmodule ToweropsWeb.Plugs.UpdateSessionActivityTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Accounts
|
||||
alias ToweropsWeb.Plugs.UpdateSessionActivity
|
||||
|
||||
describe "init/1" do
|
||||
test "returns options unchanged" do
|
||||
assert UpdateSessionActivity.init([]) == []
|
||||
assert UpdateSessionActivity.init(some: :option) == [some: :option]
|
||||
end
|
||||
end
|
||||
|
||||
describe "call/2" do
|
||||
test "returns conn unchanged" do
|
||||
conn = Plug.Test.init_test_session(build_conn(), %{})
|
||||
|
||||
result = UpdateSessionActivity.call(conn, [])
|
||||
|
||||
assert result == conn
|
||||
end
|
||||
|
||||
test "spawns background task to update session activity" do
|
||||
user = user_fixture()
|
||||
{token, user_token} = Accounts.generate_user_session_token_with_record(user)
|
||||
|
||||
now = DateTime.utc_now()
|
||||
|
||||
# Create browser session
|
||||
{:ok, _session} =
|
||||
Accounts.create_browser_session(%{
|
||||
user_id: user.id,
|
||||
user_token_id: user_token.id,
|
||||
device_name: "Test Browser",
|
||||
ip_address: "127.0.0.1",
|
||||
user_agent: "Mozilla/5.0 Test",
|
||||
last_activity_at: now,
|
||||
expires_at: DateTime.add(now, 30, :day)
|
||||
})
|
||||
|
||||
# Call plug with session token - should spawn background task without blocking
|
||||
result =
|
||||
build_conn()
|
||||
|> Plug.Test.init_test_session(%{user_token: token})
|
||||
|> UpdateSessionActivity.call([])
|
||||
|
||||
# Should return immediately (not block on background task)
|
||||
assert result
|
||||
end
|
||||
|
||||
test "handles missing session token gracefully" do
|
||||
build_conn()
|
||||
|> Plug.Test.init_test_session(%{})
|
||||
|> UpdateSessionActivity.call([])
|
||||
|
||||
# Should not raise an error - test passes if no exception
|
||||
end
|
||||
|
||||
test "handles invalid session token gracefully" do
|
||||
build_conn()
|
||||
|> Plug.Test.init_test_session(%{user_token: "invalid-token-does-not-exist"})
|
||||
|> UpdateSessionActivity.call([])
|
||||
|
||||
# Wait for background task to complete
|
||||
Process.sleep(100)
|
||||
|
||||
# Should not raise an error - test passes if no exception
|
||||
end
|
||||
|
||||
test "does not block the request while updating" do
|
||||
user = user_fixture()
|
||||
{token, user_token} = Accounts.generate_user_session_token_with_record(user)
|
||||
|
||||
now = DateTime.utc_now()
|
||||
|
||||
{:ok, _session} =
|
||||
Accounts.create_browser_session(%{
|
||||
user_id: user.id,
|
||||
user_token_id: user_token.id,
|
||||
device_name: "Test Browser",
|
||||
ip_address: "127.0.0.1",
|
||||
user_agent: "Mozilla/5.0 Test",
|
||||
last_activity_at: now,
|
||||
expires_at: DateTime.add(now, 30, :day)
|
||||
})
|
||||
|
||||
# Time the request - should be fast even though update happens in background
|
||||
start_time = System.monotonic_time(:millisecond)
|
||||
|
||||
build_conn()
|
||||
|> Plug.Test.init_test_session(%{user_token: token})
|
||||
|> UpdateSessionActivity.call([])
|
||||
|
||||
end_time = System.monotonic_time(:millisecond)
|
||||
duration = end_time - start_time
|
||||
|
||||
# Should complete very quickly (under 50ms) since update is async
|
||||
assert duration < 50
|
||||
end
|
||||
|
||||
test "handles nil token gracefully" do
|
||||
build_conn()
|
||||
|> Plug.Test.init_test_session(%{user_token: nil})
|
||||
|> UpdateSessionActivity.call([])
|
||||
|
||||
# Should not raise an error - test passes if no exception
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue