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

1import hashlib 

2from datetime import timedelta 

3 

4import pytest 

5from django.db import IntegrityError 

6from django.utils import timezone 

7from webauthn.helpers.structs import AuthenticatorTransport, PublicKeyCredentialDescriptor 

8 

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 

17 

18 

19@pytest.mark.django_db 

20def test_as_credential_descriptors(user): 

21 credential_1 = WebAuthnCredentialFactory(user=user, transports=["hybrid", "ble", "made_up_transport"]) 

22 

23 descriptors = as_credential_descriptors(WebAuthnCredential.objects.all()) 

24 assert len(descriptors) == 1 

25 

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] 

31 

32 

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") 

37 

38 credential_str = str(attestation.credential) 

39 

40 assert str(attestation) == f"{credential_str} (fmt=packed)" 

41 

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() 

46 

47 assert attestation.natural_key() == (attestation.credential.credential_id_sha256,) 

48 

49 

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 

55 

56 

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() 

62 

63 credential = WebAuthnCredentialFactory(credential_id=credential_id) 

64 credential.save() 

65 assert credential.credential_id_sha256.hex() == expected_hash_sha256 

66 

67 

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" 

72 

73 cred1 = WebAuthnCredentialFactory(credential_id=credential_id) 

74 

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 

80 

81 # At this point, the hash will be generated and the unique constraint will be violated. 

82 cred2.save() 

83 

84 

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" 

89 

90 cred1 = WebAuthnCredentialFactory(credential_id=credential_id) 

91 

92 with django_assert_num_queries(1): 

93 assert WebAuthnCredential.get_by_credential_id(credential_id) == cred1 

94 

95 with pytest.raises(WebAuthnCredential.DoesNotExist): 

96 WebAuthnCredential.get_by_credential_id(b"non_existent_credential_id") is None 

97 

98 

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() 

103 

104 assert WebAuthnCredential.get_credential_id_sha256(credential_id) == expected_hash_sha256 

105 

106 

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" 

111 

112 cred = WebAuthnCredentialFactory(credential_id=credential_id) 

113 

114 assert cred.natural_key() == (cred.credential_id_sha256,) 

115 

116 

117@pytest.mark.django_db 

118def test_credential_get_credential_descriptors_for_user(user): 

119 other_user = UserFactory() 

120 

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()) 

126 

127 WebAuthnCredentialFactory(user=other_user, transports=["usb"]) 

128 

129 descriptors = WebAuthnCredential.get_credential_descriptors_for_user(user) 

130 assert len(descriptors) == 3 

131 

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] 

140 

141 

142def test_credential_manager(): 

143 assert isinstance(WebAuthnCredential.objects, WebAuthnCredentialManager) 

144 

145 

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() 

150 

151 cred = WebAuthnCredentialFactory(credential_id=credential_id) 

152 

153 assert WebAuthnCredential.objects.get_by_natural_key(credential_id_sha256) == cred 

154 

155 

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"]) 

160 

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]