1
+ import { vi , describe , it , expect , beforeEach } from 'vitest'
2
+ // Import testing functions from vitest library
3
+ import { PasskeyAuthService } from '../PasskeyAuthService'
4
+ // Import the class we want to test
5
+ import { AuthenticationResult , SetupResult } from '../types'
6
+ // Import authentication and setup result types
7
+ import { Capacitor } from '@capacitor/core'
8
+
9
+ // Replace real Capacitor with our mock to control its behavior
10
+ vi . mock ( '@capacitor/core' , ( ) => ( {
11
+ Capacitor : {
12
+ isNativePlatform : vi . fn ( )
13
+ }
14
+ } ) )
15
+
16
+ // Get mocked Capacitor for type safety
17
+ const mockCapacitor = vi . mocked ( Capacitor )
18
+
19
+ // Mock WebAuthn API for testing passkey functionality
20
+ const mockCredentials = {
21
+ get : vi . fn ( ) , // For authentication with existing passkey
22
+ create : vi . fn ( ) // For creating new passkey
23
+ }
24
+
25
+ describe ( 'PasskeyAuthService' , ( ) => {
26
+ let passkeyAuth : PasskeyAuthService
27
+ // Declare variable for the service instance we're testing
28
+
29
+ beforeEach ( ( ) => {
30
+ // This function runs before each test for clean state
31
+ passkeyAuth = new PasskeyAuthService ( )
32
+ // Create new service instance
33
+ vi . clearAllMocks ( )
34
+ // Reset all mocks to initial state
35
+
36
+ // Set up global browser objects for each test
37
+ Object . defineProperty ( global , 'navigator' , {
38
+ value : { credentials : mockCredentials } ,
39
+ writable : true
40
+ } )
41
+ // Mock navigator.credentials (WebAuthn API)
42
+
43
+ Object . defineProperty ( global , 'window' , {
44
+ value : {
45
+ PublicKeyCredential : function ( ) { } , // Mock constructor for WebAuthn support
46
+ location : { hostname : 'messenger.adamant.im' } , // Mock site domain
47
+ crypto : {
48
+ getRandomValues : vi . fn ( ( ) => new Uint8Array ( 32 ) ) // Mock random number generator
49
+ }
50
+ } ,
51
+ writable : true
52
+ } )
53
+ } )
54
+
55
+ describe ( 'User tries to authenticate with passkey in web browser' , ( ) => {
56
+ beforeEach ( ( ) => {
57
+ // For these tests simulate web browser (not mobile app)
58
+ mockCapacitor . isNativePlatform . mockReturnValue ( false )
59
+ } )
60
+
61
+ it ( 'should successfully authenticate when user confirms passkey' , async ( ) => {
62
+ // Simulate successful passkey confirmation by user
63
+ mockCredentials . get . mockResolvedValue ( { id : 'credential-id' , type : 'public-key' } )
64
+
65
+ // User clicks login button and confirms through passkey
66
+ const result = await passkeyAuth . authorizeUser ( )
67
+
68
+ // Check that credentials.get was called with correct WebAuthn parameters
69
+ expect ( mockCredentials . get ) . toHaveBeenCalledWith ( {
70
+ publicKey : {
71
+ challenge : expect . any ( Uint8Array ) ,
72
+ rpId : 'messenger.adamant.im' ,
73
+ userVerification : 'preferred' ,
74
+ timeout : 30000
75
+ }
76
+ } )
77
+ // Check that authentication was successful
78
+ expect ( result ) . toBe ( AuthenticationResult . Success )
79
+ } )
80
+
81
+ it ( 'should return cancel when user cancels passkey prompt with NotAllowedError' , async ( ) => {
82
+ // Simulate user cancellation (clicked "Cancel" in system dialog)
83
+ const cancelError = new Error ( 'User cancelled' )
84
+ cancelError . name = 'NotAllowedError'
85
+ mockCredentials . get . mockRejectedValue ( cancelError )
86
+
87
+ // User starts authentication but cancels it in system dialog
88
+ const result = await passkeyAuth . authorizeUser ( )
89
+
90
+ // Expect "cancel" result, not error
91
+ expect ( result ) . toBe ( AuthenticationResult . Cancel )
92
+ } )
93
+
94
+ it ( 'should return cancel when user cancels passkey prompt with AbortError' , async ( ) => {
95
+ // Simulate user cancellation with different error type
96
+ const cancelError = new Error ( 'User aborted' )
97
+ cancelError . name = 'AbortError'
98
+ mockCredentials . get . mockRejectedValue ( cancelError )
99
+
100
+ // User starts authentication but cancels it
101
+ const result = await passkeyAuth . authorizeUser ( )
102
+
103
+ // Should also return Cancel for AbortError
104
+ expect ( result ) . toBe ( AuthenticationResult . Cancel )
105
+ } )
106
+
107
+ it ( 'should return failed when error is not cancellation' , async ( ) => {
108
+ // Simulate error that is NOT NotAllowedError or AbortError
109
+ const authError = new Error ( 'Authentication failed' )
110
+ authError . name = 'InvalidStateError'
111
+ mockCredentials . get . mockRejectedValue ( authError )
112
+
113
+ // User tries to login but gets non-cancellation error
114
+ const result = await passkeyAuth . authorizeUser ( )
115
+
116
+ // Should return Failed (not Cancel) because error is not cancellation type
117
+ expect ( result ) . toBe ( AuthenticationResult . Failed )
118
+ } )
119
+
120
+ it ( 'should return failed when browser does not support WebAuthn' , async ( ) => {
121
+ // Mock old browser without WebAuthn API support
122
+ Object . defineProperty ( global , 'navigator' , {
123
+ value : { } , // Remove credentials from navigator
124
+ writable : true
125
+ } )
126
+
127
+ // User tries to use passkey in incompatible browser
128
+ const result = await passkeyAuth . authorizeUser ( )
129
+
130
+ // Expect failure due to lack of support
131
+ expect ( result ) . toBe ( AuthenticationResult . Failed )
132
+ } )
133
+ } )
134
+
135
+ describe ( 'User tries to authenticate with passkey in mobile app' , ( ) => {
136
+ beforeEach ( ( ) => {
137
+ // Simulate mobile application
138
+ mockCapacitor . isNativePlatform . mockReturnValue ( true )
139
+ } )
140
+
141
+ it ( 'should return failed as passkeys not supported in mobile app' , async ( ) => {
142
+ // Passkeys are not yet implemented in mobile app
143
+ const result = await passkeyAuth . authorizeUser ( )
144
+
145
+ // Expect failure as feature is only available in web version
146
+ expect ( result ) . toBe ( AuthenticationResult . Failed )
147
+ } )
148
+ } )
149
+
150
+ describe ( 'User wants to setup passkey authentication' , ( ) => {
151
+ beforeEach ( ( ) => {
152
+ // Setup is only possible in browser
153
+ mockCapacitor . isNativePlatform . mockReturnValue ( false )
154
+ } )
155
+
156
+ it ( 'should successfully create passkey when user confirms' , async ( ) => {
157
+ // Simulate successful passkey creation (user confirmed via biometrics/PIN)
158
+ mockCredentials . create . mockResolvedValue ( {
159
+ id : 'new-credential-id' ,
160
+ type : 'public-key'
161
+ } )
162
+
163
+ // User clicked "Setup passkey" and completed creation process
164
+ const result = await passkeyAuth . setupPasskey ( )
165
+
166
+ // Setup should succeed
167
+ expect ( result ) . toBe ( SetupResult . Success )
168
+ } )
169
+
170
+ it ( 'should return cancel when user cancels passkey creation' , async ( ) => {
171
+ // Simulate user cancelling passkey creation
172
+ const cancelError = new Error ( 'User cancelled setup' )
173
+ cancelError . name = 'AbortError'
174
+ mockCredentials . create . mockRejectedValue ( cancelError )
175
+
176
+ // User starts setup but cancels it
177
+ const result = await passkeyAuth . setupPasskey ( )
178
+
179
+ // Expect "cancel" result
180
+ expect ( result ) . toBe ( SetupResult . Cancel )
181
+ } )
182
+
183
+ it ( 'should return failed when passkey creation fails' , async ( ) => {
184
+ // Simulate creation error (e.g., device issues)
185
+ const creationError = new Error ( 'Creation failed' )
186
+ mockCredentials . create . mockRejectedValue ( creationError )
187
+
188
+ // User tries to setup passkey but process fails with error
189
+ const result = await passkeyAuth . setupPasskey ( )
190
+
191
+ // Expect failed setup
192
+ expect ( result ) . toBe ( SetupResult . Failed )
193
+ } )
194
+
195
+ it ( 'should return failed when trying to setup in mobile app' , async ( ) => {
196
+ // Simulate setup attempt in mobile app
197
+ mockCapacitor . isNativePlatform . mockReturnValue ( true )
198
+
199
+ // User tries to setup passkey in mobile app
200
+ const result = await passkeyAuth . setupPasskey ( )
201
+
202
+ // Expect failure as feature is not supported in mobile apps
203
+ expect ( result ) . toBe ( SetupResult . Failed )
204
+ } )
205
+ } )
206
+ } )
0 commit comments