Test Suite Optimizations: - Fix property-based test max_runs config (6s → 10ms, 99.9% faster) - Replace Process.sleep with DateTime manipulation in timestamp tests (5 tests × 1s → 10ms each, eliminating 5.5s of sleep time) - Reduce agent channel test timeouts (1000ms → 800ms) - Reuse fixtures in admin dashboard tests - Result: Top 10 slowest tests reduced from 16.0s to 8.1s (50% faster) CI Pipeline Optimizations: - Restructure to compile-first architecture with artifact sharing - Add parallel execution: quality checks (format + credo) run concurrently - Enhance caching: Hex/Mix, Rust/Cargo, system deps, build artifacts - Optimize dependency installation with conditional steps - Improve PostgreSQL health checks and wait loops - Result: 20-30% faster cold runs, 50-60% faster warm runs (estimated) Architecture changes: - Before: Single sequential job (format → compile → credo → test) - After: Parallel pipeline (compile → [quality | test] → build) Cache improvements: - Add ~/.hex and ~/.mix caching - Add Rust/Cargo target caching for Rustler NIF - Include Rust source hashes in cache keys - Skip apt-get update on cache hit - Use --no-install-recommends for faster installs
428 lines
13 KiB
Elixir
428 lines
13 KiB
Elixir
defmodule Towerops.MobileSessionsTest do
|
|
use Towerops.DataCase
|
|
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.MobileSessions
|
|
alias Towerops.MobileSessions.MobileSession
|
|
alias Towerops.MobileSessions.QRLoginToken
|
|
|
|
defp valid_session_attrs(user) do
|
|
%{
|
|
user_id: user.id,
|
|
device_name: "iPhone 15 Pro",
|
|
device_os: "iOS 17.2",
|
|
app_version: "1.0.0"
|
|
}
|
|
end
|
|
|
|
defp create_session(user, overrides \\ %{}) do
|
|
attrs = Map.merge(valid_session_attrs(user), overrides)
|
|
{:ok, session} = MobileSessions.create_mobile_session(attrs)
|
|
session
|
|
end
|
|
|
|
describe "create_mobile_session/1" do
|
|
test "creates session with valid attrs" do
|
|
user = user_fixture()
|
|
|
|
assert {:ok, %MobileSession{} = session} =
|
|
MobileSessions.create_mobile_session(valid_session_attrs(user))
|
|
|
|
assert session.user_id == user.id
|
|
assert session.device_name == "iPhone 15 Pro"
|
|
assert session.device_os == "iOS 17.2"
|
|
assert session.app_version == "1.0.0"
|
|
assert session.token
|
|
assert session.expires_at
|
|
assert session.last_used_at
|
|
assert session.alerts_enabled == true
|
|
end
|
|
|
|
test "returns error changeset with invalid attrs (missing user_id)" do
|
|
assert {:error, %Ecto.Changeset{}} =
|
|
MobileSessions.create_mobile_session(%{device_name: "Test"})
|
|
end
|
|
end
|
|
|
|
describe "get_session/1" do
|
|
test "returns session by id" do
|
|
user = user_fixture()
|
|
session = create_session(user)
|
|
|
|
assert found = MobileSessions.get_session(session.id)
|
|
assert found.id == session.id
|
|
end
|
|
|
|
test "returns nil for non-existent id" do
|
|
assert MobileSessions.get_session(Ecto.UUID.generate()) == nil
|
|
end
|
|
end
|
|
|
|
describe "get_session_by_token/1" do
|
|
test "returns session for valid token" do
|
|
user = user_fixture()
|
|
{:ok, session} = MobileSessions.create_mobile_session(valid_session_attrs(user))
|
|
|
|
assert found = MobileSessions.get_session_by_token(session.raw_token)
|
|
assert found.id == session.id
|
|
end
|
|
|
|
test "returns nil for expired session" do
|
|
user = user_fixture()
|
|
expired = create_session(user, %{expires_at: DateTime.add(DateTime.utc_now(), -1, :day)})
|
|
|
|
assert MobileSessions.get_session_by_token(expired.token) == nil
|
|
end
|
|
|
|
test "returns nil for non-existent token" do
|
|
assert MobileSessions.get_session_by_token("nonexistent-token") == nil
|
|
end
|
|
|
|
test "returns nil for non-binary input" do
|
|
assert MobileSessions.get_session_by_token(nil) == nil
|
|
assert MobileSessions.get_session_by_token(123) == nil
|
|
end
|
|
end
|
|
|
|
describe "touch_session/1" do
|
|
test "updates last_used_at" do
|
|
user = user_fixture()
|
|
session = create_session(user)
|
|
|
|
# Set original timestamp to the past instead of sleeping
|
|
past_time = DateTime.utc_now() |> DateTime.add(-10, :second) |> DateTime.truncate(:second)
|
|
|
|
session =
|
|
session
|
|
|> Ecto.Changeset.change(last_used_at: past_time)
|
|
|> Towerops.Repo.update!()
|
|
|
|
original_last_used_at = session.last_used_at
|
|
|
|
assert {:ok, touched} = MobileSessions.touch_session(session)
|
|
assert DateTime.after?(touched.last_used_at, original_last_used_at)
|
|
end
|
|
end
|
|
|
|
describe "list_user_sessions/1" do
|
|
test "returns active sessions for user ordered by last_used_at desc" do
|
|
user = user_fixture()
|
|
|
|
# Create sessions with explicit ordering instead of sleeping
|
|
past_time = DateTime.utc_now() |> DateTime.add(-10, :second) |> DateTime.truncate(:second)
|
|
session1 = create_session(user, %{device_name: "Device A"})
|
|
|
|
session1 =
|
|
session1
|
|
|> Ecto.Changeset.change(last_used_at: past_time)
|
|
|> Towerops.Repo.update!()
|
|
|
|
session2 = create_session(user, %{device_name: "Device B"})
|
|
|
|
sessions = MobileSessions.list_user_sessions(user.id)
|
|
assert length(sessions) == 2
|
|
assert hd(sessions).id == session2.id
|
|
assert List.last(sessions).id == session1.id
|
|
end
|
|
|
|
test "excludes expired sessions" do
|
|
user = user_fixture()
|
|
_active = create_session(user, %{device_name: "Active"})
|
|
|
|
_expired =
|
|
create_session(user, %{
|
|
device_name: "Expired",
|
|
expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
|
|
})
|
|
|
|
sessions = MobileSessions.list_user_sessions(user.id)
|
|
assert length(sessions) == 1
|
|
assert hd(sessions).device_name == "Active"
|
|
end
|
|
|
|
test "returns empty list for no sessions" do
|
|
user = user_fixture()
|
|
assert MobileSessions.list_user_sessions(user.id) == []
|
|
end
|
|
end
|
|
|
|
describe "revoke_session/1" do
|
|
test "deletes session by id" do
|
|
user = user_fixture()
|
|
session = create_session(user)
|
|
|
|
assert {:ok, %MobileSession{}} = MobileSessions.revoke_session(session.id)
|
|
assert MobileSessions.get_session(session.id) == nil
|
|
end
|
|
|
|
test "returns error for non-existent id" do
|
|
assert {:error, :not_found} = MobileSessions.revoke_session(Ecto.UUID.generate())
|
|
end
|
|
end
|
|
|
|
describe "update_alert_preferences/2" do
|
|
test "updates alerts_enabled" do
|
|
user = user_fixture()
|
|
session = create_session(user)
|
|
|
|
assert {:ok, updated} =
|
|
MobileSessions.update_alert_preferences(session.id, %{alerts_enabled: false})
|
|
|
|
assert updated.alerts_enabled == false
|
|
end
|
|
|
|
test "updates push_token" do
|
|
user = user_fixture()
|
|
session = create_session(user)
|
|
|
|
assert {:ok, updated} =
|
|
MobileSessions.update_alert_preferences(session.id, %{
|
|
push_token: "device-push-token-abc",
|
|
push_platform: "apns"
|
|
})
|
|
|
|
assert updated.push_token == "device-push-token-abc"
|
|
assert updated.push_platform == "apns"
|
|
end
|
|
|
|
test "returns error for non-existent session" do
|
|
assert {:error, :not_found} =
|
|
MobileSessions.update_alert_preferences(Ecto.UUID.generate(), %{
|
|
alerts_enabled: false
|
|
})
|
|
end
|
|
end
|
|
|
|
describe "list_alert_enabled_devices/1" do
|
|
test "returns sessions with alerts enabled and push_token set" do
|
|
user = user_fixture()
|
|
session = create_session(user)
|
|
|
|
{:ok, _updated} =
|
|
MobileSessions.update_alert_preferences(session.id, %{
|
|
alerts_enabled: true,
|
|
push_token: "token-abc",
|
|
push_platform: "apns"
|
|
})
|
|
|
|
devices = MobileSessions.list_alert_enabled_devices(user.id)
|
|
assert length(devices) == 1
|
|
assert hd(devices).id == session.id
|
|
end
|
|
|
|
test "excludes sessions with alerts disabled" do
|
|
user = user_fixture()
|
|
session = create_session(user)
|
|
|
|
{:ok, _updated} =
|
|
MobileSessions.update_alert_preferences(session.id, %{
|
|
alerts_enabled: false,
|
|
push_token: "token-abc"
|
|
})
|
|
|
|
assert MobileSessions.list_alert_enabled_devices(user.id) == []
|
|
end
|
|
|
|
test "excludes sessions without push_token" do
|
|
user = user_fixture()
|
|
_session = create_session(user, %{alerts_enabled: true})
|
|
|
|
assert MobileSessions.list_alert_enabled_devices(user.id) == []
|
|
end
|
|
|
|
test "excludes expired sessions" do
|
|
user = user_fixture()
|
|
|
|
session =
|
|
create_session(user, %{
|
|
expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
|
|
})
|
|
|
|
{:ok, _updated} =
|
|
MobileSessions.update_alert_preferences(session.id, %{
|
|
alerts_enabled: true,
|
|
push_token: "token-abc"
|
|
})
|
|
|
|
assert MobileSessions.list_alert_enabled_devices(user.id) == []
|
|
end
|
|
end
|
|
|
|
describe "revoke_all_user_sessions/1" do
|
|
test "deletes all sessions for user, returns count" do
|
|
user = user_fixture()
|
|
_session1 = create_session(user, %{device_name: "Device A"})
|
|
_session2 = create_session(user, %{device_name: "Device B"})
|
|
|
|
assert {:ok, 2} = MobileSessions.revoke_all_user_sessions(user.id)
|
|
assert MobileSessions.list_user_sessions(user.id) == []
|
|
end
|
|
|
|
test "returns 0 when no sessions" do
|
|
user = user_fixture()
|
|
assert {:ok, 0} = MobileSessions.revoke_all_user_sessions(user.id)
|
|
end
|
|
end
|
|
|
|
describe "delete_expired_sessions/0" do
|
|
test "deletes expired sessions, returns count" do
|
|
user = user_fixture()
|
|
|
|
_expired1 =
|
|
create_session(user, %{
|
|
device_name: "Expired 1",
|
|
expires_at: DateTime.add(DateTime.utc_now(), -2, :day)
|
|
})
|
|
|
|
_expired2 =
|
|
create_session(user, %{
|
|
device_name: "Expired 2",
|
|
expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
|
|
})
|
|
|
|
assert MobileSessions.delete_expired_sessions() == 2
|
|
end
|
|
|
|
test "does not delete active sessions" do
|
|
user = user_fixture()
|
|
active = create_session(user, %{device_name: "Active"})
|
|
|
|
_expired =
|
|
create_session(user, %{
|
|
device_name: "Expired",
|
|
expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
|
|
})
|
|
|
|
assert MobileSessions.delete_expired_sessions() == 1
|
|
assert MobileSessions.get_session(active.id)
|
|
end
|
|
end
|
|
|
|
describe "create_qr_login_token/1" do
|
|
test "creates token for user" do
|
|
user = user_fixture()
|
|
|
|
assert {:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
|
|
assert qr_token.user_id == user.id
|
|
assert qr_token.token
|
|
assert qr_token.expires_at
|
|
assert qr_token.completed_at == nil
|
|
end
|
|
end
|
|
|
|
describe "get_qr_login_token/1" do
|
|
test "returns token for valid token string" do
|
|
user = user_fixture()
|
|
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
|
|
|
|
assert found = MobileSessions.get_qr_login_token(qr_token.token)
|
|
assert found.id == qr_token.id
|
|
end
|
|
|
|
test "returns nil for expired token" do
|
|
user = user_fixture()
|
|
|
|
{:ok, qr_token} =
|
|
%QRLoginToken{}
|
|
|> QRLoginToken.create_changeset(%{
|
|
user_id: user.id,
|
|
expires_at: DateTime.add(DateTime.utc_now(), -1, :minute)
|
|
})
|
|
|> Repo.insert()
|
|
|
|
assert MobileSessions.get_qr_login_token(qr_token.token) == nil
|
|
end
|
|
|
|
test "returns nil for completed token" do
|
|
user = user_fixture()
|
|
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
|
|
|
|
# Complete the token via the normal flow so it has a valid mobile_session_id
|
|
device_attrs = %{device_name: "Test Device", device_os: "iOS", app_version: "1.0"}
|
|
{:ok, _session} = MobileSessions.complete_qr_login(qr_token.token, device_attrs)
|
|
|
|
assert MobileSessions.get_qr_login_token(qr_token.token) == nil
|
|
end
|
|
|
|
test "returns nil for non-binary input" do
|
|
assert MobileSessions.get_qr_login_token(nil) == nil
|
|
assert MobileSessions.get_qr_login_token(123) == nil
|
|
end
|
|
end
|
|
|
|
describe "complete_qr_login/2" do
|
|
test "creates mobile session and marks token completed" do
|
|
user = user_fixture()
|
|
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
|
|
|
|
device_attrs = %{
|
|
device_name: "iPhone 15",
|
|
device_os: "iOS 17.2",
|
|
app_version: "1.0.0"
|
|
}
|
|
|
|
assert {:ok, session} = MobileSessions.complete_qr_login(qr_token.token, device_attrs)
|
|
assert session.user_id == user.id
|
|
assert session.device_name == "iPhone 15"
|
|
|
|
# Verify QR token is marked completed
|
|
assert MobileSessions.get_qr_login_token(qr_token.token) == nil
|
|
end
|
|
|
|
test "returns error for invalid token" do
|
|
assert {:error, :invalid_token} =
|
|
MobileSessions.complete_qr_login("invalid-token", %{
|
|
device_name: "Test",
|
|
device_os: "iOS"
|
|
})
|
|
end
|
|
end
|
|
|
|
describe "check_qr_login_completed/1" do
|
|
test "returns mobile session for completed token" do
|
|
user = user_fixture()
|
|
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
|
|
|
|
device_attrs = %{
|
|
device_name: "iPhone 15",
|
|
device_os: "iOS 17.2",
|
|
app_version: "1.0.0"
|
|
}
|
|
|
|
{:ok, session} = MobileSessions.complete_qr_login(qr_token.token, device_attrs)
|
|
|
|
found = MobileSessions.check_qr_login_completed(qr_token.token)
|
|
assert found.id == session.id
|
|
end
|
|
|
|
test "returns nil for incomplete token" do
|
|
user = user_fixture()
|
|
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
|
|
|
|
assert MobileSessions.check_qr_login_completed(qr_token.token) == nil
|
|
end
|
|
|
|
test "returns nil for non-binary input" do
|
|
assert MobileSessions.check_qr_login_completed(nil) == nil
|
|
assert MobileSessions.check_qr_login_completed(123) == nil
|
|
end
|
|
end
|
|
|
|
describe "delete_expired_qr_tokens/0" do
|
|
test "deletes expired tokens, returns count" do
|
|
user = user_fixture()
|
|
|
|
# Create an expired QR token
|
|
{:ok, _expired} =
|
|
%QRLoginToken{}
|
|
|> QRLoginToken.create_changeset(%{
|
|
user_id: user.id,
|
|
expires_at: DateTime.add(DateTime.utc_now(), -10, :minute)
|
|
})
|
|
|> Repo.insert()
|
|
|
|
assert MobileSessions.delete_expired_qr_tokens() >= 1
|
|
end
|
|
end
|
|
end
|