3.8 KiB · text 5af55d9
defmodule GitGud.Accounts.WebauthnTest do
@moduledoc """
Backend-only checks for the WebAuthn surface — challenge generation,
serialization shape, and CRUD on stored credentials. The full attest
/ assert flow lives in browser-driven manual testing; Wax has its own
cryptographic test suite upstream.
"""
use GitGud.DataCase, async: false
alias GitGud.Accounts.Webauthn
alias GitGud.Accounts.WebauthnCredential
alias GitGud.Repo
import GitGud.AccountsFixtures
describe "registration_challenge / serialize_registration_options" do
test "returns a Wax challenge bound to the configured origin" do
user = user_fixture()
challenge = Webauthn.registration_challenge(user)
assert %Wax.Challenge{type: :attestation, bytes: bytes} = challenge
assert byte_size(bytes) >= 16
end
test "serialized options have base64url-encoded challenge + user id" do
user = user_fixture()
challenge = Webauthn.registration_challenge(user)
options = Webauthn.serialize_registration_options(user, challenge)
assert %{
"challenge" => challenge_b64,
"user" => %{"id" => user_id_b64, "name" => email}
} = options
# Base.url_decode64/2 either succeeds or returns :error.
assert {:ok, _} = Base.url_decode64(challenge_b64, padding: false)
assert {:ok, _} = Base.url_decode64(user_id_b64, padding: false)
assert email == user.email
assert is_list(options["pubKeyCredParams"])
end
test "previously-registered credentials show up in excludeCredentials" do
user = user_fixture()
cred_id = :crypto.strong_rand_bytes(32)
insert_credential!(user, cred_id, "yubikey")
options =
Webauthn.serialize_registration_options(
user,
Webauthn.registration_challenge(user)
)
assert [%{"id" => exclude_b64, "type" => "public-key"}] = options["excludeCredentials"]
assert {:ok, decoded} = Base.url_decode64(exclude_b64, padding: false)
assert decoded == cred_id
end
end
describe "authentication_challenge / serialize_authentication_options" do
test "lists the user's credentials in allowCredentials" do
user = user_fixture()
cred_id = :crypto.strong_rand_bytes(32)
insert_credential!(user, cred_id, "yubikey")
options =
Webauthn.serialize_authentication_options(
Webauthn.authentication_challenge(user)
)
assert [%{"id" => allow_b64, "type" => "public-key"}] = options["allowCredentials"]
assert {:ok, decoded} = Base.url_decode64(allow_b64, padding: false)
assert decoded == cred_id
end
end
describe "CRUD" do
setup do
user = user_fixture()
cred = insert_credential!(user, :crypto.strong_rand_bytes(32), "yubikey")
{:ok, user: user, cred: cred}
end
test "list + count + has_credentials?", %{user: user, cred: cred} do
assert [%WebauthnCredential{id: id}] = Webauthn.list_credentials(user)
assert id == cred.id
assert Webauthn.count(user) == 1
assert Webauthn.has_credentials?(user)
end
test "rename", %{cred: cred} do
assert {:ok, renamed} = Webauthn.rename_credential(cred, "Office YubiKey")
assert renamed.name == "Office YubiKey"
end
test "delete", %{user: user, cred: cred} do
assert {:ok, _} = Webauthn.delete_credential(cred)
assert Webauthn.list_credentials(user) == []
refute Webauthn.has_credentials?(user)
end
end
defp insert_credential!(user, cred_id, name) do
%WebauthnCredential{user_id: user.id, credential_id: cred_id}
|> WebauthnCredential.create_changeset(%{
user_id: user.id,
credential_id: cred_id,
public_key: :erlang.term_to_binary(%{stub: true}),
sign_count: 0,
name: name
})
|> Repo.insert!()
end
end