diff --git a/README.md b/README.md index bf4a9eee..b48e8ed0 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,21 @@ If ok run the unittests: task unittests ``` +If you have access to an archivist instance then one can run functional tests. The URL +and authtoken are required. The authtoken must be stored in a file in the credentials +subdirectory credentials/authtoken (say). + +These tests will create artefacts on the archivist instance so it is **not** recommended that +they be run in a production environment. + +Set 2 environment variables and execute: + +```bash +export TEST_ARCHIVIST="https://rkvst.poc.jitsuin.io" +export TEST_AUTHTOKEN=credentials/authtoken +task functests +``` + #### Testing Other Python Versions ##### Python 3.6 diff --git a/Taskfile.yml b/Taskfile.yml index 5e85d81d..147a9696 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -34,8 +34,8 @@ tasks: deps: [about] cmds: - ./scripts/builder.sh python3 --version - - ./scripts/builder.sh pycodestyle --format=pylint archivist unittests examples - - ./scripts/builder.sh python3 -m pylint --rcfile=pylintrc archivist unittests examples + - ./scripts/builder.sh pycodestyle --format=pylint archivist functests unittests examples + - ./scripts/builder.sh python3 -m pylint --rcfile=pylintrc archivist functests unittests examples clean: desc: Clean git repo @@ -53,8 +53,14 @@ tasks: desc: Format code using black deps: [about] cmds: - - ./scripts/builder.sh black archivist examples unittests + - ./scripts/builder.sh black archivist examples functests unittests + functests: + desc: Run functests - requires an archivist instance and a authtoken + deps: [about] + cmds: + - ./scripts/builder.sh ./scripts/functests.sh + publish: desc: pubish wheel package (will require username and password) deps: [about] diff --git a/archivist/access_policies.py b/archivist/access_policies.py new file mode 100644 index 00000000..dd65e640 --- /dev/null +++ b/archivist/access_policies.py @@ -0,0 +1,290 @@ +"""Access_Policies interface + + Access to the access_policies endpoint. + + The user is not expected to use this class directly. It is an attribute of the + :class:`Archivist` class. + + For example instantiate an Archivist instance and execute the methods of the class: + + .. code-block:: python + + with open(".auth_token", mode="r") as tokenfile: + authtoken = tokenfile.read().strip() + + # Initialize connection to Archivist + arch = Archivist( + "https://rkvst.poc.jitsuin.io", + auth=authtoken, + ) + asset = arch.access_policies.create(...) + +""" + +from copy import deepcopy + +from .constants import ( + SEP, + ACCESS_POLICIES_SUBPATH, + ACCESS_POLICIES_LABEL, + ASSETS_LABEL, +) + +from .assets import Asset + +DEFAULT_PAGE_SIZE = 500 + + +class _AccessPoliciesClient: + """AccessPoliciesClient + + Access to access_policies entitiies using CRUD interface. This class is usually + accessed as an attribute of the Archivist class. + + Args: + archivist (Archivist): :class:`Archivist` instance + + """ + + def __init__(self, archivist): + self._archivist = archivist + + def create(self, props, filters, access_permissions): + """Create access policy + + Creates access policy with defined attributes. + + Args: + props (dict): properties of created access policy. + filters (list): assets filters + access permissions (list): list of access permissions + + Returns: + :class:`AccessPolicy` instance + + """ + return self.create_from_data( + self.__query(props, filters=filters, access_permissions=access_permissions), + ) + + def create_from_data(self, data): + """Create access policy + + Creates access policy with request body from data stream. + Suitable for reading data from a file using json.load or yaml.load + + Args: + data (dict): request body of access policy. + + Returns: + :class:`AccessPolicy` instance + + """ + return AccessPolicy( + **self._archivist.post( + f"{ACCESS_POLICIES_SUBPATH}/{ACCESS_POLICIES_LABEL}", + data, + ) + ) + + def read(self, identity): + """Read Access Policy + + Reads access policy. + + Args: + identity (str): access_policies identity e.g. access_policies/xxxxxxxxxxxxxxxxxxxxxxx + + Returns: + :class:`AccessPolicy` instance + + """ + return AccessPolicy( + **self._archivist.get( + ACCESS_POLICIES_SUBPATH, + identity, + ) + ) + + def update(self, identity, props=None, filters=None, access_permissions=None): + """Update Access Policy + + Update access policy. + + Args: + identity (str): access_policies identity e.g. access_policies/xxxxxxxxxxxxxxxxxxxxxxx + props (dict): properties of created access policy. + filters (list): assets filters + access permissions (list): list of access permissions + + Returns: + :class:`AccessPolicy` instance + + """ + return AccessPolicy( + **self._archivist.patch( + ACCESS_POLICIES_SUBPATH, + identity, + self.__query( + props, filters=filters, access_permissions=access_permissions + ), + ) + ) + + def delete(self, identity): + """Delete Access Policy + + Deletes access policy. + + Args: + identity (str): access_policies identity e.g. access_policies/xxxxxxxxxxxxxxxxxxxxxxx + + Returns: + :class:`AccessPolicy` instance - empty? + + """ + return self._archivist.delete(ACCESS_POLICIES_SUBPATH, identity) + + @staticmethod + def __query(props, *, filters=None, access_permissions=None): + query = deepcopy(props) if props else {} + if filters is not None: + query["filters"] = filters + + if access_permissions is not None: + query["access_permissions"] = access_permissions + + return query + + def count(self, *, display_name=None): + """Count access policies. + + Counts number of access policies that match criteria. + + Args: + display_name (str): display name (optional0 + + Returns: + integer count of access policies. + + """ + query = {"display_name": display_name} if display_name is not None else None + return self._archivist.count( + f"{ACCESS_POLICIES_SUBPATH}/{ACCESS_POLICIES_LABEL}", + query=query, + ) + + def list( + self, + *, + page_size=DEFAULT_PAGE_SIZE, + display_name=None, + ): + """List access policies. + + List access policiess that match criteria. + + Args: + display_name (str): display name (optional0 + page_size (int): optional page size. (Rarely used). + + Returns: + iterable that returns :class:`AccessPolicy` instances + + """ + query = {"display_name": display_name} if display_name is not None else None + return ( + AccessPolicy(**a) + for a in self._archivist.list( + f"{ACCESS_POLICIES_SUBPATH}/{ACCESS_POLICIES_LABEL}", + ACCESS_POLICIES_LABEL, + page_size=page_size, + query=query, + ) + ) + + # additional queries on different endpoints + def count_matching_assets(self, access_policy_id): + """Count assets that match access_policy. + + Counts number of assets that match an access_polocy. + + Args: + access_policy_id (str): e.g. access_policies/xxxxxxxxxxxxxxx + + Returns: + integer count of assets. + + """ + return self._archivist.count( + SEP.join((ACCESS_POLICIES_SUBPATH, access_policy_id, ASSETS_LABEL)), + ) + + def list_matching_assets( + self, + access_policy_id, + *, + page_size=DEFAULT_PAGE_SIZE, + ): + """List matching assets. + + List assets that match access policy. + + Args: + access_policy_id (str): e.g. access_policies/xxxxxxxxxxxxxxx + page_size (int): optional page size. (Rarely used). + + Returns: + iterable that returns :class:`Asset` instances + + """ + return ( + Asset(**a) + for a in self._archivist.list( + SEP.join((ACCESS_POLICIES_SUBPATH, access_policy_id, ASSETS_LABEL)), + ASSETS_LABEL, + page_size=page_size, + ) + ) + + def count_matching_access_policies(self, asset_id): + """Count access policies that match asset. + + Counts number of access policies that match asset. + + Args: + asset_id (str): e.g. assets/xxxxxxxxxxxxxxx + + Returns: + integer count of access policies. + + """ + return self._archivist.count( + SEP.join((ACCESS_POLICIES_SUBPATH, asset_id, ACCESS_POLICIES_LABEL)), + ) + + def list_matching_access_policies(self, asset_id, *, page_size=DEFAULT_PAGE_SIZE): + """List matching access policies. + + List access policies that match asset. + + Args: + asset_id (str): e.g. assets/xxxxxxxxxxxxxxx + page_size (int): optional page size. (Rarely used). + + Returns: + iterable that returns :class:`AccessPolicy` instances + + """ + return ( + AccessPolicy(**a) + for a in self._archivist.list( + SEP.join((ACCESS_POLICIES_SUBPATH, asset_id, ACCESS_POLICIES_LABEL)), + ACCESS_POLICIES_LABEL, + page_size=page_size, + ) + ) + + +class AccessPolicy(dict): + """AccessPolicy object""" diff --git a/archivist/access_policies.py.disable b/archivist/access_policies.py.disable deleted file mode 100644 index 97ba3a13..00000000 --- a/archivist/access_policies.py.disable +++ /dev/null @@ -1,128 +0,0 @@ -"""access_policies interface - - NOT TESTED -""" - -from .constants import ( - SEP, - ACCESS_POLICIES_SUBPATH, - ACCESS_POLICIES_LABEL, - ASSETS_LABEL, -) - -DEFAULT_PAGE_SIZE = 500 - - -class _AccessPoliciesClient: - """docstring""" - - def __init__(self, archivist): - """docstring""" - self._archivist = archivist - - def create(self, request): - """docstring""" - - return AccessPolicy( - **self._archivist.post( - f"{ACCESS_POLICIES_SUBPATH}/{ACCESS_POLICIES_LABEL}", - request, - ) - ) - - def read(self, identity): - """docstring""" - return AccessPolicy( - **self._archivist.get( - ACCESS_POLICIES_SUBPATH, - identity, - ) - ) - - def update(self, identity, request): - """docstring""" - return AccessPolicy( - **self._archivist.patch( - ACCESS_POLICIES_SUBPATH, - identity, - request, - ) - ) - - def delete(self, identity): - """docstring""" - return self._archivist.delete(ACCESS_POLICIES_SUBPATH, identity) - - @staticmethod - def __query(props): - """docstring""" - query = props or {} - return query - - def count(self, *, query=None): - """docstring""" - return self._archivist.count( - f"{ACCESS_POLICIES_SUBPATH}/{ACCESS_POLICIES_LABEL}", - query=self.__query(query), - ) - - def list(self, *, page_size=DEFAULT_PAGE_SIZE, query=None): - """docstring""" - return ( - AccessPolicy(**a) - for a in self._archivist.list( - f"{ACCESS_POLICIES_SUBPATH}/{ACCESS_POLICIES_LABEL}", - ACCESS_POLICIES_LABEL, - page_size=page_size, - query=self.__query(query), - ) - ) - - # additional queries on different endpoints - def count_matching_assets(self, access_policy_id, *, query=None): - """docstring""" - return self._archivist.count( - SEP.join((ACCESS_POLICIES_SUBPATH, access_policy_id, ASSETS_LABEL)), - ASSETS_LABEL, - query=self.__query(query), - ) - - def list_matching_assets( - self, access_policy_id, *, page_size=DEFAULT_PAGE_SIZE, query=None - ): - """docstring""" - return ( - AccessPolicy(**a) - for a in self._archivist.list( - SEP.join((ACCESS_POLICIES_SUBPATH, access_policy_id, ASSETS_LABEL)), - ASSETS_LABEL, - page_size=page_size, - query=self.__query(query), - ) - ) - - def count_matching_access_policies(self, asset_id, *, query=None): - """docstring""" - return self._archivist.count( - SEP.join((ACCESS_POLICIES_SUBPATH, asset_id, ACCESS_POLICIES_LABEL)), - ACCESS_POLICIES_LABEL, - query=self.__query(query), - ) - - def list_matching_access_policies( - self, asset_id, *, page_size=DEFAULT_PAGE_SIZE, query=None - ): - """docstring""" - return ( - AccessPolicy(**a) - for a in self._archivist.list( - SEP.join((ACCESS_POLICIES_SUBPATH, asset_id, ASSETS_LABEL)), - ACCESS_POLICIES_LABEL, - page_size=page_size, - query=self.__query(query), - ) - ) - - -class AccessPolicy(dict): - """AccessPolicy object""" diff --git a/archivist/archivist.py b/archivist/archivist.py index 60e6fa11..e10a0566 100644 --- a/archivist/archivist.py +++ b/archivist/archivist.py @@ -53,6 +53,17 @@ from .events import _EventsClient from .locations import _LocationsClient from .attachments import _AttachmentsClient +from .access_policies import _AccessPoliciesClient +from .subjects import _SubjectsClient + +CLIENTS = { + "assets": _AssetsClient, + "events": _EventsClient, + "locations": _LocationsClient, + "attachments": _AttachmentsClient, + "access_policies": _AccessPoliciesClient, + "subjects": _SubjectsClient, +} class Archivist: # pylint: disable=too-many-instance-attributes @@ -100,57 +111,16 @@ def __init__(self, url, *, auth=None, cert=None, verify=True): self._locations = None self._attachments = None - @property - def assets(self): - """Assets endpoint - - Cached assets endpoint. - - Returns: - instance of class that represents a CRUD interface to assets. - """ - if self._assets is None: - self._assets = _AssetsClient(self) - return self._assets - - @property - def events(self): - """events endpoint - - Cached events endpoint. - - Returns: - instance of class that represents a CRUD interface to events. - """ - if self._events is None: - self._events = _EventsClient(self) - return self._events - - @property - def locations(self): - """Locations endpoint - - Cached locations endpoint. + def __getattr__(self, value): + """Create endpoints on demand""" + client = CLIENTS.get(value) - Returns: - instance of class that represents a CRUD interface to locations. - """ - if self._locations is None: - self._locations = _LocationsClient(self) - return self._locations + if client is None: + raise AttributeError - @property - def attachments(self): - """Attachments endpoint - - Cached attachments endpoint. - - Returns: - instance of class that represents a upload/download interface to attachments. - """ - if self._attachments is None: - self._attachments = _AttachmentsClient(self) - return self._attachments + c = client(self) + super().__setattr__(value, c) + return c @property def headers(self): diff --git a/archivist/assets.py b/archivist/assets.py index 5328c494..1091da15 100644 --- a/archivist/assets.py +++ b/archivist/assets.py @@ -49,7 +49,7 @@ class _AssetsClient: def __init__(self, archivist): self._archivist = archivist - def create(self, behaviours, attrs, confirm=False): + def create(self, behaviours, attrs, *, confirm=False): """Create asset Creates asset with defined behaviours and attributes. @@ -71,14 +71,14 @@ def create(self, behaviours, attrs, confirm=False): confirm=confirm, ) - def create_from_data(self, data, confirm=False): + def create_from_data(self, data, *, confirm=False): """Create asset Creates asset with request body from data stream. Suitable for reading data from a file using json.load or yaml.load Args: - data (dict): list os accpted behaviours for this asset. + data (dict): request bosy of asset. confirm (bool): if True wait for asset to be confirmed on DLT. Returns: diff --git a/archivist/constants.py b/archivist/constants.py index cd1da344..900a7467 100644 --- a/archivist/constants.py +++ b/archivist/constants.py @@ -27,3 +27,6 @@ ACCESS_POLICIES_SUBPATH = "iam/v1" ACCESS_POLICIES_LABEL = "access_policies" + +SUBJECTS_SUBPATH = "iam/v1" +SUBJECTS_LABEL = "subjects" diff --git a/archivist/events.py b/archivist/events.py index 9a2159cc..f46dec10 100644 --- a/archivist/events.py +++ b/archivist/events.py @@ -83,7 +83,7 @@ def create_from_data(self, asset_id, data, *, confirm=False): Args: asset_id (str): asset identity e.g. assets/xxxxxxxxxxxxxxxxxxxxxxxxxx - attrs (dict): attributes and asset_attributes of created event. + attrs (dict): request body of event. confirm (bool): if True wait for event to be confirmed on DLT. Returns: diff --git a/archivist/locations.py b/archivist/locations.py index 43ba19e4..62730356 100644 --- a/archivist/locations.py +++ b/archivist/locations.py @@ -39,7 +39,6 @@ class _LocationsClient: Args: archivist (Archivist): :class:`Archivist` instance - """ def __init__(self, archivist): @@ -67,7 +66,7 @@ def create_from_data(self, data): Suitable for reading data from a file using json.load or yaml.load Args: - data (dict): list of accepted behaviours for this asset. + data (dict): request body of location. Returns: :class:`Location` instance diff --git a/archivist/subjects.py b/archivist/subjects.py new file mode 100644 index 00000000..209e28ea --- /dev/null +++ b/archivist/subjects.py @@ -0,0 +1,218 @@ +"""Subjects interface + + Access to the subjects endpoint. + + The user is not expected to use this class directly. It is an attribute of the + :class:`Archivist` class. + + For example instantiate an Archivist instance and execute the methods of the class: + + .. code-block:: python + + with open(".auth_token", mode="r") as tokenfile: + authtoken = tokenfile.read().strip() + + # Initialize connection to Archivist + arch = Archivist( + "https://rkvst.poc.jitsuin.io", + auth=authtoken, + ) + asset = arch.subjects.create(...) + +""" + +from .constants import ( + SUBJECTS_SUBPATH, + SUBJECTS_LABEL, +) + +DEFAULT_PAGE_SIZE = 500 + + +class _SubjectsClient: + """SubjectsClient + + Access to subjects entitiies using CRUD interface. This class is usually + accessed as an attribute of the Archivist class. + + Args: + archivist (Archivist): :class:`Archivist` instance + + """ + + def __init__(self, archivist): + self._archivist = archivist + + def create(self, display_name, wallet_pub_keys, tessera_pub_keys): + """Create subject + + Creates subject with defined attributes. + + Args: + display_name (str): dispaly name of subject. + wallet_pub_keys (list): wallet public keys + tessera_pub_keys (list): tessera public keys + + Returns: + :class:`Subject` instance + + """ + return self.create_from_data( + self.__query( + display_name=display_name, + wallet_pub_keys=wallet_pub_keys, + tessera_pub_keys=tessera_pub_keys, + ), + ) + + def create_from_data(self, data): + """Create subject + + Creates subject with request body from data stream. + Suitable for reading data from a file using json.load or yaml.load + + Args: + data (dict): request body of subject. + + Returns: + :class:`Subject` instance + + """ + return Subject( + **self._archivist.post( + f"{SUBJECTS_SUBPATH}/{SUBJECTS_LABEL}", + data, + ) + ) + + def read(self, identity): + """Read Subject + + Reads subject. + + Args: + identity (str): subjects identity e.g. subjects/xxxxxxxxxxxxxxxxxxxxxxx + + Returns: + :class:`Subject` instance + + """ + return Subject( + **self._archivist.get( + SUBJECTS_SUBPATH, + identity, + ) + ) + + def update( + self, + identity, + *, + display_name=None, + wallet_pub_keys=None, + tessera_pub_keys=None, + ): + """Update Subject + + Update subject. + + Args: + identity (str): subjects identity e.g. subjects/xxxxxxxxxxxxxxxxxxxxxxx + display_name (str): display name of subject. + wallet_pub_keys (list): wallet public keys + tessera_pub_keys (list): tessera public keys + + Returns: + :class:`Subject` instance + + """ + return Subject( + **self._archivist.patch( + SUBJECTS_SUBPATH, + identity, + self.__query( + display_name=display_name, + wallet_pub_keys=wallet_pub_keys, + tessera_pub_keys=tessera_pub_keys, + ), + ) + ) + + def delete(self, identity): + """Delete Subject + + Deletes subject. + + Args: + identity (str): subjects identity e.g. subjects/xxxxxxxxxxxxxxxxxxxxxxx + + Returns: + :class:`Subject` instance - empty? + + """ + return self._archivist.delete(SUBJECTS_SUBPATH, identity) + + @staticmethod + def __query(*, display_name=None, wallet_pub_keys=None, tessera_pub_keys=None): + query = {} + + if display_name is not None: + query["display_name"] = display_name + + if wallet_pub_keys is not None: + query["wallet_pub_key"] = wallet_pub_keys + + if tessera_pub_keys is not None: + query["tessera_pub_key"] = tessera_pub_keys + + return query + + def count(self, *, display_name=None): + """Count subjects. + + Counts number of subjects that match criteria. + + Args: + display_name (str): display name (optional0 + + Returns: + integer count of subjects. + + """ + return self._archivist.count( + f"{SUBJECTS_SUBPATH}/{SUBJECTS_LABEL}", + query=self.__query(display_name=display_name), + ) + + def list( + self, + *, + page_size=DEFAULT_PAGE_SIZE, + display_name=None, + ): + """List subjects. + + List subjects that match criteria. + TODO: filtering on display_name does not currently work + + Args: + display_name (str): display name (optional) + page_size (int): optional page size. (Rarely used). + + Returns: + iterable that returns :class:`Subject` instances + + """ + return ( + Subject(**a) + for a in self._archivist.list( + f"{SUBJECTS_SUBPATH}/{SUBJECTS_LABEL}", + SUBJECTS_LABEL, + page_size=page_size, + query=self.__query(display_name=display_name), + ) + ) + + +class Subject(dict): + """Subject object""" diff --git a/credentials/.gitignore b/credentials/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/credentials/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/docs/access_policies_filter.rst b/docs/access_policies_filter.rst new file mode 100644 index 00000000..543aee0d --- /dev/null +++ b/docs/access_policies_filter.rst @@ -0,0 +1,10 @@ +.. _access_policies_filter: + +Filter IAM access_policies +........................... + +.. literalinclude:: ../examples/access_policies_filter.py + :language: python + + + diff --git a/docs/access_policy_create.rst b/docs/access_policy_create.rst new file mode 100644 index 00000000..cd24679b --- /dev/null +++ b/docs/access_policy_create.rst @@ -0,0 +1,10 @@ +.. _access_policy_create: + +Create IAM access_policy +........................ + +.. literalinclude:: ../examples/access_policy_create.py + :language: python + + + diff --git a/docs/getting_started.rst b/docs/getting_started.rst index f8ece9d3..2a673a01 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -16,3 +16,8 @@ See the examples directory. create_event filter_events + access_policy_create + access_policies_filter + + subject_create + subjects_filter diff --git a/docs/iam/access_policies.rst b/docs/iam/access_policies.rst new file mode 100644 index 00000000..f04cf0cb --- /dev/null +++ b/docs/iam/access_policies.rst @@ -0,0 +1,11 @@ + +.. _access_policies: + +IAM Access Policies +-------------------- + + +.. automodule:: archivist.access_policies + :members: + :private-members: + diff --git a/docs/iam/index.rst b/docs/iam/index.rst new file mode 100644 index 00000000..32973703 --- /dev/null +++ b/docs/iam/index.rst @@ -0,0 +1,12 @@ +.. _iam: + +Identity and Access Management (IAM) +==================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + access_policies + subjects + diff --git a/docs/iam/subjects.rst b/docs/iam/subjects.rst new file mode 100644 index 00000000..5a9fdc6f --- /dev/null +++ b/docs/iam/subjects.rst @@ -0,0 +1,11 @@ + +.. _subjects: + +IAM Subjects +-------------------- + + +.. automodule:: archivist.subjects + :members: + :private-members: + diff --git a/docs/index.rst b/docs/index.rst index 7e64e474..fb2340d0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,8 @@ Jitsuin Archivist locations attachments + iam/index + archivist timestamp errors diff --git a/docs/subject_create.rst b/docs/subject_create.rst new file mode 100644 index 00000000..0f5d302b --- /dev/null +++ b/docs/subject_create.rst @@ -0,0 +1,10 @@ +.. _subject_create: + +Create IAM subject +.................. + +.. literalinclude:: ../examples/subject_create.py + :language: python + + + diff --git a/docs/subjects_filter.rst b/docs/subjects_filter.rst new file mode 100644 index 00000000..33c61570 --- /dev/null +++ b/docs/subjects_filter.rst @@ -0,0 +1,10 @@ +.. _subjects_filter: + +Filter IAM subjects +.................... + +.. literalinclude:: ../examples/subjects_filter.py + :language: python + + + diff --git a/examples/access_policies_filter.py b/examples/access_policies_filter.py new file mode 100644 index 00000000..cc1274c1 --- /dev/null +++ b/examples/access_policies_filter.py @@ -0,0 +1,41 @@ +"""Filter IAM access_policies of a archivist connection given url to Archivist and user Token. + +Main function parses in a url to the Archivist and a token, which is a user authorization. +The main function would initialize an archivist connection using the url and +the token, called "arch", then call arch.access_policies.list() with suitable properties and +attributes. + +""" + +from archivist.archivist import Archivist + + +def main(): + """Main function of filtering access_policies.""" + with open(".auth_token", mode="r") as tokenfile: + authtoken = tokenfile.read().strip() + + # Initialize connection to Archivist + arch = Archivist( + "https://rkvst.poc.jitsuin.io", + auth=authtoken, + ) + + # count access_policies... + print( + "no.of access_policies", + arch.access_policies.count(display_name="Some display name"), + ) + + # iterate through the generator.... + for access_policy in arch.access_policies.list(display_name="Some display name"): + print("access_policy", access_policy) + + # alternatively one could pull the list for all access policies and cache locally... + access_policies = list(arch.access_policies.list()) + for access_policy in access_policies: + print("access_policy", access_policy) + + +if __name__ == "__main__": + main() diff --git a/examples/access_policy_create.py b/examples/access_policy_create.py new file mode 100644 index 00000000..3bdb1646 --- /dev/null +++ b/examples/access_policy_create.py @@ -0,0 +1,79 @@ +"""Create a IAM access_policy given url to Archivist and user Token. + +Main function parses in +a url to the Archivist and a token, which is a user authorization. +The main function would initialize an archivist connection using the url and +the token, called "arch", then call arch.access_policys.create() and the access_policy +will be created. +""" + +from archivist.archivist import Archivist + + +def main(): + """Main function of create access_policy. + + Parse in user input of url and auth token and use them to + create an example archivist connection and create an asset. + + """ + with open(".auth_token", mode="r") as tokenfile: + authtoken = tokenfile.read().strip() + + arch = Archivist( + "https://rkvst.poc.jitsuin.io", + auth=authtoken, + ) + + props = { + "display_name": "Friendly name of the policy", + "description": "Description of the policy", + } + filters = [ + { + "or": [ + "attributes.arc_home_location_identity=" + "locations/5ea815f0-4de1-4a84-9377-701e880fe8ae", + "attributes.arc_home_location_identity=" + "locations/27eed70b-9e2b-4db1-b8c4-e36505350dcc", + ] + }, + { + "or": [ + "attributes.arc_display_type=Valve", + "attributes.arc_display_type=Pump", + ] + }, + { + "or": [ + "attributes.ext_vendor_name=SynsationIndustries", + ] + }, + ] + access_permissions = [ + { + "asset_attributes_read": ["toner_colour", "toner_type"], + "asset_attributes_write": ["toner_colour"], + "behaviours": ["Attachments", "Firmware", "Maintenance", "RecordEvidence"], + "event_arc_display_type_read": ["toner_type", "toner_colour"], + "event_arc_display_type_write": ["toner_replacement"], + "include_attributes": [ + "arc_display_name", + "arc_display_type", + "arc_firmware_version", + ], + "subjects": [ + "subjects/6a951b62-0a26-4c22-a886-1082297b063b", + "subjects/a24306e5-dc06-41ba-a7d6-2b6b3e1df48d", + ], + "user_attributes": [ + {"or": ["group:maintainers", "group:supervisors"]}, + ], + } + ] + access_policy = arch.access_policies.create(props, filters, access_permissions) + print("access Policy", access_policy) + + +if __name__ == "__main__": + main() diff --git a/examples/subject_create.py b/examples/subject_create.py new file mode 100644 index 00000000..7bbad873 --- /dev/null +++ b/examples/subject_create.py @@ -0,0 +1,37 @@ +"""Create a IAM subject given url to Archivist and user Token. + +Main function parses in +a url to the Archivist and a token, which is a user authorization. +The main function would initialize an archivist connection using the url and +the token, called "arch", then call arch.subjects.create() and the subject will be created. +""" + +from archivist.archivist import Archivist + + +def main(): + """Main function of create subject. + + Parse in user input of url and auth token and use them to + create an example archivist connection and create an asset. + + """ + with open(".auth_token", mode="r") as tokenfile: + authtoken = tokenfile.read().strip() + + # Initialize connection to Archivist + arch = Archivist( + "https://rkvst.poc.jitsuin.io", + auth=authtoken, + ) + + subject = arch.subjects.create( + "Some display name", + ["walletkey1"], + ["tesserakey1"], + ) + print("Subject", subject) + + +if __name__ == "__main__": + main() diff --git a/examples/subjects_filter.py b/examples/subjects_filter.py new file mode 100644 index 00000000..275f0ad5 --- /dev/null +++ b/examples/subjects_filter.py @@ -0,0 +1,38 @@ +"""Filter IAM subjects of a archivist connection given url to Archivist and user Token. + +Main function parses in a url to the Archivist and a token, which is a user authorization. +The main function would initialize an archivist connection using the url and +the token, called "arch", then call arch.subjects.list() with suitable properties and +attributes. + +""" + +from archivist.archivist import Archivist + + +def main(): + """Main function of filtering subjects.""" + with open(".auth_token", mode="r") as tokenfile: + authtoken = tokenfile.read().strip() + + # Initialize connection to Archivist + arch = Archivist( + "https://rkvst.poc.jitsuin.io", + auth=authtoken, + ) + + # count subjects... + print("no.of subjects", arch.subjects.count(display_name="Some display name")) + + # iterate through the generator.... + for subject in arch.subjects.list(display_name="Some display name"): + print("subject", subject) + + # alternatively one could pull the list for all subjects and cache locally... + subjects = list(arch.subjects.list()) + for subject in subjects: + print("subject", subject) + + +if __name__ == "__main__": + main() diff --git a/functests/__init__.py b/functests/__init__.py new file mode 100644 index 00000000..6e29a834 --- /dev/null +++ b/functests/__init__.py @@ -0,0 +1,3 @@ +""" +functional tests +""" diff --git a/functests/execaccess_policies.py b/functests/execaccess_policies.py new file mode 100644 index 00000000..23dd9e7c --- /dev/null +++ b/functests/execaccess_policies.py @@ -0,0 +1,155 @@ +""" +Test access_policies +""" + +from copy import deepcopy +from os import environ +from unittest import TestCase +from uuid import uuid4 + +from archivist.archivist import Archivist + +# pylint: disable=fixme +# pylint: disable=missing-docstring +# pylint: disable=unused-variable + +DISPLAY_NAME = "AccessPolicy display name" +PROPS = { + "display_name": DISPLAY_NAME, + "description": "Policy description", +} +FILTERS = [ + { + "or": [ + "attributes.arc_home_location_identity=locations/5ea815f0-4de1-4a84-9377-701e880fe8ae", + "attributes.arc_home_location_identity=locations/27eed70b-9e2b-4db1-b8c4-e36505350dcc", + ] + }, + { + "or": [ + "attributes.arc_display_type=Valve", + "attributes.arc_display_type=Pump", + ] + }, + { + "or": [ + "attributes.ext_vendor_name=SynsationIndustries", + ] + }, +] + +ACCESS_PERMISSIONS = [ + { + "subjects": [ + "subjects/6a951b62-0a26-4c22-a886-1082297b063b", + "subjects/a24306e5-dc06-41ba-a7d6-2b6b3e1df48d", + ], + "behaviours": ["Attachments", "Firmware", "Maintenance", "RecordEvidence"], + "include_attributes": [ + "arc_display_name", + "arc_display_type", + "arc_firmware_version", + ], + "user_attributes": [{"or": ["group:maintainers", "group:supervisors"]}], + } +] + + +class TestAccessPolicies(TestCase): + """ + Test Archivist AccessPolicies Create method + """ + + maxDiff = None + + @classmethod + def setUpClass(cls): + with open(environ["TEST_AUTHTOKEN"]) as fd: + auth = fd.read().strip() + cls.arch = Archivist(environ["TEST_ARCHIVIST"], auth=auth, verify=False) + cls.props = deepcopy(PROPS) + cls.props["display_name"] = f"{DISPLAY_NAME} {uuid4()}" + + def test_access_policies_create(self): + """ + Test access_policy creation + """ + access_policy = self.arch.access_policies.create( + self.props, FILTERS, ACCESS_PERMISSIONS + ) + self.assertEqual( + access_policy["display_name"], + self.props["display_name"], + msg="Incorrect display name", + ) + + def test_access_policies_update(self): + """ + Test access_policy update + """ + access_policy = self.arch.access_policies.create( + self.props, FILTERS, ACCESS_PERMISSIONS + ) + self.assertEqual( + access_policy["display_name"], + self.props["display_name"], + msg="Incorrect display name", + ) + access_policy = self.arch.access_policies.update( + access_policy["identity"], + props=self.props, + filters=FILTERS, + access_permissions=ACCESS_PERMISSIONS, + ) + + def test_access_policies_delete(self): + """ + Test access_policy delete + """ + access_policy = self.arch.access_policies.create( + self.props, FILTERS, ACCESS_PERMISSIONS + ) + self.assertEqual( + access_policy["display_name"], + self.props["display_name"], + msg="Incorrect display name", + ) + access_policy = self.arch.access_policies.delete( + access_policy["identity"], + ) + self.assertEqual( + access_policy, + {}, + msg="Incorrect access_policy", + ) + + def test_access_policies_list(self): + """ + Test access_policy list + """ + # TODO: filtering on display_name does not currently work... + access_policies = self.arch.access_policies.list( + display_name=self.props["display_name"] + ) + for access_policy in access_policies: + self.assertEqual( + access_policy["display_name"], + self.props["display_name"], + msg="Incorrect display name", + ) + self.assertGreater( + len(access_policy["display_name"]), + 0, + msg="Incorrect display name", + ) + + def test_access_policies_count(self): + """ + Test access_policy count + """ + count = self.arch.access_policies.count() + self.assertGreaterEqual( + count, + 0, + msg="Count is zero", + ) diff --git a/functests/execsubjects.py b/functests/execsubjects.py new file mode 100644 index 00000000..e8961470 --- /dev/null +++ b/functests/execsubjects.py @@ -0,0 +1,119 @@ +""" +Test subjects +""" + +from os import environ +from unittest import TestCase +from uuid import uuid4 + +from archivist.archivist import Archivist + +# pylint: disable=fixme +# pylint: disable=missing-docstring +# pylint: disable=unused-variable + +DISPLAY_NAME = "Subject display name" +WALLET_PUB_KEYS = [ + ( + "045c4ce14da4a0b393f9e961344aa19a868918c5ca50716e9f48a1ce5d3b6adcd27575" + "c1c8d53f3c8dba1e32384cfa21b04c2fc89ea56b9490f17db87a44da260b" + ) +] +TESSERA_PUB_KEYS = ["SOvH7mhAbVHK7MSBWXUe96ptpPP3GbWi0M7tsE1jVCc="] + + +class TestSubjects(TestCase): + """ + Test Archivist Subjects Create method + """ + + maxDiff = None + + @classmethod + def setUpClass(cls): + with open(environ["TEST_AUTHTOKEN"]) as fd: + auth = fd.read().strip() + cls.arch = Archivist(environ["TEST_ARCHIVIST"], auth=auth, verify=False) + cls.display_name = f"{DISPLAY_NAME} {uuid4()}" + + def test_subjects_create(self): + """ + Test subject creation + """ + subject = self.arch.subjects.create( + self.display_name, WALLET_PUB_KEYS, TESSERA_PUB_KEYS + ) + self.assertEqual( + subject["display_name"], + self.display_name, + msg="Incorrect display name", + ) + + def test_subjects_update(self): + """ + Test subject update + """ + subject = self.arch.subjects.create( + self.display_name, WALLET_PUB_KEYS, TESSERA_PUB_KEYS + ) + self.assertEqual( + subject["display_name"], + self.display_name, + msg="Incorrect display name", + ) + subject = self.arch.subjects.update( + subject["identity"], + display_name=self.display_name, + wallet_pub_keys=WALLET_PUB_KEYS, + tessera_pub_keys=TESSERA_PUB_KEYS, + ) + + def test_subjects_delete(self): + """ + Test subject delete + """ + subject = self.arch.subjects.create( + self.display_name, WALLET_PUB_KEYS, TESSERA_PUB_KEYS + ) + self.assertEqual( + subject["display_name"], + self.display_name, + msg="Incorrect display name", + ) + subject = self.arch.subjects.delete( + subject["identity"], + ) + self.assertEqual( + subject, + {}, + msg="Incorrect subject", + ) + + def test_subjects_list(self): + """ + Test subject list + """ + # TODO: filtering on display_name does not currently work... + subjects = self.arch.subjects.list(display_name=self.display_name) + for subject in subjects: + # self.assertEqual( + # subject["display_name"], + # self.display_name, + # msg="Incorrect display name", + # ) + self.assertGreater( + len(subject["display_name"]), + 0, + msg="Incorrect display name", + ) + + def test_subjects_count(self): + """ + Test subject count + """ + count = self.arch.subjects.count() + self.assertGreater( + count, + 0, + msg="Count is zero", + ) diff --git a/scripts/builder.sh b/scripts/builder.sh index 2a4b45b6..756740b9 100755 --- a/scripts/builder.sh +++ b/scripts/builder.sh @@ -6,11 +6,12 @@ # # ./scripts/builder.sh /bin/bash # for shell # ./scripts/builder.sh # enters python REPL -# ./scripts/builder.sh autopep8 -i -r python # autopep8s all code docker run \ --rm -it \ -v $(pwd):/home/builder \ -u $(id -u):$(id -g) \ + -e TEST_ARCHIVIST \ + -e TEST_AUTHTOKEN \ jitsuin-archivist-python-builder \ "$@" diff --git a/scripts/functests.sh b/scripts/functests.sh new file mode 100755 index 00000000..e06589c1 --- /dev/null +++ b/scripts/functests.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# +# run functional tests +# +if [ -z "${TEST_ARCHIVIST}" ] +then + echo "TEST_ARCHIVIST is undefined" + exit 1 +fi +if [ -z "${TEST_AUTHTOKEN}" ] +then + echo "TEST_AUTHTOKEN is undefined" + exit 1 +fi +if [ ! -s "${TEST_AUTHTOKEN}" ] +then + echo "${TEST_AUTHTOKEN} does not exist" + exit 1 +fi + +python3 --version + +export PYTHONWARNINGS="ignore:Unverified HTTPS request" +python3 -m unittest discover -v -p exec*.py -s functests diff --git a/scripts/unittests.sh b/scripts/unittests.sh index 4e4b3862..f2f41db0 100755 --- a/scripts/unittests.sh +++ b/scripts/unittests.sh @@ -8,8 +8,9 @@ rm -f coverage.xml rm -rf htmlcov COVERAGE="coverage" ${COVERAGE} --version -${COVERAGE} run --branch --source archivist -m unittest discover -v +${COVERAGE} run --branch --source archivist -m unittest -v ${COVERAGE} annotate ${COVERAGE} html ${COVERAGE} xml ${COVERAGE} report + diff --git a/setup.cfg b/setup.cfg index 0d29f8a9..42859efe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,3 +12,5 @@ omit = archivist/logger.py # so simple - not worth testing archivist/timestamp.py + # omit functional tests + exec* diff --git a/unittests/testaccess_policies.py b/unittests/testaccess_policies.py new file mode 100644 index 00000000..f9fd954e --- /dev/null +++ b/unittests/testaccess_policies.py @@ -0,0 +1,554 @@ +""" +Test access policies +""" + +import json +from unittest import TestCase, mock + +from archivist.archivist import Archivist +from archivist.constants import ( + ROOT, + HEADERS_REQUEST_TOTAL_COUNT, + HEADERS_TOTAL_COUNT, + ACCESS_POLICIES_SUBPATH, + ACCESS_POLICIES_LABEL, + ASSETS_LABEL, +) +from archivist.errors import ArchivistBadRequestError +from archivist.access_policies import DEFAULT_PAGE_SIZE + +from .mock_response import MockResponse +from .testassets import RESPONSE as ASSET + + +# pylint: disable=missing-docstring +# pylint: disable=unused-variable + +PROPS = { + "display_name": "Policy display name", + "description": "Policy description", +} +FILTERS = [ + { + "or": [ + "attributes.arc_home_location_identity=locations/5ea815f0-4de1-4a84-9377-701e880fe8ae", + "attributes.arc_home_location_identity=locations/27eed70b-9e2b-4db1-b8c4-e36505350dcc", + ] + }, + { + "or": [ + "attributes.arc_display_type=Valve", + "attributes.arc_display_type=Pump", + ] + }, + { + "or": [ + "attributes.ext_vendor_name=SynsationIndustries", + ] + }, +] + +ACCESS_PERMISSIONS = [ + { + "subjects": [ + "subjects/6a951b62-0a26-4c22-a886-1082297b063b", + "subjects/a24306e5-dc06-41ba-a7d6-2b6b3e1df48d", + ], + "behaviours": ["Attachments", "Firmware", "Maintenance", "RecordEvidence"], + "include_attributes": [ + "arc_display_name", + "arc_display_type", + "arc_firmware_version", + ], + "user_attributes": [{"or": ["group:maintainers", "group:supervisors"]}], + } +] + +IDENTITY = f"{ACCESS_POLICIES_LABEL}/xxxxxxxx" +SUBPATH = f"{ACCESS_POLICIES_SUBPATH}/{ACCESS_POLICIES_LABEL}" +ASSET_ID = f"{ASSETS_LABEL}/yyyyyyyy" + +RESPONSE = { + **PROPS, + "identity": IDENTITY, + "filters": FILTERS, + "access_permissions": ACCESS_PERMISSIONS, +} +REQUEST = { + **PROPS, + "filters": FILTERS, + "access_permissions": ACCESS_PERMISSIONS, +} +REQUEST_DATA = json.dumps(REQUEST) +UPDATE_DATA = json.dumps(PROPS) + + +class TestAccessPolicies(TestCase): + """ + Test Archivist AccessPolicies Create method + """ + + maxDiff = None + + def setUp(self): + self.arch = Archivist("url", auth="authauthauth") + + @mock.patch("requests.post") + def test_access_policies_create(self, mock_post): + """ + Test access_policy creation + """ + mock_post.return_value = MockResponse(200, **RESPONSE) + + access_policy = self.arch.access_policies.create( + PROPS, FILTERS, ACCESS_PERMISSIONS + ) + self.assertEqual( + tuple(mock_post.call_args), + ( + ((f"url/{ROOT}/{SUBPATH}"),), + { + "data": REQUEST_DATA, + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="CREATE method called incorrectly", + ) + self.assertEqual( + access_policy, + RESPONSE, + msg="CREATE method called incorrectly", + ) + + @mock.patch("requests.get") + def test_access_policies_read(self, mock_get): + """ + Test access_policy reading + """ + mock_get.return_value = MockResponse(200, **RESPONSE) + + access_policy = self.arch.access_policies.read(IDENTITY) + self.assertEqual( + tuple(mock_get.call_args), + ( + ((f"url/{ROOT}/{ACCESS_POLICIES_SUBPATH}/{IDENTITY}"),), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + + @mock.patch("requests.delete") + def test_access_policies_delete(self, mock_delete): + """ + Test access_policy deleting + """ + mock_delete.return_value = MockResponse(200, {}) + + access_policy = self.arch.access_policies.delete(IDENTITY) + self.assertEqual( + tuple(mock_delete.call_args), + ( + ((f"url/{ROOT}/{ACCESS_POLICIES_SUBPATH}/{IDENTITY}"),), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="DELETE method called incorrectly", + ) + + @mock.patch("requests.patch") + def test_access_policies_update(self, mock_patch): + """ + Test access_policy deleting + """ + mock_patch.return_value = MockResponse(200, **RESPONSE) + + access_policy = self.arch.access_policies.update( + IDENTITY, + PROPS, + ) + self.assertEqual( + tuple(mock_patch.call_args), + ( + ((f"url/{ROOT}/{ACCESS_POLICIES_SUBPATH}/{IDENTITY}"),), + { + "data": UPDATE_DATA, + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="PATCH method called incorrectly", + ) + + @mock.patch("requests.get") + def test_access_policies_read_with_error(self, mock_get): + """ + Test read method with error + """ + mock_get.return_value = MockResponse(400) + with self.assertRaises(ArchivistBadRequestError): + resp = self.arch.access_policies.read(IDENTITY) + + @mock.patch("requests.get") + def test_access_policies_count(self, mock_get): + """ + Test access_policy counting + """ + mock_get.return_value = MockResponse( + 200, + headers={HEADERS_TOTAL_COUNT: 1}, + access_policies=[ + RESPONSE, + ], + ) + + count = self.arch.access_policies.count() + self.assertEqual( + tuple(mock_get.call_args), + ( + ((f"url/{ROOT}/{SUBPATH}" "?page_size=1"),), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + HEADERS_REQUEST_TOTAL_COUNT: "true", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + self.assertEqual( + count, + 1, + msg="Incorrect count", + ) + + @mock.patch("requests.get") + def test_access_policies_count_by_name(self, mock_get): + """ + Test access_policy counting + """ + mock_get.return_value = MockResponse( + 200, + headers={HEADERS_TOTAL_COUNT: 1}, + access_policies=[ + RESPONSE, + ], + ) + + count = self.arch.access_policies.count( + display_name="Policy display name", + ) + self.assertEqual( + tuple(mock_get.call_args), + ( + ( + ( + f"url/{ROOT}/{SUBPATH}" + "?page_size=1" + "&display_name=Policy display name" + ), + ), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + HEADERS_REQUEST_TOTAL_COUNT: "true", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + + @mock.patch("requests.get") + def test_access_policies_list(self, mock_get): + """ + Test access_policy listing + """ + mock_get.return_value = MockResponse( + 200, + access_policies=[ + RESPONSE, + ], + ) + + access_policies = list(self.arch.access_policies.list()) + self.assertEqual( + len(access_policies), + 1, + msg="incorrect number of access_policies", + ) + for access_policy in access_policies: + self.assertEqual( + access_policy, + RESPONSE, + msg="Incorrect access_policy listed", + ) + + for a in mock_get.call_args_list: + self.assertEqual( + tuple(a), + ( + (f"url/{ROOT}/{SUBPATH}?page_size={DEFAULT_PAGE_SIZE}",), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + + @mock.patch("requests.get") + def test_access_policies_list_by_name(self, mock_get): + """ + Test access_policy listing + """ + mock_get.return_value = MockResponse( + 200, + access_policies=[ + RESPONSE, + ], + ) + + access_policies = list( + self.arch.access_policies.list( + display_name="Policy display name", + ) + ) + self.assertEqual( + len(access_policies), + 1, + msg="incorrect number of access_policies", + ) + for access_policy in access_policies: + self.assertEqual( + access_policy, + RESPONSE, + msg="Incorrect access_policy listed", + ) + + for a in mock_get.call_args_list: + self.assertEqual( + tuple(a), + ( + ( + ( + f"url/{ROOT}/{SUBPATH}" + f"?page_size={DEFAULT_PAGE_SIZE}" + "&display_name=Policy display name" + ), + ), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + + @mock.patch("requests.get") + def test_access_policies_count_matching_access_policies(self, mock_get): + """ + Test access_policy counting + """ + mock_get.return_value = MockResponse( + 200, + headers={HEADERS_TOTAL_COUNT: 1}, + access_policies=[ + RESPONSE, + ], + ) + + count = self.arch.access_policies.count_matching_access_policies(ASSET_ID) + self.assertEqual( + tuple(mock_get.call_args), + ( + ( + ( + f"url/{ROOT}/{ACCESS_POLICIES_SUBPATH}/{ASSET_ID}/{ACCESS_POLICIES_LABEL}" + "?page_size=1" + ), + ), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + HEADERS_REQUEST_TOTAL_COUNT: "true", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + self.assertEqual( + count, + 1, + msg="Incorrect count", + ) + + @mock.patch("requests.get") + def test_access_policies_list_matching_access_policies(self, mock_get): + """ + Test access_policy counting + """ + mock_get.return_value = MockResponse( + 200, + headers={HEADERS_TOTAL_COUNT: 1}, + access_policies=[ + RESPONSE, + ], + ) + access_policies = list( + self.arch.access_policies.list_matching_access_policies(ASSET_ID) + ) + self.assertEqual( + len(access_policies), + 1, + msg="incorrect number of access_policies", + ) + for access_policy in access_policies: + self.assertEqual( + access_policy, + RESPONSE, + msg="Incorrect access_policy listed", + ) + + for a in mock_get.call_args_list: + self.assertEqual( + tuple(a), + ( + ( + f"url/{ROOT}/{ACCESS_POLICIES_SUBPATH}/{ASSET_ID}/{ACCESS_POLICIES_LABEL}" + f"?page_size={DEFAULT_PAGE_SIZE}", + ), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + + @mock.patch("requests.get") + def test_access_policies_count_matching_assets(self, mock_get): + """ + Test access_policy counting + """ + mock_get.return_value = MockResponse( + 200, + headers={HEADERS_TOTAL_COUNT: 1}, + assets=[ + ASSET, + ], + ) + + count = self.arch.access_policies.count_matching_assets(IDENTITY) + self.assertEqual( + tuple(mock_get.call_args), + ( + ( + ( + f"url/{ROOT}/{ACCESS_POLICIES_SUBPATH}/{IDENTITY}/{ASSETS_LABEL}" + "?page_size=1" + ), + ), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + HEADERS_REQUEST_TOTAL_COUNT: "true", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + self.assertEqual( + count, + 1, + msg="Incorrect count", + ) + + @mock.patch("requests.get") + def test_access_policies_list_matching_assets(self, mock_get): + """ + Test access_policy counting + """ + mock_get.return_value = MockResponse( + 200, + headers={HEADERS_TOTAL_COUNT: 1}, + assets=[ + ASSET, + ], + ) + assets = list(self.arch.access_policies.list_matching_assets(IDENTITY)) + self.assertEqual( + len(assets), + 1, + msg="incorrect number of assets", + ) + for asset in assets: + self.assertEqual( + asset, + ASSET, + msg="Incorrect asset listed", + ) + + for a in mock_get.call_args_list: + self.assertEqual( + tuple(a), + ( + ( + f"url/{ROOT}/{ACCESS_POLICIES_SUBPATH}/{IDENTITY}/{ASSETS_LABEL}" + f"?page_size={DEFAULT_PAGE_SIZE}", + ), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) diff --git a/unittests/testarchivist.py b/unittests/testarchivist.py index f8f2ef08..bfdff4c5 100644 --- a/unittests/testarchivist.py +++ b/unittests/testarchivist.py @@ -20,7 +20,6 @@ # pylint: disable=unused-variable # pylint: disable=missing-docstring -# pylint: disable=unnecessary-comprehension class TestArchivist(TestCase): @@ -74,6 +73,8 @@ def test_archivist(self): arch.verify, msg="verify must be True", ) + with self.assertRaises(AttributeError): + e = arch.Illegal_endpoint def test_archivist_no_verify(self): """ @@ -598,8 +599,7 @@ def test_list(self, mock_get): }, ], ) - listing = self.arch.list("path/path", "things") - responses = [r for r in listing] + responses = list(self.arch.list("path/path", "things")) self.assertEqual( len(responses), 1, @@ -635,9 +635,8 @@ def test_list_with_error(self, mock_get): }, ], ) - listing = self.arch.list("path/path", "things") with self.assertRaises(ArchivistBadRequestError): - responses = [r for r in listing] + responses = list(self.arch.list("path/path", "things")) @mock.patch("requests.get") def test_list_with_bad_field(self, mock_get): @@ -652,9 +651,8 @@ def test_list_with_bad_field(self, mock_get): }, ], ) - listing = self.arch.list("path/path", "badthings") with self.assertRaises(ArchivistBadFieldError): - responses = [r for r in listing] + responses = list(self.arch.list("path/path", "badthings")) @mock.patch("requests.get") def test_list_with_headers(self, mock_get): @@ -669,12 +667,13 @@ def test_list_with_headers(self, mock_get): }, ], ) - listing = self.arch.list( - "path/path", - "things", - headers={"headerfield1": "headervalue1"}, + responses = list( + self.arch.list( + "path/path", + "things", + headers={"headerfield1": "headervalue1"}, + ) ) - responses = [r for r in listing] self.assertEqual( len(responses), 1, @@ -711,12 +710,13 @@ def test_list_with_query(self, mock_get): }, ], ) - listing = self.arch.list( - "path/path", - "things", - query={"queryfield1": "queryvalue1"}, + responses = list( + self.arch.list( + "path/path", + "things", + query={"queryfield1": "queryvalue1"}, + ) ) - responses = [r for r in listing] self.assertEqual( len(responses), 1, @@ -756,12 +756,13 @@ def test_list_with_page_size(self, mock_get): }, ], ) - listing = self.arch.list( - "path/path", - "things", - page_size=2, + responses = list( + self.arch.list( + "path/path", + "things", + page_size=2, + ) ) - responses = [r for r in listing] self.assertEqual( len(responses), 2, @@ -823,12 +824,13 @@ def test_list_with_multiple_pages(self, mock_get): ], ), ] - listing = self.arch.list( - "path/path", - "things", - page_size=2, + responses = list( + self.arch.list( + "path/path", + "things", + page_size=2, + ) ) - responses = [r for r in listing] self.assertEqual( len(responses), 4, diff --git a/unittests/testassets.py b/unittests/testassets.py index 9eb8b8ea..d2bb95a4 100644 --- a/unittests/testassets.py +++ b/unittests/testassets.py @@ -21,7 +21,6 @@ from .mock_response import MockResponse # pylint: disable=missing-docstring -# pylint: disable=unnecessary-comprehension # pylint: disable=unused-variable @@ -555,8 +554,7 @@ def test_assets_list(self, mock_get): ], ) - listing = self.arch.assets.list() - assets = [a for a in listing] + assets = list(self.arch.assets.list()) self.assertEqual( len(assets), 1, @@ -598,13 +596,14 @@ def test_assets_list_with_query(self, mock_get): ], ) - listing = self.arch.assets.list( - props={ - "confirmation_status": "CONFIRMED", - }, - attrs={"arc_firmware_version": "1.0"}, + assets = list( + self.arch.assets.list( + props={ + "confirmation_status": "CONFIRMED", + }, + attrs={"arc_firmware_version": "1.0"}, + ) ) - assets = [a for a in listing] self.assertEqual( len(assets), 1, diff --git a/unittests/testevents.py b/unittests/testevents.py index 6c7e5c2b..2f2980c7 100644 --- a/unittests/testevents.py +++ b/unittests/testevents.py @@ -23,7 +23,6 @@ from .mock_response import MockResponse # pylint: disable=missing-docstring -# pylint: disable=unnecessary-comprehension # pylint: disable=unused-variable ASSET_ID = f"{ASSETS_LABEL}/xxxxxxxxxxxxxxxxxxxx" @@ -696,8 +695,7 @@ def test_events_list(self, mock_get): ], ) - listing = self.arch.events.list(asset_id=ASSET_ID) - events = [a for a in listing] + events = list(self.arch.events.list(asset_id=ASSET_ID)) self.assertEqual( len(events), 1, @@ -746,14 +744,15 @@ def test_events_list_with_query(self, mock_get): ], ) - listing = self.arch.events.list( - asset_id=ASSET_ID, - props={ - "confirmation_status": "CONFIRMED", - }, - attrs={"arc_firmware_version": "1.0"}, + events = list( + self.arch.events.list( + asset_id=ASSET_ID, + props={ + "confirmation_status": "CONFIRMED", + }, + attrs={"arc_firmware_version": "1.0"}, + ) ) - events = [a for a in listing] self.assertEqual( len(events), 1, @@ -804,13 +803,14 @@ def test_events_list_with_wildcard_asset(self, mock_get): ], ) - listing = self.arch.events.list( - props={ - "confirmation_status": "CONFIRMED", - }, - attrs={"arc_firmware_version": "1.0"}, + events = list( + self.arch.events.list( + props={ + "confirmation_status": "CONFIRMED", + }, + attrs={"arc_firmware_version": "1.0"}, + ) ) - events = [a for a in listing] self.assertEqual( len(events), 1, diff --git a/unittests/testlocations.py b/unittests/testlocations.py index 62224446..ce64cb5d 100644 --- a/unittests/testlocations.py +++ b/unittests/testlocations.py @@ -20,7 +20,6 @@ # pylint: disable=missing-docstring -# pylint: disable=unnecessary-comprehension # pylint: disable=unused-variable PROPS = { @@ -251,8 +250,7 @@ def test_locations_list(self, mock_get): ], ) - listing = self.arch.locations.list() - locations = [a for a in listing] + locations = list(self.arch.locations.list()) self.assertEqual( len(locations), 1, @@ -294,11 +292,12 @@ def test_locations_list_with_query(self, mock_get): ], ) - listing = self.arch.locations.list( - props={"display_name": "Macclesfield, Cheshire"}, - attrs={"director": "John Smith"}, + locations = list( + self.arch.locations.list( + props={"display_name": "Macclesfield, Cheshire"}, + attrs={"director": "John Smith"}, + ) ) - locations = [a for a in listing] self.assertEqual( len(locations), 1, diff --git a/unittests/testsubjects.py b/unittests/testsubjects.py new file mode 100644 index 00000000..e8607af3 --- /dev/null +++ b/unittests/testsubjects.py @@ -0,0 +1,351 @@ +""" +Test subjects +""" + +import json +from unittest import TestCase, mock + +from archivist.archivist import Archivist +from archivist.constants import ( + ROOT, + HEADERS_REQUEST_TOTAL_COUNT, + HEADERS_TOTAL_COUNT, + SUBJECTS_SUBPATH, + SUBJECTS_LABEL, +) +from archivist.errors import ArchivistBadRequestError +from archivist.subjects import DEFAULT_PAGE_SIZE + +from .mock_response import MockResponse + + +# pylint: disable=missing-docstring +# pylint: disable=unused-variable + +DISPLAY_NAME = "Subject display name" +WALLET_PUB_KEYS = [ + "wallet1", + "wallet2", +] +WALLET_ADDRESSES = [ + "address1", + "address2", +] +TESSERA_PUB_KEYS = [ + "tessera1", + "tessera2", +] +IDENTITY = f"{SUBJECTS_LABEL}/xxxxxxxx" +SUBPATH = f"{SUBJECTS_SUBPATH}/{SUBJECTS_LABEL}" + +RESPONSE = { + "identity": IDENTITY, + "display_name": DISPLAY_NAME, + "wallet_pub_key": WALLET_PUB_KEYS, + "wallet_address": WALLET_ADDRESSES, + "tessera_pub_key": TESSERA_PUB_KEYS, +} +REQUEST = { + "display_name": DISPLAY_NAME, + "wallet_pub_key": WALLET_PUB_KEYS, + "tessera_pub_key": TESSERA_PUB_KEYS, +} +REQUEST_DATA = json.dumps(REQUEST) +UPDATE_DATA = json.dumps({"display_name": DISPLAY_NAME}) + + +class TestSubjects(TestCase): + """ + Test Archivist Subjects Create method + """ + + maxDiff = None + + def setUp(self): + self.arch = Archivist("url", auth="authauthauth") + + @mock.patch("requests.post") + def test_subjects_create(self, mock_post): + """ + Test subject creation + """ + mock_post.return_value = MockResponse(200, **RESPONSE) + + subject = self.arch.subjects.create( + DISPLAY_NAME, WALLET_PUB_KEYS, TESSERA_PUB_KEYS + ) + self.assertEqual( + tuple(mock_post.call_args), + ( + ((f"url/{ROOT}/{SUBPATH}"),), + { + "data": REQUEST_DATA, + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="CREATE method called incorrectly", + ) + self.assertEqual( + subject, + RESPONSE, + msg="CREATE method called incorrectly", + ) + + @mock.patch("requests.get") + def test_subjects_read(self, mock_get): + """ + Test subject reading + """ + mock_get.return_value = MockResponse(200, **RESPONSE) + + subject = self.arch.subjects.read(IDENTITY) + self.assertEqual( + tuple(mock_get.call_args), + ( + ((f"url/{ROOT}/{SUBJECTS_SUBPATH}/{IDENTITY}"),), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + + @mock.patch("requests.delete") + def test_subjects_delete(self, mock_delete): + """ + Test subject deleting + """ + mock_delete.return_value = MockResponse(200, {}) + + subject = self.arch.subjects.delete(IDENTITY) + self.assertEqual( + tuple(mock_delete.call_args), + ( + ((f"url/{ROOT}/{SUBJECTS_SUBPATH}/{IDENTITY}"),), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="DELETE method called incorrectly", + ) + + @mock.patch("requests.patch") + def test_subjects_update(self, mock_patch): + """ + Test subject deleting + """ + mock_patch.return_value = MockResponse(200, **RESPONSE) + + subject = self.arch.subjects.update( + IDENTITY, + display_name=DISPLAY_NAME, + ) + self.assertEqual( + tuple(mock_patch.call_args), + ( + ((f"url/{ROOT}/{SUBJECTS_SUBPATH}/{IDENTITY}"),), + { + "data": UPDATE_DATA, + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="PATCH method called incorrectly", + ) + + @mock.patch("requests.get") + def test_subjects_read_with_error(self, mock_get): + """ + Test read method with error + """ + mock_get.return_value = MockResponse(400) + with self.assertRaises(ArchivistBadRequestError): + resp = self.arch.subjects.read(IDENTITY) + + @mock.patch("requests.get") + def test_subjects_count(self, mock_get): + """ + Test subject counting + """ + mock_get.return_value = MockResponse( + 200, + headers={HEADERS_TOTAL_COUNT: 1}, + subjects=[ + RESPONSE, + ], + ) + + count = self.arch.subjects.count() + self.assertEqual( + tuple(mock_get.call_args), + ( + ((f"url/{ROOT}/{SUBPATH}" "?page_size=1"),), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + HEADERS_REQUEST_TOTAL_COUNT: "true", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + self.assertEqual( + count, + 1, + msg="Incorrect count", + ) + + @mock.patch("requests.get") + def test_subjects_count_by_name(self, mock_get): + """ + Test subject counting + """ + mock_get.return_value = MockResponse( + 200, + headers={HEADERS_TOTAL_COUNT: 1}, + subjects=[ + RESPONSE, + ], + ) + + count = self.arch.subjects.count( + display_name="Subject display name", + ) + self.assertEqual( + tuple(mock_get.call_args), + ( + ( + ( + f"url/{ROOT}/{SUBPATH}" + "?page_size=1" + "&display_name=Subject display name" + ), + ), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + HEADERS_REQUEST_TOTAL_COUNT: "true", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + + @mock.patch("requests.get") + def test_subjects_list(self, mock_get): + """ + Test subject listing + """ + mock_get.return_value = MockResponse( + 200, + subjects=[ + RESPONSE, + ], + ) + + subjects = list(self.arch.subjects.list()) + self.assertEqual( + len(subjects), + 1, + msg="incorrect number of subjects", + ) + for subject in subjects: + self.assertEqual( + subject, + RESPONSE, + msg="Incorrect subject listed", + ) + + for a in mock_get.call_args_list: + self.assertEqual( + tuple(a), + ( + (f"url/{ROOT}/{SUBPATH}?page_size={DEFAULT_PAGE_SIZE}",), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + ) + + @mock.patch("requests.get") + def test_subjects_list_by_name(self, mock_get): + """ + Test subject listing + """ + mock_get.return_value = MockResponse( + 200, + subjects=[ + RESPONSE, + ], + ) + + subjects = list( + self.arch.subjects.list( + display_name="Subject display name", + ) + ) + self.assertEqual( + len(subjects), + 1, + msg="incorrect number of subjects", + ) + for subject in subjects: + self.assertEqual( + subject, + RESPONSE, + msg="Incorrect subject listed", + ) + + for a in mock_get.call_args_list: + self.assertEqual( + tuple(a), + ( + ( + ( + f"url/{ROOT}/{SUBPATH}" + f"?page_size={DEFAULT_PAGE_SIZE}" + "&display_name=Subject display name" + ), + ), + { + "headers": { + "content-type": "application/json", + "authorization": "Bearer authauthauth", + }, + "verify": True, + "cert": None, + }, + ), + msg="GET method called incorrectly", + )