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:
parent
f5bef9b997
commit
7e5da340d3
1 changed files with 200 additions and 0 deletions
200
test/towerops/accounts/user_credential_test.exs
Normal file
200
test/towerops/accounts/user_credential_test.exs
Normal 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
|
||||
Loading…
Add table
Reference in a new issue