From aee971ea8e36fd311969443a54942c782d7529ed Mon Sep 17 00:00:00 2001 From: Stephen Mwangi Date: Thu, 29 Jan 2026 16:58:24 +0300 Subject: [PATCH] refactor: split api.py by domain --- pyproject.toml | 1 + snap_http/__init__.py | 3 - snap_http/api.py | 801 -------- snap_http/api/__init__.py | 67 + snap_http/api/apps.py | 87 + snap_http/api/assertions.py | 34 + snap_http/api/changes.py | 12 + snap_http/api/fde.py | 39 + snap_http/api/interfaces.py | 98 + snap_http/api/model.py | 21 + snap_http/api/options.py | 28 + snap_http/api/snaps.py | 258 +++ snap_http/api/snapshots.py | 48 + snap_http/api/systems.py | 46 + snap_http/api/users.py | 34 + snap_http/api/validation_sets.py | 97 + tests/integration/test_api.py | 622 ------ tests/integration/test_apps.py | 266 +++ tests/integration/test_assertions.py | 52 + tests/integration/test_options.py | 123 ++ tests/integration/test_snaps.py | 147 ++ tests/integration/test_snapshots.py | 29 + tests/unit/api/__init__.py | 0 tests/unit/api/conftest.py | 7 + tests/unit/api/test_apps.py | 589 ++++++ tests/unit/api/test_assertions.py | 107 + tests/unit/api/test_changes.py | 83 + tests/unit/api/test_fde.py | 205 ++ tests/unit/api/test_interfaces.py | 97 + tests/unit/api/test_model.py | 43 + tests/unit/api/test_options.py | 65 + tests/unit/api/test_snaps.py | 919 +++++++++ tests/unit/api/test_snapshots.py | 74 + tests/unit/api/test_systems.py | 95 + tests/unit/api/test_users.py | 107 + tests/unit/api/test_validation_sets.py | 164 ++ tests/unit/test_api.py | 2536 ------------------------ 37 files changed, 4042 insertions(+), 3962 deletions(-) delete mode 100644 snap_http/api.py create mode 100644 snap_http/api/__init__.py create mode 100644 snap_http/api/apps.py create mode 100644 snap_http/api/assertions.py create mode 100644 snap_http/api/changes.py create mode 100644 snap_http/api/fde.py create mode 100644 snap_http/api/interfaces.py create mode 100644 snap_http/api/model.py create mode 100644 snap_http/api/options.py create mode 100644 snap_http/api/snaps.py create mode 100644 snap_http/api/snapshots.py create mode 100644 snap_http/api/systems.py create mode 100644 snap_http/api/users.py create mode 100644 snap_http/api/validation_sets.py delete mode 100644 tests/integration/test_api.py create mode 100644 tests/integration/test_apps.py create mode 100644 tests/integration/test_assertions.py create mode 100644 tests/integration/test_options.py create mode 100644 tests/integration/test_snaps.py create mode 100644 tests/integration/test_snapshots.py create mode 100644 tests/unit/api/__init__.py create mode 100644 tests/unit/api/conftest.py create mode 100644 tests/unit/api/test_apps.py create mode 100644 tests/unit/api/test_assertions.py create mode 100644 tests/unit/api/test_changes.py create mode 100644 tests/unit/api/test_fde.py create mode 100644 tests/unit/api/test_interfaces.py create mode 100644 tests/unit/api/test_model.py create mode 100644 tests/unit/api/test_options.py create mode 100644 tests/unit/api/test_snaps.py create mode 100644 tests/unit/api/test_snapshots.py create mode 100644 tests/unit/api/test_systems.py create mode 100644 tests/unit/api/test_users.py create mode 100644 tests/unit/api/test_validation_sets.py delete mode 100644 tests/unit/test_api.py diff --git a/pyproject.toml b/pyproject.toml index 446fef2..7881b49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ mypy = "^1.7.1" exclude = [ ".tox", "snap_http/__init__.py", + "snap_http/api/__init__.py", ] [tool.ruff.mccabe] diff --git a/snap_http/__init__.py b/snap_http/__init__.py index cb90df0..f3b29f1 100644 --- a/snap_http/__init__.py +++ b/snap_http/__init__.py @@ -19,9 +19,6 @@ snapshots, forget_snapshot, save_snapshot, - snapshots, - forget_snapshot, - save_snapshot, switch, switch_all, unhold, diff --git a/snap_http/api.py b/snap_http/api.py deleted file mode 100644 index ece2c41..0000000 --- a/snap_http/api.py +++ /dev/null @@ -1,801 +0,0 @@ -""" -Functions for interacting with the snapd REST API. -See https://snapcraft.io/docs/snapd-api for documentation of the API. - -Permissions are based on the user calling the API, most mutative interactions -(install, refresh, etc) require root. -""" - -from typing import Any, Dict, List, Literal, Optional, Union - -from . import http -from .types import AssertionData, FileUpload, FormData, SnapdResponse - - -def check_change(cid: str) -> SnapdResponse: - """Checks the status of snapd change with id `cid`.""" - return http.get("/changes/" + cid) - - -def check_changes() -> SnapdResponse: - """Checks the status of all snapd changes.""" - return http.get("/changes?select=all") - - -def enable(name: str) -> SnapdResponse: - """Enables a previously disabled snap by `name`.""" - return http.post("/snaps/" + name, {"action": "enable"}) - - -def enable_all(names: List[str]) -> SnapdResponse: - """Like `enable_snap`, but for the list of snaps in `names`. - - NOTE: as of 2024-01-08, enable/disable is not yet supported for multiple snaps. - """ - return http.post("/snaps", {"action": "enable", "snaps": names}) - - -def disable(name: str) -> SnapdResponse: - """Disables a snap by `name`, making its binaries and services unavailable.""" - return http.post("/snaps/" + name, {"action": "disable"}) - - -def disable_all(names: List[str]) -> SnapdResponse: - """Like `disable_snap`, but for the list of snaps in `names`. - - NOTE: as of 2024-01-08, enable/disable is not yet supported for multiple snaps. - """ - return http.post("/snaps", {"action": "disable", "snaps": names}) - - -def hold( - name: str, - *, - hold_level: Literal["general", "auto-refresh"] = "general", - time: str = "forever", -) -> SnapdResponse: - """Holds a snap by `name` at `hold_level` until `time`. - - :param time: RFC3339 timestamp to hold the snap until, or "forever". - """ - return http.post( - "/snaps/" + name, {"action": "hold", "hold-level": hold_level, "time": time} - ) - - -def hold_all( - names: List[str], - *, - hold_level: Literal["general", "auto-refresh"] = "general", - time: str = "forever", -) -> SnapdResponse: - """Like `hold_snap`, but for the list of snaps in `names`.""" - return http.post( - "/snaps", - {"action": "hold", "snaps": names, "hold-level": hold_level, "time": time}, - ) - - -def install( - name: str, - *, - revision: Optional[str] = None, - channel: Optional[str] = None, - classic: bool = False, -) -> SnapdResponse: - """Installs a snap by `name` at `revision`, tracking `channel`. - - :param revision: revision to install. Defaults to latest. - :param channel: channel to track. Defaults to stable. - :param classic: if `True`, snap is installed in classic containment mode. - """ - body: Dict[str, Union[str, bool]] = {"action": "install"} - - if revision is not None: - body["revision"] = revision - - if channel is not None: - body["channel"] = channel - - if classic: - body["classic"] = classic - - return http.post("/snaps/" + name, body) - - -def install_all(names: List[str]) -> SnapdResponse: - """Installs all snaps in `names` using the latest rev of the stable channel, with strict - confinement. - """ - return http.post("/snaps", {"action": "install", "snaps": names}) - - -def sideload( - file_paths: List[str], - *, - classic: bool = False, - dangerous: bool = False, - devmode: bool = False, - jailmode: bool = False, - system_restart_immediate: bool = False, -) -> SnapdResponse: - """Sideload a snap from the local filesystem. - - :param file_paths: Paths to the snap files to install. - :param classic: if true, put snaps in classic mode and disable - security confinement - :param dangerous: if true, install the given snap files even if there are - no pre-acknowledged signatures for them - :param devmode: if true, put snaps in development mode and disable - security confinement - :param jailmode: if true, put snaps in enforced confinement mode - :param system_restart_immediate: if true, makes any system restart, - immediately and without delay (requires snapd 2.52) - """ - data: Dict[str, Union[str, bool]] = {"action": "install"} - - if classic: - data["classic"] = classic - - if dangerous: - data["dangerous"] = dangerous - - if devmode: - data["devmode"] = devmode - - if jailmode: - data["jailmode"] = jailmode - - if system_restart_immediate: - data["system-restart-immediate"] = system_restart_immediate - - files = [FileUpload(name="snap", path=file_path) for file_path in file_paths] - - return http.post("/snaps", FormData(data=data, files=files)) - - -def refresh( - name: str, - *, - revision: Optional[str] = None, - channel: Optional[str] = None, - classic: bool = False, -) -> SnapdResponse: - """Refreshes a snap by `name`, to `revision`, tracking `channel`. - - :param revision: revision to refresh to. Defaults to latest. - :param channel: channel to switch tracking to. Default to stable. - :param classic: If `True`, snap is changed to classic containment mode. - """ - body: Dict[str, Union[str, bool]] = {"action": "refresh"} - - if revision is not None: - body["revision"] = revision - - if channel is not None: - body["channel"] = channel - - if classic: - body["classic"] = classic - - return http.post("/snaps/" + name, body) - - -def refresh_all(names: Optional[List[str]] = None) -> SnapdResponse: - """Refreshes all snaps in `names` to the latest revision. If `names` is not provided or empty, - all snaps are refreshed. - """ - body: Dict[str, Union[str, List[str]]] = {"action": "refresh"} - - if names: - body["snaps"] = names - - return http.post("/snaps", body) - - -def revert( - name: str, *, revision: Optional[str] = None, classic: Optional[bool] = None -) -> SnapdResponse: - """Reverts a snap, switching what revision is currently installed. - - :param revision: If provided, the revision to switch to. Otherwise, the revision used prior to - the last refresh is used. - :param classic: If `True`, confinement is changed to classic. If `False`, confinement is - changed to strict. If not provided, confinement is left as-is. - """ - body: Dict[str, Union[str, bool]] = {"action": "revert"} - - if revision is not None: - body["revision"] = revision - - if classic is not None: - body["classic"] = classic - - return http.post("/snaps/" + name, body) - - -def revert_all(names: List[str]) -> SnapdResponse: - """Reverts all snaps in `names` to the revision used prior to the last refresh.""" - return http.post("/snaps", {"action": "revert", "snaps": names}) - - -def remove(name: str, - purge: Optional[bool] = False, - terminate: Optional[bool] = False) -> SnapdResponse: - """Uninstalls a snap identified by `name`.""" - body = { - "action": "remove", - "purge": purge, - "terminate": terminate, - } - - return http.post("/snaps/" + name, body) - - -def remove_all(names: List[str]) -> SnapdResponse: - """Uninstalls all snaps identified in `names`.""" - return http.post("/snaps", {"action": "remove", "snaps": names}) - - -def snapshots() -> SnapdResponse: - """Gets a list of all snapshots.""" - return http.get("/snapshots") - - -def forget_snapshot(id: str, snaps: Optional[List[str]] = None, users: Optional[List[str]] = None) -> SnapdResponse: - """Deletes a snapshot identified by `id`. - - :param snap_id: The ID of the snapshot to delete. - """ - - body: Dict[str, Union[str, List[str]]] = { - "action": "forget", - "set": id - } - - if snaps is not None: - body["snaps"] = snaps - if users is not None: - body["users"] = users - - return http.post("/snapshots", body) - - -def save_snapshot( - snaps: Optional[List[str]] = None, - users: Optional[List[str]] = None, -) -> SnapdResponse: - """Saves a snapshot of the current state of the system. - - :param name: The name of the snapshot. - :param users: array of user names to whom snapshots are to be restricted . - :param snaps: Optional list of snaps to include in the snapshot. - """ - body: Dict[str, Union[str, List[str]]] = {"action": "snapshot"} - - if users is not None: - body["users"] = users - if snaps is not None: - body["snaps"] = snaps - - return http.post("/snaps", body) - - -def switch(name: str, *, channel: str = "stable") -> SnapdResponse: - """Switches the tracking channel of snap `name`.""" - return http.post("/snaps/" + name, {"action": "switch", "channel": channel}) - - -def switch_all(names: List[str], channel: str = "stable") -> SnapdResponse: - """Switches the tracking channels of all snaps in `names`. - - NOTE: as of 2024-01-08, switch is not yet supported for multiple snaps. - """ - return http.post("/snaps", {"action": "switch", "channel": channel, "snaps": names}) - - -def unhold(name: str) -> SnapdResponse: - """Removes the hold on a snap, allowing it to refresh on its usual schedule.""" - return http.post("/snaps/" + name, {"action": "unhold"}) - - -def unhold_all(names: List[str]) -> SnapdResponse: - """Removes the holds on all snaps in `names`, allowing them to refresh on their usual - schedule. - """ - return http.post("/snaps", {"action": "unhold", "snaps": names}) - - -def list() -> SnapdResponse: - """GETs a list of installed snaps. - - This stomps on builtins.list, so please import it namespaced. - """ - return http.get("/snaps") - - -def list_all() -> SnapdResponse: - """GETs a list of all installed snaps including disabled ones. - """ - return http.get("/snaps?select=all") - -# Configuration: get and set snap options - - -def get_conf(name: str, *, keys: Optional[List[str]] = None) -> SnapdResponse: - """Get the configuration details for the snap `name`. - - :param name: the name of the snap. - :param keys: retrieve the configuration for these specific `keys`. Dotted - keys can be used to retrieve nested values. - """ - query_params = {} - if keys: - query_params["keys"] = ",".join(keys) - - return http.get(f"/snaps/{name}/conf", query_params=query_params) - - -def set_conf(name: str, config: Dict[str, Any]) -> SnapdResponse: - """Set the configuration details for the snap `name`. - - :param name: the name of the snap. - :param config: A key-value mapping of snap configuration. - Keys can be dotted, `None` can be used to unset config options. - """ - return http.put(f"/snaps/{name}/conf", config) - - -# Connections: get connections - - -def get_connections( - snap: Optional[str] = None, select: Optional[str] = None, interface: Optional[str] = None -) -> SnapdResponse: - """Retrieve connections from snapd. - - :param snap: Optional; The name of the snap to filter connections. - :param select: Optional; When set to all, unconnected slots and plugs are included in the results. - :param interface: Optional; Limit results to the selected interface. - :return: A SnapdResponse containing the snapd response for the connections query. - """ - query_params = { - k: v - for k, v in { - "snap": snap, - "select": select, - "interface": interface, - }.items() - if v - } - - return http.get("/connections", query_params=query_params) - - -# Interfaces: get/connect/disconnect interfaces - - -def get_interfaces( - select: Optional[str] = None, - slots: bool = False, - plugs: bool = False, - doc: bool = False, - names: Optional[str] = None, -) -> SnapdResponse: - """Retrieve interfaces from snapd. - - :param select: Optional; When set to "all", unconnected slots and plugs are included in the results. - :param slots: Optional; If True, includes only slot connections in the results. - :param plugs: Optional; If True, includes only plug connections in the results. - :param doc: Optional; If True, includes additional documentation details in the response. - :param names: Optional; If given, includes names of interfaces in the results - :return: A SnapdResponse containing the snapd response for the interfaces query. - """ - query_params = { - k: v - for k, v in { - "select": select, - "slots": slots, - "plugs": plugs, - "doc": doc, - "names": names, - }.items() - if v - } - - return http.get("/interfaces", query_params=query_params) - - -def connect_interface( - in_snap: str, in_slot: str, out_snap: str, out_plug: str -) -> SnapdResponse: - """ - Establish a connection between a snap plug and a snap slot. - - :param in_snap: The name of the snap providing the slot. - :param in_slot: The slot name within the providing snap. - :param out_snap: The name of the snap requiring the plug connection. - :param out_plug: The plug name within the requesting snap. - :return: A SnapdResponse containing the response from the snapd API. - """ - connect_action_body = { - "action": "connect", - "slots": [{"snap": in_snap, "slot": in_slot}], - "plugs": [{"snap": out_snap, "plug": out_plug}], - } - return http.post("/interfaces", body=connect_action_body) - - -def disconnect_interface( - in_snap: str, in_slot: str, out_snap: str, out_plug: str -) -> SnapdResponse: - """ - Remove an existing connection between a snap plug and a snap slot. - - :param in_snap: The name of the snap providing the slot. - :param in_slot: The slot name within the providing snap. - :param out_snap: The name of the snap requiring the plug connection. - :param out_plug: The plug name within the requesting snap. - :return: A SnapdResponse containing the response from the snapd API. - """ - disconnect_action_body = { - "action": "disconnect", - "slots": [{"snap": in_snap, "slot": in_slot}], - "plugs": [{"snap": out_snap, "plug": out_plug}], - } - return http.post("/interfaces", body=disconnect_action_body) - - -# Model: get model and remodel - -def get_model() -> SnapdResponse: - """ - GETs the active model assertion of system. - :return: A SnapdResponse containing the response from the snapd API. - """ - return http.get("/model") - - -def remodel(new_model_assertion: str, offline: bool = False) -> SnapdResponse: - """ - Replace the current model assertion of system - :param new_model_assertion: New model assertion content - :param offline: enables offline remodelling - :return: A SnapdResponse containing the response from the snapd API. - """ - body = {"new-model" : new_model_assertion, "offline": offline} - return http.post("/model", body=body) - - -# Validation sets: list/refresh validation sets - -def get_validation_sets() -> SnapdResponse: - """ - GET all enabled validation sets - :return: A SnapdResponse containing the response from the snapd API. - """ - return http.get("/validation-sets") - - -def get_validation_set(account_id: str, validation_set_name: str) -> SnapdResponse: - """ - GET specific validation set - :param account_id: Identifier for the developer account (creator of the validation-set). - :param validation_set_name: Name of the validation set. - :return: A SnapdResponse containing the response from the snapd API. - """ - return http.get(f"/validation-sets/{account_id}/{validation_set_name}") - - -def refresh_validation_set(account_id: str, validation_set_name: str, validation_set_sequence: Optional[int] = None) -> SnapdResponse: - """ - Refresh validation set of system - :param account_id: Identifier for the developer account (creator of the validation-set). - :param validation_set_name: Name of the validation set. - :param validation_set_sequence: Sequence value of the validation set - :return: A SnapdResponse containing the response from the snapd API. - """ - validation_set_str = f"{account_id}/{validation_set_name}" - if validation_set_sequence is not None: - validation_set_str += f"={validation_set_sequence}" - - body = { - "action": "refresh", - "validation-sets": [ - validation_set_str - ], - } - return http.post("/snaps", body=body) - - -def forget_validation_set(account_id: str, validation_set_name: str, validation_set_sequence: Optional[int] = None) -> SnapdResponse: - """ - Forget a validation set of system - :param account_id: Identifier for the developer account (creator of the validation-set). - :param validation_set_name: Name of the validation set. - :return: A SnapdResponse containing the response from the snapd API. - """ - - body: Dict[str, Union[str, int]] = { - "action": "forget" - } - - if validation_set_sequence is not None: - body["sequence"] = validation_set_sequence - - return http.post(f"/validation-sets/{account_id}/{validation_set_name}", body=body) - - -def enforce_validation_set(account_id: str, validation_set_name: str, validation_set_sequence: Optional[int] = None) -> SnapdResponse: - """ - Enforce a validation set of system - :param account_id: Identifier for the developer account (creator of the validation-set). - :param validation_set_name: Name of the validation set. - :return: A SnapdResponse containing the response from the snapd API. - """ - body: Dict[str, Union[str, int]] = { - "action": "apply", - "mode": "enforce" - } - - if validation_set_sequence is not None: - body["sequence"] = validation_set_sequence - - return http.post(f"/validation-sets/{account_id}/{validation_set_name}", body=body) - - -def monitor_validation_set(account_id: str, validation_set_name: str, validation_set_sequence: Optional[int] = None) -> SnapdResponse: - """ - Apply a validation set of system - :param account_id: Identifier for the developer account (creator of the validation-set). - :param validation_set_name: Name of the validation set. - :return: A SnapdResponse containing the response from the snapd API. - """ - body: Dict[str, Union[str, int]] = { - "action": "apply", - "mode": "monitor" - } - - if validation_set_sequence is not None: - body["sequence"] = validation_set_sequence - - return http.post(f"/validation-sets/{account_id}/{validation_set_name}", body=body) - - -# System: Get and perform action with recovery system -def get_recovery_systems() -> SnapdResponse: - """ - GET all recovery systems - :return: A SnapdResponse containing the response from the snapd API. - """ - return http.get("/systems") - -def get_recovery_system(label: str) -> SnapdResponse: - """ - GET specific recovery system - :return: A SnapdResponse containing the response from the snapd API. - """ - return http.get(f"/systems/{label}") - - -def perform_system_action(action: str, mode: str)-> SnapdResponse: - """ - Attempt to perform an action with the current active recovery system. - :param action: Action to perform, which is either “reboot”, “create” or “do”. - :param mode: The mode to transition to either "run", "recover", "install" or "factory-reset". - :return: A SnapdResponse containing the response from the snapd API. - """ - body = { - "action": action, - "mode": mode - } - return http.post("/systems", body=body) - - -def perform_recovery_action(label: str, action: str, mode: str)-> SnapdResponse: - """ - Attempt to perform an action with the current active recovery system. - :param label: Label to specify recovery system. - :param action: Action to perform, which is either “reboot”, “create” or “do”. - :param mode: The mode to transition to either "run", "recover", "install" or "factory-reset". - :return: A SnapdResponse containing the response from the snapd API. - """ - body = { - "action": action, - "mode": mode - } - return http.post(f"/systems/{label}", body=body) - - - -# Assertions: list and add assertions - - -def get_assertion_types() -> SnapdResponse: - """GETs the list of assertion types.""" - return http.get("/assertions") - - -def get_assertions( - assertion_type: str, filters: Optional[Dict[str, Any]] = None -) -> SnapdResponse: - """GETs all the assertions of the given type. - - The response is a stream of assertions separated by double newlines. - - :param assertion_type: The type of the assertion. - :param filters: A (assertion-header, filter-value) mapping to filter - assertions with. Examples of headers are: username, authority-id, - account-id, series, publisher, snap-name, and publisher-id. - """ - return http.get(f"/assertions/{assertion_type}", query_params=filters) - - -def add_assertion(assertion: str) -> SnapdResponse: - """Add an assertion to the system assertion database. - - :param assertion: The assertion to add. It may also be a newer revision - of a pre-existing assertion that it will replace. - """ - body = AssertionData(assertion) - return http.post("/assertions", body) - - -# Users - - -def list_users() -> SnapdResponse: - """Get information on user accounts.""" - return http.get("/users") - - -def add_user( - username: str, - email: str, - sudoer: bool = False, - known: bool = False, - force_managed: bool = False, - automatic: bool = False, -) -> SnapdResponse: - """Create a local user.""" - body = { - "action": "create", - "username": username, - "email": email, - "sudoer": sudoer, - "known": known, - "force-managed": force_managed, - "automatic": automatic, - } - return http.post("/users", body) - - -def remove_user(username: str) -> SnapdResponse: - """Remove a local user.""" - body = {"action": "remove", "username": username} - return http.post("/users", body) - - -# Apps and Services - - -def get_apps(names: List[str] = [], services_only: bool = False) -> SnapdResponse: - """List available apps. - - :param services_only: Return only services. - :param names: List apps for the snaps in `names` only. - """ - query_params = {} - - if services_only: - query_params["select"] = "service" - - if names: - query_params["names"] = ",".join(names) - - return http.get("/apps", query_params=query_params) - - -def start(name: str, enable: bool = False) -> SnapdResponse: - """Start the service `name`. - - :param enable: arranges to have the service start at system start. - """ - return http.post( - "/apps", - {"action": "start", "names": [name], "enable": enable}, - ) - - -def start_all(names: List[str], enable: bool = False) -> SnapdResponse: - """Start the services in `names`. - - :param enable: arranges to have the service start at system start. - """ - return http.post( - "/apps", - {"action": "start", "names": names, "enable": enable}, - ) - - -def stop(name: str, disable: bool = False) -> SnapdResponse: - """Stop the service `name`. - - :param disable: arranges to no longer start the service at system start. - """ - return http.post( - "/apps", - {"action": "stop", "names": [name], "disable": disable}, - ) - - -def stop_all(names: List[str], disable: bool = False) -> SnapdResponse: - """Stop the services in `names`. - - :param disable: arranges to no longer start the service at system start. - """ - return http.post( - "/apps", - {"action": "stop", "names": names, "disable": disable}, - ) - - -def restart(name: str, reload: bool = False) -> SnapdResponse: - """Restart the service `name`. - - :param reload: try to reload the service instead of restarting. - """ - return http.post( - "/apps", - {"action": "restart", "names": [name], "reload": reload}, - ) - - -def restart_all(names: List[str], reload: bool = False) -> SnapdResponse: - """Restart the services in `names`. - - :param reload: try to reload the service instead of restarting. - """ - return http.post( - "/apps", - {"action": "restart", "names": names, "reload": reload}, - ) - -# FDE recovery keys - - -def get_keyslots() -> SnapdResponse: - """Enumerate keyslots""" - return http.get("/system-volumes") - - -def generate_recovery_key() -> SnapdResponse: - """Generate a recovery key.""" - return http.post( - "/system-volumes", - {"action": "generate-recovery-key"}, - ) - - -def update_recovery_key( - key_id: str, keyslot_name: str, replace: bool = False -) -> SnapdResponse: - """Add or replace the recovery key for a keyslot. - - :param key_id: unique id from generating the recovery key. - :param keyslot_name: name of the keyslot to generate. - :param replace: if `True`, updates the recovery key for the keyslot. - """ - if replace: - action = "replace-recovery-key" - else: - action = "add-recovery-key" - - return http.post( - "/system-volumes", - { - "action": action, - "key-id": key_id, - "keyslots": [{"name": keyslot_name}], - }, - ) diff --git a/snap_http/api/__init__.py b/snap_http/api/__init__.py new file mode 100644 index 0000000..8984110 --- /dev/null +++ b/snap_http/api/__init__.py @@ -0,0 +1,67 @@ +""" +Functions for interacting with the snapd REST API. +See https://snapcraft.io/docs/snapd-api for documentation of the API. + +Permissions are based on the user calling the API, most mutative interactions +(install, refresh, etc) require root. +""" + +from .apps import ( + get_apps, + restart, + restart_all, + start, + start_all, + stop, + stop_all, +) +from .assertions import add_assertion, get_assertion_types, get_assertions +from .changes import check_change, check_changes +from .fde import generate_recovery_key, get_keyslots, update_recovery_key +from .interfaces import ( + connect_interface, + disconnect_interface, + get_connections, + get_interfaces, +) +from .model import get_model, remodel +from .options import get_conf, set_conf +from .snaps import ( + disable, + disable_all, + enable, + enable_all, + hold, + hold_all, + install, + install_all, + list, + list_all, + refresh, + refresh_all, + remove, + remove_all, + revert, + revert_all, + sideload, + switch, + switch_all, + unhold, + unhold_all, +) +from .snapshots import forget_snapshot, save_snapshot, snapshots +from .systems import ( + get_recovery_system, + get_recovery_systems, + perform_recovery_action, + perform_system_action, +) +from .users import add_user, list_users, remove_user +from .validation_sets import ( + enforce_validation_set, + forget_validation_set, + get_validation_set, + get_validation_sets, + monitor_validation_set, + refresh_validation_set, +) diff --git a/snap_http/api/apps.py b/snap_http/api/apps.py new file mode 100644 index 0000000..42f8096 --- /dev/null +++ b/snap_http/api/apps.py @@ -0,0 +1,87 @@ +from typing import List + +from .. import http +from ..types import SnapdResponse + + +def get_apps(names: List[str] = [], services_only: bool = False) -> SnapdResponse: + """List available apps. + + :param services_only: Return only services. + :param names: List apps for the snaps in `names` only. + """ + query_params = {} + + if services_only: + query_params["select"] = "service" + + if names: + query_params["names"] = ",".join(names) + + return http.get("/apps", query_params=query_params) + + +def start(name: str, enable: bool = False) -> SnapdResponse: + """Start the service `name`. + + :param enable: arranges to have the service start at system start. + """ + return http.post( + "/apps", + {"action": "start", "names": [name], "enable": enable}, + ) + + +def start_all(names: List[str], enable: bool = False) -> SnapdResponse: + """Start the services in `names`. + + :param enable: arranges to have the service start at system start. + """ + return http.post( + "/apps", + {"action": "start", "names": names, "enable": enable}, + ) + + +def stop(name: str, disable: bool = False) -> SnapdResponse: + """Stop the service `name`. + + :param disable: arranges to no longer start the service at system start. + """ + return http.post( + "/apps", + {"action": "stop", "names": [name], "disable": disable}, + ) + + +def stop_all(names: List[str], disable: bool = False) -> SnapdResponse: + """Stop the services in `names`. + + :param disable: arranges to no longer start the service at system start. + """ + return http.post( + "/apps", + {"action": "stop", "names": names, "disable": disable}, + ) + + +def restart(name: str, reload: bool = False) -> SnapdResponse: + """Restart the service `name`. + + :param reload: try to reload the service instead of restarting. + """ + return http.post( + "/apps", + {"action": "restart", "names": [name], "reload": reload}, + ) + + +def restart_all(names: List[str], reload: bool = False) -> SnapdResponse: + """Restart the services in `names`. + + :param reload: try to reload the service instead of restarting. + """ + return http.post( + "/apps", + {"action": "restart", "names": names, "reload": reload}, + ) diff --git a/snap_http/api/assertions.py b/snap_http/api/assertions.py new file mode 100644 index 0000000..8412177 --- /dev/null +++ b/snap_http/api/assertions.py @@ -0,0 +1,34 @@ +from typing import Any, Dict, Optional + +from .. import http +from ..types import AssertionData, SnapdResponse + + +def get_assertion_types() -> SnapdResponse: + """GETs the list of assertion types.""" + return http.get("/assertions") + + +def get_assertions( + assertion_type: str, filters: Optional[Dict[str, Any]] = None +) -> SnapdResponse: + """GETs all the assertions of the given type. + + The response is a stream of assertions separated by double newlines. + + :param assertion_type: The type of the assertion. + :param filters: A (assertion-header, filter-value) mapping to filter + assertions with. Examples of headers are: username, authority-id, + account-id, series, publisher, snap-name, and publisher-id. + """ + return http.get(f"/assertions/{assertion_type}", query_params=filters) + + +def add_assertion(assertion: str) -> SnapdResponse: + """Add an assertion to the system assertion database. + + :param assertion: The assertion to add. It may also be a newer revision + of a pre-existing assertion that it will replace. + """ + body = AssertionData(assertion) + return http.post("/assertions", body) diff --git a/snap_http/api/changes.py b/snap_http/api/changes.py new file mode 100644 index 0000000..8730cb9 --- /dev/null +++ b/snap_http/api/changes.py @@ -0,0 +1,12 @@ +from .. import http +from ..types import SnapdResponse + + +def check_change(cid: str) -> SnapdResponse: + """Checks the status of snapd change with id `cid`.""" + return http.get("/changes/" + cid) + + +def check_changes() -> SnapdResponse: + """Checks the status of all snapd changes.""" + return http.get("/changes?select=all") diff --git a/snap_http/api/fde.py b/snap_http/api/fde.py new file mode 100644 index 0000000..80d17a7 --- /dev/null +++ b/snap_http/api/fde.py @@ -0,0 +1,39 @@ +from .. import http +from ..types import SnapdResponse + + +def get_keyslots() -> SnapdResponse: + """Enumerate keyslots""" + return http.get("/system-volumes") + + +def generate_recovery_key() -> SnapdResponse: + """Generate a recovery key.""" + return http.post( + "/system-volumes", + {"action": "generate-recovery-key"}, + ) + + +def update_recovery_key( + key_id: str, keyslot_name: str, replace: bool = False +) -> SnapdResponse: + """Add or replace the recovery key for a keyslot. + + :param key_id: unique id from generating the recovery key. + :param keyslot_name: name of the keyslot to generate. + :param replace: if `True`, updates the recovery key for the keyslot. + """ + if replace: + action = "replace-recovery-key" + else: + action = "add-recovery-key" + + return http.post( + "/system-volumes", + { + "action": action, + "key-id": key_id, + "keyslots": [{"name": keyslot_name}], + }, + ) diff --git a/snap_http/api/interfaces.py b/snap_http/api/interfaces.py new file mode 100644 index 0000000..7e83b5e --- /dev/null +++ b/snap_http/api/interfaces.py @@ -0,0 +1,98 @@ +from typing import Optional + +from .. import http +from ..types import SnapdResponse + + +def get_connections( + snap: Optional[str] = None, select: Optional[str] = None, interface: Optional[str] = None +) -> SnapdResponse: + """Retrieve connections from snapd. + + :param snap: Optional; The name of the snap to filter connections. + :param select: Optional; When set to all, unconnected slots and plugs are included in the results. + :param interface: Optional; Limit results to the selected interface. + :return: A SnapdResponse containing the snapd response for the connections query. + """ + query_params = { + k: v + for k, v in { + "snap": snap, + "select": select, + "interface": interface, + }.items() + if v + } + + return http.get("/connections", query_params=query_params) + + +def get_interfaces( + select: Optional[str] = None, + slots: bool = False, + plugs: bool = False, + doc: bool = False, + names: Optional[str] = None, +) -> SnapdResponse: + """Retrieve interfaces from snapd. + + :param select: Optional; When set to "all", unconnected slots and plugs are included in the results. + :param slots: Optional; If True, includes only slot connections in the results. + :param plugs: Optional; If True, includes only plug connections in the results. + :param doc: Optional; If True, includes additional documentation details in the response. + :param names: Optional; If given, includes names of interfaces in the results + :return: A SnapdResponse containing the snapd response for the interfaces query. + """ + query_params = { + k: v + for k, v in { + "select": select, + "slots": slots, + "plugs": plugs, + "doc": doc, + "names": names, + }.items() + if v + } + + return http.get("/interfaces", query_params=query_params) + + +def connect_interface( + in_snap: str, in_slot: str, out_snap: str, out_plug: str +) -> SnapdResponse: + """ + Establish a connection between a snap plug and a snap slot. + + :param in_snap: The name of the snap providing the slot. + :param in_slot: The slot name within the providing snap. + :param out_snap: The name of the snap requiring the plug connection. + :param out_plug: The plug name within the requesting snap. + :return: A SnapdResponse containing the response from the snapd API. + """ + connect_action_body = { + "action": "connect", + "slots": [{"snap": in_snap, "slot": in_slot}], + "plugs": [{"snap": out_snap, "plug": out_plug}], + } + return http.post("/interfaces", body=connect_action_body) + + +def disconnect_interface( + in_snap: str, in_slot: str, out_snap: str, out_plug: str +) -> SnapdResponse: + """ + Remove an existing connection between a snap plug and a snap slot. + + :param in_snap: The name of the snap providing the slot. + :param in_slot: The slot name within the providing snap. + :param out_snap: The name of the snap requiring the plug connection. + :param out_plug: The plug name within the requesting snap. + :return: A SnapdResponse containing the response from the snapd API. + """ + disconnect_action_body = { + "action": "disconnect", + "slots": [{"snap": in_snap, "slot": in_slot}], + "plugs": [{"snap": out_snap, "plug": out_plug}], + } + return http.post("/interfaces", body=disconnect_action_body) diff --git a/snap_http/api/model.py b/snap_http/api/model.py new file mode 100644 index 0000000..43faf66 --- /dev/null +++ b/snap_http/api/model.py @@ -0,0 +1,21 @@ +from .. import http +from ..types import SnapdResponse + + +def get_model() -> SnapdResponse: + """ + GETs the active model assertion of system. + :return: A SnapdResponse containing the response from the snapd API. + """ + return http.get("/model") + + +def remodel(new_model_assertion: str, offline: bool = False) -> SnapdResponse: + """ + Replace the current model assertion of system + :param new_model_assertion: New model assertion content + :param offline: enables offline remodelling + :return: A SnapdResponse containing the response from the snapd API. + """ + body = {"new-model" : new_model_assertion, "offline": offline} + return http.post("/model", body=body) diff --git a/snap_http/api/options.py b/snap_http/api/options.py new file mode 100644 index 0000000..5aa7be4 --- /dev/null +++ b/snap_http/api/options.py @@ -0,0 +1,28 @@ +from typing import Any, Dict, List, Optional + +from .. import http +from ..types import SnapdResponse + + +def get_conf(name: str, *, keys: Optional[List[str]] = None) -> SnapdResponse: + """Get the configuration details for the snap `name`. + + :param name: the name of the snap. + :param keys: retrieve the configuration for these specific `keys`. Dotted + keys can be used to retrieve nested values. + """ + query_params = {} + if keys: + query_params["keys"] = ",".join(keys) + + return http.get(f"/snaps/{name}/conf", query_params=query_params) + + +def set_conf(name: str, config: Dict[str, Any]) -> SnapdResponse: + """Set the configuration details for the snap `name`. + + :param name: the name of the snap. + :param config: A key-value mapping of snap configuration. + Keys can be dotted, `None` can be used to unset config options. + """ + return http.put(f"/snaps/{name}/conf", config) diff --git a/snap_http/api/snaps.py b/snap_http/api/snaps.py new file mode 100644 index 0000000..687300c --- /dev/null +++ b/snap_http/api/snaps.py @@ -0,0 +1,258 @@ +from typing import Dict, List, Literal, Optional, Union + +from .. import http +from ..types import FileUpload, FormData, SnapdResponse + + +def enable(name: str) -> SnapdResponse: + """Enables a previously disabled snap by `name`.""" + return http.post("/snaps/" + name, {"action": "enable"}) + + +def enable_all(names: List[str]) -> SnapdResponse: + """Like `enable_snap`, but for the list of snaps in `names`. + + NOTE: as of 2024-01-08, enable/disable is not yet supported for multiple snaps. + """ + return http.post("/snaps", {"action": "enable", "snaps": names}) + + +def disable(name: str) -> SnapdResponse: + """Disables a snap by `name`, making its binaries and services unavailable.""" + return http.post("/snaps/" + name, {"action": "disable"}) + + +def disable_all(names: List[str]) -> SnapdResponse: + """Like `disable_snap`, but for the list of snaps in `names`. + + NOTE: as of 2024-01-08, enable/disable is not yet supported for multiple snaps. + """ + return http.post("/snaps", {"action": "disable", "snaps": names}) + + +def hold( + name: str, + *, + hold_level: Literal["general", "auto-refresh"] = "general", + time: str = "forever", +) -> SnapdResponse: + """Holds a snap by `name` at `hold_level` until `time`. + + :param time: RFC3339 timestamp to hold the snap until, or "forever". + """ + return http.post( + "/snaps/" + name, {"action": "hold", "hold-level": hold_level, "time": time} + ) + + +def hold_all( + names: List[str], + *, + hold_level: Literal["general", "auto-refresh"] = "general", + time: str = "forever", +) -> SnapdResponse: + """Like `hold_snap`, but for the list of snaps in `names`.""" + return http.post( + "/snaps", + {"action": "hold", "snaps": names, "hold-level": hold_level, "time": time}, + ) + + +def install( + name: str, + *, + revision: Optional[str] = None, + channel: Optional[str] = None, + classic: bool = False, +) -> SnapdResponse: + """Installs a snap by `name` at `revision`, tracking `channel`. + + :param revision: revision to install. Defaults to latest. + :param channel: channel to track. Defaults to stable. + :param classic: if `True`, snap is installed in classic containment mode. + """ + body: Dict[str, Union[str, bool]] = {"action": "install"} + + if revision is not None: + body["revision"] = revision + + if channel is not None: + body["channel"] = channel + + if classic: + body["classic"] = classic + + return http.post("/snaps/" + name, body) + + +def install_all(names: List[str]) -> SnapdResponse: + """Installs all snaps in `names` using the latest rev of the stable channel, with strict + confinement. + """ + return http.post("/snaps", {"action": "install", "snaps": names}) + + +def sideload( + file_paths: List[str], + *, + classic: bool = False, + dangerous: bool = False, + devmode: bool = False, + jailmode: bool = False, + system_restart_immediate: bool = False, +) -> SnapdResponse: + """Sideload a snap from the local filesystem. + + :param file_paths: Paths to the snap files to install. + :param classic: if true, put snaps in classic mode and disable + security confinement + :param dangerous: if true, install the given snap files even if there are + no pre-acknowledged signatures for them + :param devmode: if true, put snaps in development mode and disable + security confinement + :param jailmode: if true, put snaps in enforced confinement mode + :param system_restart_immediate: if true, makes any system restart, + immediately and without delay (requires snapd 2.52) + """ + data: Dict[str, Union[str, bool]] = {"action": "install"} + + if classic: + data["classic"] = classic + + if dangerous: + data["dangerous"] = dangerous + + if devmode: + data["devmode"] = devmode + + if jailmode: + data["jailmode"] = jailmode + + if system_restart_immediate: + data["system-restart-immediate"] = system_restart_immediate + + files = [FileUpload(name="snap", path=file_path) for file_path in file_paths] + + return http.post("/snaps", FormData(data=data, files=files)) + + +def refresh( + name: str, + *, + revision: Optional[str] = None, + channel: Optional[str] = None, + classic: bool = False, +) -> SnapdResponse: + """Refreshes a snap by `name`, to `revision`, tracking `channel`. + + :param revision: revision to refresh to. Defaults to latest. + :param channel: channel to switch tracking to. Default to stable. + :param classic: If `True`, snap is changed to classic containment mode. + """ + body: Dict[str, Union[str, bool]] = {"action": "refresh"} + + if revision is not None: + body["revision"] = revision + + if channel is not None: + body["channel"] = channel + + if classic: + body["classic"] = classic + + return http.post("/snaps/" + name, body) + + +def refresh_all(names: Optional[List[str]] = None) -> SnapdResponse: + """Refreshes all snaps in `names` to the latest revision. If `names` is not provided or empty, + all snaps are refreshed. + """ + body: Dict[str, Union[str, List[str]]] = {"action": "refresh"} + + if names: + body["snaps"] = names + + return http.post("/snaps", body) + + +def revert( + name: str, *, revision: Optional[str] = None, classic: Optional[bool] = None +) -> SnapdResponse: + """Reverts a snap, switching what revision is currently installed. + + :param revision: If provided, the revision to switch to. Otherwise, the revision used prior to + the last refresh is used. + :param classic: If `True`, confinement is changed to classic. If `False`, confinement is + changed to strict. If not provided, confinement is left as-is. + """ + body: Dict[str, Union[str, bool]] = {"action": "revert"} + + if revision is not None: + body["revision"] = revision + + if classic is not None: + body["classic"] = classic + + return http.post("/snaps/" + name, body) + + +def revert_all(names: List[str]) -> SnapdResponse: + """Reverts all snaps in `names` to the revision used prior to the last refresh.""" + return http.post("/snaps", {"action": "revert", "snaps": names}) + + +def remove(name: str, + purge: Optional[bool] = False, + terminate: Optional[bool] = False) -> SnapdResponse: + """Uninstalls a snap identified by `name`.""" + body = { + "action": "remove", + "purge": purge, + "terminate": terminate, + } + + return http.post("/snaps/" + name, body) + + +def remove_all(names: List[str]) -> SnapdResponse: + """Uninstalls all snaps identified in `names`.""" + return http.post("/snaps", {"action": "remove", "snaps": names}) + + +def switch(name: str, *, channel: str = "stable") -> SnapdResponse: + """Switches the tracking channel of snap `name`.""" + return http.post("/snaps/" + name, {"action": "switch", "channel": channel}) + + +def switch_all(names: List[str], channel: str = "stable") -> SnapdResponse: + """Switches the tracking channels of all snaps in `names`. + + NOTE: as of 2024-01-08, switch is not yet supported for multiple snaps. + """ + return http.post("/snaps", {"action": "switch", "channel": channel, "snaps": names}) + + +def unhold(name: str) -> SnapdResponse: + """Removes the hold on a snap, allowing it to refresh on its usual schedule.""" + return http.post("/snaps/" + name, {"action": "unhold"}) + + +def unhold_all(names: List[str]) -> SnapdResponse: + """Removes the holds on all snaps in `names`, allowing them to refresh on their usual + schedule. + """ + return http.post("/snaps", {"action": "unhold", "snaps": names}) + + +def list() -> SnapdResponse: + """GETs a list of installed snaps. + + This stomps on builtins.list, so please import it namespaced. + """ + return http.get("/snaps") + + +def list_all() -> SnapdResponse: + """GETs a list of all installed snaps including disabled ones. + """ + return http.get("/snaps?select=all") diff --git a/snap_http/api/snapshots.py b/snap_http/api/snapshots.py new file mode 100644 index 0000000..27cce19 --- /dev/null +++ b/snap_http/api/snapshots.py @@ -0,0 +1,48 @@ +from typing import Dict, List, Optional, Union + +from .. import http +from ..types import SnapdResponse + + +def snapshots() -> SnapdResponse: + """Gets a list of all snapshots.""" + return http.get("/snapshots") + + +def save_snapshot( + snaps: Optional[List[str]] = None, + users: Optional[List[str]] = None, +) -> SnapdResponse: + """Saves a snapshot of the current state of the system. + + :param name: The name of the snapshot. + :param users: array of user names to whom snapshots are to be restricted . + :param snaps: Optional list of snaps to include in the snapshot. + """ + body: Dict[str, Union[str, List[str]]] = {"action": "snapshot"} + + if users is not None: + body["users"] = users + if snaps is not None: + body["snaps"] = snaps + + return http.post("/snaps", body) + + +def forget_snapshot(id: str, snaps: Optional[List[str]] = None, users: Optional[List[str]] = None) -> SnapdResponse: + """Deletes a snapshot identified by `id`. + + :param snap_id: The ID of the snapshot to delete. + """ + + body: Dict[str, Union[str, List[str]]] = { + "action": "forget", + "set": id + } + + if snaps is not None: + body["snaps"] = snaps + if users is not None: + body["users"] = users + + return http.post("/snapshots", body) diff --git a/snap_http/api/systems.py b/snap_http/api/systems.py new file mode 100644 index 0000000..f3b6d02 --- /dev/null +++ b/snap_http/api/systems.py @@ -0,0 +1,46 @@ +from .. import http +from ..types import SnapdResponse + + +def get_recovery_systems() -> SnapdResponse: + """ + GET all recovery systems + :return: A SnapdResponse containing the response from the snapd API. + """ + return http.get("/systems") + +def get_recovery_system(label: str) -> SnapdResponse: + """ + GET specific recovery system + :return: A SnapdResponse containing the response from the snapd API. + """ + return http.get(f"/systems/{label}") + + +def perform_system_action(action: str, mode: str)-> SnapdResponse: + """ + Attempt to perform an action with the current active recovery system. + :param action: Action to perform, which is either “reboot”, “create” or “do”. + :param mode: The mode to transition to either "run", "recover", "install" or "factory-reset". + :return: A SnapdResponse containing the response from the snapd API. + """ + body = { + "action": action, + "mode": mode + } + return http.post("/systems", body=body) + + +def perform_recovery_action(label: str, action: str, mode: str)-> SnapdResponse: + """ + Attempt to perform an action with the current active recovery system. + :param label: Label to specify recovery system. + :param action: Action to perform, which is either “reboot”, “create” or “do”. + :param mode: The mode to transition to either "run", "recover", "install" or "factory-reset". + :return: A SnapdResponse containing the response from the snapd API. + """ + body = { + "action": action, + "mode": mode + } + return http.post(f"/systems/{label}", body=body) diff --git a/snap_http/api/users.py b/snap_http/api/users.py new file mode 100644 index 0000000..a2b9750 --- /dev/null +++ b/snap_http/api/users.py @@ -0,0 +1,34 @@ +from .. import http +from ..types import SnapdResponse + + +def list_users() -> SnapdResponse: + """Get information on user accounts.""" + return http.get("/users") + + +def add_user( + username: str, + email: str, + sudoer: bool = False, + known: bool = False, + force_managed: bool = False, + automatic: bool = False, +) -> SnapdResponse: + """Create a local user.""" + body = { + "action": "create", + "username": username, + "email": email, + "sudoer": sudoer, + "known": known, + "force-managed": force_managed, + "automatic": automatic, + } + return http.post("/users", body) + + +def remove_user(username: str) -> SnapdResponse: + """Remove a local user.""" + body = {"action": "remove", "username": username} + return http.post("/users", body) diff --git a/snap_http/api/validation_sets.py b/snap_http/api/validation_sets.py new file mode 100644 index 0000000..eab9371 --- /dev/null +++ b/snap_http/api/validation_sets.py @@ -0,0 +1,97 @@ +from typing import Dict, Optional, Union + +from .. import http +from ..types import SnapdResponse + + +def get_validation_sets() -> SnapdResponse: + """ + GET all enabled validation sets + :return: A SnapdResponse containing the response from the snapd API. + """ + return http.get("/validation-sets") + + +def get_validation_set(account_id: str, validation_set_name: str) -> SnapdResponse: + """ + GET specific validation set + :param account_id: Identifier for the developer account (creator of the validation-set). + :param validation_set_name: Name of the validation set. + :return: A SnapdResponse containing the response from the snapd API. + """ + return http.get(f"/validation-sets/{account_id}/{validation_set_name}") + + +def refresh_validation_set(account_id: str, validation_set_name: str, validation_set_sequence: Optional[int] = None) -> SnapdResponse: + """ + Refresh validation set of system + :param account_id: Identifier for the developer account (creator of the validation-set). + :param validation_set_name: Name of the validation set. + :param validation_set_sequence: Sequence value of the validation set + :return: A SnapdResponse containing the response from the snapd API. + """ + validation_set_str = f"{account_id}/{validation_set_name}" + if validation_set_sequence is not None: + validation_set_str += f"={validation_set_sequence}" + + body = { + "action": "refresh", + "validation-sets": [ + validation_set_str + ], + } + return http.post("/snaps", body=body) + + +def forget_validation_set(account_id: str, validation_set_name: str, validation_set_sequence: Optional[int] = None) -> SnapdResponse: + """ + Forget a validation set of system + :param account_id: Identifier for the developer account (creator of the validation-set). + :param validation_set_name: Name of the validation set. + :return: A SnapdResponse containing the response from the snapd API. + """ + + body: Dict[str, Union[str, int]] = { + "action": "forget" + } + + if validation_set_sequence is not None: + body["sequence"] = validation_set_sequence + + return http.post(f"/validation-sets/{account_id}/{validation_set_name}", body=body) + + +def enforce_validation_set(account_id: str, validation_set_name: str, validation_set_sequence: Optional[int] = None) -> SnapdResponse: + """ + Enforce a validation set of system + :param account_id: Identifier for the developer account (creator of the validation-set). + :param validation_set_name: Name of the validation set. + :return: A SnapdResponse containing the response from the snapd API. + """ + body: Dict[str, Union[str, int]] = { + "action": "apply", + "mode": "enforce" + } + + if validation_set_sequence is not None: + body["sequence"] = validation_set_sequence + + return http.post(f"/validation-sets/{account_id}/{validation_set_name}", body=body) + + +def monitor_validation_set(account_id: str, validation_set_name: str, validation_set_sequence: Optional[int] = None) -> SnapdResponse: + """ + Apply a validation set of system + :param account_id: Identifier for the developer account (creator of the validation-set). + :param validation_set_name: Name of the validation set. + :return: A SnapdResponse containing the response from the snapd API. + """ + body: Dict[str, Union[str, int]] = { + "action": "apply", + "mode": "monitor" + } + + if validation_set_sequence is not None: + body["sequence"] = validation_set_sequence + + return http.post(f"/validation-sets/{account_id}/{validation_set_name}", body=body) diff --git a/tests/integration/test_api.py b/tests/integration/test_api.py deleted file mode 100644 index 1d2a577..0000000 --- a/tests/integration/test_api.py +++ /dev/null @@ -1,622 +0,0 @@ -import snap_http - -from tests.utils import ( - assertion_exists, - get_snap_details, - is_snap_installed, - remove_assertion, - wait_for, -) - - -# configuration: get and set snap options - - -def test_get_config(test_snap): - """Test getting snap configuration.""" - response = snap_http.get_conf("test-snap") - assert response.status_code == 200 - assert response.result == { - "foo": {"bar": "default", "baz": "default"}, - "port": 8080, - } - - -def test_get_specific_config_value(test_snap): - """Test getting specific snap configuration.""" - response = snap_http.get_conf("test-snap", keys=["port"]) - assert response.status_code == 200 - assert response.result == {"port": 8080} - - -def test_get_nested_config_value(test_snap): - """Test getting a specific nested snap configuration.""" - response = snap_http.get_conf("test-snap", keys=["foo.bar"]) - assert response.status_code == 200 - assert response.result == {"foo.bar": "default"} - - -def test_set_config(test_snap): - """Test setting snap configuration.""" - before = snap_http.get_conf("test-snap") - assert before.result == { - "foo": {"bar": "default", "baz": "default"}, - "port": 8080, - } - - response = wait_for(snap_http.set_conf)( - "test-snap", - { - "foo": {"bar": "qux", "baz": "quux"}, - "port": 8080, - }, - ) - assert response.status_code == 202 - - after = snap_http.get_conf("test-snap") - assert after.result == { - "foo": {"bar": "qux", "baz": "quux"}, - "port": 8080, - } - - -def test_set_specific_config_value(test_snap): - """Test setting specific snap configuration.""" - before = snap_http.get_conf("test-snap") - assert before.result == { - "foo": {"bar": "default", "baz": "default"}, - "port": 8080, - } - - response = wait_for(snap_http.set_conf)( - "test-snap", - {"port": 443, "foo.baz": "lambda"}, - ) - assert response.status_code == 202 - - after = snap_http.get_conf("test-snap") - assert after.result == { - "foo": {"bar": "default", "baz": "lambda"}, - "port": 443, - } - - -def test_set_config_with_invalid_key(test_snap): - """Test setting config with an invalid key.""" - before = snap_http.get_conf("test-snap") - assert before.result == { - "foo": {"bar": "default", "baz": "default"}, - "port": 8080, - } - - response = wait_for(snap_http.set_conf)("test-snap", {"foo /bar": 80}) - assert response.status_code == 202 - - change = snap_http.check_change(response.change).result - assert change["status"] == "Error" - assert 'invalid option name: "foo /bar"' in change["err"] - - # confirm settings haven't changed - after = snap_http.get_conf("test-snap") - assert after.result == { - "foo": {"bar": "default", "baz": "default"}, - "port": 8080, - } - - -def test_unset_config_value(test_snap): - """Test unsetting snap configuration.""" - before = snap_http.get_conf("test-snap") - assert before.result == { - "foo": {"bar": "default", "baz": "default"}, - "port": 8080, - } - - response = wait_for(snap_http.set_conf)( - "test-snap", - {"foo.bar": "meta"}, - ) - assert response.status_code == 202 - - after = snap_http.get_conf("test-snap", keys=["foo.bar"]) - assert after.result == {"foo.bar": "meta"} - - response = wait_for(snap_http.set_conf)( - "test-snap", - {"foo.bar": None}, - ) - assert response.status_code == 202 - - after_unset = snap_http.get_conf("test-snap", keys=["foo.bar"]) - assert after_unset.result == {"foo.bar": "default"} - - -# snaps: list and manage installed snaps - - -def test_list_snaps(): - """Test listing snaps.""" - installed_snaps = {snap["name"] for snap in snap_http.list().result} - assert "snapd" in installed_snaps - - -def test_list_all_snaps(): - """Test listing snaps.""" - installed_snaps = {snap["name"] for snap in snap_http.list_all().result} - assert "snapd" in installed_snaps - - -def test_install_snap_from_the_store(hello_world_snap_declaration_assertion): - """Test installing a snap from the store.""" - assert is_snap_installed("hello-world") is False - - response = wait_for(snap_http.install)("hello-world") - assert response.status_code == 202 - assert is_snap_installed("hello-world") is True - - wait_for(snap_http.remove)("hello-world") - remove_assertion(**hello_world_snap_declaration_assertion[1]) - - -def test_remove_snap(test_snap): - """Test removing a snap.""" - assert is_snap_installed("test-snap") is True - - response = wait_for(snap_http.remove)("test-snap") - assert response.status_code == 202 - assert is_snap_installed("test-snap") is False - - -def test_sideload_snap_no_flags( - local_hello_world_snap_path, - hello_world_snap_declaration_assertion, -): - """Test sideloading a snap with no flags specified.""" - assert is_snap_installed("hello-world") is False - - # ack the assertion - snap_http.add_assertion(hello_world_snap_declaration_assertion[0]) - # sideload - response = wait_for(snap_http.sideload)( - file_paths=[local_hello_world_snap_path], - ) - assert response.status_code == 202 - - snap = get_snap_details("hello-world") - assert snap["status"] == "active" - assert snap["confinement"] == "strict" - assert snap["devmode"] is False - assert snap["jailmode"] is False - - wait_for(snap_http.remove)("hello-world") - - -def test_sideload_snap_in_devmode_confinement(local_test_snap_path): - """Test sideloading a snap in devmode confinement.""" - assert is_snap_installed("test-snap") is False - - response = wait_for(snap_http.sideload)( - file_paths=[local_test_snap_path], - devmode=True, - ) - assert response.status_code == 202 - - snap = get_snap_details("test-snap") - assert snap["status"] == "active" - assert snap["confinement"] == "devmode" - assert snap["devmode"] is True - assert snap["jailmode"] is False - - wait_for(snap_http.remove)("test-snap") - - -def test_sideload_dangerous_snap(local_hello_world_snap_path): - """Test sideloading a snap in dangerous mode.""" - assert is_snap_installed("hello-world") is False - - response = wait_for(snap_http.sideload)( - file_paths=[local_hello_world_snap_path], - dangerous=True, - ) - assert response.status_code == 202 - - snap = get_snap_details("hello-world") - assert snap["status"] == "active" - assert snap["confinement"] == "strict" - assert snap["devmode"] is False - assert snap["jailmode"] is False - - wait_for(snap_http.remove)("hello-world") - - -def test_sideload_snap_with_enforced_confinement( - local_hello_world_snap_path, - hello_world_snap_declaration_assertion, -): - """Test sideloading a snap with enforced confinement.""" - assert is_snap_installed("hello-world") is False - - # ack the assertion - snap_http.add_assertion(hello_world_snap_declaration_assertion[0]) - # sideload - response = wait_for(snap_http.sideload)( - file_paths=[local_hello_world_snap_path], - jailmode=True, - ) - assert response.status_code == 202 - - snap = get_snap_details("hello-world") - assert snap["status"] == "active" - assert snap["confinement"] == "strict" - assert snap["devmode"] is False - assert snap["jailmode"] is True - - wait_for(snap_http.remove)("hello-world") - - -def test_sideload_multiple_snaps( - local_test_snap_path, - local_hello_world_snap_path, - hello_world_snap_declaration_assertion, -): - """Test sideloading multiple snaps.""" - assert is_snap_installed("test-snap") is False - assert is_snap_installed("hello-world") is False - - # ack the assertion - snap_http.add_assertion(hello_world_snap_declaration_assertion[0]) - # sideload - response = wait_for(snap_http.sideload)( - file_paths=[local_test_snap_path, local_hello_world_snap_path], - devmode=True, - ) - assert response.status_code == 202 - - assert is_snap_installed("test-snap") is True - assert is_snap_installed("hello-world") is True - - wait_for(snap_http.remove_all)(["test-snap", "hello-world"]) - - -# Assertions: list and add assertions - - -def test_get_assertion_types(): - """Test getting assertion types.""" - response = snap_http.get_assertion_types() - assert response.status_code == 200 - types = response.result["types"] - assert len(types) > 0 - assert "account" in types - assert "model" in types - assert "snap-declaration" in types - assert "store" in types - - -def test_get_assertions(): - """Test getting assertions.""" - response = snap_http.get_assertions("snap-declaration") - assert response.status_code == 200 - assert len(response.result) > 0 - assert b"type: snap-declaration" in response.result - - -def test_get_assertions_with_filters(hello_world_snap_declaration_assertion): - """Test getting assertions with filters.""" - assertion, metadata = hello_world_snap_declaration_assertion - - before = snap_http.get_assertions( - "snap-declaration", filters={"snap-id": metadata["snap_id"]} - ) - assert before.result == b"" - - response = snap_http.add_assertion(assertion) - assert response.status_code == 200 - - after = snap_http.get_assertions( - "snap-declaration", - filters={"snap-id": metadata["snap_id"], "series": metadata["series"]}, - ) - assert after.result.decode() == assertion - - -def test_add_an_assertion(hello_world_snap_declaration_assertion): - """Test adding an assertion.""" - assertion, metadata = hello_world_snap_declaration_assertion - assert assertion_exists(**metadata) is False - - response = snap_http.add_assertion(assertion) - assert response.status_code == 200 - assert assertion_exists(**metadata) is True - - -# Apps and Services - - -def test_get_all_apps(test_snap): - """Test getting all apps.""" - response = snap_http.get_apps() - assert response.status_code == 200 - - result = sorted(response.result, key=lambda app: app["name"], reverse=True) - assert len(result) > 3 # 3 apps come from `test_snap` - apps = list(filter(lambda app: app["snap"] == "test-snap", result)) - assert apps == [ - {"snap": "test-snap", "name": "test-snap"}, - { - "snap": "test-snap", - "name": "hello-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - { - "snap": "test-snap", - "name": "bye-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - ] - - -def test_get_all_apps_in_snap(test_snap): - """Test getting apps in a single snap.""" - response = snap_http.get_apps(names=["test-snap"]) - assert response.status_code == 200 - - apps = sorted(response.result, key=lambda app: app["name"], reverse=True) - assert apps == [ - {"snap": "test-snap", "name": "test-snap"}, - { - "snap": "test-snap", - "name": "hello-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - { - "snap": "test-snap", - "name": "bye-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - ] - - -def test_get_services_only(test_snap): - """Test getting services only.""" - response = snap_http.get_apps(names=["test-snap"], services_only=True) - assert response.status_code == 200 - - apps = sorted(response.result, key=lambda app: app["name"], reverse=True) - assert apps == [ - { - "snap": "test-snap", - "name": "hello-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - { - "snap": "test-snap", - "name": "bye-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - ] - - -def test_start_single_service(test_snap): - """Test starting a single service.""" - response = wait_for(snap_http.start)("test-snap.hello-svc") - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - apps = sorted(after.result, key=lambda app: app["name"], reverse=True) - assert apps[0]["name"] == "hello-svc" - assert apps[0]["active"] is True - assert "enabled" not in apps[0] - assert apps[1]["name"] == "bye-svc" - assert "active" not in apps[1] - assert "enabled" not in apps[1] - - -def test_start_all_services_in_single_snap(test_snap): - """Test starting all services in a single snap.""" - response = wait_for(snap_http.start)("test-snap") - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - apps = sorted(after.result, key=lambda app: app["name"], reverse=True) - assert apps[0]["name"] == "hello-svc" - assert apps[0]["active"] is True - assert "enabled" not in apps[0] - assert apps[1]["name"] == "bye-svc" - assert apps[1]["active"] is True - assert "enabled" not in apps[1] - - -def test_start_multiple_services(test_snap): - """Test starting multiple services individually.""" - response = wait_for(snap_http.start_all)( - ["test-snap.hello-svc", "test-snap.bye-svc"] - ) - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - apps = sorted(after.result, key=lambda app: app["name"], reverse=True) - assert apps[0]["name"] == "hello-svc" - assert apps[0]["active"] is True - assert "enabled" not in apps[0] - assert apps[1]["name"] == "bye-svc" - assert apps[1]["active"] is True - assert "enabled" not in apps[1] - - -def test_start_and_enable_service(test_snap): - """Test starting and enabling a single service.""" - response = wait_for(snap_http.start)("test-snap.hello-svc", enable=True) - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - app = sorted(after.result, key=lambda app: app["name"], reverse=True)[0] - assert app["name"] == "hello-svc" - assert app["active"] is True - assert app["enabled"] is True - - -def test_stop_single_service(test_snap): - """Test stopping a single service.""" - wait_for(snap_http.start)("test-snap.hello-svc") - - response = wait_for(snap_http.stop)("test-snap.hello-svc") - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - app = sorted(after.result, key=lambda app: app["name"], reverse=True)[0] - assert app["name"] == "hello-svc" - assert "active" not in app - assert "enabled" not in app - - -def test_stop_all_services_in_single_snap(test_snap): - """Test stopping all services in a single snap.""" - wait_for(snap_http.start)("test-snap") - - response = wait_for(snap_http.stop)("test-snap") - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - apps = sorted(after.result, key=lambda app: app["name"], reverse=True) - assert apps[0]["name"] == "hello-svc" - assert "active" not in apps[0] - assert "enabled" not in apps[0] - assert apps[1]["name"] == "bye-svc" - assert "active" not in apps[1] - assert "enabled" not in apps[1] - - -def test_stop_multiple_services(test_snap): - """Test stopping multiple services individually.""" - wait_for(snap_http.start)("test-snap") - - response = wait_for(snap_http.stop_all)( - ["test-snap.hello-svc", "test-snap.bye-svc"] - ) - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - apps = sorted(after.result, key=lambda app: app["name"], reverse=True) - assert apps[0]["name"] == "hello-svc" - assert "active" not in apps[0] - assert "enabled" not in apps[0] - assert apps[1]["name"] == "bye-svc" - assert "active" not in apps[1] - assert "enabled" not in apps[1] - - -def test_stop_and_disable_service(test_snap): - """Test stopping and disabling a single service.""" - wait_for(snap_http.start)("test-snap", enable=True) - - response = wait_for(snap_http.stop)("test-snap.hello-svc", disable=True) - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - apps = sorted(after.result, key=lambda app: app["name"], reverse=True) - assert apps[0]["name"] == "hello-svc" - assert "active" not in apps[0] - assert "enabled" not in apps[0] - assert apps[1]["name"] == "bye-svc" - assert apps[1]["active"] is True - assert apps[1]["enabled"] is True - - -def test_restart_single_service(test_snap): - """Test restarting a single service.""" - wait_for(snap_http.start)("test-snap.hello-svc") - - response = wait_for(snap_http.restart)("test-snap.hello-svc") - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - app = sorted(after.result, key=lambda app: app["name"], reverse=True)[0] - assert app["name"] == "hello-svc" - assert app["active"] is True - assert "enabled" not in app - - -def test_restart_all_services_in_single_snap(test_snap): - """Test restarting all services in a single snap.""" - wait_for(snap_http.start)("test-snap") - - response = wait_for(snap_http.restart)("test-snap") - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - apps = sorted(after.result, key=lambda app: app["name"], reverse=True) - assert apps[0]["name"] == "hello-svc" - assert apps[0]["active"] is True - assert "enabled" not in apps[0] - assert apps[1]["name"] == "bye-svc" - assert apps[1]["active"] is True - assert "enabled" not in apps[1] - - -def test_restart_multiple_services(test_snap): - """Test restarting multiple services individually.""" - wait_for(snap_http.start)("test-snap") - - response = wait_for(snap_http.restart_all)( - ["test-snap.hello-svc", "test-snap.bye-svc"] - ) - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - apps = sorted(after.result, key=lambda app: app["name"], reverse=True) - assert apps[0]["name"] == "hello-svc" - assert apps[0]["active"] is True - assert "enabled" not in apps[0] - assert apps[1]["name"] == "bye-svc" - assert apps[1]["active"] is True - assert "enabled" not in apps[1] - - -def test_reload_service(test_snap): - """Test reloading a single service.""" - wait_for(snap_http.start)("test-snap") - - response = wait_for(snap_http.restart)("test-snap.hello-svc", reload=True) - assert response.status_code == 202 - - after = snap_http.get_apps(names=["test-snap"], services_only=True) - apps = sorted(after.result, key=lambda app: app["name"], reverse=True) - assert apps[0]["name"] == "hello-svc" - assert apps[0]["active"] is True - assert "enabled" not in apps[0] - assert apps[1]["name"] == "bye-svc" - assert apps[1]["active"] is True - assert "enabled" not in apps[1] - - -def test_save_snapshot(test_snap): - """Test saving a snapshot.""" - response = wait_for(snap_http.save_snapshot)(snaps=["snapd"]) - assert response.status_code == 202 - - set_id = response.result["set-id"] - snapshots = snap_http.snapshots().result - assert any(shot['id'] == set_id for shot in snapshots) - - -def test_forget_snapshot(test_snap): - """Test forgetting a snapshot.""" - response = wait_for(snap_http.save_snapshot)(snaps=["snapd"]) - assert response.status_code == 202 - - set_id = response.result["set-id"] - snapshots = snap_http.snapshots().result - assert any(shot['id'] == set_id for shot in snapshots) - - response = wait_for(snap_http.forget_snapshot)(set_id) - assert response.status_code == 202 - - snapshots = snap_http.snapshots().result - assert not any(shot['id'] == set_id for shot in snapshots) diff --git a/tests/integration/test_apps.py b/tests/integration/test_apps.py new file mode 100644 index 0000000..3785151 --- /dev/null +++ b/tests/integration/test_apps.py @@ -0,0 +1,266 @@ +import snap_http + +from tests.utils import wait_for + + +def test_get_all_apps(test_snap): + """Test getting all apps.""" + response = snap_http.get_apps() + assert response.status_code == 200 + + result = sorted(response.result, key=lambda app: app["name"], reverse=True) + assert len(result) > 3 # 3 apps come from `test_snap` + apps = list(filter(lambda app: app["snap"] == "test-snap", result)) + assert apps == [ + {"snap": "test-snap", "name": "test-snap"}, + { + "snap": "test-snap", + "name": "hello-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + { + "snap": "test-snap", + "name": "bye-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + ] + + +def test_get_all_apps_in_snap(test_snap): + """Test getting apps in a single snap.""" + response = snap_http.get_apps(names=["test-snap"]) + assert response.status_code == 200 + + apps = sorted(response.result, key=lambda app: app["name"], reverse=True) + assert apps == [ + {"snap": "test-snap", "name": "test-snap"}, + { + "snap": "test-snap", + "name": "hello-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + { + "snap": "test-snap", + "name": "bye-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + ] + + +def test_get_services_only(test_snap): + """Test getting services only.""" + response = snap_http.get_apps(names=["test-snap"], services_only=True) + assert response.status_code == 200 + + apps = sorted(response.result, key=lambda app: app["name"], reverse=True) + assert apps == [ + { + "snap": "test-snap", + "name": "hello-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + { + "snap": "test-snap", + "name": "bye-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + ] + + +def test_start_single_service(test_snap): + """Test starting a single service.""" + response = wait_for(snap_http.start)("test-snap.hello-svc") + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + apps = sorted(after.result, key=lambda app: app["name"], reverse=True) + assert apps[0]["name"] == "hello-svc" + assert apps[0]["active"] is True + assert "enabled" not in apps[0] + assert apps[1]["name"] == "bye-svc" + assert "active" not in apps[1] + assert "enabled" not in apps[1] + + +def test_start_all_services_in_single_snap(test_snap): + """Test starting all services in a single snap.""" + response = wait_for(snap_http.start)("test-snap") + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + apps = sorted(after.result, key=lambda app: app["name"], reverse=True) + assert apps[0]["name"] == "hello-svc" + assert apps[0]["active"] is True + assert "enabled" not in apps[0] + assert apps[1]["name"] == "bye-svc" + assert apps[1]["active"] is True + assert "enabled" not in apps[1] + + +def test_start_multiple_services(test_snap): + """Test starting multiple services individually.""" + response = wait_for(snap_http.start_all)( + ["test-snap.hello-svc", "test-snap.bye-svc"] + ) + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + apps = sorted(after.result, key=lambda app: app["name"], reverse=True) + assert apps[0]["name"] == "hello-svc" + assert apps[0]["active"] is True + assert "enabled" not in apps[0] + assert apps[1]["name"] == "bye-svc" + assert apps[1]["active"] is True + assert "enabled" not in apps[1] + + +def test_start_and_enable_service(test_snap): + """Test starting and enabling a single service.""" + response = wait_for(snap_http.start)("test-snap.hello-svc", enable=True) + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + app = sorted(after.result, key=lambda app: app["name"], reverse=True)[0] + assert app["name"] == "hello-svc" + assert app["active"] is True + assert app["enabled"] is True + + +def test_stop_single_service(test_snap): + """Test stopping a single service.""" + wait_for(snap_http.start)("test-snap.hello-svc") + + response = wait_for(snap_http.stop)("test-snap.hello-svc") + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + app = sorted(after.result, key=lambda app: app["name"], reverse=True)[0] + assert app["name"] == "hello-svc" + assert "active" not in app + assert "enabled" not in app + + +def test_stop_all_services_in_single_snap(test_snap): + """Test stopping all services in a single snap.""" + wait_for(snap_http.start)("test-snap") + + response = wait_for(snap_http.stop)("test-snap") + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + apps = sorted(after.result, key=lambda app: app["name"], reverse=True) + assert apps[0]["name"] == "hello-svc" + assert "active" not in apps[0] + assert "enabled" not in apps[0] + assert apps[1]["name"] == "bye-svc" + assert "active" not in apps[1] + assert "enabled" not in apps[1] + + +def test_stop_multiple_services(test_snap): + """Test stopping multiple services individually.""" + wait_for(snap_http.start)("test-snap") + + response = wait_for(snap_http.stop_all)( + ["test-snap.hello-svc", "test-snap.bye-svc"] + ) + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + apps = sorted(after.result, key=lambda app: app["name"], reverse=True) + assert apps[0]["name"] == "hello-svc" + assert "active" not in apps[0] + assert "enabled" not in apps[0] + assert apps[1]["name"] == "bye-svc" + assert "active" not in apps[1] + assert "enabled" not in apps[1] + + +def test_stop_and_disable_service(test_snap): + """Test stopping and disabling a single service.""" + wait_for(snap_http.start)("test-snap", enable=True) + + response = wait_for(snap_http.stop)("test-snap.hello-svc", disable=True) + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + apps = sorted(after.result, key=lambda app: app["name"], reverse=True) + assert apps[0]["name"] == "hello-svc" + assert "active" not in apps[0] + assert "enabled" not in apps[0] + assert apps[1]["name"] == "bye-svc" + assert apps[1]["active"] is True + assert apps[1]["enabled"] is True + + +def test_restart_single_service(test_snap): + """Test restarting a single service.""" + wait_for(snap_http.start)("test-snap.hello-svc") + + response = wait_for(snap_http.restart)("test-snap.hello-svc") + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + app = sorted(after.result, key=lambda app: app["name"], reverse=True)[0] + assert app["name"] == "hello-svc" + assert app["active"] is True + assert "enabled" not in app + + +def test_restart_all_services_in_single_snap(test_snap): + """Test restarting all services in a single snap.""" + wait_for(snap_http.start)("test-snap") + + response = wait_for(snap_http.restart)("test-snap") + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + apps = sorted(after.result, key=lambda app: app["name"], reverse=True) + assert apps[0]["name"] == "hello-svc" + assert apps[0]["active"] is True + assert "enabled" not in apps[0] + assert apps[1]["name"] == "bye-svc" + assert apps[1]["active"] is True + assert "enabled" not in apps[1] + + +def test_restart_multiple_services(test_snap): + """Test restarting multiple services individually.""" + wait_for(snap_http.start)("test-snap") + + response = wait_for(snap_http.restart_all)( + ["test-snap.hello-svc", "test-snap.bye-svc"] + ) + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + apps = sorted(after.result, key=lambda app: app["name"], reverse=True) + assert apps[0]["name"] == "hello-svc" + assert apps[0]["active"] is True + assert "enabled" not in apps[0] + assert apps[1]["name"] == "bye-svc" + assert apps[1]["active"] is True + assert "enabled" not in apps[1] + + +def test_reload_service(test_snap): + """Test reloading a single service.""" + wait_for(snap_http.start)("test-snap") + + response = wait_for(snap_http.restart)("test-snap.hello-svc", reload=True) + assert response.status_code == 202 + + after = snap_http.get_apps(names=["test-snap"], services_only=True) + apps = sorted(after.result, key=lambda app: app["name"], reverse=True) + assert apps[0]["name"] == "hello-svc" + assert apps[0]["active"] is True + assert "enabled" not in apps[0] + assert apps[1]["name"] == "bye-svc" + assert apps[1]["active"] is True + assert "enabled" not in apps[1] diff --git a/tests/integration/test_assertions.py b/tests/integration/test_assertions.py new file mode 100644 index 0000000..962c13b --- /dev/null +++ b/tests/integration/test_assertions.py @@ -0,0 +1,52 @@ +import snap_http + +from tests.utils import assertion_exists + + +def test_get_assertion_types(): + """Test getting assertion types.""" + response = snap_http.get_assertion_types() + assert response.status_code == 200 + types = response.result["types"] + assert len(types) > 0 + assert "account" in types + assert "model" in types + assert "snap-declaration" in types + assert "store" in types + + +def test_get_assertions(): + """Test getting assertions.""" + response = snap_http.get_assertions("snap-declaration") + assert response.status_code == 200 + assert len(response.result) > 0 + assert b"type: snap-declaration" in response.result + + +def test_get_assertions_with_filters(hello_world_snap_declaration_assertion): + """Test getting assertions with filters.""" + assertion, metadata = hello_world_snap_declaration_assertion + + before = snap_http.get_assertions( + "snap-declaration", filters={"snap-id": metadata["snap_id"]} + ) + assert before.result == b"" + + response = snap_http.add_assertion(assertion) + assert response.status_code == 200 + + after = snap_http.get_assertions( + "snap-declaration", + filters={"snap-id": metadata["snap_id"], "series": metadata["series"]}, + ) + assert after.result.decode() == assertion + + +def test_add_an_assertion(hello_world_snap_declaration_assertion): + """Test adding an assertion.""" + assertion, metadata = hello_world_snap_declaration_assertion + assert assertion_exists(**metadata) is False + + response = snap_http.add_assertion(assertion) + assert response.status_code == 200 + assert assertion_exists(**metadata) is True diff --git a/tests/integration/test_options.py b/tests/integration/test_options.py new file mode 100644 index 0000000..8ca132c --- /dev/null +++ b/tests/integration/test_options.py @@ -0,0 +1,123 @@ +import snap_http + +from tests.utils import wait_for + + +def test_get_config(test_snap): + """Test getting snap configuration.""" + response = snap_http.get_conf("test-snap") + assert response.status_code == 200 + assert response.result == { + "foo": {"bar": "default", "baz": "default"}, + "port": 8080, + } + + +def test_get_specific_config_value(test_snap): + """Test getting specific snap configuration.""" + response = snap_http.get_conf("test-snap", keys=["port"]) + assert response.status_code == 200 + assert response.result == {"port": 8080} + + +def test_get_nested_config_value(test_snap): + """Test getting a specific nested snap configuration.""" + response = snap_http.get_conf("test-snap", keys=["foo.bar"]) + assert response.status_code == 200 + assert response.result == {"foo.bar": "default"} + + +def test_set_config(test_snap): + """Test setting snap configuration.""" + before = snap_http.get_conf("test-snap") + assert before.result == { + "foo": {"bar": "default", "baz": "default"}, + "port": 8080, + } + + response = wait_for(snap_http.set_conf)( + "test-snap", + { + "foo": {"bar": "qux", "baz": "quux"}, + "port": 8080, + }, + ) + assert response.status_code == 202 + + after = snap_http.get_conf("test-snap") + assert after.result == { + "foo": {"bar": "qux", "baz": "quux"}, + "port": 8080, + } + + +def test_set_specific_config_value(test_snap): + """Test setting specific snap configuration.""" + before = snap_http.get_conf("test-snap") + assert before.result == { + "foo": {"bar": "default", "baz": "default"}, + "port": 8080, + } + + response = wait_for(snap_http.set_conf)( + "test-snap", + {"port": 443, "foo.baz": "lambda"}, + ) + assert response.status_code == 202 + + after = snap_http.get_conf("test-snap") + assert after.result == { + "foo": {"bar": "default", "baz": "lambda"}, + "port": 443, + } + + +def test_set_config_with_invalid_key(test_snap): + """Test setting config with an invalid key.""" + before = snap_http.get_conf("test-snap") + assert before.result == { + "foo": {"bar": "default", "baz": "default"}, + "port": 8080, + } + + response = wait_for(snap_http.set_conf)("test-snap", {"foo /bar": 80}) + assert response.status_code == 202 + + change = snap_http.check_change(response.change).result + assert change["status"] == "Error" + assert 'invalid option name: "foo /bar"' in change["err"] + + # confirm settings haven't changed + after = snap_http.get_conf("test-snap") + assert after.result == { + "foo": {"bar": "default", "baz": "default"}, + "port": 8080, + } + + +def test_unset_config_value(test_snap): + """Test unsetting snap configuration.""" + before = snap_http.get_conf("test-snap") + assert before.result == { + "foo": {"bar": "default", "baz": "default"}, + "port": 8080, + } + + response = wait_for(snap_http.set_conf)( + "test-snap", + {"foo.bar": "meta"}, + ) + assert response.status_code == 202 + + after = snap_http.get_conf("test-snap", keys=["foo.bar"]) + assert after.result == {"foo.bar": "meta"} + + response = wait_for(snap_http.set_conf)( + "test-snap", + {"foo.bar": None}, + ) + assert response.status_code == 202 + + after_unset = snap_http.get_conf("test-snap", keys=["foo.bar"]) + assert after_unset.result == {"foo.bar": "default"} + diff --git a/tests/integration/test_snaps.py b/tests/integration/test_snaps.py new file mode 100644 index 0000000..228424a --- /dev/null +++ b/tests/integration/test_snaps.py @@ -0,0 +1,147 @@ +import snap_http + +from tests.utils import get_snap_details, is_snap_installed, remove_assertion, wait_for + + +def test_list_snaps(): + """Test listing snaps.""" + installed_snaps = {snap["name"] for snap in snap_http.list().result} + assert "snapd" in installed_snaps + + +def test_list_all_snaps(): + """Test listing snaps.""" + installed_snaps = {snap["name"] for snap in snap_http.list_all().result} + assert "snapd" in installed_snaps + + +def test_install_snap_from_the_store(hello_world_snap_declaration_assertion): + """Test installing a snap from the store.""" + assert is_snap_installed("hello-world") is False + + response = wait_for(snap_http.install)("hello-world") + assert response.status_code == 202 + assert is_snap_installed("hello-world") is True + + wait_for(snap_http.remove)("hello-world") + remove_assertion(**hello_world_snap_declaration_assertion[1]) + + +def test_remove_snap(test_snap): + """Test removing a snap.""" + assert is_snap_installed("test-snap") is True + + response = wait_for(snap_http.remove)("test-snap") + assert response.status_code == 202 + assert is_snap_installed("test-snap") is False + + +def test_sideload_snap_no_flags( + local_hello_world_snap_path, + hello_world_snap_declaration_assertion, +): + """Test sideloading a snap with no flags specified.""" + assert is_snap_installed("hello-world") is False + + # ack the assertion + snap_http.add_assertion(hello_world_snap_declaration_assertion[0]) + # sideload + response = wait_for(snap_http.sideload)( + file_paths=[local_hello_world_snap_path], + ) + assert response.status_code == 202 + + snap = get_snap_details("hello-world") + assert snap["status"] == "active" + assert snap["confinement"] == "strict" + assert snap["devmode"] is False + assert snap["jailmode"] is False + + wait_for(snap_http.remove)("hello-world") + + +def test_sideload_snap_in_devmode_confinement(local_test_snap_path): + """Test sideloading a snap in devmode confinement.""" + assert is_snap_installed("test-snap") is False + + response = wait_for(snap_http.sideload)( + file_paths=[local_test_snap_path], + devmode=True, + ) + assert response.status_code == 202 + + snap = get_snap_details("test-snap") + assert snap["status"] == "active" + assert snap["confinement"] == "devmode" + assert snap["devmode"] is True + assert snap["jailmode"] is False + + wait_for(snap_http.remove)("test-snap") + + +def test_sideload_dangerous_snap(local_hello_world_snap_path): + """Test sideloading a snap in dangerous mode.""" + assert is_snap_installed("hello-world") is False + + response = wait_for(snap_http.sideload)( + file_paths=[local_hello_world_snap_path], + dangerous=True, + ) + assert response.status_code == 202 + + snap = get_snap_details("hello-world") + assert snap["status"] == "active" + assert snap["confinement"] == "strict" + assert snap["devmode"] is False + assert snap["jailmode"] is False + + wait_for(snap_http.remove)("hello-world") + + +def test_sideload_snap_with_enforced_confinement( + local_hello_world_snap_path, + hello_world_snap_declaration_assertion, +): + """Test sideloading a snap with enforced confinement.""" + assert is_snap_installed("hello-world") is False + + # ack the assertion + snap_http.add_assertion(hello_world_snap_declaration_assertion[0]) + # sideload + response = wait_for(snap_http.sideload)( + file_paths=[local_hello_world_snap_path], + jailmode=True, + ) + assert response.status_code == 202 + + snap = get_snap_details("hello-world") + assert snap["status"] == "active" + assert snap["confinement"] == "strict" + assert snap["devmode"] is False + assert snap["jailmode"] is True + + wait_for(snap_http.remove)("hello-world") + + +def test_sideload_multiple_snaps( + local_test_snap_path, + local_hello_world_snap_path, + hello_world_snap_declaration_assertion, +): + """Test sideloading multiple snaps.""" + assert is_snap_installed("test-snap") is False + assert is_snap_installed("hello-world") is False + + # ack the assertion + snap_http.add_assertion(hello_world_snap_declaration_assertion[0]) + # sideload + response = wait_for(snap_http.sideload)( + file_paths=[local_test_snap_path, local_hello_world_snap_path], + devmode=True, + ) + assert response.status_code == 202 + + assert is_snap_installed("test-snap") is True + assert is_snap_installed("hello-world") is True + + wait_for(snap_http.remove_all)(["test-snap", "hello-world"]) diff --git a/tests/integration/test_snapshots.py b/tests/integration/test_snapshots.py new file mode 100644 index 0000000..d85ded0 --- /dev/null +++ b/tests/integration/test_snapshots.py @@ -0,0 +1,29 @@ +import snap_http + +from tests.utils import wait_for + + +def test_save_snapshot(test_snap): + """Test saving a snapshot.""" + response = wait_for(snap_http.save_snapshot)(snaps=["snapd"]) + assert response.status_code == 202 + + set_id = response.result["set-id"] + snapshots = snap_http.snapshots().result + assert any(shot['id'] == set_id for shot in snapshots) + + +def test_forget_snapshot(test_snap): + """Test forgetting a snapshot.""" + response = wait_for(snap_http.save_snapshot)(snaps=["snapd"]) + assert response.status_code == 202 + + set_id = response.result["set-id"] + snapshots = snap_http.snapshots().result + assert any(shot['id'] == set_id for shot in snapshots) + + response = wait_for(snap_http.forget_snapshot)(set_id) + assert response.status_code == 202 + + snapshots = snap_http.snapshots().result + assert not any(shot['id'] == set_id for shot in snapshots) diff --git a/tests/unit/api/__init__.py b/tests/unit/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/api/conftest.py b/tests/unit/api/conftest.py new file mode 100644 index 0000000..aed1829 --- /dev/null +++ b/tests/unit/api/conftest.py @@ -0,0 +1,7 @@ +import pytest + + +@pytest.fixture(autouse=True) +def no_requests(monkeypatch): + """Removes _make_request from snap_http.http to prevent inadvertent requests going out.""" + monkeypatch.delattr("snap_http.http._make_request") diff --git a/tests/unit/api/test_apps.py b/tests/unit/api/test_apps.py new file mode 100644 index 0000000..8ad5248 --- /dev/null +++ b/tests/unit/api/test_apps.py @@ -0,0 +1,589 @@ +import pytest + +from snap_http import api, http, types + + +def test_get_apps_all_apps_on_system(monkeypatch): + """`api.get_apps` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=[ + { + "snap": "lxd", + "name": "activate", + "daemon": "oneshot", + "daemon-scope": "system", + "enabled": True, + }, + { + "snap": "test-snap", + "name": "bye-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + { + "snap": "test-snap", + "name": "hello-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + {"snap": "test-snap", "name": "test-snap"}, + ], + ) + + def mock_get(path, query_params): + assert path == "/apps" + assert query_params == {} + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_apps() + assert result == mock_response + + +def test_get_apps_from_specific_snaps(monkeypatch): + """`api.get_apps` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=[ + { + "snap": "lxd", + "name": "activate", + "daemon": "oneshot", + "daemon-scope": "system", + "enabled": True, + }, + { + "snap": "lxd", + "name": "user-daemon", + "daemon": "simple", + "daemon-scope": "system", + "enabled": True, + "activators": [ + { + "Name": "unix", + "Type": "socket", + "Active": True, + "Enabled": True, + } + ], + }, + { + "snap": "test-snap", + "name": "bye-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + { + "snap": "test-snap", + "name": "hello-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + {"snap": "test-snap", "name": "test-snap"}, + ], + ) + + def mock_get(path, query_params): + assert path == "/apps" + assert query_params == {"names": "lxd,test-snap"} + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_apps(names=["lxd", "test-snap"]) + assert result == mock_response + + +def test_get_apps_filter_services_only(monkeypatch): + """`api.get_apps` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=[ + { + "snap": "test-snap", + "name": "bye-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + { + "snap": "test-snap", + "name": "hello-svc", + "daemon": "simple", + "daemon-scope": "system", + }, + {"snap": "test-snap", "name": "test-snap"}, + ], + ) + + def mock_get(path, query_params): + assert path == "/apps" + assert query_params == {"names": "test-snap", "select": "service"} + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_apps(names=["test-snap"], services_only=True) + assert result == mock_response + + +def test_get_apps_exception(monkeypatch): + """`api.get_apps` raises a `http.SnapdHttpException`.""" + + def mock_get(path, query_params): + assert path == "/apps" + + raise http.SnapdHttpException( + { + "message": 'snap "idonotexist" not found', + "kind": "snap-not-found", + "value": "idonotexist", + } + ) + + monkeypatch.setattr(http, "get", mock_get) + + with pytest.raises(http.SnapdHttpException): + api.get_apps(names=["idonotexist"]) + + +def test_start(monkeypatch): + """`api.start` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "start", + "enable": False, + "names": ["test-snap"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.start("test-snap") + assert result == mock_response + + +def test_start_and_enable(monkeypatch): + """`api.start` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "start", + "enable": True, + "names": ["test-snap"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.start("test-snap", True) + assert result == mock_response + + +def test_start_exception(monkeypatch): + """`api.start` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/apps" + + raise http.SnapdHttpException( + { + "message": 'snap "idonotexist" not found', + "kind": "snap-not-found", + "value": "idonotexist", + } + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.start("idonotexist") + + +def test_start_all(monkeypatch): + """`api.start_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "start", + "enable": False, + "names": ["test-snap", "lxd", "multipass"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.start_all(["test-snap", "lxd", "multipass"]) + assert result == mock_response + + +def test_start_all_and_enable(monkeypatch): + """`api.start_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "start", + "enable": True, + "names": ["test-snap", "lxd", "multipass"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.start_all(["test-snap", "lxd", "multipass"], True) + assert result == mock_response + + +def test_start_all_exception(monkeypatch): + """`api.start_all` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/apps" + + raise http.SnapdHttpException( + { + "message": 'snap "idonotexist" not found', + "kind": "snap-not-found", + "value": "idonotexist", + } + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.start_all(["idonotexist", "lxd"]) + + +def test_stop(monkeypatch): + """`api.stop` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "stop", + "disable": False, + "names": ["test-snap"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.stop("test-snap") + assert result == mock_response + + +def test_stop_and_disable(monkeypatch): + """`api.stop` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "stop", + "disable": True, + "names": ["test-snap"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.stop("test-snap", True) + assert result == mock_response + + +def test_stop_exception(monkeypatch): + """`api.stop` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/apps" + + raise http.SnapdHttpException( + { + "message": 'snap "idonotexist" not found', + "kind": "snap-not-found", + "value": "idonotexist", + } + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.stop("idonotexist") + + +def test_stop_all(monkeypatch): + """`api.stop_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "stop", + "disable": False, + "names": ["test-snap", "lxd", "multipass"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.stop_all(["test-snap", "lxd", "multipass"]) + assert result == mock_response + + +def test_stop_all_and_disable(monkeypatch): + """`api.stop_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "stop", + "disable": True, + "names": ["test-snap", "lxd", "multipass"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.stop_all(["test-snap", "lxd", "multipass"], True) + assert result == mock_response + + +def test_stop_all_exception(monkeypatch): + """`api.stop_all` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/apps" + + raise http.SnapdHttpException( + { + "message": 'snap "idonotexist" not found', + "kind": "snap-not-found", + "value": "idonotexist", + } + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.stop_all(["idonotexist", "lxd"]) + + +def test_restart(monkeypatch): + """`api.restart` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "restart", + "reload": False, + "names": ["test-snap"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.restart("test-snap") + assert result == mock_response + + +def test_restart_and_reload(monkeypatch): + """`api.restart` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "restart", + "reload": True, + "names": ["test-snap"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.restart("test-snap", True) + assert result == mock_response + + +def test_restart_exception(monkeypatch): + """`api.restart` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/apps" + + raise http.SnapdHttpException( + { + "message": 'snap "idonotexist" not found', + "kind": "snap-not-found", + "value": "idonotexist", + } + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.restart("idonotexist") + + +def test_restart_all(monkeypatch): + """`api.restart_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "restart", + "reload": False, + "names": ["test-snap", "lxd", "multipass"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.restart_all(["test-snap", "lxd", "multipass"]) + assert result == mock_response + + +def test_restart_all_and_reload(monkeypatch): + """`api.restart_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/apps" + assert body == { + "action": "restart", + "reload": True, + "names": ["test-snap", "lxd", "multipass"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.restart_all(["test-snap", "lxd", "multipass"], True) + assert result == mock_response + + +def test_restart_all_exception(monkeypatch): + """`api.restart_all` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/apps" + + raise http.SnapdHttpException( + { + "message": 'snap "idonotexist" not found', + "kind": "snap-not-found", + "value": "idonotexist", + } + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.restart_all(["idonotexist", "lxd"]) diff --git a/tests/unit/api/test_assertions.py b/tests/unit/api/test_assertions.py new file mode 100644 index 0000000..ad0aa74 --- /dev/null +++ b/tests/unit/api/test_assertions.py @@ -0,0 +1,107 @@ +import pytest + +from snap_http import api, http, types + + +def test_get_assertion_types(monkeypatch): + """`api.get_assertion_types` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={ + "types": ["account", "base-declaration", "serial", "system-user"], + }, + ) + + def mock_get(path): + assert path == "/assertions" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_assertion_types() + assert result == mock_response + + +def test_get_assertions(monkeypatch): + """`api.get_assertions` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=( + "assertion-header0: value0\n\nsignature0\n\n" + "assertion-header: value\nassertion-header1: value1\n\nsignature" + ).encode(), + ) + + def mock_get(path, query_params): + assert path == "/assertions/serial-request" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_assertions("serial-request") + assert result == mock_response + + +def test_get_assertions_with_filters(monkeypatch): + """`api.get_assertions` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=b"assertion-header0: value0\n\nsignature0", + ) + + def mock_get(path, query_params): + assert path == "/assertions/serial-request" + assert query_params == {"assertion-header0": "value0"} + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_assertions( + "serial-request", filters={"assertion-header0": "value0"} + ) + assert result == mock_response + + +def test_add_assertion(monkeypatch): + """`api.add_assertion` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=None, + ) + + def mock_post(path, assertion): + assert path == "/assertions" + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.add_assertion("assertion-header0: value0\n\nsignature0") + assert result == mock_response + + +def test_add_assertion_exception(monkeypatch): + """`api.add_assertion` raises a `http.SnapdHttpException`.""" + + def mock_post(path, assertion): + assert path == "/assertions" + + raise http.SnapdHttpException( + {"message": "cannot decode request body into assertions: unexpected EOF"} + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.add_assertion("not an assertion") diff --git a/tests/unit/api/test_changes.py b/tests/unit/api/test_changes.py new file mode 100644 index 0000000..6ac9672 --- /dev/null +++ b/tests/unit/api/test_changes.py @@ -0,0 +1,83 @@ +import pytest + +from snap_http import api, http, types + + +def test_check_change(monkeypatch): + """`api.check_change` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={ + "id": "1", + "kind": "install-snap", + "summary": 'Install "placeholder" snap', + "status": "Done", + }, + ) + + def mock_get(path): + assert path == "/changes/1" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.check_change("1") + + assert result == mock_response + + +def test_check_change_exception(monkeypatch): + """`api.check_change` raises a `http.SnapdHttpException`.""" + + def mock_get(path): + assert path == "/changes/1" + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "get", mock_get) + + with pytest.raises(http.SnapdHttpException): + _ = api.check_change("1") + + +def test_check_changes(monkeypatch): + """`api.check_changes` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={ + "id": "1", + "kind": "install-snap", + "summary": 'Install "placeholder" snap', + "status": "Done", + }, + ) + + def mock_get(path): + assert path == "/changes?select=all" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.check_changes() + + assert result == mock_response + + +def test_check_changes_exception(monkeypatch): + """`api.check_changes` raises a `http.SnapdHttpException`.""" + + def mock_get(path): + assert path == "/changes?select=all" + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "get", mock_get) + + with pytest.raises(http.SnapdHttpException): + _ = api.check_changes() diff --git a/tests/unit/api/test_fde.py b/tests/unit/api/test_fde.py new file mode 100644 index 0000000..041ba8f --- /dev/null +++ b/tests/unit/api/test_fde.py @@ -0,0 +1,205 @@ +import pytest + +from snap_http import api, http, types + + +def test_get_keyslots(monkeypatch): + """`api.get_keyslots` returns a `types.SnapdResponse`.""" + + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="Accepted", + result={ + "by-container-role": { + "system-data": { + "volume-name": "pc", + "name": "ubuntu-data", + "encrypted": True, + "keyslots": {"default-recovery": {"type": "recovery"}}, + } + } + }, + ) + + def mock_get(path): + assert path == "/system-volumes" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_keyslots() + assert result == mock_response + + +def test_get_keyslots_exception(monkeypatch): + """`api.get_keyslots` raises a `http.SnapdHttpException`.""" + + def mock_get(path): + assert path == "/system-volumes" + + raise http.SnapdHttpException( + { + "message": "Bad Request", + "kind": "bad-request", + "value": {"reason": "bad-request"}, + } + ) + + monkeypatch.setattr(http, "get", mock_get) + + with pytest.raises(http.SnapdHttpException): + api.get_keyslots() + + +def test_generate_recovery_key(monkeypatch): + """`api.generate_recovery_key` returns a `types.SnapdResponse`.""" + + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="Accepted", + result={ + "key-id": "myrec_key1", + "recovery-key": "21720-04915-27494-19258-36455-33442-54786-27068", + }, + ) + + def mock_post(path, body): + assert path == "/system-volumes" + assert body == { + "action": "generate-recovery-key", + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.generate_recovery_key() + assert result == mock_response + + +def test_generate_recovery_key_exception(monkeypatch): + """`api.generate_recovery_key` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/system-volumes" + assert body == {"action": "generate-recovery-key"} + + raise http.SnapdHttpException( + { + "message": "Bad Request", + "kind": "not-supported", + "value": {"reason": "not-supported"}, + } + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.generate_recovery_key() + + +def test_add_recovery_key(monkeypatch): + """`api.update_recovery_key` returns a `types.SnapdResponse` for `replace`=`False`.""" + + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/system-volumes" + assert body == { + "action": "add-recovery-key", + "key-id": "real-key-id", + "keyslots": [{"name": "mykeyslot"}], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.update_recovery_key("real-key-id", "mykeyslot", False) + assert result == mock_response + + +def test_add_recovery_key_exception(monkeypatch): + """`api.update_recovery_key` raises a `http.SnapdHttpException` for `replace`=`False`.""" + + def mock_post(path, body): + assert path == "/system-volumes" + assert body == { + "action": "add-recovery-key", + "key-id": "fake-key-id", + "keyslots": [{"name": "mykeyslot"}], + } + + raise http.SnapdHttpException( + { + "message": "invalid recovery key: not found", + "kind": "invalid-recovery-key", + "value": {"reason": "not-found"}, + } + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.update_recovery_key("fake-key-id", "mykeyslot", False) + + +def test_replace_recovery_key(monkeypatch): + """`api.update_recovery_key` returns a `types.SnapdResponse` for `replace`=`True`.""" + + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/system-volumes" + assert body == { + "action": "replace-recovery-key", + "key-id": "real-key-id", + "keyslots": [{"name": "mykeyslot"}], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.update_recovery_key("real-key-id", "mykeyslot", True) + assert result == mock_response + + +def test_replace_recovery_key_exception(monkeypatch): + """`api.update_recovery_key` raises a `http.SnapdHttpException` for `replace`=`True`.""" + + def mock_post(path, body): + assert path == "/system-volumes" + assert body == { + "action": "replace-recovery-key", + "key-id": "fake-key-id", + "keyslots": [{"name": "mykeyslot"}], + } + + raise http.SnapdHttpException( + { + "message": "invalid recovery key: not found", + "kind": "invalid-recovery-key", + "value": {"reason": "not-found"}, + } + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.update_recovery_key("fake-key-id", "mykeyslot", True) diff --git a/tests/unit/api/test_interfaces.py b/tests/unit/api/test_interfaces.py new file mode 100644 index 0000000..f2b106b --- /dev/null +++ b/tests/unit/api/test_interfaces.py @@ -0,0 +1,97 @@ +from snap_http import api, http, types + + +def test_get_connections(monkeypatch): + """`api.get_connections` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + def mock_get(path, query_params): + assert path == "/connections" + assert query_params == {"snap": "placeholder", "select": "all", "interface": "snapd"} + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_connections(snap="placeholder", select="all", interface="snapd") + + assert result == mock_response + + +def test_get_interfaces(monkeypatch): + """`api.get_interfaces` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + def mock_get(path, query_params): + assert path == "/interfaces" + assert query_params == {"select": "all", "slots": True, "plugs": True, "doc": True, "names": "snapd"} + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_interfaces(select="all", slots=True, plugs=True, doc=True, names="snapd") + + assert result == mock_response + + +def test_connect_interface(monkeypatch): + """`api.connect_interface` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="OK", + result={}, + ) + + def mock_post(path, body): + assert path == "/interfaces" + assert body == { + "action": "connect", + "slots": [{"snap": "placeholder1", "slot": "config"}], + "plugs": [{"snap": "placeholder2", "plug": "config"}], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.connect_interface(in_snap="placeholder1", in_slot="config", out_snap="placeholder2", out_plug="config") + + assert result == mock_response + + +def test_disconnect_interface(monkeypatch): + """`api.disconnect_interface` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="OK", + result={}, + ) + + def mock_post(path, body): + assert path == "/interfaces" + assert body == { + "action": "disconnect", + "slots": [{"snap": "placeholder1", "slot": "config"}], + "plugs": [{"snap": "placeholder2", "plug": "config"}], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.disconnect_interface(in_snap="placeholder1", in_slot="config", out_snap="placeholder2", out_plug="config") + + assert result == mock_response diff --git a/tests/unit/api/test_model.py b/tests/unit/api/test_model.py new file mode 100644 index 0000000..3bda046 --- /dev/null +++ b/tests/unit/api/test_model.py @@ -0,0 +1,43 @@ +from snap_http import api, http, types + + +def test_get_model(monkeypatch) -> None: + """`api.get_model` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + def mock_get(path): + assert path == "/model" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_model() + + assert result == mock_response + + +def test_remodel(monkeypatch): + """`api.remodel` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="OK", + result={}, + ) + + def mock_post(path, body): + assert path == "/model" + assert body == {"new-model": "dummy_model_assertion", "offline" : True} + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.remodel("dummy_model_assertion", True) + + assert result == mock_response diff --git a/tests/unit/api/test_options.py b/tests/unit/api/test_options.py new file mode 100644 index 0000000..1eac59d --- /dev/null +++ b/tests/unit/api/test_options.py @@ -0,0 +1,65 @@ +from snap_http import api, http, types + + +def test_get_conf(monkeypatch): + """`api.get_conf` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + def mock_get(path, query_params): + assert path == "/snaps/placeholder/conf" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_conf("placeholder") + + assert result == mock_response + + +def test_get_specific_config_values(monkeypatch): + """`api.get_conf` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={"foo.bar": "default", "port": 8080}, + ) + + def mock_get(path, query_params): + assert path == "/snaps/placeholder/conf" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_conf("placeholder", keys=["foo.bar", "port"]) + + assert result == mock_response + + +def test_set_conf(monkeypatch): + """`api.set_conf` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_put(path, _): + assert path == "/snaps/placeholder/conf" + + return mock_response + + monkeypatch.setattr(http, "put", mock_put) + + result = api.set_conf("placeholder", {"foo": "bar"}) + + assert result == mock_response diff --git a/tests/unit/api/test_snaps.py b/tests/unit/api/test_snaps.py new file mode 100644 index 0000000..e009c1a --- /dev/null +++ b/tests/unit/api/test_snaps.py @@ -0,0 +1,919 @@ +import tempfile + +import pytest + +from snap_http import api, http, types + + +def test_enable(monkeypatch): + """`api.enable` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "enable"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.enable("placeholder") + + assert result == mock_response + + +def test_enable_exception(monkeypatch): + """`api.enable` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "enable"} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.enable("placeholder") + + +def test_enable_all_exception(monkeypatch): + """`api.enable_all` raises a `http.SnapdHttpException`. + + NOTE: as of 2024-01-08, enable/disable is not yet supported for multiple snaps. + """ + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "enable", "snaps": ["placeholder1", "placeholder2"]} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.enable_all(["placeholder1", "placeholder2"]) + + +def test_disable(monkeypatch): + """`api.disable` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "disable"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.disable("placeholder") + + assert result == mock_response + + +def test_disable_exception(monkeypatch): + """`api.disable` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "disable"} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.disable("placeholder") + + +def test_disable_all_exception(monkeypatch): + """`api.enable_all` raises a `http.SnapdHttpException`. + + NOTE: as of 2024-01-08, enable/disable is not yet supported for multiple snaps. + """ + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "disable", "snaps": ["placeholder1", "placeholder2"]} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.disable_all(["placeholder1", "placeholder2"]) + + +def test_hold(monkeypatch): + """`api.hold` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "hold", "hold-level": "general", "time": "forever"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.hold("placeholder") + + assert result == mock_response + + +def test_hold_exception(monkeypatch): + """`api.hold` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "hold", "hold-level": "general", "time": "forever"} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.hold("placeholder") + + +def test_hold_all(monkeypatch): + """`api.hold_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps" + assert body == { + "action": "hold", + "snaps": ["placeholder1", "placeholder2"], + "hold-level": "general", + "time": "forever", + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.hold_all(["placeholder1", "placeholder2"]) + + assert result == mock_response + + +def test_hold_all_exception(monkeypatch): + """`api.hold_all` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps" + assert body == { + "action": "hold", + "snaps": ["placeholder1", "placeholder2"], + "hold-level": "general", + "time": "forever", + } + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.hold_all(["placeholder1", "placeholder2"]) + + +def test_install(monkeypatch): + """`api.install` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "install"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.install("placeholder") + + assert result == mock_response + + +def test_install_revision(monkeypatch): + """`api.install` returns a `types.SnapdResponse` when provided a specific revision to install.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "install", "revision": "1"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.install("placeholder", revision="1") + + assert result == mock_response + + +def test_install_channel(monkeypatch): + """`api.install` returns a `types.SnapdResponse` when provided a specific channel to install from.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "install", "channel": "latest/beta"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.install("placeholder", channel="latest/beta") + + assert result == mock_response + + +def test_install_classic(monkeypatch): + """`api.install` returns a `types.SnapdResponse` when installing with classic confinement.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "install", "classic": True} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.install("placeholder", classic=True) + + assert result == mock_response + + +def test_install_exception(monkeypatch): + """`api.install` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "install"} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.install("placeholder") + + +def test_install_all(monkeypatch): + """`api.install_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "install", "snaps": ["placeholder1", "placeholder2"]} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.install_all(["placeholder1", "placeholder2"]) + + assert result == mock_response + + +def test_install_all_exception(monkeypatch): + """`api.install_all` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "install", "snaps": ["placeholder1", "placeholder2"]} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.install_all(["placeholder1", "placeholder2"]) + + +def test_sideload_snap(monkeypatch): + """`api.sideload` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body: types.FormData): + assert path == "/snaps" + assert body.data == {"action": "install"} + assert len(body.files) == 1 + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + with tempfile.NamedTemporaryFile() as tmp: + result = api.sideload([tmp.name]) + + assert result == mock_response + + +def test_sideload_with_classic_confinement(monkeypatch): + """`api.sideload` returns a `types.SnapdResponse` when sideloading with classic confinement.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body: types.FormData): + assert path == "/snaps" + assert body.data == {"action": "install", "classic": True} + assert len(body.files) == 1 + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + with tempfile.NamedTemporaryFile() as tmp: + result = api.sideload([tmp.name], classic=True) + + assert result == mock_response + + +def test_sideload_dangerous_snap(monkeypatch): + """`api.sideload` returns a `types.SnapdResponse` when sideloading in dangerous mode.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body: types.FormData): + assert path == "/snaps" + assert body.data == {"action": "install", "dangerous": True} + assert len(body.files) == 1 + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + with tempfile.NamedTemporaryFile() as tmp: + result = api.sideload([tmp.name], dangerous=True) + + assert result == mock_response + + +def test_sideload_snap_with_devmode_confinement(monkeypatch): + """`api.sideload` returns a `types.SnapdResponse` when sideloading in devmode confinement.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body: types.FormData): + assert path == "/snaps" + assert body.data == {"action": "install", "devmode": True} + assert len(body.files) == 1 + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + with tempfile.NamedTemporaryFile() as tmp: + result = api.sideload([tmp.name], devmode=True) + + assert result == mock_response + + +def test_sideload_snap_with_enforced_confinement(monkeypatch): + """`api.sideload` returns a `types.SnapdResponse` when sideloading with enforced confinement.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body: types.FormData): + assert path == "/snaps" + assert body.data == {"action": "install", "jailmode": True} + assert len(body.files) == 1 + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + with tempfile.NamedTemporaryFile() as tmp: + result = api.sideload([tmp.name], jailmode=True) + + assert result == mock_response + + +def test_sideload_snap_restart_system(monkeypatch): + """`api.sideload` returns a `types.SnapdResponse` when a system restart is required.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body: types.FormData): + assert path == "/snaps" + assert body.data == {"action": "install", "system-restart-immediate": True} + assert len(body.files) == 1 + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + with tempfile.NamedTemporaryFile() as tmp: + result = api.sideload([tmp.name], system_restart_immediate=True) + + assert result == mock_response + + +def test_sideload_multiple_snaps(monkeypatch): + """`api.sideload` returns a `types.SnapdResponse` when sideloading multiple snaps.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body: types.FormData): + assert path == "/snaps" + assert body.data == {"action": "install"} + assert len(body.files) == 2 + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + with tempfile.NamedTemporaryFile() as tmp1, tempfile.NamedTemporaryFile() as tmp2: + result = api.sideload([tmp1.name, tmp2.name]) + + assert result == mock_response + + +def test_sideload_snap_exception(monkeypatch): + """`api.sideload` raises a `http.SnapHttpException`.""" + + def mock_post(path, body: types.FormData): + assert path == "/snaps" + assert body.data == {"action": "install"} + assert len(body.files) == 1 + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with tempfile.NamedTemporaryFile() as tmp, pytest.raises(http.SnapdHttpException): + api.sideload([tmp.name]) + + +def test_revert(monkeypatch): + """`api.revert` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "revert"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.revert("placeholder") + + assert result == mock_response + + +def test_revert_revision(monkeypatch): + """`api.revert` returns a `types.SnapdResponse` when given a specific revision.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "revert", "revision": "1"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.revert("placeholder", revision="1") + + assert result == mock_response + + +def test_revert_classic(monkeypatch): + """`api.revert` returns a `types.SnapdResponse` when reverting to classic confinement.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "revert", "classic": True} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.revert("placeholder", classic=True) + + assert result == mock_response + + +def test_revert_exception(monkeypatch): + """`api.revert` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "revert"} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.revert("placeholder") + + +def test_revert_all(monkeypatch): + """`api.revert_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "revert", "snaps": ["placeholder1", "placeholder2"]} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.revert_all(["placeholder1", "placeholder2"]) + + assert result == mock_response + + +def test_revert_all_exception(monkeypatch): + """`api.revert_all` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "revert", "snaps": ["placeholder1", "placeholder2"]} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.revert_all(["placeholder1", "placeholder2"]) + + +def test_remove(monkeypatch): + """`api.remove` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "remove", + "purge": False, + "terminate": False} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.remove("placeholder") + + assert result == mock_response + + +def test_remove_exception(monkeypatch): + """`api.remove` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "remove", + "purge": False, + "terminate": False} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.remove("placeholder") + + +def test_remove_all(monkeypatch): + """`api.remove_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "remove", "snaps": ["placeholder1", "placeholder2"]} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.remove_all(["placeholder1", "placeholder2"]) + + assert result == mock_response + + +def test_remove_all_exception(monkeypatch): + """`api.remove_all` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "remove", "snaps": ["placeholder1", "placeholder2"]} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.remove_all(["placeholder1", "placeholder2"]) + + +def test_switch(monkeypatch): + """`api.switch` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "switch", "channel": "stable"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.switch("placeholder") + + assert result == mock_response + + +def test_switch_exception(monkeypatch): + """`api.switch` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "switch", "channel": "stable"} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.switch("placeholder") + + +def test_switch_all_exception(monkeypatch): + """`api.revert_all` raises a `http.SnapdHttpException`. + + NOTE: as of 2024-01-08, switch is not yet supported for multiple snaps. + """ + + def mock_post(path, body): + assert path == "/snaps" + assert body == { + "action": "switch", + "channel": "stable", + "snaps": ["placeholder1", "placeholder2"], + } + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.switch_all(["placeholder1", "placeholder2"]) + + +def test_unhold(monkeypatch): + """`api.unhold` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "unhold"} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.unhold("placeholder") + + assert result == mock_response + + +def test_unhold_exception(monkeypatch): + """`api.unhold` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps/placeholder" + assert body == {"action": "unhold"} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.unhold("placeholder") + + +def test_unhold_all(monkeypatch): + """`api.unhold_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="Accepted", + result=None, + change="1", + ) + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "unhold", "snaps": ["placeholder1", "placeholder2"]} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.unhold_all(["placeholder1", "placeholder2"]) + + assert result == mock_response + + +def test_unhold_all_exception(monkeypatch): + """`api.revert_all` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/snaps" + assert body == {"action": "unhold", "snaps": ["placeholder1", "placeholder2"]} + + raise http.SnapdHttpException() + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + _ = api.unhold_all(["placeholder1", "placeholder2"]) + + +def test_list(monkeypatch): + """`api.list` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=[{"title": "placeholder1"}, {"title": "placeholder2"}], + ) + + def mock_get(path): + assert path == "/snaps" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.list() + + assert result == mock_response + + +def test_list_all(monkeypatch): + """`api.list_all` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=[{"title": "placeholder1"}, {"title": "placeholder2"}], + ) + + def mock_get(path): + assert path == "/snaps?select=all" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.list_all() + + assert result == mock_response diff --git a/tests/unit/api/test_snapshots.py b/tests/unit/api/test_snapshots.py new file mode 100644 index 0000000..8d743ee --- /dev/null +++ b/tests/unit/api/test_snapshots.py @@ -0,0 +1,74 @@ +from snap_http import api, http, types + + +def test_snapshots(monkeypatch): + """`api.snapshots` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=[{"title": "placeholder1"}, {"title": "placeholder2"}], + ) + + def mock_get(path): + assert path == "/snapshots" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.snapshots() + + assert result == mock_response + + +def test_save_snapshot(monkeypatch): + """`api.snapshots` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="OK", + result=[{"title": "placeholder1"}, {"title": "placeholder2"}], + ) + + def mock_post(path, body): + assert path == "/snaps" + assert body == { + "action": "snapshot", + "snaps": ["snapd"], + "users": ["user1"],} + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.save_snapshot(snaps=["snapd"], users=["user1"]) + + assert result == mock_response + + +def test_forget_snapshot(monkeypatch): + """`api.forget_snapshot` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="OK", + result=[{"title": "placeholder1"}, {"title": "placeholder2"}], + ) + + def mock_post(path, body): + assert path == "/snapshots" + assert body == { + "action": "forget", + "set": 1, + "snaps": ["snapd"], + "users": ["user1"], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.forget_snapshot(1, snaps=["snapd"], users=["user1"]) + + assert result == mock_response diff --git a/tests/unit/api/test_systems.py b/tests/unit/api/test_systems.py new file mode 100644 index 0000000..809b59f --- /dev/null +++ b/tests/unit/api/test_systems.py @@ -0,0 +1,95 @@ +from snap_http import api, http, types + + +def test_get_recovery_systems(monkeypatch) -> None: + """`api.get_recovery_systems` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + def mock_get(path): + assert path == "/systems" + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_recovery_systems() + + assert result == mock_response + +def test_get_recovery_system(monkeypatch) -> None: + """`api.get_recovery_system` returns a `types.SnapdResponse`.""" + label="20251022" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + def mock_get(path): + assert path == f"/systems/{label}" + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_recovery_system(label) + + assert result == mock_response + +def test_perform_system_action(monkeypatch) -> None: + """`api.perform_system_action` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="OK", + result={}, + ) + + action: str = "do" + mode: str = "recover" + def mock_post(path, body): + assert path == "/systems" + assert body == { + "action": action, + "mode": mode + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.perform_system_action(action, mode) + + assert result == mock_response + + +def test_perform_recovery_action(monkeypatch) -> None: + """`api.perform_recovery_action` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="OK", + result={}, + ) + + action: str = "do" + mode: str = "recover" + label: str = "20250410" + def mock_post(path, body): + assert path == f"/systems/{label}" + assert body == { + "action": action, + "mode": mode + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.perform_recovery_action(label, action, mode) + + assert result == mock_response diff --git a/tests/unit/api/test_users.py b/tests/unit/api/test_users.py new file mode 100644 index 0000000..72bf801 --- /dev/null +++ b/tests/unit/api/test_users.py @@ -0,0 +1,107 @@ +import pytest + +from snap_http import api, http, types + + +def test_list_users(monkeypatch): + """`api.list_users` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=[{"id": 1, "username": "john", "email": "john.doe@example.com"}], + ) + + def mock_get(path): + assert path == "/users" + + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.list_users() + assert result == mock_response + + +def test_add_user(monkeypatch): + """`api.add_user` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result=[ + { + "username": "john-doe", + "ssh-keys": [ + "ssh-ed25519 some-ssh-key john@localhost # " + 'snapd {"origin":"store","email":"john.doe@example.com"}' + ], + } + ], + ) + + def mock_post(path, body): + assert path == "/users" + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.add_user( + username="john-doe", email="john.doe@example.com", force_managed=True + ) + assert result == mock_response + + +def test_add_user_exception(monkeypatch): + """`api.add_user` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/users" + + raise http.SnapdHttpException( + {"message": "cannot create user: device already managed"} + ) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.add_user(username="john-doe", email="john.doe@example.com") + + +def test_remove_user(monkeypatch): + """`api.remove_user` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={ + "removed": [ + {"id": 4, "username": "john-doe", "email": "john.doe@example.com"} + ] + }, + ) + + def mock_post(path, body): + assert path == "/users" + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.remove_user(username="john-doe") + assert result == mock_response + + +def test_remove_user_exception(monkeypatch): + """`api.remove_user` raises a `http.SnapdHttpException`.""" + + def mock_post(path, body): + assert path == "/users" + + raise http.SnapdHttpException({"message": 'user "jane-doe" is not known'}) + + monkeypatch.setattr(http, "post", mock_post) + + with pytest.raises(http.SnapdHttpException): + api.remove_user(username="jane-doe") diff --git a/tests/unit/api/test_validation_sets.py b/tests/unit/api/test_validation_sets.py new file mode 100644 index 0000000..af33198 --- /dev/null +++ b/tests/unit/api/test_validation_sets.py @@ -0,0 +1,164 @@ +from snap_http import api, http, types + + +def test_get_validation_sets(monkeypatch) -> None: + """`api.get_validation_sets` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + def mock_get(path): + assert path == "/validation-sets" + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_validation_sets() + + assert result == mock_response + + +def test_get_validation_set(monkeypatch) -> None: + """`api.get_validation_set` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + account_id: str = "device-platform" + validation_set_name: str = "dev-validation-set" + + def mock_get(path): + assert path == f"/validation-sets/{account_id}/{validation_set_name}" + return mock_response + + monkeypatch.setattr(http, "get", mock_get) + + result = api.get_validation_set(account_id, validation_set_name) + + assert result == mock_response + + +def test_refresh_validation_set(monkeypatch) -> None: + """`api.refresh_validation_set` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="async", + status_code=202, + status="OK", + result={}, + ) + + account_id: str = "device-platform" + validation_set_name: str = "dev-validation-set" + validation_set_sequence: int = 12 + + def mock_post(path, body): + assert path == "/snaps" + assert body == { + "action": "refresh", + "validation-sets": [ + f"{account_id}/{validation_set_name}={validation_set_sequence}" + ], + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.refresh_validation_set(account_id, validation_set_name, validation_set_sequence) + + assert result == mock_response + + +def test_enforce_validation_set(monkeypatch) -> None: + """`api.enforce_validation_set` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + account_id: str = "device-platform" + validation_set_name: str = "dev-validation-set" + validation_set_sequence: int = 12 + + def mock_post(path, body): + assert path == f"/validation-sets/{account_id}/{validation_set_name}" + assert body == { + "action": "apply", + "mode": "enforce", + "sequence": validation_set_sequence + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.enforce_validation_set(account_id, validation_set_name, validation_set_sequence) + + assert result == mock_response + + +def test_forget_validation_set(monkeypatch) -> None: + """`api.refresh_validation_set` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + account_id: str = "device-platform" + validation_set_name: str = "dev-validation-set" + validation_set_sequence: int = 12 + + def mock_post(path, body): + assert path == f"/validation-sets/{account_id}/{validation_set_name}" + assert body == { + "action": "forget", + "sequence": validation_set_sequence + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.forget_validation_set(account_id, validation_set_name, validation_set_sequence) + + assert result == mock_response + + +def test_monitor_validation_set(monkeypatch) -> None: + """`api.refresh_validation_set` returns a `types.SnapdResponse`.""" + mock_response = types.SnapdResponse( + type="sync", + status_code=200, + status="OK", + result={}, + ) + + account_id: str = "device-platform" + validation_set_name: str = "dev-validation-set" + validation_set_sequence: int = 12 + + def mock_post(path, body): + assert path == f"/validation-sets/{account_id}/{validation_set_name}" + assert body == { + "action": "apply", + "mode": "monitor", + "sequence": validation_set_sequence + } + + return mock_response + + monkeypatch.setattr(http, "post", mock_post) + + result = api.monitor_validation_set(account_id, validation_set_name, validation_set_sequence) + + assert result == mock_response diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py deleted file mode 100644 index 7a5d434..0000000 --- a/tests/unit/test_api.py +++ /dev/null @@ -1,2536 +0,0 @@ -import tempfile - -import pytest - -from snap_http import api, http, types - - -@pytest.fixture(autouse=True) -def no_requests(monkeypatch): - """Removes _make_request from snap_http.http to prevent inadvertent requests going out.""" - monkeypatch.delattr("snap_http.http._make_request") - - -def test_check_change(monkeypatch): - """`api.check_change` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={ - "id": "1", - "kind": "install-snap", - "summary": 'Install "placeholder" snap', - "status": "Done", - }, - ) - - def mock_get(path): - assert path == "/changes/1" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.check_change("1") - - assert result == mock_response - - -def test_check_change_exception(monkeypatch): - """`api.check_change` raises a `http.SnapdHttpException`.""" - - def mock_get(path): - assert path == "/changes/1" - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "get", mock_get) - - with pytest.raises(http.SnapdHttpException): - _ = api.check_change("1") - - -def test_check_changes(monkeypatch): - """`api.check_changes` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={ - "id": "1", - "kind": "install-snap", - "summary": 'Install "placeholder" snap', - "status": "Done", - }, - ) - - def mock_get(path): - assert path == "/changes?select=all" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.check_changes() - - assert result == mock_response - - -def test_check_changes_exception(monkeypatch): - """`api.check_changes` raises a `http.SnapdHttpException`.""" - - def mock_get(path): - assert path == "/changes?select=all" - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "get", mock_get) - - with pytest.raises(http.SnapdHttpException): - _ = api.check_changes() - - -def test_enable(monkeypatch): - """`api.enable` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "enable"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.enable("placeholder") - - assert result == mock_response - - -def test_enable_exception(monkeypatch): - """`api.enable` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "enable"} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.enable("placeholder") - - -def test_enable_all_exception(monkeypatch): - """`api.enable_all` raises a `http.SnapdHttpException`. - - NOTE: as of 2024-01-08, enable/disable is not yet supported for multiple snaps. - """ - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "enable", "snaps": ["placeholder1", "placeholder2"]} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.enable_all(["placeholder1", "placeholder2"]) - - -def test_disable(monkeypatch): - """`api.disable` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "disable"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.disable("placeholder") - - assert result == mock_response - - -def test_disable_exception(monkeypatch): - """`api.disable` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "disable"} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.disable("placeholder") - - -def test_disable_all_exception(monkeypatch): - """`api.enable_all` raises a `http.SnapdHttpException`. - - NOTE: as of 2024-01-08, enable/disable is not yet supported for multiple snaps. - """ - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "disable", "snaps": ["placeholder1", "placeholder2"]} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.disable_all(["placeholder1", "placeholder2"]) - - -def test_hold(monkeypatch): - """`api.hold` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "hold", "hold-level": "general", "time": "forever"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.hold("placeholder") - - assert result == mock_response - - -def test_hold_exception(monkeypatch): - """`api.hold` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "hold", "hold-level": "general", "time": "forever"} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.hold("placeholder") - - -def test_hold_all(monkeypatch): - """`api.hold_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps" - assert body == { - "action": "hold", - "snaps": ["placeholder1", "placeholder2"], - "hold-level": "general", - "time": "forever", - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.hold_all(["placeholder1", "placeholder2"]) - - assert result == mock_response - - -def test_hold_all_exception(monkeypatch): - """`api.hold_all` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps" - assert body == { - "action": "hold", - "snaps": ["placeholder1", "placeholder2"], - "hold-level": "general", - "time": "forever", - } - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.hold_all(["placeholder1", "placeholder2"]) - - -def test_install(monkeypatch): - """`api.install` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "install"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.install("placeholder") - - assert result == mock_response - - -def test_install_revision(monkeypatch): - """`api.install` returns a `types.SnapdResponse` when provided a specific revision to install.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "install", "revision": "1"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.install("placeholder", revision="1") - - assert result == mock_response - - -def test_install_channel(monkeypatch): - """`api.install` returns a `types.SnapdResponse` when provided a specific channel to install from.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "install", "channel": "latest/beta"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.install("placeholder", channel="latest/beta") - - assert result == mock_response - - -def test_install_classic(monkeypatch): - """`api.install` returns a `types.SnapdResponse` when installing with classic confinement.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "install", "classic": True} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.install("placeholder", classic=True) - - assert result == mock_response - - -def test_install_exception(monkeypatch): - """`api.install` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "install"} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.install("placeholder") - - -def test_install_all(monkeypatch): - """`api.install_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "install", "snaps": ["placeholder1", "placeholder2"]} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.install_all(["placeholder1", "placeholder2"]) - - assert result == mock_response - - -def test_install_all_exception(monkeypatch): - """`api.install_all` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "install", "snaps": ["placeholder1", "placeholder2"]} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.install_all(["placeholder1", "placeholder2"]) - - -def test_sideload_snap(monkeypatch): - """`api.sideload` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body: types.FormData): - assert path == "/snaps" - assert body.data == {"action": "install"} - assert len(body.files) == 1 - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - with tempfile.NamedTemporaryFile() as tmp: - result = api.sideload([tmp.name]) - - assert result == mock_response - - -def test_sideload_with_classic_confinement(monkeypatch): - """`api.sideload` returns a `types.SnapdResponse` when sideloading with classic confinement.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body: types.FormData): - assert path == "/snaps" - assert body.data == {"action": "install", "classic": True} - assert len(body.files) == 1 - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - with tempfile.NamedTemporaryFile() as tmp: - result = api.sideload([tmp.name], classic=True) - - assert result == mock_response - - -def test_sideload_dangerous_snap(monkeypatch): - """`api.sideload` returns a `types.SnapdResponse` when sideloading in dangerous mode.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body: types.FormData): - assert path == "/snaps" - assert body.data == {"action": "install", "dangerous": True} - assert len(body.files) == 1 - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - with tempfile.NamedTemporaryFile() as tmp: - result = api.sideload([tmp.name], dangerous=True) - - assert result == mock_response - - -def test_sideload_snap_with_devmode_confinement(monkeypatch): - """`api.sideload` returns a `types.SnapdResponse` when sideloading in devmode confinement.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body: types.FormData): - assert path == "/snaps" - assert body.data == {"action": "install", "devmode": True} - assert len(body.files) == 1 - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - with tempfile.NamedTemporaryFile() as tmp: - result = api.sideload([tmp.name], devmode=True) - - assert result == mock_response - - -def test_sideload_snap_with_enforced_confinement(monkeypatch): - """`api.sideload` returns a `types.SnapdResponse` when sideloading with enforced confinement.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body: types.FormData): - assert path == "/snaps" - assert body.data == {"action": "install", "jailmode": True} - assert len(body.files) == 1 - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - with tempfile.NamedTemporaryFile() as tmp: - result = api.sideload([tmp.name], jailmode=True) - - assert result == mock_response - - -def test_sideload_snap_restart_system(monkeypatch): - """`api.sideload` returns a `types.SnapdResponse` when a system restart is required.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body: types.FormData): - assert path == "/snaps" - assert body.data == {"action": "install", "system-restart-immediate": True} - assert len(body.files) == 1 - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - with tempfile.NamedTemporaryFile() as tmp: - result = api.sideload([tmp.name], system_restart_immediate=True) - - assert result == mock_response - - -def test_sideload_multiple_snaps(monkeypatch): - """`api.sideload` returns a `types.SnapdResponse` when sideloading multiple snaps.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body: types.FormData): - assert path == "/snaps" - assert body.data == {"action": "install"} - assert len(body.files) == 2 - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - with tempfile.NamedTemporaryFile() as tmp1, tempfile.NamedTemporaryFile() as tmp2: - result = api.sideload([tmp1.name, tmp2.name]) - - assert result == mock_response - - -def test_sideload_snap_exception(monkeypatch): - """`api.sideload` raises a `http.SnapHttpException`.""" - - def mock_post(path, body: types.FormData): - assert path == "/snaps" - assert body.data == {"action": "install"} - assert len(body.files) == 1 - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with tempfile.NamedTemporaryFile() as tmp, pytest.raises(http.SnapdHttpException): - api.sideload([tmp.name]) - - -def test_revert(monkeypatch): - """`api.revert` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "revert"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.revert("placeholder") - - assert result == mock_response - - -def test_revert_revision(monkeypatch): - """`api.revert` returns a `types.SnapdResponse` when given a specific revision.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "revert", "revision": "1"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.revert("placeholder", revision="1") - - assert result == mock_response - - -def test_revert_classic(monkeypatch): - """`api.revert` returns a `types.SnapdResponse` when reverting to classic confinement.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "revert", "classic": True} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.revert("placeholder", classic=True) - - assert result == mock_response - - -def test_revert_exception(monkeypatch): - """`api.revert` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "revert"} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.revert("placeholder") - - -def test_revert_all(monkeypatch): - """`api.revert_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "revert", "snaps": ["placeholder1", "placeholder2"]} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.revert_all(["placeholder1", "placeholder2"]) - - assert result == mock_response - - -def test_revert_all_exception(monkeypatch): - """`api.revert_all` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "revert", "snaps": ["placeholder1", "placeholder2"]} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.revert_all(["placeholder1", "placeholder2"]) - - -def test_remove(monkeypatch): - """`api.remove` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "remove", - "purge": False, - "terminate": False} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.remove("placeholder") - - assert result == mock_response - - -def test_remove_exception(monkeypatch): - """`api.remove` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "remove", - "purge": False, - "terminate": False} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.remove("placeholder") - - -def test_remove_all(monkeypatch): - """`api.remove_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "remove", "snaps": ["placeholder1", "placeholder2"]} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.remove_all(["placeholder1", "placeholder2"]) - - assert result == mock_response - - -def test_remove_all_exception(monkeypatch): - """`api.remove_all` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "remove", "snaps": ["placeholder1", "placeholder2"]} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.remove_all(["placeholder1", "placeholder2"]) - - -def test_switch(monkeypatch): - """`api.switch` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "switch", "channel": "stable"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.switch("placeholder") - - assert result == mock_response - - -def test_switch_exception(monkeypatch): - """`api.switch` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "switch", "channel": "stable"} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.switch("placeholder") - - -def test_switch_all_exception(monkeypatch): - """`api.revert_all` raises a `http.SnapdHttpException`. - - NOTE: as of 2024-01-08, switch is not yet supported for multiple snaps. - """ - - def mock_post(path, body): - assert path == "/snaps" - assert body == { - "action": "switch", - "channel": "stable", - "snaps": ["placeholder1", "placeholder2"], - } - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.switch_all(["placeholder1", "placeholder2"]) - - -def test_unhold(monkeypatch): - """`api.unhold` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "unhold"} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.unhold("placeholder") - - assert result == mock_response - - -def test_unhold_exception(monkeypatch): - """`api.unhold` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps/placeholder" - assert body == {"action": "unhold"} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.unhold("placeholder") - - -def test_unhold_all(monkeypatch): - """`api.unhold_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "unhold", "snaps": ["placeholder1", "placeholder2"]} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.unhold_all(["placeholder1", "placeholder2"]) - - assert result == mock_response - - -def test_unhold_all_exception(monkeypatch): - """`api.revert_all` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/snaps" - assert body == {"action": "unhold", "snaps": ["placeholder1", "placeholder2"]} - - raise http.SnapdHttpException() - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - _ = api.unhold_all(["placeholder1", "placeholder2"]) - - -def test_list(monkeypatch): - """`api.list` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=[{"title": "placeholder1"}, {"title": "placeholder2"}], - ) - - def mock_get(path): - assert path == "/snaps" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.list() - - assert result == mock_response - - -def test_list_all(monkeypatch): - """`api.list_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=[{"title": "placeholder1"}, {"title": "placeholder2"}], - ) - - def mock_get(path): - assert path == "/snaps?select=all" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.list_all() - - assert result == mock_response - - -def test_snapshots(monkeypatch): - """`api.snapshots` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=[{"title": "placeholder1"}, {"title": "placeholder2"}], - ) - - def mock_get(path): - assert path == "/snapshots" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.snapshots() - - assert result == mock_response - - -def test_save_snapshot(monkeypatch): - """`api.snapshots` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="OK", - result=[{"title": "placeholder1"}, {"title": "placeholder2"}], - ) - - def mock_post(path, body): - assert path == "/snaps" - assert body == { - "action": "snapshot", - "snaps": ["snapd"], - "users": ["user1"],} - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.save_snapshot(snaps=["snapd"], users=["user1"]) - - assert result == mock_response - - -def test_forget_snapshot(monkeypatch): - """`api.forget_snapshot` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="OK", - result=[{"title": "placeholder1"}, {"title": "placeholder2"}], - ) - - def mock_post(path, body): - assert path == "/snapshots" - assert body == { - "action": "forget", - "set": 1, - "snaps": ["snapd"], - "users": ["user1"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.forget_snapshot(1, snaps=["snapd"], users=["user1"]) - - assert result == mock_response - - -def test_get_conf(monkeypatch): - """`api.get_conf` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - def mock_get(path, query_params): - assert path == "/snaps/placeholder/conf" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_conf("placeholder") - - assert result == mock_response - - -def test_get_specific_config_values(monkeypatch): - """`api.get_conf` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={"foo.bar": "default", "port": 8080}, - ) - - def mock_get(path, query_params): - assert path == "/snaps/placeholder/conf" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_conf("placeholder", keys=["foo.bar", "port"]) - - assert result == mock_response - - -def test_set_conf(monkeypatch): - """`api.set_conf` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_put(path, _): - assert path == "/snaps/placeholder/conf" - - return mock_response - - monkeypatch.setattr(http, "put", mock_put) - - result = api.set_conf("placeholder", {"foo": "bar"}) - - assert result == mock_response - - -def test_get_connections(monkeypatch): - """`api.get_connections` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - def mock_get(path, query_params): - assert path == "/connections" - assert query_params == {"snap": "placeholder", "select": "all", "interface": "snapd"} - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_connections(snap="placeholder", select="all", interface="snapd") - - assert result == mock_response - - -def test_get_interfaces(monkeypatch): - """`api.get_interfaces` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - def mock_get(path, query_params): - assert path == "/interfaces" - assert query_params == {"select": "all", "slots": True, "plugs": True, "doc": True, "names": "snapd"} - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_interfaces(select="all", slots=True, plugs=True, doc=True, names="snapd") - - assert result == mock_response - - -def test_connect_interface(monkeypatch): - """`api.connect_interface` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="OK", - result={}, - ) - - def mock_post(path, body): - assert path == "/interfaces" - assert body == { - "action": "connect", - "slots": [{"snap": "placeholder1", "slot": "config"}], - "plugs": [{"snap": "placeholder2", "plug": "config"}], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.connect_interface(in_snap="placeholder1", in_slot="config", out_snap="placeholder2", out_plug="config") - - assert result == mock_response - - -def test_disconnect_interface(monkeypatch): - """`api.disconnect_interface` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="OK", - result={}, - ) - - def mock_post(path, body): - assert path == "/interfaces" - assert body == { - "action": "disconnect", - "slots": [{"snap": "placeholder1", "slot": "config"}], - "plugs": [{"snap": "placeholder2", "plug": "config"}], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.disconnect_interface(in_snap="placeholder1", in_slot="config", out_snap="placeholder2", out_plug="config") - - assert result == mock_response - - -def test_get_model(monkeypatch) -> None: - """`api.get_model` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - def mock_get(path): - assert path == "/model" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_model() - - assert result == mock_response - - -def test_remodel(monkeypatch): - """`api.remodel` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="OK", - result={}, - ) - - def mock_post(path, body): - assert path == "/model" - assert body == {"new-model": "dummy_model_assertion", "offline" : True} - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.remodel("dummy_model_assertion", True) - - assert result == mock_response - - -def test_get_validation_sets(monkeypatch) -> None: - """`api.get_validation_sets` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - def mock_get(path): - assert path == "/validation-sets" - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_validation_sets() - - assert result == mock_response - - -def test_get_validation_set(monkeypatch) -> None: - """`api.get_validation_set` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - account_id: str = "device-platform" - validation_set_name: str = "dev-validation-set" - - def mock_get(path): - assert path == f"/validation-sets/{account_id}/{validation_set_name}" - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_validation_set(account_id, validation_set_name) - - assert result == mock_response - - -def test_refresh_validation_set(monkeypatch) -> None: - """`api.refresh_validation_set` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="OK", - result={}, - ) - - account_id: str = "device-platform" - validation_set_name: str = "dev-validation-set" - validation_set_sequence: int = 12 - - def mock_post(path, body): - assert path == "/snaps" - assert body == { - "action": "refresh", - "validation-sets": [ - f"{account_id}/{validation_set_name}={validation_set_sequence}" - ], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.refresh_validation_set(account_id, validation_set_name, validation_set_sequence) - - assert result == mock_response - - -def test_monitor_validation_set(monkeypatch) -> None: - """`api.refresh_validation_set` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - account_id: str = "device-platform" - validation_set_name: str = "dev-validation-set" - validation_set_sequence: int = 12 - - def mock_post(path, body): - assert path == f"/validation-sets/{account_id}/{validation_set_name}" - assert body == { - "action": "apply", - "mode": "monitor", - "sequence": validation_set_sequence - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.monitor_validation_set(account_id, validation_set_name, validation_set_sequence) - - assert result == mock_response - - -def test_enforce_validation_set(monkeypatch) -> None: - """`api.enforce_validation_set` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - account_id: str = "device-platform" - validation_set_name: str = "dev-validation-set" - validation_set_sequence: int = 12 - - def mock_post(path, body): - assert path == f"/validation-sets/{account_id}/{validation_set_name}" - assert body == { - "action": "apply", - "mode": "enforce", - "sequence": validation_set_sequence - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.enforce_validation_set(account_id, validation_set_name, validation_set_sequence) - - assert result == mock_response - - -def test_forget_validation_set(monkeypatch) -> None: - """`api.refresh_validation_set` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - account_id: str = "device-platform" - validation_set_name: str = "dev-validation-set" - validation_set_sequence: int = 12 - - def mock_post(path, body): - assert path == f"/validation-sets/{account_id}/{validation_set_name}" - assert body == { - "action": "forget", - "sequence": validation_set_sequence - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.forget_validation_set(account_id, validation_set_name, validation_set_sequence) - - assert result == mock_response - - -def test_get_recovery_systems(monkeypatch) -> None: - """`api.get_recovery_systems` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - def mock_get(path): - assert path == "/systems" - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_recovery_systems() - - assert result == mock_response - -def test_get_recovery_system(monkeypatch) -> None: - """`api.get_recovery_system` returns a `types.SnapdResponse`.""" - label="20251022" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={}, - ) - - def mock_get(path): - assert path == f"/systems/{label}" - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_recovery_system(label) - - assert result == mock_response - -def test_perform_system_action(monkeypatch) -> None: - """`api.perform_system_action` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="OK", - result={}, - ) - - action: str = "do" - mode: str = "recover" - def mock_post(path, body): - assert path == "/systems" - assert body == { - "action": action, - "mode": mode - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.perform_system_action(action, mode) - - assert result == mock_response - - -def test_perform_recovery_action(monkeypatch) -> None: - """`api.perform_recovery_action` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="OK", - result={}, - ) - - action: str = "do" - mode: str = "recover" - label: str = "20250410" - def mock_post(path, body): - assert path == f"/systems/{label}" - assert body == { - "action": action, - "mode": mode - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.perform_recovery_action(label, action, mode) - - assert result == mock_response - - -def test_get_assertion_types(monkeypatch): - """`api.get_assertion_types` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={ - "types": ["account", "base-declaration", "serial", "system-user"], - }, - ) - - def mock_get(path): - assert path == "/assertions" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_assertion_types() - assert result == mock_response - - -def test_get_assertions(monkeypatch): - """`api.get_assertions` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=( - "assertion-header0: value0\n\nsignature0\n\n" - "assertion-header: value\nassertion-header1: value1\n\nsignature" - ).encode(), - ) - - def mock_get(path, query_params): - assert path == "/assertions/serial-request" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_assertions("serial-request") - assert result == mock_response - - -def test_get_assertions_with_filters(monkeypatch): - """`api.get_assertions` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=b"assertion-header0: value0\n\nsignature0", - ) - - def mock_get(path, query_params): - assert path == "/assertions/serial-request" - assert query_params == {"assertion-header0": "value0"} - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_assertions( - "serial-request", filters={"assertion-header0": "value0"} - ) - assert result == mock_response - - -def test_add_assertion(monkeypatch): - """`api.add_assertion` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=None, - ) - - def mock_post(path, assertion): - assert path == "/assertions" - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.add_assertion("assertion-header0: value0\n\nsignature0") - assert result == mock_response - - -def test_add_assertion_exception(monkeypatch): - """`api.add_assertion` raises a `http.SnapdHttpException`.""" - - def mock_post(path, assertion): - assert path == "/assertions" - - raise http.SnapdHttpException( - {"message": "cannot decode request body into assertions: unexpected EOF"} - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.add_assertion("not an assertion") - - -def test_list_users(monkeypatch): - """`api.list_users` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=[{"id": 1, "username": "john", "email": "john.doe@example.com"}], - ) - - def mock_get(path): - assert path == "/users" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.list_users() - assert result == mock_response - - -def test_add_user(monkeypatch): - """`api.add_user` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=[ - { - "username": "john-doe", - "ssh-keys": [ - "ssh-ed25519 some-ssh-key john@localhost # " - 'snapd {"origin":"store","email":"john.doe@example.com"}' - ], - } - ], - ) - - def mock_post(path, body): - assert path == "/users" - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.add_user( - username="john-doe", email="john.doe@example.com", force_managed=True - ) - assert result == mock_response - - -def test_add_user_exception(monkeypatch): - """`api.add_user` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/users" - - raise http.SnapdHttpException( - {"message": "cannot create user: device already managed"} - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.add_user(username="john-doe", email="john.doe@example.com") - - -def test_remove_user(monkeypatch): - """`api.remove_user` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result={ - "removed": [ - {"id": 4, "username": "john-doe", "email": "john.doe@example.com"} - ] - }, - ) - - def mock_post(path, body): - assert path == "/users" - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.remove_user(username="john-doe") - assert result == mock_response - - -def test_remove_user_exception(monkeypatch): - """`api.remove_user` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/users" - - raise http.SnapdHttpException({"message": 'user "jane-doe" is not known'}) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.remove_user(username="jane-doe") - - -# Apps and Services - - -def test_get_apps_all_apps_on_system(monkeypatch): - """`api.get_apps` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=[ - { - "snap": "lxd", - "name": "activate", - "daemon": "oneshot", - "daemon-scope": "system", - "enabled": True, - }, - { - "snap": "test-snap", - "name": "bye-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - { - "snap": "test-snap", - "name": "hello-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - {"snap": "test-snap", "name": "test-snap"}, - ], - ) - - def mock_get(path, query_params): - assert path == "/apps" - assert query_params == {} - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_apps() - assert result == mock_response - - -def test_get_apps_from_specific_snaps(monkeypatch): - """`api.get_apps` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=[ - { - "snap": "lxd", - "name": "activate", - "daemon": "oneshot", - "daemon-scope": "system", - "enabled": True, - }, - { - "snap": "lxd", - "name": "user-daemon", - "daemon": "simple", - "daemon-scope": "system", - "enabled": True, - "activators": [ - { - "Name": "unix", - "Type": "socket", - "Active": True, - "Enabled": True, - } - ], - }, - { - "snap": "test-snap", - "name": "bye-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - { - "snap": "test-snap", - "name": "hello-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - {"snap": "test-snap", "name": "test-snap"}, - ], - ) - - def mock_get(path, query_params): - assert path == "/apps" - assert query_params == {"names": "lxd,test-snap"} - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_apps(names=["lxd", "test-snap"]) - assert result == mock_response - - -def test_get_apps_filter_services_only(monkeypatch): - """`api.get_apps` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="OK", - result=[ - { - "snap": "test-snap", - "name": "bye-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - { - "snap": "test-snap", - "name": "hello-svc", - "daemon": "simple", - "daemon-scope": "system", - }, - {"snap": "test-snap", "name": "test-snap"}, - ], - ) - - def mock_get(path, query_params): - assert path == "/apps" - assert query_params == {"names": "test-snap", "select": "service"} - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_apps(names=["test-snap"], services_only=True) - assert result == mock_response - - -def test_get_apps_exception(monkeypatch): - """`api.get_apps` raises a `http.SnapdHttpException`.""" - - def mock_get(path, query_params): - assert path == "/apps" - - raise http.SnapdHttpException( - { - "message": 'snap "idonotexist" not found', - "kind": "snap-not-found", - "value": "idonotexist", - } - ) - - monkeypatch.setattr(http, "get", mock_get) - - with pytest.raises(http.SnapdHttpException): - api.get_apps(names=["idonotexist"]) - - -def test_start(monkeypatch): - """`api.start` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "start", - "enable": False, - "names": ["test-snap"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.start("test-snap") - assert result == mock_response - - -def test_start_and_enable(monkeypatch): - """`api.start` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "start", - "enable": True, - "names": ["test-snap"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.start("test-snap", True) - assert result == mock_response - - -def test_start_exception(monkeypatch): - """`api.start` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/apps" - - raise http.SnapdHttpException( - { - "message": 'snap "idonotexist" not found', - "kind": "snap-not-found", - "value": "idonotexist", - } - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.start("idonotexist") - - -def test_start_all(monkeypatch): - """`api.start_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "start", - "enable": False, - "names": ["test-snap", "lxd", "multipass"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.start_all(["test-snap", "lxd", "multipass"]) - assert result == mock_response - - -def test_start_all_and_enable(monkeypatch): - """`api.start_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "start", - "enable": True, - "names": ["test-snap", "lxd", "multipass"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.start_all(["test-snap", "lxd", "multipass"], True) - assert result == mock_response - - -def test_start_all_exception(monkeypatch): - """`api.start_all` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/apps" - - raise http.SnapdHttpException( - { - "message": 'snap "idonotexist" not found', - "kind": "snap-not-found", - "value": "idonotexist", - } - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.start_all(["idonotexist", "lxd"]) - - -def test_stop(monkeypatch): - """`api.stop` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "stop", - "disable": False, - "names": ["test-snap"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.stop("test-snap") - assert result == mock_response - - -def test_stop_and_disable(monkeypatch): - """`api.stop` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "stop", - "disable": True, - "names": ["test-snap"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.stop("test-snap", True) - assert result == mock_response - - -def test_stop_exception(monkeypatch): - """`api.stop` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/apps" - - raise http.SnapdHttpException( - { - "message": 'snap "idonotexist" not found', - "kind": "snap-not-found", - "value": "idonotexist", - } - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.stop("idonotexist") - - -def test_stop_all(monkeypatch): - """`api.stop_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "stop", - "disable": False, - "names": ["test-snap", "lxd", "multipass"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.stop_all(["test-snap", "lxd", "multipass"]) - assert result == mock_response - - -def test_stop_all_and_disable(monkeypatch): - """`api.stop_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "stop", - "disable": True, - "names": ["test-snap", "lxd", "multipass"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.stop_all(["test-snap", "lxd", "multipass"], True) - assert result == mock_response - - -def test_stop_all_exception(monkeypatch): - """`api.stop_all` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/apps" - - raise http.SnapdHttpException( - { - "message": 'snap "idonotexist" not found', - "kind": "snap-not-found", - "value": "idonotexist", - } - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.stop_all(["idonotexist", "lxd"]) - - -def test_restart(monkeypatch): - """`api.restart` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "restart", - "reload": False, - "names": ["test-snap"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.restart("test-snap") - assert result == mock_response - - -def test_restart_and_reload(monkeypatch): - """`api.restart` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "restart", - "reload": True, - "names": ["test-snap"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.restart("test-snap", True) - assert result == mock_response - - -def test_restart_exception(monkeypatch): - """`api.restart` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/apps" - - raise http.SnapdHttpException( - { - "message": 'snap "idonotexist" not found', - "kind": "snap-not-found", - "value": "idonotexist", - } - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.restart("idonotexist") - - -def test_restart_all(monkeypatch): - """`api.restart_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "restart", - "reload": False, - "names": ["test-snap", "lxd", "multipass"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.restart_all(["test-snap", "lxd", "multipass"]) - assert result == mock_response - - -def test_restart_all_and_reload(monkeypatch): - """`api.restart_all` returns a `types.SnapdResponse`.""" - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/apps" - assert body == { - "action": "restart", - "reload": True, - "names": ["test-snap", "lxd", "multipass"], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.restart_all(["test-snap", "lxd", "multipass"], True) - assert result == mock_response - - -def test_restart_all_exception(monkeypatch): - """`api.restart_all` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/apps" - - raise http.SnapdHttpException( - { - "message": 'snap "idonotexist" not found', - "kind": "snap-not-found", - "value": "idonotexist", - } - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.restart_all(["idonotexist", "lxd"]) - - -def test_get_keyslots(monkeypatch): - """`api.get_keyslots` returns a `types.SnapdResponse`.""" - - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="Accepted", - result={ - "by-container-role": { - "system-data": { - "volume-name": "pc", - "name": "ubuntu-data", - "encrypted": True, - "keyslots": {"default-recovery": {"type": "recovery"}}, - } - } - }, - ) - - def mock_get(path): - assert path == "/system-volumes" - - return mock_response - - monkeypatch.setattr(http, "get", mock_get) - - result = api.get_keyslots() - assert result == mock_response - - -def test_get_keyslots_exception(monkeypatch): - """`api.get_keyslots` raises a `http.SnapdHttpException`.""" - - def mock_get(path): - assert path == "/system-volumes" - - raise http.SnapdHttpException( - { - "message": "Bad Request", - "kind": "bad-request", - "value": {"reason": "bad-request"}, - } - ) - - monkeypatch.setattr(http, "get", mock_get) - - with pytest.raises(http.SnapdHttpException): - api.get_keyslots() - - -def test_generate_recovery_key(monkeypatch): - """`api.generate_recovery_key` returns a `types.SnapdResponse`.""" - - mock_response = types.SnapdResponse( - type="sync", - status_code=200, - status="Accepted", - result={ - "key-id": "myrec_key1", - "recovery-key": "21720-04915-27494-19258-36455-33442-54786-27068", - }, - ) - - def mock_post(path, body): - assert path == "/system-volumes" - assert body == { - "action": "generate-recovery-key", - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.generate_recovery_key() - assert result == mock_response - - -def test_generate_recovery_key_exception(monkeypatch): - """`api.generate_recovery_key` raises a `http.SnapdHttpException`.""" - - def mock_post(path, body): - assert path == "/system-volumes" - assert body == {"action": "generate-recovery-key"} - - raise http.SnapdHttpException( - { - "message": "Bad Request", - "kind": "not-supported", - "value": {"reason": "not-supported"}, - } - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.generate_recovery_key() - - -def test_add_recovery_key(monkeypatch): - """`api.update_recovery_key` returns a `types.SnapdResponse` for `replace`=`False`.""" - - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/system-volumes" - assert body == { - "action": "add-recovery-key", - "key-id": "real-key-id", - "keyslots": [{"name": "mykeyslot"}], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.update_recovery_key("real-key-id", "mykeyslot", False) - assert result == mock_response - - -def test_add_recovery_key_exception(monkeypatch): - """`api.update_recovery_key` raises a `http.SnapdHttpException` for `replace`=`False`.""" - - def mock_post(path, body): - assert path == "/system-volumes" - assert body == { - "action": "add-recovery-key", - "key-id": "fake-key-id", - "keyslots": [{"name": "mykeyslot"}], - } - - raise http.SnapdHttpException( - { - "message": "invalid recovery key: not found", - "kind": "invalid-recovery-key", - "value": {"reason": "not-found"}, - } - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.update_recovery_key("fake-key-id", "mykeyslot", False) - - -def test_replace_recovery_key(monkeypatch): - """`api.update_recovery_key` returns a `types.SnapdResponse` for `replace`=`True`.""" - - mock_response = types.SnapdResponse( - type="async", - status_code=202, - status="Accepted", - result=None, - change="1", - ) - - def mock_post(path, body): - assert path == "/system-volumes" - assert body == { - "action": "replace-recovery-key", - "key-id": "real-key-id", - "keyslots": [{"name": "mykeyslot"}], - } - - return mock_response - - monkeypatch.setattr(http, "post", mock_post) - - result = api.update_recovery_key("real-key-id", "mykeyslot", True) - assert result == mock_response - - -def test_replace_recovery_key_exception(monkeypatch): - """`api.update_recovery_key` raises a `http.SnapdHttpException` for `replace`=`True`.""" - - def mock_post(path, body): - assert path == "/system-volumes" - assert body == { - "action": "replace-recovery-key", - "key-id": "fake-key-id", - "keyslots": [{"name": "mykeyslot"}], - } - - raise http.SnapdHttpException( - { - "message": "invalid recovery key: not found", - "kind": "invalid-recovery-key", - "value": {"reason": "not-found"}, - } - ) - - monkeypatch.setattr(http, "post", mock_post) - - with pytest.raises(http.SnapdHttpException): - api.update_recovery_key("fake-key-id", "mykeyslot", True)