Skip to content

Commit 07a6583

Browse files
committed
add tests
1 parent c644fda commit 07a6583

File tree

4 files changed

+454
-78
lines changed

4 files changed

+454
-78
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { vi, describe, it, expect, beforeEach } from 'vitest'
2+
import { BiometricAuthService } from '../BiometricAuthService'
3+
import { AuthenticationResult, SetupResult } from '../types'
4+
import { Capacitor } from '@capacitor/core'
5+
import { NativeBiometric } from '@capgo/capacitor-native-biometric'
6+
7+
// Replace imports with mocks to control their behavior in tests
8+
vi.mock('@capacitor/core', () => ({
9+
Capacitor: {
10+
isNativePlatform: vi.fn()
11+
}
12+
}))
13+
14+
vi.mock('@capgo/capacitor-native-biometric', () => ({
15+
NativeBiometric: {
16+
verifyIdentity: vi.fn(),
17+
isAvailable: vi.fn()
18+
}
19+
}))
20+
21+
// Get mocked instances for type safety
22+
const mockCapacitor = vi.mocked(Capacitor)
23+
const mockNativeBiometric = vi.mocked(NativeBiometric)
24+
25+
describe('BiometricAuthService', () => {
26+
let biometricAuth: BiometricAuthService
27+
28+
beforeEach(() => {
29+
// Create new service instance before each test for isolation
30+
biometricAuth = new BiometricAuthService()
31+
// Clear all mocks so tests don't affect each other
32+
vi.clearAllMocks()
33+
})
34+
35+
describe('User tries to authenticate with biometrics on mobile device', () => {
36+
beforeEach(() => {
37+
// Simulate that we're on mobile platform (Android/iOS)
38+
mockCapacitor.isNativePlatform.mockReturnValue(true)
39+
})
40+
41+
it('should successfully authenticate when user provides valid biometric', async () => {
42+
// Simulate successful biometric verification (fingerprint/Face ID)
43+
mockNativeBiometric.verifyIdentity.mockResolvedValue(undefined)
44+
45+
// User clicks "Login with biometric" button
46+
const result = await biometricAuth.authorizeUser()
47+
48+
// Check that verifyIdentity was called with correct parameters
49+
expect(mockNativeBiometric.verifyIdentity).toHaveBeenCalledWith({
50+
reason: 'Login to ADAMANT Messenger',
51+
title: 'Biometric Authentication'
52+
})
53+
// Expect successful authentication
54+
expect(result).toBe(AuthenticationResult.Success)
55+
})
56+
57+
it('should return cancel when user cancels biometric prompt', async () => {
58+
// Simulate user clicking "Cancel" in biometric dialog (must include 'cancel' in message)
59+
const cancelError = new Error('User cancelled biometric authentication')
60+
mockNativeBiometric.verifyIdentity.mockRejectedValue(cancelError)
61+
62+
// User starts authentication but cancels it
63+
const result = await biometricAuth.authorizeUser()
64+
65+
// Expect "cancel" result, not error
66+
expect(result).toBe(AuthenticationResult.Cancel)
67+
})
68+
69+
it('should return failed when biometric authentication fails', async () => {
70+
// Simulate unsuccessful biometric recognition (error WITHOUT 'cancel' keyword)
71+
const authError = new Error('Biometric authentication failed')
72+
mockNativeBiometric.verifyIdentity.mockRejectedValue(authError)
73+
74+
// User tries to login but biometric is not recognized
75+
const result = await biometricAuth.authorizeUser()
76+
77+
// Expect failed authentication
78+
expect(result).toBe(AuthenticationResult.Failed)
79+
})
80+
81+
it('should return failed when error does not contain cancel keyword', async () => {
82+
// Simulate error that does not contain 'cancel' (should be Failed, not Cancel)
83+
const nonCancelError = new Error('Biometric sensor unavailable')
84+
mockNativeBiometric.verifyIdentity.mockRejectedValue(nonCancelError)
85+
86+
// User tries to authenticate but gets non-cancel error
87+
const result = await biometricAuth.authorizeUser()
88+
89+
// Should return Failed (not Cancel) because error doesn't contain 'cancel'
90+
expect(result).toBe(AuthenticationResult.Failed)
91+
})
92+
})
93+
94+
describe('User tries to authenticate with biometrics in web browser', () => {
95+
beforeEach(() => {
96+
// Simulate that we're in web browser, not native app
97+
mockCapacitor.isNativePlatform.mockReturnValue(false)
98+
})
99+
100+
it('should return failed as biometrics not available in browser', async () => {
101+
// Biometrics are not available in browser
102+
const result = await biometricAuth.authorizeUser()
103+
104+
// Expect authentication to fail
105+
expect(result).toBe(AuthenticationResult.Failed)
106+
})
107+
})
108+
109+
describe('User wants to setup biometric authentication', () => {
110+
beforeEach(() => {
111+
// Setup is only possible on mobile devices
112+
mockCapacitor.isNativePlatform.mockReturnValue(true)
113+
})
114+
115+
it('should successfully setup when device supports biometrics', async () => {
116+
// Device supports biometrics (has fingerprint sensor/Face ID)
117+
mockNativeBiometric.isAvailable.mockResolvedValue({
118+
isAvailable: true,
119+
biometryType: 1
120+
})
121+
122+
// User tries to enable biometric authentication
123+
const result = await biometricAuth.setupBiometric()
124+
125+
// Setup should succeed
126+
expect(result).toBe(SetupResult.Success)
127+
})
128+
129+
it('should fail setup when device does not support biometrics', async () => {
130+
// Device doesn't support biometrics (old device without sensors)
131+
mockNativeBiometric.isAvailable.mockResolvedValue({
132+
isAvailable: false,
133+
biometryType: 0
134+
})
135+
136+
// User tries to enable biometrics on unsupported device
137+
const result = await biometricAuth.setupBiometric()
138+
139+
// Setup should fail
140+
expect(result).toBe(SetupResult.Failed)
141+
})
142+
143+
it('should return cancel when user cancels biometric setup', async () => {
144+
// Simulate user cancelling during setup (includes 'cancel' keyword)
145+
const cancelError = new Error('User cancelled biometric setup')
146+
mockNativeBiometric.isAvailable.mockRejectedValue(cancelError)
147+
148+
// User starts biometric setup but cancels it
149+
const result = await biometricAuth.setupBiometric()
150+
151+
// Should return Cancel for setup cancellation
152+
expect(result).toBe(SetupResult.Cancel)
153+
})
154+
})
155+
})
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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

Comments
 (0)