diff --git a/.github/ISSUE_TEMPLATE/bug_form.yaml b/.github/ISSUE_TEMPLATE/bug_form.yaml index de98042ff..f010851a0 100644 --- a/.github/ISSUE_TEMPLATE/bug_form.yaml +++ b/.github/ISSUE_TEMPLATE/bug_form.yaml @@ -1,5 +1,5 @@ name: "Bug Report Form" -description: "Report a bug or a similiar issue." +description: "Report a bug or a similar issue." body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7b2d69f95..13ec62b3d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Bug Report -about: Report a bug or a similiar issue - the classic way +about: Report a bug or a similar issue - the classic way title: '' labels: '' assignees: '' @@ -18,7 +18,7 @@ If you want to suggest a feature or have any other question, please use our #### Description diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d6e05f6a9..231c88ff4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -36,7 +36,7 @@ - [ ] All new and existing tests passed. -*I provide my contribution under the terms of the [license](./../../LICENSE.txt) of this repository and I affirm the [Developer Certificate of Origin][dco].* +*I provide my contribution under the terms of the [license](./../LICENSE.txt) of this repository and I affirm the [Developer Certificate of Origin][dco].* [dco]: https://developercertificate.org/ diff --git a/README.md b/README.md index 59ddbf9fc..b58d1e5e4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Vorta is a backup client for macOS and Linux desktops. It integrates the mighty [BorgBackup](https://borgbackup.readthedocs.io) with your desktop environment to protect your data from disk failure, ransomware and theft. -![](https://files.qmax.us/vorta/screencast-8-small.gif) +https://github.com/m3nu/vorta/assets/3916435/a622a148-5373-4ae0-87bc-4ca1d6f6202e ## Why is this great? 🤩 diff --git a/noxfile.py b/noxfile.py index 804b8d60c..b5f6fdfa0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -28,10 +28,10 @@ @nox.parametrize("borgbackup", supported_borgbackup_versions) def run_tests(session, borgbackup): # install borgbackup - if (sys.platform == 'darwin'): + if sys.platform == 'darwin': # in macOS there's currently no fuse package which works with borgbackup directly session.install(f"borgbackup=={borgbackup}") - elif (borgbackup == "1.1.18"): + elif borgbackup == "1.1.18": # borgbackup 1.1.18 doesn't support pyfuse3 session.install("llfuse") session.install(f"borgbackup[llfuse]=={borgbackup}") diff --git a/package/fix_app_qt_folder_names_for_codesign.py b/package/fix_app_qt_folder_names_for_codesign.py index 0adfb03f9..cbd5805de 100644 --- a/package/fix_app_qt_folder_names_for_codesign.py +++ b/package/fix_app_qt_folder_names_for_codesign.py @@ -18,10 +18,10 @@ def create_symlink(folder: Path) -> None: """Create the appropriate symlink in the MacOS folder pointing to the Resources folder. """ - sibbling = Path(str(folder).replace("MacOS", "")) + sibling = Path(str(folder).replace("MacOS", "")) # PyQt6/Qt/qml/QtQml/Models.2 - root = str(sibbling).partition("Contents")[2].lstrip("/") + root = str(sibling).partition("Contents")[2].lstrip("/") # ../../../../ backward = "../" * (root.count("/") + 1) # ../../../../Resources/PyQt6/Qt/qml/QtQml/Models.2 @@ -41,7 +41,7 @@ def fix_dll(dll: Path) -> None: def match_func(pth: str) -> Optional[str]: """Callback function for MachO.rewriteLoadCommands() that is - called on every lookup path setted in the DLL headers. + called on every lookup path set in the DLL headers. By returning None for system libraries, it changes nothing. Else we return a relative path pointing to the good file in the MacOS folder. @@ -73,7 +73,7 @@ def find_problematic_folders(folder: Path) -> Generator[Path, None, None]: """Recursively yields problematic folders (containing a dot in their name).""" for path in folder.iterdir(): if not path.is_dir() or path.is_symlink(): - # Skip simlinks as they are allowed (even with a dot) + # Skip symlinks as they are allowed (even with a dot) continue if "." in path.name: yield path @@ -83,7 +83,7 @@ def find_problematic_folders(folder: Path) -> Generator[Path, None, None]: def move_contents_to_resources(folder: Path) -> Generator[Path, None, None]: """Recursively move any non symlink file from a problematic folder - to the sibbling one in Resources. + to the sibling one in Resources. """ for path in folder.iterdir(): if path.is_symlink(): @@ -91,10 +91,10 @@ def move_contents_to_resources(folder: Path) -> Generator[Path, None, None]: if path.name == "qml": yield from move_contents_to_resources(path) else: - sibbling = Path(str(path).replace("MacOS", "Resources")) - sibbling.parent.mkdir(parents=True, exist_ok=True) - shutil.move(path, sibbling) - yield sibbling + sibling = Path(str(path).replace("MacOS", "Resources")) + sibling.parent.mkdir(parents=True, exist_ok=True) + shutil.move(path, sibling) + yield sibling def main(args: List[str]) -> int: diff --git a/setup.cfg b/setup.cfg index 6cda372a8..e0cdd9319 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ install_requires = pyobjc-core < 10; sys_platform == 'darwin' pyobjc-framework-Cocoa < 10; sys_platform == 'darwin' pyobjc-framework-LaunchServices < 10; sys_platform == 'darwin' + pyobjc-framework-CoreWLAN < 10; sys_platform == 'darwin' tests_require = pytest pytest-qt diff --git a/src/vorta/_version.py b/src/vorta/_version.py index 55616197f..8969d4966 100644 --- a/src/vorta/_version.py +++ b/src/vorta/_version.py @@ -1 +1 @@ -__version__ = '0.9.1-beta3' +__version__ = '0.9.1' diff --git a/src/vorta/application.py b/src/vorta/application.py index 34c1acf91..8857a51eb 100644 --- a/src/vorta/application.py +++ b/src/vorta/application.py @@ -308,11 +308,6 @@ def check_failed_response(self, result: Dict[str, Any]): Displays a `QMessageBox` with an error message depending on the return code of the `BorgJob`. - - Parameters - ---------- - repo_url : str - The url of the repo of concern """ # extract data from the params for the borg job repo_url = result['params']['repo_url'] @@ -344,7 +339,7 @@ def check_failed_response(self, result: Dict[str, Any]): elif returncode > 128: # 128+N - killed by signal N (e.g. 137 == kill -9) signal = returncode - 128 - text = self.tr('Repository data check for repo was killed by signal %s.') % (signal) + text = self.tr('Repository data check for repo was killed by signal %s.') % signal infotext = self.tr('The process running the check job got a kill signal. Try again.') else: # Real error diff --git a/src/vorta/assets/UI/abouttab.ui b/src/vorta/assets/UI/abouttab.ui index 791b72915..52f40f7b1 100644 --- a/src/vorta/assets/UI/abouttab.ui +++ b/src/vorta/assets/UI/abouttab.ui @@ -213,7 +213,7 @@ - <html><head/><body><p><a href="https://github.com/borgbase/vorta"><span style=" text-decoration: underline; color:#0984e3;">Click here</span></a> for view Git repo.</p></body></html> + <html><head/><body><p><a href="https://github.com/borgbase/vorta"><span style=" text-decoration: underline; color:#0984e3;">Click here</span></a> to view Git repo.</p></body></html> true @@ -241,7 +241,7 @@ 20 - + Vorta is a cross-platform, open-source client designed to simplify the management of Borg backups. diff --git a/src/vorta/assets/icons/settings_wheel.svg b/src/vorta/assets/icons/settings_wheel.svg index 326c4c686..05295e599 100644 --- a/src/vorta/assets/icons/settings_wheel.svg +++ b/src/vorta/assets/icons/settings_wheel.svg @@ -1 +1 @@ - + diff --git a/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml b/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml index 5c11d044e..b0c8a76b0 100644 --- a/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml +++ b/src/vorta/assets/metadata/com.borgbase.Vorta.appdata.xml @@ -1,6 +1,8 @@ com.borgbase.Vorta + com.borgbase.Vorta.desktop + Vorta contributors Vorta GPL-3.0 CC0-1.0 @@ -40,18 +42,13 @@ - +
    +
  • First production 0.9 release
  • Exclude GUI. By @diivi (#1846)
  • Backup settings.db before migrations. By @AdwaitSalankar (#1848)
  • Loosen platformdirs dependency (#1843)
  • -
-
-
- - -
  • Unit test improvements and coverage increase. By @bigtedde (#1787)
  • Profile sidebar and new setting interface. By @bigtedde (#1809)
  • Update macOS notarization for use with notarytool (#1831)
  • diff --git a/src/vorta/borg/jobs_manager.py b/src/vorta/borg/jobs_manager.py index 2028535d1..4659f3e7f 100644 --- a/src/vorta/borg/jobs_manager.py +++ b/src/vorta/borg/jobs_manager.py @@ -25,9 +25,9 @@ def repo_id(self): @abstractmethod def cancel(self): """ - Cancel can be called when the job is not started. It is the responsability of FuncJob to not cancel job if + Cancel can be called when the job is not started. It is the responsibility of FuncJob to not cancel job if no job is running. - The cancel mehod of JobsManager calls the cancel method on the running jobs only. Other jobs are dequeued. + The cancel method of JobsManager calls the cancel method on the running jobs only. Other jobs are dequeued. """ pass @@ -50,6 +50,7 @@ def __init__(self, jobs): self.current_job = None def run(self): + job = None while True: try: job = self.jobs.get(False) @@ -58,7 +59,8 @@ def run(self): job.run() logger.debug("Finish job for site: %s", job.repo_id()) except queue.Empty: - logger.debug("No more jobs for site: %s", job.repo_id()) + if job is not None: + logger.debug("No more jobs for site: %s", job.repo_id()) return @@ -77,19 +79,20 @@ def __init__(self): def is_worker_running(self, site=None): """ - See if there are any active jobs. The user can't start a backup if a job is - running. The scheduler can. + See if there are any active jobs. + The user can't start a backup if a job is running. The scheduler can. + + If site is None, check if there is any worker active for any site (repo). + If site is not None, only check if there is a worker active for the given site (repo). """ - # Check status for specific site (repo) - if site in self.workers: - return self.workers[site].is_alive() + if site is not None: + if site in self.workers: + if self.workers[site].is_alive(): + return True else: - return False - - # Check if *any* worker is active - for _, worker in self.workers.items(): - if worker.is_alive(): - return True + for _, worker in self.workers.items(): + if worker.is_alive(): + return True return False def add_job(self, job): diff --git a/src/vorta/network_status/abc.py b/src/vorta/network_status/abc.py index 60f9353ac..7f74fc16b 100644 --- a/src/vorta/network_status/abc.py +++ b/src/vorta/network_status/abc.py @@ -24,7 +24,7 @@ def get_network_status_monitor(cls) -> 'NetworkStatusMonitor': def is_network_status_available(self): """Is the network status really available, and not just a dummy implementation?""" - return type(self) != NetworkStatusMonitor + return type(self) is not NetworkStatusMonitor def is_network_metered(self) -> bool: """Is the currently connected network a metered connection?""" diff --git a/src/vorta/network_status/darwin.py b/src/vorta/network_status/darwin.py index 279fc13aa..1ee2baf11 100644 --- a/src/vorta/network_status/darwin.py +++ b/src/vorta/network_status/darwin.py @@ -1,6 +1,8 @@ import subprocess from datetime import datetime as dt -from typing import Iterator, Optional +from typing import Iterator, List, Optional + +from CoreWLAN import CWInterface, CWNetwork, CWWiFiClient from vorta.log import logger from vorta.network_status.abc import NetworkStatusMonitor, SystemWifiInfo @@ -8,38 +10,65 @@ class DarwinNetworkStatus(NetworkStatusMonitor): def is_network_metered(self) -> bool: - return any(is_network_metered(d) for d in get_network_devices()) + interface: CWInterface = self._get_wifi_interface() + network: Optional[CWNetwork] = interface.lastNetworkJoined() + + if network: + is_ios_hotspot = network.isPersonalHotspot() + else: + is_ios_hotspot = False + + return is_ios_hotspot or any(is_network_metered_with_android(d) for d in get_network_devices()) def get_current_wifi(self) -> Optional[str]: """ - Get current SSID or None if Wifi is off. - - From https://gist.github.com/keithweaver/00edf356e8194b89ed8d3b7bbead000c + Get current SSID or None if Wi-Fi is off. """ - cmd = [ - '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport', - '-I', - ] - process = subprocess.Popen(cmd, stdout=subprocess.PIPE) - out, err = process.communicate() - process.wait() - for line in out.decode(errors='ignore').split('\n'): - split_line = line.strip().split(':') - if split_line[0] == 'SSID': - return split_line[1].strip() - - def get_known_wifis(self): + interface: Optional[CWInterface] = self._get_wifi_interface() + if not interface: + return None + + # If the user has Wi-Fi turned off lastNetworkJoined will return None. + network: Optional[CWNetwork] = interface.lastNetworkJoined() + + if network: + network_name = network.ssid() + return network_name + else: + return None + + def get_known_wifis(self) -> List[SystemWifiInfo]: """ - Listing all known Wifi networks isn't possible any more from macOS 11. Instead we - just return the current Wifi. + Use the program, "networksetup", to get the list of know Wi-Fi networks. """ + wifis = [] - current_wifi = self.get_current_wifi() - if current_wifi is not None: - wifis.append(SystemWifiInfo(ssid=current_wifi, last_connected=dt.now())) + interface: Optional[CWInterface] = self._get_wifi_interface() + if not interface: + return [] + + interface_name = interface.name() + output = call_networksetup_listpreferredwirelessnetworks(interface_name) + + result = [] + for line in output.strip().splitlines(): + if line.strip().startswith("Preferred networks"): + continue + elif not line.strip(): + continue + else: + result.append(line.strip()) + + for wifi_network_name in result: + wifis.append(SystemWifiInfo(ssid=wifi_network_name, last_connected=dt.now())) return wifis + def _get_wifi_interface(self) -> Optional[CWInterface]: + wifi_client: CWWiFiClient = CWWiFiClient.sharedWiFiClient() + interface: Optional[CWInterface] = wifi_client.interface() + return interface + def get_network_devices() -> Iterator[str]: for line in call_networksetup_listallhardwareports().splitlines(): @@ -47,7 +76,7 @@ def get_network_devices() -> Iterator[str]: yield line.split()[1].strip().decode('ascii') -def is_network_metered(bsd_device) -> bool: +def is_network_metered_with_android(bsd_device) -> bool: return b'ANDROID_METERED' in call_ipconfig_getpacket(bsd_device) @@ -66,3 +95,11 @@ def call_networksetup_listallhardwareports(): return subprocess.check_output(cmd) except subprocess.CalledProcessError: logger.debug("Command %s failed", ' '.join(cmd)) + + +def call_networksetup_listpreferredwirelessnetworks(interface) -> str: + command = ['/usr/sbin/networksetup', '-listpreferredwirelessnetworks', interface] + try: + return subprocess.check_output(command).decode(encoding='utf-8') + except subprocess.CalledProcessError: + logger.debug("Command %s failed", " ".join(command)) diff --git a/src/vorta/profile_export.py b/src/vorta/profile_export.py index fa26ac5c6..a370ce1d7 100644 --- a/src/vorta/profile_export.py +++ b/src/vorta/profile_export.py @@ -36,7 +36,7 @@ def schema_version(self): def repo_url(self): if ( 'repo' in self._profile_dict - and type(self._profile_dict['repo']) == dict + and isinstance(self._profile_dict['repo'], dict) and 'url' in self._profile_dict['repo'] ): return self._profile_dict['repo']['url'] diff --git a/src/vorta/scheduler.py b/src/vorta/scheduler.py index 7a3fcee5d..aa8b2859c 100644 --- a/src/vorta/scheduler.py +++ b/src/vorta/scheduler.py @@ -70,7 +70,7 @@ def __init__(self): self.bus = bus self.bus.connect(service, path, interface, name, "b", self.loginSuspendNotify) else: - logger.warn('Failed to connect to DBUS interface to detect sleep/resume events') + logger.warning('Failed to connect to DBUS interface to detect sleep/resume events') @QtCore.pyqtSlot(bool) def loginSuspendNotify(self, suspend: bool): diff --git a/src/vorta/views/about_tab.py b/src/vorta/views/about_tab.py index 41928b400..da6d791a2 100644 --- a/src/vorta/views/about_tab.py +++ b/src/vorta/views/about_tab.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime from PyQt6 import QtCore, uic @@ -28,6 +29,9 @@ def __init__(self, parent=None): ) self.gpl_logo.setPixmap(get_colored_icon('gpl_logo', scaled_height=40, return_qpixmap=True)) self.python_logo.setPixmap(get_colored_icon('python_logo', scaled_height=40, return_qpixmap=True)) + copyright_text = self.copyrightLabel.text() + copyright_text = copyright_text.replace('2020', str(datetime.now().year)) + self.copyrightLabel.setText(copyright_text) def set_borg_details(self, version, path): self.borgVersion.setText(version) diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index cb4d41843..d2af5757b 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -875,7 +875,7 @@ def confirm_dialog(self, title, text): return msg.exec() == QMessageBox.StandardButton.Yes def delete_action(self): - # Since this function modify the UI, we can't put the whole function in a JobQUeue. + # Since this function modify the UI, we can't put the whole function in a JobQueue. # determine selected archives archives = [] diff --git a/src/vorta/views/diff_result.py b/src/vorta/views/diff_result.py index 5d262efa5..da74ff727 100644 --- a/src/vorta/views/diff_result.py +++ b/src/vorta/views/diff_result.py @@ -381,7 +381,6 @@ def parse_diff_lines(lines: List[str], model: 'DiffTree'): if not parsed_line: raise Exception("Couldn't parse diff output `{}`".format(line)) - continue path = PurePath(parsed_line['path']) file_type = FileType.FILE diff --git a/src/vorta/views/partials/treemodel.py b/src/vorta/views/partials/treemodel.py index a184a5428..ceff3eb46 100644 --- a/src/vorta/views/partials/treemodel.py +++ b/src/vorta/views/partials/treemodel.py @@ -610,7 +610,7 @@ def getItem(self, path: Union[PurePath, PathLike]) -> Optional[FileSystemItem[T] if isinstance(path, PurePath): path = path.parts - return self.root.get_path(path) # handels empty path + return self.root.get_path(path) # handles empty path def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): """ diff --git a/src/vorta/views/profile_add_edit_dialog.py b/src/vorta/views/profile_add_edit_dialog.py index 56d040c1a..75b767d91 100644 --- a/src/vorta/views/profile_add_edit_dialog.py +++ b/src/vorta/views/profile_add_edit_dialog.py @@ -27,7 +27,7 @@ def __init__(self, parent=None): self.name_blank = trans_late('AddProfileWindow', 'Please enter a profile name.') self.name_exists = trans_late('AddProfileWindow', 'A profile with this name already exists.') - # Call validate to set inital messages + # Call validate to set initial messages self.buttonBox.button(QDialogButtonBox.StandardButton.Save).setEnabled(self.validate()) def _set_status(self, text): diff --git a/src/vorta/views/repo_tab.py b/src/vorta/views/repo_tab.py index 3b04ffa23..e9f38dbad 100644 --- a/src/vorta/views/repo_tab.py +++ b/src/vorta/views/repo_tab.py @@ -40,7 +40,7 @@ def __init__(self, parent=None): # compression or speed on a unified scale. this is not 1-dimensional and also depends # on the input data. so we just tell what we know for sure. # "auto" is used for some slower / older algorithms to avoid wasting a lot of time - # on uncompressible data. + # on incompressible data. self.repoCompression.addItem(self.tr('LZ4 (modern, default)'), 'lz4') self.repoCompression.addItem(self.tr('Zstandard Level 3 (modern)'), 'zstd,3') self.repoCompression.addItem(self.tr('Zstandard Level 8 (modern)'), 'zstd,8') diff --git a/src/vorta/views/schedule_tab.py b/src/vorta/views/schedule_tab.py index 43de6730d..a03b191c7 100644 --- a/src/vorta/views/schedule_tab.py +++ b/src/vorta/views/schedule_tab.py @@ -1,5 +1,5 @@ from PyQt6 import QtCore, uic -from PyQt6.QtCore import QDateTime, QLocale +from PyQt6.QtCore import QDateTime, QLocale, Qt from PyQt6.QtWidgets import ( QAbstractItemView, QApplication, @@ -202,7 +202,7 @@ def populate_wifi(self): def save_wifi_item(self, item): db_item = WifiSettingModel.get(ssid=item.text(), profile=self.profile().id) - db_item.allowed = item.checkState() == 2 + db_item.allowed = item.checkState() == Qt.CheckState.Checked db_item.save() def save_profile_attr(self, attr, new_value): diff --git a/src/vorta/views/source_tab.py b/src/vorta/views/source_tab.py index 3739c3d83..f97834740 100644 --- a/src/vorta/views/source_tab.py +++ b/src/vorta/views/source_tab.py @@ -331,7 +331,7 @@ def source_remove(self): profile = self.profile() # sort indexes, starting with lowest indexes.sort() - # remove each selected row, starting with highest index (otherways, higher indexes become invalid) + # remove each selected row, starting with the highest index (otherwise, higher indexes become invalid) for index in reversed(indexes): db_item = SourceFileModel.get( dir=self.sourceFilesWidget.item(index.row(), SourceColumn.Path).text(), diff --git a/tests/network_manager/test_darwin.py b/tests/network_manager/test_darwin.py index 70c96cd2e..7d900dd44 100644 --- a/tests/network_manager/test_darwin.py +++ b/tests/network_manager/test_darwin.py @@ -1,25 +1,118 @@ +from unittest.mock import MagicMock + import pytest from vorta.network_status import darwin +def test_get_current_wifi_when_wifi_is_on(mocker): + mock_interface = MagicMock() + mock_network = MagicMock() + mock_interface.lastNetworkJoined.return_value = mock_network + mock_network.ssid.return_value = "Coffee Shop Wifi" + + instance = darwin.DarwinNetworkStatus() + mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface) + + result = instance.get_current_wifi() + + assert result == "Coffee Shop Wifi" + + +def test_get_current_wifi_when_wifi_is_off(mocker): + mock_interface = MagicMock() + mock_interface.lastNetworkJoined.return_value = None + + instance = darwin.DarwinNetworkStatus() + mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface) + + result = instance.get_current_wifi() + + assert result is None + + +def test_get_current_wifi_when_no_wifi_interface(mocker): + instance = darwin.DarwinNetworkStatus() + mocker.patch.object(instance, "_get_wifi_interface", return_value=None) + + result = instance.get_current_wifi() + + assert result is None + + +@pytest.mark.parametrize("is_hotspot_enabled", [True, False]) +def test_network_is_metered_with_ios(mocker, is_hotspot_enabled): + mock_interface = MagicMock() + mock_network = MagicMock() + mock_interface.lastNetworkJoined.return_value = mock_network + mock_network.isPersonalHotspot.return_value = is_hotspot_enabled + + instance = darwin.DarwinNetworkStatus() + mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface) + + result = instance.is_network_metered() + + assert result == is_hotspot_enabled + + +def test_network_is_metered_when_wifi_is_off(mocker): + mock_interface = MagicMock() + mock_interface.lastNetworkJoined.return_value = None + + instance = darwin.DarwinNetworkStatus() + mocker.patch.object(instance, "_get_wifi_interface", return_value=mock_interface) + + result = instance.is_network_metered() + + assert result is False + + @pytest.mark.parametrize( 'getpacket_output_name, expected', [ ('normal_router', False), - ('phone', True), + ('android_phone', True), ], ) -def test_is_network_metered(getpacket_output_name, expected, monkeypatch): +def test_is_network_metered_with_android(getpacket_output_name, expected, monkeypatch): def mock_getpacket(device): assert device == 'en0' return GETPACKET_OUTPUTS[getpacket_output_name] monkeypatch.setattr(darwin, 'call_ipconfig_getpacket', mock_getpacket) - result = darwin.is_network_metered('en0') + result = darwin.is_network_metered_with_android('en0') assert result == expected +def test_get_known_wifi_networks_when_wifi_interface_exists(monkeypatch): + networksetup_output = """ +Preferred networks on en0: + Home Network + Coffee Shop Wifi + iPhone + + Office Wifi + """ + monkeypatch.setattr( + darwin, "call_networksetup_listpreferredwirelessnetworks", lambda interface_name: networksetup_output + ) + + network_status = darwin.DarwinNetworkStatus() + result = network_status.get_known_wifis() + + assert len(result) == 4 + assert result[0].ssid == "Home Network" + + +def test_get_known_wifi_networks_when_no_wifi_interface(mocker): + instance = darwin.DarwinNetworkStatus() + mocker.patch.object(instance, "_get_wifi_interface", return_value=None) + + results = instance.get_known_wifis() + + assert results == [] + + def test_get_network_devices(monkeypatch): monkeypatch.setattr(darwin, 'call_networksetup_listallhardwareports', lambda: NETWORKSETUP_OUTPUT) @@ -55,7 +148,7 @@ def test_get_network_devices(monkeypatch): server_identifier (ip): 172.16.12.1 end (none): """, - 'phone': b"""\ + 'android_phone': b"""\ op = BOOTREPLY htype = 1 flags = 0 diff --git a/tests/unit/test_treemodel.py b/tests/unit/test_treemodel.py index 1b76d5856..dd2b9717e 100644 --- a/tests/unit/test_treemodel.py +++ b/tests/unit/test_treemodel.py @@ -87,7 +87,7 @@ def test_get(self): item.add(child2) item.add(child3) - # test get inexistent subpath + # test get nonexistent subpath assert item.get('unknown') is None assert item.get('unknown', default='default') == 'default'