WellNuo/services/__tests__/wifiPasswordStore.test.ts
Sergei bbc45ddb5f Implement secure WiFi password storage using SecureStore
- Create wifiPasswordStore service for encrypted password storage
- Replace AsyncStorage with SecureStore for WiFi credentials
- Add automatic migration from AsyncStorage to SecureStore
- Integrate WiFi password cleanup into logout process
- Add comprehensive test suite for password storage operations
- Update setup-wifi screen to use secure storage

Security improvements:
- WiFi passwords now stored encrypted via expo-secure-store
- Passwords automatically cleared on user logout
- Seamless migration for existing users

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-29 11:13:37 -08:00

318 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Tests for WiFi Password Secure Storage Service
*/
import * as SecureStore from 'expo-secure-store';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
saveWiFiPassword,
getWiFiPassword,
getAllWiFiPasswords,
removeWiFiPassword,
clearAllWiFiPasswords,
migrateFromAsyncStorage,
} from '../wifiPasswordStore';
// Mock SecureStore
jest.mock('expo-secure-store', () => ({
setItemAsync: jest.fn(),
getItemAsync: jest.fn(),
deleteItemAsync: jest.fn(),
}));
// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage', () => ({
default: {
getItem: jest.fn(),
removeItem: jest.fn(),
},
}));
describe('wifiPasswordStore', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('saveWiFiPassword', () => {
it('should save a new WiFi password', async () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
await saveWiFiPassword('MyNetwork', 'password123');
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
JSON.stringify({ MyNetwork: 'password123' })
);
});
it('should update an existing WiFi password', async () => {
const existingPasswords = { Network1: 'pass1', Network2: 'pass2' };
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
JSON.stringify(existingPasswords)
);
await saveWiFiPassword('Network1', 'newpass1');
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
JSON.stringify({ Network1: 'newpass1', Network2: 'pass2' })
);
});
it('should add password to existing map', async () => {
const existingPasswords = { Network1: 'pass1' };
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
JSON.stringify(existingPasswords)
);
await saveWiFiPassword('Network2', 'pass2');
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
JSON.stringify({ Network1: 'pass1', Network2: 'pass2' })
);
});
});
describe('getWiFiPassword', () => {
it('should return password for existing network', async () => {
const passwords = { MyNetwork: 'password123', OtherNetwork: 'pass456' };
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
JSON.stringify(passwords)
);
const password = await getWiFiPassword('MyNetwork');
expect(password).toBe('password123');
});
it('should return undefined for non-existing network', async () => {
const passwords = { MyNetwork: 'password123' };
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
JSON.stringify(passwords)
);
const password = await getWiFiPassword('NonExistent');
expect(password).toBeUndefined();
});
it('should return undefined when no passwords saved', async () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
const password = await getWiFiPassword('MyNetwork');
expect(password).toBeUndefined();
});
it('should handle SecureStore errors gracefully', async () => {
(SecureStore.getItemAsync as jest.Mock).mockRejectedValueOnce(
new Error('SecureStore error')
);
const password = await getWiFiPassword('MyNetwork');
expect(password).toBeUndefined();
});
});
describe('getAllWiFiPasswords', () => {
it('should return all saved passwords', async () => {
const passwords = { Network1: 'pass1', Network2: 'pass2', Network3: 'pass3' };
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
JSON.stringify(passwords)
);
const result = await getAllWiFiPasswords();
expect(result).toEqual(passwords);
});
it('should return empty object when no passwords saved', async () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
const result = await getAllWiFiPasswords();
expect(result).toEqual({});
});
it('should handle SecureStore errors gracefully', async () => {
(SecureStore.getItemAsync as jest.Mock).mockRejectedValueOnce(
new Error('SecureStore error')
);
const result = await getAllWiFiPasswords();
expect(result).toEqual({});
});
});
describe('removeWiFiPassword', () => {
it('should remove specific network password', async () => {
const passwords = { Network1: 'pass1', Network2: 'pass2', Network3: 'pass3' };
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
JSON.stringify(passwords)
);
await removeWiFiPassword('Network2');
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
JSON.stringify({ Network1: 'pass1', Network3: 'pass3' })
);
});
it('should delete key when removing last password', async () => {
const passwords = { Network1: 'pass1' };
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
JSON.stringify(passwords)
);
await removeWiFiPassword('Network1');
expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('WIFI_PASSWORDS');
expect(SecureStore.setItemAsync).not.toHaveBeenCalled();
});
it('should handle removal of non-existent network', async () => {
const passwords = { Network1: 'pass1', Network2: 'pass2' };
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
JSON.stringify(passwords)
);
await removeWiFiPassword('NonExistent');
// Should still update the store with unchanged passwords
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
JSON.stringify(passwords)
);
});
});
describe('clearAllWiFiPasswords', () => {
it('should delete all WiFi passwords', async () => {
await clearAllWiFiPasswords();
expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('WIFI_PASSWORDS');
});
it('should handle errors gracefully', async () => {
(SecureStore.deleteItemAsync as jest.Mock).mockRejectedValueOnce(
new Error('Delete failed')
);
await expect(clearAllWiFiPasswords()).rejects.toThrow('Delete failed');
});
});
describe('migrateFromAsyncStorage', () => {
it('should migrate passwords from AsyncStorage to SecureStore', async () => {
const oldPasswords = { Network1: 'pass1', Network2: 'pass2' };
// No existing data in SecureStore
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
// Old data in AsyncStorage
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(
JSON.stringify(oldPasswords)
);
await migrateFromAsyncStorage();
// Should save to SecureStore
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
JSON.stringify(oldPasswords)
);
// Should remove from AsyncStorage
expect(AsyncStorage.removeItem).toHaveBeenCalledWith('WIFI_PASSWORDS');
expect(AsyncStorage.removeItem).toHaveBeenCalledWith('LAST_WIFI_PASSWORD');
});
it('should skip migration if data already exists in SecureStore', async () => {
const existingPasswords = { Network1: 'pass1' };
// Data already exists in SecureStore
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
JSON.stringify(existingPasswords)
);
await migrateFromAsyncStorage();
// Should not call AsyncStorage
expect(AsyncStorage.getItem).not.toHaveBeenCalled();
expect(SecureStore.setItemAsync).not.toHaveBeenCalled();
});
it('should skip migration if no data in AsyncStorage', async () => {
// No data in SecureStore
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
// No data in AsyncStorage
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(null);
await migrateFromAsyncStorage();
// Should not migrate anything
expect(SecureStore.setItemAsync).not.toHaveBeenCalled();
expect(AsyncStorage.removeItem).not.toHaveBeenCalled();
});
it('should not throw error on migration failure', async () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
(AsyncStorage.getItem as jest.Mock).mockRejectedValueOnce(
new Error('AsyncStorage error')
);
// Should not throw
await expect(migrateFromAsyncStorage()).resolves.toBeUndefined();
});
});
describe('Special characters in SSID and password', () => {
it('should handle special characters in SSID', async () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
const ssid = "Network's-Name!@#$%^&*()";
const password = 'password123';
await saveWiFiPassword(ssid, password);
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
JSON.stringify({ [ssid]: password })
);
});
it('should handle special characters in password', async () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
const ssid = 'MyNetwork';
const password = "P@ssw0rd!#$%^&*()_+-=[]{}|;':,.<>?";
await saveWiFiPassword(ssid, password);
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
JSON.stringify({ [ssid]: password })
);
});
it('should handle unicode characters', async () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
const ssid = '我的网络';
const password = 'пароль123';
await saveWiFiPassword(ssid, password);
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
JSON.stringify({ [ssid]: password })
);
});
});
});