async def async_server(request, virtual_smoothie_env, loop): if _should_skip_api1(request): pytest.skip('requires api1 only') elif _should_skip_api2(request): pytest.skip('requires api2 only') with request.param(loop) as hw: if request.param == using_api1: app = init(hw) app['api_version'] = 1 else: app = init(hw) app['api_version'] = 2 yield app await app.shutdown()
async def test_wifi_list(virtual_smoothie_env, loop, aiohttp_client, monkeypatch): app = init() cli = await loop.create_task(aiohttp_client(app)) expected_res = [{ 'ssid': 'Opentrons', 'signal': 81, 'active': True }, { 'ssid': 'Big Duck', 'signal': 47, 'active': False }, { 'ssid': 'HP-Print-1-LaserJet Pro', 'signal': 35, 'active': False }, { 'ssid': 'Guest Wireless', 'signal': 24, 'active': False }] async def mock_available(): return expected_res monkeypatch.setattr(nmcli, 'available_ssids', mock_available) expected = json.dumps({'list': expected_res}) resp = await cli.get('/wifi/list') text = await resp.text() assert resp.status == 200 assert text == expected
async def test_wifi_configure( virtual_smoothie_env, loop, test_client, monkeypatch): app = init(loop) cli = await loop.create_task(test_client(app)) msg = "Device 'wlan0' successfully activated with '076aa998-0275-4aa0-bf85-e9629021e267'." # noqa async def mock_configure(ssid, securityType=None, psk=None, hidden=False): # Command: nmcli device wifi connect "{ssid}" password "{psk}" return True, msg monkeypatch.setattr(nmcli, 'configure', mock_configure) expected = {'ssid': 'Opentrons', 'message': msg} resp = await cli.post( '/wifi/configure', json={'ssid': 'Opentrons', 'psk': 'scrt sqrl'}) body = await resp.json() assert resp.status == 201 assert body == expected resp = await cli.post( '/wifi/configure', json={'ssid': 'asasd', 'foo': 'bar'}) assert resp.status == 400 body = await resp.json() assert 'message' in body
async def test_list_keys(loop, test_client, wifi_keys_tempdir): dummy_names = ['ad12d1df199bc912', 'cbdda8124128cf', '812410990c5412'] app = init(loop) cli = await loop.create_task(test_client(app)) empty_resp = await cli.get('/wifi/keys') assert empty_resp.status == 200 empty_body = await empty_resp.json() assert empty_body == {'keys': []} for dn in dummy_names: os.mkdir(os.path.join(wifi_keys_tempdir, dn)) open(os.path.join(wifi_keys_tempdir, dn, 'test.pem'), 'w').write('hi') resp = await cli.get('/wifi/keys') assert resp.status == 200 body = await resp.json() keys = body['keys'] assert len(keys) == 3 for dn in dummy_names: for keyfile in keys: if keyfile['id'] == dn: assert keyfile['name'] == 'test.pem' assert keyfile['uri'] == '/wifi/keys/{}'.format(dn) break else: raise KeyError(dn)
async def test_ignore_updates(virtual_smoothie_env, loop, aiohttp_client): tmpdir = tempfile.mkdtemp("files") ignore_name = "testy_ignore.json" serverlib_fallback.filepath = os.path.join(tmpdir, ignore_name) app = init() cli = await loop.create_task(aiohttp_client(app)) # Test no ignore file found r0 = await cli.get('/update/ignore') r0body = await r0.text() assert json.loads(r0body) == {'version': None} # Test that values are set correctly ignore = {'version': '3.1.3'} r1 = await cli.post('/update/ignore', json=ignore) assert r1.status == 200 # Test that you cannot pass an empty version ignore2 = {'version': ''} r2 = await cli.post('/update/ignore', json=ignore2) assert r2.status == 400 # Test that version in the temporary directory is still '3.1.3' r3 = await cli.get('/update/ignore') r3body = await r3.text() assert json.loads(r3body) == {'version': '3.1.3'}
async def test_get(virtual_smoothie_env, loop, test_client): app = init(loop) cli = await loop.create_task(test_client(app)) resp = await cli.get('/settings') body = await resp.json() assert resp.status == 200 validate_response_body(body)
async def async_server(request, virtual_smoothie_env, loop): if request.node.get_marker('api1_only') and request.param != using_api1: pytest.skip('requires api1 only') elif request.node.get_marker('api2_only') and request.param != using_api2: pytest.skip('requires api2 only') with request.param(loop): app = init(loop) app['api_version'] = 1 if request.param == using_api1 else 2 yield app await app.shutdown()
async def test_add_key_no_key(loop, aiohttp_client): """Test response when no key supplied""" app = init() cli = await loop.create_task(aiohttp_client(app)) with patch("opentrons.system.wifi.add_key") as p: r = await cli.post('/wifi/keys', data={}) p.assert_not_called() assert r.status == 400
async def test_key_lifecycle(loop, test_client, wifi_keys_tempdir): with tempfile.TemporaryDirectory() as source_td: app = init(loop) cli = await loop.create_task(test_client(app)) empty_resp = await cli.get('/wifi/keys') assert empty_resp.status == 200 empty_body = await empty_resp.json() assert empty_body == {'keys': []} results = {} # We should be able to add multiple keys for fn in ['test1.pem', 'test2.pem', 'test3.pem']: path = os.path.join(source_td, fn) with open(path, 'w') as f: f.write(str(random.getrandbits(2048))) upload_resp = await cli.post('/wifi/keys', data={'key': open(path, 'rb')}) assert upload_resp.status == 201 upload_body = await upload_resp.json() assert 'uri' in upload_body assert 'id' in upload_body assert 'name' in upload_body assert upload_body['name'] == os.path.basename(fn) assert upload_body['uri'] == '/wifi/keys/'\ + upload_body['id'] results[fn] = upload_body # We should not be able to upload a duplicate dup_resp = await cli.post( '/wifi/keys', data={'key': open(os.path.join(source_td, 'test1.pem'))}) assert dup_resp.status == 200 dup_body = await dup_resp.json() assert 'message' in dup_body # We should be able to see them all list_resp = await cli.get('/wifi/keys') assert list_resp.status == 200 list_body = await list_resp.json() keys = list_body['keys'] assert len(keys) == 3 for elem in keys: assert elem['id'] in [r['id'] for r in results.values()] for fn, data in results.items(): del_resp = await cli.delete(data['uri']) assert del_resp.status == 200 del_body = await del_resp.json() assert 'message' in del_body del_list_resp = await cli.get('/wifi/keys') del_list_body = await del_list_resp.json() assert data['id'] not in [k['id'] for k in del_list_body['keys']] dup_del_resp = await cli.delete(results['test1.pem']['uri']) assert dup_del_resp.status == 404
async def test_remove_key(arg, remove_key_return, expected_status, expected_body, loop, aiohttp_client): app = init() cli = await loop.create_task(aiohttp_client(app)) with patch("opentrons.system.wifi.remove_key") as p: p.return_value = remove_key_return r = await cli.delete("/wifi/keys/" + arg) p.assert_called_once_with(arg) assert r.status == expected_status assert await r.json() == expected_body
async def test_set(virtual_smoothie_env, loop, test_client): app = init(loop) cli = await loop.create_task(test_client(app)) test_id = 'disableHomeOnBoot' resp = await cli.post('/settings', json={"id": test_id, "value": True}) body = await resp.json() assert resp.status == 200 validate_response_body(body) test_setting = list( filter(lambda x: x.get('id') == test_id, body.get('settings')))[0] assert test_setting.get('value')
async def test_health(virtual_smoothie_env, loop, aiohttp_client): app = init() cli = await loop.create_task(aiohttp_client(app)) expected = json.dumps({ 'name': 'opentrons-dev', 'api_version': __version__, 'fw_version': 'Virtual Smoothie', 'logs': ['/logs/serial.log', '/logs/api.log'], 'system_version': '0.0.0' }) resp = await cli.get('/health') text = await resp.text() assert resp.status == 200 assert text == expected
async def test_add_key_response(add_key_return, expected_status, expected_body, loop, aiohttp_client, wifi_keys_tempdir): with tempfile.TemporaryDirectory() as source_td: app = init() cli = await loop.create_task(aiohttp_client(app)) path = os.path.join(source_td, "t.pem") with open(path, 'w') as f: f.write(str(random.getrandbits(20))) with patch("opentrons.system.wifi.add_key") as p: p.return_value = add_key_return r = await cli.post('/wifi/keys', data={'key': open(path, 'rb')}) assert r.status == expected_status assert await r.json() == expected_body
async def test_restart(virtual_smoothie_env, monkeypatch, loop, test_client): test_data = {"test": "pass"} async def mock_restart(request): return web.json_response(test_data) monkeypatch.setattr(serverlib_fallback, 'restart', mock_restart) app = init(loop) cli = await loop.create_task(test_client(app)) expected = json.dumps(test_data) resp = await cli.post('/server/restart') text = await resp.text() assert resp.status == 200 assert text == expected
async def test_available_resets(virtual_smoothie_env, loop, test_client): app = init(loop) cli = await loop.create_task(test_client(app)) resp = await cli.get('/settings/reset/options') body = await resp.json() options_list = body.get('options') assert resp.status == 200 for key in ['tipProbe', 'labwareCalibration', 'bootScripts']: for opt in options_list: if opt['id'] == key: assert 'name' in opt assert 'description' in opt break else: raise KeyError(key)
async def test_networking_status(virtual_smoothie_env, loop, aiohttp_client, monkeypatch): app = init() cli = await loop.create_task(aiohttp_client(app)) async def mock_is_connected(): return 'full' connection_statuses = { 'wlan0': { # test "--" gets mapped to None 'ipAddress': None, 'macAddress': 'B8:27:EB:5F:A6:89', # test "--" gets mapped to None 'gatewayAddress': None, 'state': 'disconnected', 'type': 'wifi' }, 'eth0': { 'ipAddress': '169.254.229.173/16', 'macAddress': 'B8:27:EB:39:C0:9A', # test missing output gets mapped to None 'gatewayAddress': None, 'state': 'connected', 'type': 'ethernet' } } async def mock_iface_info(k: nmcli.NETWORK_IFACES): return connection_statuses[k.value] monkeypatch.setattr(nmcli, 'is_connected', mock_is_connected) monkeypatch.setattr(nmcli, 'iface_info', mock_iface_info) expected = {'status': 'full', 'interfaces': connection_statuses} resp = await cli.get('/networking/status') body_json = await resp.json() assert resp.status == 200 assert body_json == expected async def mock_is_connected(): raise FileNotFoundError("No") monkeypatch.setattr(nmcli, 'is_connected', mock_is_connected) resp = await cli.get('/networking/status') assert resp.status == 500
async def test_add_key_call(loop, aiohttp_client, wifi_keys_tempdir): """Test that uploaded file is processed properly""" with tempfile.TemporaryDirectory() as source_td: app = init() cli = await loop.create_task(aiohttp_client(app)) # We should be able to add multiple keys for fn in ['test1.pem', 'test2.pem', 'test3.pem']: path = os.path.join(source_td, fn) with open(path, 'w') as f: f.write(str(random.getrandbits(20))) with patch("opentrons.system.wifi.add_key") as p: await cli.post('/wifi/keys', data={'key': open(path, 'rb')}) with open(path, 'rb') as f: p.assert_called_once_with(fn, f.read())
async def test_restart(virtual_smoothie_env, monkeypatch, async_server, aiohttp_client): test_data = {"test": "pass"} loop = asyncio.get_event_loop() async def mock_restart(request): return web.json_response(test_data) monkeypatch.setattr(serverlib_fallback, 'restart', mock_restart) hw = async_server['com.opentrons.hardware'] app = init(hw) cli = await loop.create_task(aiohttp_client(app)) expected = json.dumps(test_data) resp = await cli.post('/server/restart') text = await resp.text() assert resp.status == 200 assert text == expected
async def test_update_module_firmware(dummy_attached_modules, virtual_smoothie_env, loop, aiohttp_client, monkeypatch): app = init() client = await loop.create_task(aiohttp_client(app)) serial_num = 'mdYYYYMMDD123' fw_filename = 'dummyFirmware.hex' tmpdir = tempfile.mkdtemp("files") with open(os.path.join(tmpdir, fw_filename), 'wb') as fd: fd.write(bytes(0x1234)) def dummy_discover_modules(): return async def mock_enter_bootloader(module): return '/dev/modules/tty0_bootloader' monkeypatch.setattr(robot, 'discover_modules', dummy_discover_modules) monkeypatch.setattr(robot, '_attached_modules', dummy_attached_modules) monkeypatch.setattr(modules, 'enter_bootloader', mock_enter_bootloader) # ========= Happy path ========== res_msg = { 'message': 'Firmware update successful', 'avrdudeResponse': '1234 bytes of flash verified', 'filename': fw_filename } async def mock_successful_upload_to_module(module, fw_file, loop): return res_msg expected_res = res_msg monkeypatch.setattr(modules, 'update_firmware', mock_successful_upload_to_module) resp = await client.post( '/modules/{}/update'.format(serial_num), data={'module_firmware': open(os.path.join(tmpdir, fw_filename))}) assert resp.status == 200 res = await resp.json() assert res == expected_res
async def test_wifi_disconnect(loop, aiohttp_client, monkeypatch): app = init() cli = await loop.create_task(aiohttp_client(app)) msg1 = 'Connection \'ot_wifi\' successfully deactivated. ' \ 'Connection \'ot_wifi\' (fa7ed807-23ef-41f0-ab3e-34' \ '99cc5a960e) successfully deleted' async def mock_disconnect(ssid): # Command: nmcli connection down ssid return True, msg1 monkeypatch.setattr(nmcli, 'wifi_disconnect', mock_disconnect) expected = {'message': 'SSID must be specified as a string'} resp = await cli.post('/wifi/disconnect', json={}) body = await resp.json() assert resp.status == 400 assert body == expected resp = await cli.post('wifi/disconnect', json={'ssid': 'ot_wifi'}) body = await resp.json() assert resp.status == 200 assert 'message' in body msg2 = 'Connection \'ot_wifi\' successfully deactivated. \n' \ 'Error: Could not remove ssid. No connection for ssid ot_wif123' async def mock_bad_disconnect(ssid): # Command: nmcli connection down ssid return True, msg2 monkeypatch.setattr(nmcli, 'wifi_disconnect', mock_bad_disconnect) resp = await cli.post('wifi/disconnect', json={'ssid': 'ot_wifi'}) body = await resp.json() assert resp.status == 207 assert 'message' in body
async def test_log_endpoints(virtual_smoothie_env, loop, test_client): app = init(loop) cli = await loop.create_task(test_client(app)) # # Test that values are set correctly serial_name = "serial.log" serial_file = config.CONFIG['log_dir'] / serial_name data1 = {'serial': 'No, CEREAL!'} with open(serial_file, 'w') as data_file: json.dump(data1, data_file) s1 = await cli.get('/logs/serial.log') s1body = await s1.text() assert json.loads(s1body) == data1 api_name = "api.log" api_file = config.CONFIG['log_dir'] / api_name data2 = {'api': 'application program interface'} with open(api_file, 'w') as data_file: json.dump(data2, data_file) a1 = await cli.get('/logs/api.log') a1body = await a1.text() assert json.loads(a1body) == data2
async def test_update(virtual_smoothie_env, monkeypatch, loop, aiohttp_client): msg = "success" whl_name = "testy.whl" serverlib_name = "testylib.whl" fw_name = "testy.fw" tmpdir = tempfile.mkdtemp("files") for filename in [whl_name, serverlib_name, fw_name]: with open(os.path.join(tmpdir, filename), 'w') as fd: fd.write("test") async def mock_install(filename, loop=None, modeset=True): return msg monkeypatch.setattr(serverlib_fallback, '_install', mock_install) monkeypatch.setattr(robot, 'update_firmware', mock_install) app = init() cli = await loop.create_task(aiohttp_client(app)) data = { 'whl': open(os.path.join(tmpdir, whl_name)), 'serverlib': open(os.path.join(tmpdir, serverlib_name)), 'fw': open(os.path.join(tmpdir, fw_name)) } # Note: hits API server update endpoint--this test covers backward # compatibility until the update server is universally available resp = await cli.post('/server/update', data=data) expected = json.dumps({ 'message': [msg, msg, msg], 'filename': [whl_name, serverlib_name, fw_name] }) text = await resp.text() assert resp.status == 200 assert text == expected
async def test_eap_config_options(virtual_smoothie_env, loop, test_client): app = init(loop) cli = await loop.create_task(test_client(app)) resp = await cli.get('/wifi/eap-options') assert resp.status == 200 body = await resp.json() # Check that the body is shaped correctly but ignore the actual content assert 'options' in body option_keys = ('name', 'displayName', 'required', 'type') option_types = ('string', 'password', 'file') def check_option(opt_dict): for key in option_keys: assert key in opt_dict assert opt_dict['type'] in option_types for opt in body['options']: assert 'name' in opt assert 'displayName' in opt assert 'options' in opt for method_opt in opt['options']: check_option(method_opt)
async def test_fail_update_module_firmware(dummy_attached_modules, virtual_smoothie_env, loop, aiohttp_client, monkeypatch): app = init() client = await loop.create_task(aiohttp_client(app)) serial_num = 'mdYYYYMMDD123' fw_filename = 'dummyFirmware.hex' tmpdir = tempfile.mkdtemp("files") with open(os.path.join(tmpdir, fw_filename), 'wb') as fd: fd.write(bytes(0x1234)) def dummy_discover_modules(): return async def mock_enter_bootloader(module): return '/dev/modules/tty0_bootloader' monkeypatch.setattr(robot, 'discover_modules', dummy_discover_modules) monkeypatch.setattr(robot, '_attached_modules', dummy_attached_modules) monkeypatch.setattr(modules, 'enter_bootloader', mock_enter_bootloader) # ========= Case 1: Port not accessible ========= res_msg1 = { 'message': 'Firmware update failed', 'avrdudeResponse': 'ser_open(): can\'t open device', 'filename': fw_filename } async def mock_failed_upload_to_module1(serialnum, fw_file, loop): return res_msg1 expected_res1 = res_msg1 monkeypatch.setattr(modules, 'update_firmware', mock_failed_upload_to_module1) resp1 = await client.post( '/modules/{}/update'.format(serial_num), data={'module_firmware': open(os.path.join(tmpdir, fw_filename))}) assert resp1.status == 500 j1 = await resp1.json() assert j1 == expected_res1 # ========= Case 2: Corrupted file ========= res_msg2 = { 'message': 'Firmware update failed', 'avrdudeResponse': 'checksum mismatch in line 1234', 'filename': fw_filename } async def mock_failed_upload_to_module2(serialnum, fw_file, loop): return res_msg2 expected_res2 = res_msg2 monkeypatch.setattr(modules, 'update_firmware', mock_failed_upload_to_module2) resp2 = await client.post( '/modules/{}/update'.format(serial_num), data={'module_firmware': open(os.path.join(tmpdir, fw_filename))}) assert resp2.status == 400 j2 = await resp2.json() assert j2 == expected_res2 # ========= Case 3: AVRDUDE not responding ========= expected_res3 = { 'message': 'AVRDUDE not responding', 'filename': fw_filename } async def mock_failed_upload_to_module3(serialnum, fw_file, loop): await asyncio.sleep(2) monkeypatch.setattr(modules, 'update_firmware', mock_failed_upload_to_module3) update.UPDATE_TIMEOUT = 0.1 resp3 = await client.post( '/modules/{}/update'.format(serial_num), data={'module_firmware': open(os.path.join(tmpdir, fw_filename))}) assert resp3.status == 500 j3 = await resp3.json() assert j3 == expected_res3 # ========= Case 4: No module/ incorrect serial ========= wrong_serial = 'abcdef' expected_res4 = { 'message': 'Module {} not found'.format(wrong_serial), 'filename': fw_filename } resp4 = await client.post( '/modules/{}/update'.format(wrong_serial), data={'module_firmware': open(os.path.join(tmpdir, fw_filename))}) assert resp4.status == 404 j4 = await resp4.json() assert j4 == expected_res4
async def test_networking_status( virtual_smoothie_env, loop, test_client, monkeypatch): app = init(loop) cli = await loop.create_task(test_client(app)) async def mock_call(cmd): # Command: `nmcli networking connectivity` if 'connectivity' in cmd: res = 'full' elif 'wlan0' in cmd: res = '''GENERAL.HWADDR:B8:27:EB:5F:A6:89 IP4.ADDRESS[1]:-- IP4.GATEWAY:-- GENERAL.TYPE:wifi GENERAL.STATE:30 (disconnected)''' elif 'eth0' in cmd: res = '''GENERAL.HWADDR:B8:27:EB:39:C0:9A IP4.ADDRESS[1]:169.254.229.173/16 GENERAL.TYPE:ethernet GENERAL.STATE:100 (connected)''' else: res = 'incorrect nmcli call' return res, '' monkeypatch.setattr(nmcli, '_call', mock_call) expected = json.dumps({ 'status': 'full', 'interfaces': { 'wlan0': { # test "--" gets mapped to None 'ipAddress': None, 'macAddress': 'B8:27:EB:5F:A6:89', # test "--" gets mapped to None 'gatewayAddress': None, 'state': 'disconnected', 'type': 'wifi' }, 'eth0': { 'ipAddress': '169.254.229.173/16', 'macAddress': 'B8:27:EB:39:C0:9A', # test missing output gets mapped to None 'gatewayAddress': None, 'state': 'connected', 'type': 'ethernet' } } }) resp = await cli.get('/networking/status') text = await resp.text() assert resp.status == 200 assert text == expected async def mock_call(cmd): if 'connectivity' in cmd: return 'full', '' else: return '', 'this is a dummy error' monkeypatch.setattr(nmcli, '_call', mock_call) resp = await cli.get('/networking/status') assert resp.status == 500
async def async_server(hardware, virtual_smoothie_env, loop): app = init(hardware, loop=loop) yield app await app.shutdown()
async def async_server(hardware, virtual_smoothie_env, loop, aiohttp_server): testserver = await aiohttp_server(init(hardware, loop=loop)) yield testserver.app