towerops/test/towerops/mobile_sessions_test.exs
Graham McIntire 74e7a8b774
perf: optimize test suite and CI pipeline for maximum speed
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
2026-03-05 15:04:57 -06:00

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