1 # test.config - tests for configuration management
3 # Part of WiFi Radar: A utility for managing WiFi profiles on GNU/Linux.
5 # Copyright (C) 2014 Sean Robinson <robinson@tuxfamily.org>
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; version 2 of the License.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License in LICENSE.GPL for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to:
18 # Free Software Foundation, Inc.
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 from __future__
import unicode_literals
26 from configparser
import NoOptionError
, NoSectionError
27 from io
import StringIO
32 from wifiradar
.config
import (make_section_name
,
33 ConfigManager
, ConfigFileManager
)
34 from wifiradar
.misc
import PYVERSION
, NoDeviceError
37 class TestConfigManager(unittest
.TestCase
):
39 self
.config
= ConfigManager()
41 @mock.patch('wifiradar.config.Popen')
42 def test_get_network_device(self
, mock_popen
):
43 """ Test get_network_device. """
44 self
.config
.set_opt('GENERAL', 'interface', 'wlan0')
45 self
.assertEqual('wlan0', self
.config
.get_network_device())
47 # Test successful auto-detected interface.
48 mock_popen_instance
= mock
.MagicMock()
49 mock_popen_instance
.stdout
.__iter
__.return_value
= [
50 'wlan0 IEEE 802.11abgn ESSID:"WinterPalace"\n',
51 ' Mode:Managed Frequency:5.26 GHz Access Point: 00:00:00:00:00:00\n',
53 ' Retry long limit:7 RTS thr:off Fragment thr:off\n',
54 ' Power Management:off\n']
55 mock_popen
.return_value
= mock_popen_instance
56 self
.config
.set_opt('GENERAL', 'interface', 'auto_detect')
57 self
.config
.set_opt('GENERAL', 'iwconfig_command', 'mock_iwconfig')
58 self
.assertEqual('wlan0', self
.config
.get_network_device())
60 # Test failed auto-detected interface.
61 mock_popen_instance
= mock
.MagicMock()
62 mock_popen_instance
.stdout
.__iter
__.return_value
= [
63 'lo no wireless extensions.\n',
65 'eth0 no wireless extensions.\n',
67 mock_popen
.return_value
= mock_popen_instance
68 self
.config
.set_opt('GENERAL', 'interface', 'auto_detect')
69 self
.config
.set_opt('GENERAL', 'iwconfig_command', 'mock_iwconfig')
70 self
.assertRaises(NoDeviceError
,
71 self
.config
.get_network_device
)
73 def test_set_get_opt(self
):
74 """ Test set_opt and get_opt methods. """
75 self
.assertEqual(self
.config
.sections(), [])
76 self
.config
.set_opt('TEST_SECTION', 'str_opt', 'str_value')
77 self
.assertEqual('str_value',
78 self
.config
.get_opt('TEST_SECTION', 'str_opt'))
80 # Check for expected exceptions in set_opt.
81 self
.assertRaises(TypeError, self
.config
.set_opt
,
82 'TEST_SECTION', 'str_opt', True)
83 self
.assertRaises(TypeError, self
.config
.set_opt
,
84 'TEST_SECTION', 'str_opt', 0)
85 self
.assertRaises(TypeError, self
.config
.set_opt
,
86 'TEST_SECTION', 'str_opt', 1.0)
88 # Check for expected exceptions in get_opt.
89 self
.assertRaises(NoSectionError
, self
.config
.get_opt
,
90 'UNKNOWN_SECTION', 'str_opt')
91 self
.assertRaises(NoOptionError
, self
.config
.get_opt
,
92 'TEST_SECTION', 'unknown_opt')
94 def test_set_get_bool_opt(self
):
95 """ Test set_bool_opt and get_opt_as_bool methods. """
96 self
.assertEqual(self
.config
.sections(), [])
97 # Alternate True/False tests in case set_bool_opt does not change
98 # the value in 'bool_opt'
100 # Check boolean conversion.
101 self
.config
.set_bool_opt('TEST_SECTION', 'bool_opt', True)
102 self
.assertEqual(True,
103 self
.config
.get_opt_as_bool('TEST_SECTION', 'bool_opt'))
104 self
.config
.set_bool_opt('TEST_SECTION', 'bool_opt', False)
105 self
.assertEqual(False,
106 self
.config
.get_opt_as_bool('TEST_SECTION', 'bool_opt'))
108 # Check successful integer conversion.
109 self
.config
.set_bool_opt('TEST_SECTION', 'bool_opt', 1)
110 self
.assertEqual(True,
111 self
.config
.get_opt_as_bool('TEST_SECTION', 'bool_opt'))
112 self
.config
.set_bool_opt('TEST_SECTION', 'bool_opt', 0)
113 self
.assertEqual(False,
114 self
.config
.get_opt_as_bool('TEST_SECTION', 'bool_opt'))
116 # Check string conversion.
117 self
.config
.set_bool_opt('TEST_SECTION', 'bool_opt', 'True')
118 self
.assertEqual(True,
119 self
.config
.get_opt_as_bool('TEST_SECTION', 'bool_opt'))
120 self
.config
.set_bool_opt('TEST_SECTION', 'bool_opt', 'False')
121 self
.assertEqual(False,
122 self
.config
.get_opt_as_bool('TEST_SECTION', 'bool_opt'))
124 # extra possible values
125 self
.config
.set_bool_opt('TEST_SECTION', 'bool_opt', 2)
126 self
.assertEqual(True,
127 self
.config
.get_opt_as_bool('TEST_SECTION', 'bool_opt'))
129 # Check for expected exceptions in set_bool_opt.
130 self
.assertRaises(ValueError, self
.config
.set_bool_opt
,
131 'TEST_SECTION', 'bool_opt', -1)
132 self
.assertRaises(ValueError, self
.config
.set_bool_opt
,
133 'TEST_SECTION', 'bool_opt', 'Not False')
134 self
.assertRaises(ValueError, self
.config
.set_bool_opt
,
135 'TEST_SECTION', 'bool_opt', 1.0)
137 # Check for expected exceptions in get_opt_as_bool.
138 self
.config
.set_opt('TEST_SECTION', 'bool_opt', 'spam')
139 self
.assertRaises(ValueError, self
.config
.get_opt_as_bool
,
140 'TEST_SECTION', 'bool_opt')
142 def test_set_get_int_opt(self
):
143 """ Test set_int_opt and get_opt_as_int methods. """
144 self
.assertEqual(self
.config
.sections(), [])
145 self
.config
.set_int_opt('TEST_SECTION', 'int_opt', 42)
147 self
.config
.get_opt_as_int('TEST_SECTION', 'int_opt'))
149 # Check for expected exceptions in set_int_opt.
150 # Boolean values are integers, so do not try to find an exception.
151 self
.assertRaises(TypeError, self
.config
.set_int_opt
,
152 'TEST_SECTION', 'int_opt', 'eggs')
153 self
.assertRaises(TypeError, self
.config
.set_int_opt
,
154 'TEST_SECTION', 'int_opt', 1.0)
156 # Check for expected exceptions in get_opt_as_int.
157 self
.config
.set_opt('TEST_SECTION', 'int_opt', 'spam')
158 self
.assertRaises(ValueError, self
.config
.get_opt_as_int
,
159 'TEST_SECTION', 'int_opt')
161 def test_set_get_float_opt(self
):
162 """ Test set_float_opt and get_opt_as_float methods. """
163 self
.assertEqual(self
.config
.sections(), [])
164 self
.config
.set_float_opt('TEST_SECTION', 'float_opt', 42)
165 self
.assertEqual(42.0,
166 self
.config
.get_opt_as_float('TEST_SECTION', 'float_opt'))
167 self
.config
.set_float_opt('TEST_SECTION', 'float_opt', 3.0)
168 self
.assertEqual(3.0,
169 self
.config
.get_opt_as_float('TEST_SECTION', 'float_opt'))
171 # Check for expected exceptions in set_float_opt.
172 # Boolean values are integers, so do not try to find an exception.
173 self
.assertRaises(TypeError, self
.config
.set_float_opt
,
174 'TEST_SECTION', 'float_opt', 'eggs')
176 # Check for expected exceptions in get_opt_as_float.
177 self
.config
.set_opt('TEST_SECTION', 'float_opt', 'spam')
178 self
.assertRaises(ValueError, self
.config
.get_opt_as_float
,
179 'TEST_SECTION', 'float_opt')
181 def test_set_section(self
):
182 """ Test set_section method. """
183 self
.assertEqual(self
.config
.sections(), [])
185 options
= {'bool_opt': True, 'int_opt': 42, 'float_opt': 3.1415,
186 'str_opt': 'eggs and spam'}
187 self
.config
.set_section('TEST_SECTION', options
)
188 self
.assertEqual(True,
189 self
.config
.get_opt_as_bool('TEST_SECTION', 'bool_opt'))
191 self
.config
.get_opt_as_int('TEST_SECTION', 'int_opt'))
192 self
.assertEqual(3.1415,
193 self
.config
.get_opt_as_float('TEST_SECTION', 'float_opt'))
194 self
.assertEqual('eggs and spam',
195 self
.config
.get_opt('TEST_SECTION', 'str_opt'))
197 def test_profiles(self
):
198 """ Test profiles method. """
199 self
.assertEqual(self
.config
.sections(), [])
201 self
.config
.add_section('TEST_SECTION')
202 self
.config
.add_section('TEST_AP01:00:01:02:03:04:05')
203 self
.config
.add_section('spam.com:AA:BB:CC:DD:EE:FF')
205 self
.assertEqual(['TEST_AP01:00:01:02:03:04:05',
206 'spam.com:AA:BB:CC:DD:EE:FF'],
207 self
.config
.profiles())
209 def test_update(self
):
210 """ Test update method. """
211 items_orig
= dict((('eggs', 'spam'), ('ham', 'spam and spam')))
212 items_copy
= dict((('eggs', 'spam and toast'),
213 ('ham', 'spam and spam and toast')))
214 # Set up an original ConfigManager.
215 self
.config
.set_section('DEFAULT', items_orig
)
216 self
.config
.set_section('SECT01', items_orig
)
217 self
.config
.set_section('SECT02', items_orig
)
218 self
.config
.set_section('SECT03', items_orig
)
219 self
.config
.set_section('WinterPalace:00:00:00:00:00:00', items_orig
)
220 self
.config
.set_section('SummerKingdom:', items_orig
)
221 # Set up a copy ConfigManager.
222 config_copy
= ConfigManager(defaults
=items_copy
)
223 config_copy
.set_section('SECT01', items_copy
)
224 config_copy
.set_section('SECT02', items_copy
)
225 # Update the original from the copy.
226 self
.config
.update(config_copy
)
227 # Check that sections in config_copy have changed.
228 for section
in config_copy
.sections():
229 self
.assertEqual(self
.config
.items(section
), items_copy
)
230 # Check that the profiles and extra sections are unchanged.
231 for section
in self
.config
.profiles():
232 self
.assertEqual(self
.config
.items(section
), items_orig
)
233 self
.assertEqual(self
.config
.items('SECT03'), items_orig
)
236 """ Test copy method. """
237 # Copy just the DEFAULT section.
238 defaults
= [('eggs', 'spam'), ('ham', 'spam and spam')]
239 orig
= ConfigManager(defaults
=dict(defaults
))
241 self
.assertEqual(copy
.items('DEFAULT'), orig
.items('DEFAULT'))
242 # Copy multiple sections with profiles.
243 items
= dict((('eggs', 'spam'), ('ham', 'spam and spam')))
244 orig
= ConfigManager()
245 orig
.set_section('SECT01', items
)
246 orig
.set_section('SECT02', items
)
247 orig
.set_section('WinterPalace:00:00:00:00:00:00', items
)
248 orig
.set_section('SummerKingdom:', items
)
249 copy
= orig
.copy(profiles
=True)
250 for section
in orig
.sections():
251 self
.assertEqual(copy
.items(section
), orig
.items(section
))
252 # Copy multiple sections without profiles.
253 copy
= orig
.copy(profiles
=False)
254 orig
.remove_section('WinterPalace:00:00:00:00:00:00')
255 orig
.remove_section('SummerKingdom:')
256 for section
in orig
.sections():
257 self
.assertEqual(copy
.items(section
), orig
.items(section
))
260 class TestConfigFileManager(unittest
.TestCase
):
262 self
.test_conf_file
= './test/data/wifi-radar-test.conf'
264 'auto_profile_order': "[u'WinterPalace:00:09:5B:D5:03:4A']",
265 'commit_required': 'False',
266 'ifconfig_command': '/sbin/ifconfig',
267 'ifup_required': 'False',
268 'interface': 'auto_detect',
269 'iwconfig_command': '/sbin/iwconfig',
270 'iwlist_command': '/sbin/iwlist',
271 'logfile': '/var/log/wifi-radar.log',
273 'route_command': '/sbin/route',
275 self
.DHCP
= {'args': '-D -o -i dhcp_client -t ${timeout}',
276 'command': '/sbin/dhcpcd',
278 'pidfile': '/etc/dhcpc/dhcpcd-${GENERAL:interface}.pid',
280 self
.WPA
= {'args': '-B -i ${GENERAL:interface} -c ${configuration} '
281 '-D ${driver} -P ${pidfile}',
282 'command': '/usr/sbin/wpa_supplicant',
283 'configuration': '/etc/wpa_supplicant.conf',
286 'pidfile': '/var/run/wpa_supplicant.pid'}
287 self
.WINTERPALACE
= {'available': 'False',
288 'bssid': '00:09:5B:D5:03:4A',
290 'con_postscript': '',
292 'dis_postscript': '',
298 'essid': 'WinterPalace',
314 """ Test read method. """
315 self
.config_file
= ConfigFileManager(self
.test_conf_file
)
316 config
= self
.config_file
.read()
317 self
.assertEqual(config
.items('GENERAL', raw
=True), self
.GENERAL
)
318 self
.assertEqual(config
.items('DHCP', raw
=True), self
.DHCP
)
319 self
.assertEqual(config
.items('WPA', raw
=True), self
.WPA
)
320 self
.assertEqual(config
.items('WinterPalace:00:09:5B:D5:03:4A',
321 raw
=True), self
.WINTERPALACE
)
322 self
.assertEqual(config
.auto_profile_order
,
323 [u
'WinterPalace:00:09:5B:D5:03:4A'])
325 def test_read_failures(self
):
326 """ Test failure modes in the read method. """
328 self
.config_file
= ConfigFileManager(
329 self
.test_conf_file
.replace('wifi-radar-test', 'missing_file'))
330 with self
.assertRaises(IOError) as e
:
331 config
= self
.config_file
.read()
332 self
.assertEqual(2, e
.exception
)
333 # File whose permissions do not allow reading.
334 self
.config_file
= ConfigFileManager(
335 self
.test_conf_file
.replace('-test', '-unreadable'))
336 with self
.assertRaises(IOError) as e
:
337 config
= self
.config_file
.read()
338 self
.assertEqual(13, e
.exception
)
340 def test_read_string(self
):
341 """ Test read_string method. """
342 with codecs
.open(self
.test_conf_file
, 'r', encoding
='utf8') as f
:
343 test_string
= f
.read()
344 config_file
= ConfigFileManager('')
345 config
= config_file
.read_string(test_string
)
346 self
.assertEqual(config
.items('GENERAL', raw
=True), self
.GENERAL
)
347 self
.assertEqual(config
.items('DHCP', raw
=True), self
.DHCP
)
348 self
.assertEqual(config
.items('WPA', raw
=True), self
.WPA
)
349 self
.assertEqual(config
.items('WinterPalace:00:09:5B:D5:03:4A',
350 raw
=True), self
.WINTERPALACE
)
351 self
.assertEqual(config
.auto_profile_order
,
352 [u
'WinterPalace:00:09:5B:D5:03:4A'])
354 @mock.patch('wifiradar.config.move')
355 @mock.patch('codecs.open')
356 @mock.patch('tempfile.mkstemp')
357 def test_write(self
, mock_mkstemp
, mock_open
, mock_move
):
358 """ Test write method. """
359 mock_temp_file
= 'mock_file_tempXXXXXX'
360 test_file
= StringIO()
361 test_file
._close
= test_file
.close
362 test_file
.close
= lambda: None
363 mock_mkstemp
.return_value
= (1, mock_temp_file
)
364 mock_open
.return_value
= test_file
365 # Set up a copy ConfigManager.
366 config_copy
= ConfigManager()
367 config_copy
.set_section('GENERAL', self
.GENERAL
)
368 config_copy
.set_section('DHCP', self
.DHCP
)
369 config_copy
.set_section('WPA', self
.WPA
)
370 config_copy
.set_section('WinterPalace:00:09:5B:D5:03:4A',
372 config_copy
.auto_profile_order
= [u
'WinterPalace:00:09:5B:D5:03:4A']
373 self
.config_file
= ConfigFileManager('mock_file')
374 self
.config_file
.write(config_copy
)
375 with
open(self
.test_conf_file
, 'r') as f
:
377 self
.assertEqual(test_file
.getvalue(), test_data
)
379 mock_mkstemp
.assert_called_once_with(prefix
='wifi-radar.conf.')
380 mock_open
.assert_called_once_with(mock_temp_file
, 'w', encoding
='utf8')
381 mock_move
.assert_called_once_with(mock_temp_file
, 'mock_file')
384 class TestConfigFunctions(unittest
.TestCase
):
386 def test_make_section_name(self
):
387 """ Test make_section_name function. """
388 # Check single AP profiles.
389 self
.assertEqual('essid:bssid',
390 make_section_name('essid', 'bssid'))
391 self
.assertEqual('TEST_AP01:00:01:02:03:04:05',
392 make_section_name('TEST_AP01', '00:01:02:03:04:05'))
393 self
.assertEqual('spam.com:AA:BB:CC:DD:EE:FF',
394 make_section_name('spam.com', 'AA:BB:CC:DD:EE:FF'))
395 self
.assertEqual('eggs_and_ham:11:BB:CC:DD:EE:FF',
396 make_section_name('eggs_and_ham', '11:BB:CC:DD:EE:FF'))
397 self
.assertEqual('eggs:and:spam:22:BB:CC:DD:EE:FF',
398 make_section_name('eggs:and:spam', '22:BB:CC:DD:EE:FF'))
399 # Check roaming profiles.
400 self
.assertEqual('essid:',
401 make_section_name('essid', ''))
402 self
.assertEqual('TEST_AP01:',
403 make_section_name('TEST_AP01', ''))
404 self
.assertEqual('spam.com:',
405 make_section_name('spam.com', ''))
406 self
.assertEqual('eggs_and_ham:',
407 make_section_name('eggs_and_ham', ''))
408 self
.assertEqual('eggs:and:spam:',
409 make_section_name('eggs:and:spam', ''))