Add comprehensive tests for Accounts.UserCredential

Improved coverage from 28.57% to 100% by adding 13 tests covering:
- Required field validations (user_id, credential_id, public_key, name)
- Name length validation (min 1, max 100 characters)
- Optional field handling (sign_count, last_used_at, aaguid, transports, backup flags, attestation_format)
- Default values for optional fields
- touch_changeset/1 function (updates last_used_at and increments sign_count)

Overall coverage: 66.44%
This commit is contained in:
Graham McIntire 2026-01-13 07:47:37 -06:00
parent f5bef9b997
commit 7e5da340d3
No known key found for this signature in database

View file

@ -0,0 +1,200 @@
defmodule Towerops.Accounts.UserCredentialTest do
use Towerops.DataCase
alias Towerops.Accounts.UserCredential
describe "changeset/2" do
test "valid changeset with required fields" do
attrs = %{
user_id: Ecto.UUID.generate(),
credential_id: :crypto.strong_rand_bytes(16),
public_key: :crypto.strong_rand_bytes(65),
name: "My Passkey"
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
assert changeset.valid?
assert get_field(changeset, :name) == "My Passkey"
assert get_field(changeset, :credential_id) == attrs.credential_id
assert get_field(changeset, :public_key) == attrs.public_key
end
test "requires user_id" do
attrs = %{
credential_id: :crypto.strong_rand_bytes(16),
public_key: :crypto.strong_rand_bytes(65),
name: "My Passkey"
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
refute changeset.valid?
assert %{user_id: ["can't be blank"]} = errors_on(changeset)
end
test "requires credential_id" do
attrs = %{
user_id: Ecto.UUID.generate(),
public_key: :crypto.strong_rand_bytes(65),
name: "My Passkey"
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
refute changeset.valid?
assert %{credential_id: ["can't be blank"]} = errors_on(changeset)
end
test "requires public_key" do
attrs = %{
user_id: Ecto.UUID.generate(),
credential_id: :crypto.strong_rand_bytes(16),
name: "My Passkey"
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
refute changeset.valid?
assert %{public_key: ["can't be blank"]} = errors_on(changeset)
end
test "requires name" do
attrs = %{
user_id: Ecto.UUID.generate(),
credential_id: :crypto.strong_rand_bytes(16),
public_key: :crypto.strong_rand_bytes(65)
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
refute changeset.valid?
assert %{name: ["can't be blank"]} = errors_on(changeset)
end
test "validates name length minimum" do
attrs = %{
user_id: Ecto.UUID.generate(),
credential_id: :crypto.strong_rand_bytes(16),
public_key: :crypto.strong_rand_bytes(65),
name: ""
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
refute changeset.valid?
# Empty string triggers "can't be blank" before length validation
assert %{name: ["can't be blank"]} = errors_on(changeset)
end
test "validates name length maximum" do
attrs = %{
user_id: Ecto.UUID.generate(),
credential_id: :crypto.strong_rand_bytes(16),
public_key: :crypto.strong_rand_bytes(65),
name: String.duplicate("a", 101)
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
refute changeset.valid?
assert %{name: ["should be at most 100 character(s)"]} = errors_on(changeset)
end
test "accepts name at maximum length" do
attrs = %{
user_id: Ecto.UUID.generate(),
credential_id: :crypto.strong_rand_bytes(16),
public_key: :crypto.strong_rand_bytes(65),
name: String.duplicate("a", 100)
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
assert changeset.valid?
end
test "accepts optional fields" do
last_used_at = DateTime.truncate(DateTime.utc_now(), :second)
attrs = %{
user_id: Ecto.UUID.generate(),
credential_id: :crypto.strong_rand_bytes(16),
public_key: :crypto.strong_rand_bytes(65),
name: "My Passkey",
sign_count: 42,
last_used_at: last_used_at,
aaguid: :crypto.strong_rand_bytes(16),
transports: ["usb", "nfc"],
backup_eligible: true,
backup_state: true,
attestation_format: "packed"
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
assert changeset.valid?
assert get_field(changeset, :sign_count) == 42
assert DateTime.compare(get_field(changeset, :last_used_at), last_used_at) == :eq
assert get_field(changeset, :aaguid) == attrs.aaguid
assert get_field(changeset, :transports) == ["usb", "nfc"]
assert get_field(changeset, :backup_eligible) == true
assert get_field(changeset, :backup_state) == true
assert get_field(changeset, :attestation_format) == "packed"
end
test "uses default values for optional fields when not provided" do
attrs = %{
user_id: Ecto.UUID.generate(),
credential_id: :crypto.strong_rand_bytes(16),
public_key: :crypto.strong_rand_bytes(65),
name: "My Passkey"
}
changeset = UserCredential.changeset(%UserCredential{}, attrs)
assert changeset.valid?
# These defaults come from the schema definition
assert get_field(changeset, :sign_count) == 0
assert get_field(changeset, :transports) == []
assert get_field(changeset, :backup_eligible) == false
assert get_field(changeset, :backup_state) == false
end
end
describe "touch_changeset/1" do
test "updates last_used_at to current time" do
old_time = DateTime.add(DateTime.utc_now(), -3600, :second)
credential = %UserCredential{
sign_count: 5,
last_used_at: old_time
}
changeset = UserCredential.touch_changeset(credential)
last_used_at = get_field(changeset, :last_used_at)
# Should be more recent than the old time
assert DateTime.after?(last_used_at, old_time)
# Should be within last 2 seconds (account for timing + truncation)
diff = DateTime.diff(DateTime.utc_now(), last_used_at, :second)
assert diff <= 2
end
test "increments sign_count by 1" do
credential = %UserCredential{sign_count: 5}
changeset = UserCredential.touch_changeset(credential)
assert get_field(changeset, :sign_count) == 6
end
test "increments sign_count from 0" do
credential = %UserCredential{sign_count: 0}
changeset = UserCredential.touch_changeset(credential)
assert get_field(changeset, :sign_count) == 1
end
end
end