Coverage for tests/unit/test_models.py: 100%
98 statements
« prev ^ index » next coverage.py v7.5.4, created at 2024-06-23 20:55 +0000
« prev ^ index » next coverage.py v7.5.4, created at 2024-06-23 20:55 +0000
1import hashlib
2from datetime import timedelta
4import pytest
5from django.db import IntegrityError
6from django.utils import timezone
7from webauthn.helpers.structs import AuthenticatorTransport, PublicKeyCredentialDescriptor
9from django_otp_webauthn.models import (
10 WebAuthnAttestation,
11 WebAuthnAttestationManager,
12 WebAuthnCredential,
13 WebAuthnCredentialManager,
14 as_credential_descriptors,
15)
16from tests.factories import UserFactory, WebAuthnAttestationFactory, WebAuthnCredentialFactory
19@pytest.mark.django_db
20def test_as_credential_descriptors(user):
21 credential_1 = WebAuthnCredentialFactory(user=user, transports=["hybrid", "ble", "made_up_transport"])
23 descriptors = as_credential_descriptors(WebAuthnCredential.objects.all())
24 assert len(descriptors) == 1
26 descriptor = descriptors[0]
27 assert isinstance(descriptor, PublicKeyCredentialDescriptor)
28 assert descriptors[0].id == credential_1.credential_id
29 # Order is important, should be the same as the order in the transports field.
30 assert descriptors[0].transports == [AuthenticatorTransport.HYBRID, AuthenticatorTransport.BLE]
33@pytest.mark.django_db
34def test_attestation_str():
35 """Test that the __str__ method of the attestation model works."""
36 attestation = WebAuthnAttestationFactory(fmt="packed")
38 credential_str = str(attestation.credential)
40 assert str(attestation) == f"{credential_str} (fmt=packed)"
42@pytest.mark.django_db
43def test_attestation_natural_key():
44 """Test that the natural key of an attestation is the credential."""
45 attestation = WebAuthnAttestationFactory()
47 assert attestation.natural_key() == (attestation.credential.credential_id_sha256,)
50@pytest.mark.django_db
51def test_attestation_manager():
52 assert isinstance(WebAuthnAttestation.objects, WebAuthnAttestationManager)
53 attestation = WebAuthnAttestationFactory()
54 assert WebAuthnAttestation.objects.get_by_natural_key(attestation.credential.credential_id_sha256) == attestation
57@pytest.mark.django_db
58def test_credential_hash_created_on_save():
59 """Test that the credential_id_sha256 is created on save."""
60 credential_id = b"credential_id"
61 expected_hash_sha256 = hashlib.sha256(credential_id).digest().hex()
63 credential = WebAuthnCredentialFactory(credential_id=credential_id)
64 credential.save()
65 assert credential.credential_id_sha256.hex() == expected_hash_sha256
68@pytest.mark.django_db
69def test_credentials_are_unique():
70 """Test that it is impossible to create credentials sharing the same credential id."""
71 credential_id = b"credential_id"
73 cred1 = WebAuthnCredentialFactory(credential_id=credential_id)
75 with pytest.raises(IntegrityError):
76 cred2 = WebAuthnCredentialFactory()
77 assert cred2.pk is not cred1.pk
78 cred2.credential_id = credential_id
79 cred2.credential_id_sha256 = None
81 # At this point, the hash will be generated and the unique constraint will be violated.
82 cred2.save()
85@pytest.mark.django_db
86def test_get_by_credential_id(django_assert_num_queries):
87 """Test that the get_by_credential_id method works."""
88 credential_id = b"credential_id"
90 cred1 = WebAuthnCredentialFactory(credential_id=credential_id)
92 with django_assert_num_queries(1):
93 assert WebAuthnCredential.get_by_credential_id(credential_id) == cred1
95 with pytest.raises(WebAuthnCredential.DoesNotExist):
96 WebAuthnCredential.get_by_credential_id(b"non_existent_credential_id") is None
99def test_get_credential_id_sha256():
100 """Test that the get_credential_id_sha256 method works."""
101 credential_id = b"credential_id"
102 expected_hash_sha256 = hashlib.sha256(credential_id).digest()
104 assert WebAuthnCredential.get_credential_id_sha256(credential_id) == expected_hash_sha256
107@pytest.mark.django_db
108def test_credential_natural_key():
109 """Test that the natural key of a credential is the credential_id."""
110 credential_id = b"credential_id"
112 cred = WebAuthnCredentialFactory(credential_id=credential_id)
114 assert cred.natural_key() == (cred.credential_id_sha256,)
117@pytest.mark.django_db
118def test_credential_get_credential_descriptors_for_user(user):
119 other_user = UserFactory()
121 credential_1 = WebAuthnCredentialFactory(user=user, transports=["hybrid", "ble"], last_used_at=None)
122 credential_2 = WebAuthnCredentialFactory(
123 user=user, transports=["internal", "made_up_transport"], last_used_at=timezone.now() - timedelta(days=1)
124 )
125 credential_3 = WebAuthnCredentialFactory(user=user, transports=["nfc"], last_used_at=timezone.now())
127 WebAuthnCredentialFactory(user=other_user, transports=["usb"])
129 descriptors = WebAuthnCredential.get_credential_descriptors_for_user(user)
130 assert len(descriptors) == 3
132 # When using the classmethod, the credentials will be ordered by last_used_at, with the most recently used first.
133 # See source code for explanation as to why this is done.
134 assert descriptors[0].id == credential_3.credential_id
135 assert descriptors[0].transports == [AuthenticatorTransport.NFC]
136 assert descriptors[1].id == credential_2.credential_id
137 assert descriptors[1].transports == [AuthenticatorTransport.INTERNAL]
138 assert descriptors[2].id == credential_1.credential_id
139 assert descriptors[2].transports == [AuthenticatorTransport.HYBRID, AuthenticatorTransport.BLE]
142def test_credential_manager():
143 assert isinstance(WebAuthnCredential.objects, WebAuthnCredentialManager)
146@pytest.mark.django_db
147def test_credential_manager_get_by_natural_key():
148 credential_id = b"credential_id"
149 credential_id_sha256 = hashlib.sha256(credential_id).digest()
151 cred = WebAuthnCredentialFactory(credential_id=credential_id)
153 assert WebAuthnCredential.objects.get_by_natural_key(credential_id_sha256) == cred
156@pytest.mark.django_db
157def test_credential_queryset_as_credential_descriptors():
158 credential_1 = WebAuthnCredentialFactory(transports=["hybrid", "ble"])
159 credential_2 = WebAuthnCredentialFactory(transports=["internal", "made_up_transport"])
161 descriptors = WebAuthnCredential.objects.all().order_by("id").as_credential_descriptors()
162 assert len(descriptors) == 2
163 descriptors[0].id == credential_1.credential_id
164 descriptors[0].transports == [AuthenticatorTransport.HYBRID, AuthenticatorTransport.BLE]
165 descriptors[1].id == credential_2.credential_id
166 descriptors[1].transports == [AuthenticatorTransport.INTERNAL]