diff --git a/ciscosparkapi/__init__.py b/ciscosparkapi/__init__.py index e0d86ed..3536fb1 100644 --- a/ciscosparkapi/__init__.py +++ b/ciscosparkapi/__init__.py @@ -10,7 +10,6 @@ from .exceptions import ciscosparkapiException, SparkApiError from .restsession import RestSession -from .api.accesstokens import AccessToken, AccessTokensAPI from .api.people import Person, PeopleAPI from .api.rooms import Room, RoomsAPI from .api.memberships import Membership, MembershipsAPI @@ -18,6 +17,10 @@ from .api.teams import Team, TeamsAPI from .api.teammemberships import TeamMembership, TeamMembershipsAPI from .api.webhooks import Webhook, WebhooksAPI +from .api.organizations import Organization, OrganizationsAPI +from .api.licenses import License, LicensesAPI +from .api.roles import Role, RolesAPI +from .api.accesstokens import AccessToken, AccessTokensAPI __author__ = "Chris Lunsford" @@ -34,6 +37,7 @@ DEFAULT_BASE_URL = 'https://api.ciscospark.com/v1/' DEFAULT_TIMEOUT = 60 +ACCESS_TOKEN_ENVIRONMENT_VARIABLE = 'SPARK_ACCESS_TOKEN' class CiscoSparkAPI(object): @@ -60,6 +64,12 @@ class CiscoSparkAPI(object): :class:`webhooks ` + :class:`organizations ` + + :class:`licenses ` + + :class:`roles ` + :class:`access_tokens ` """ @@ -104,7 +114,7 @@ def __init__(self, access_token=None, base_url=DEFAULT_BASE_URL, assert access_token is None or isinstance(access_token, string_types) assert isinstance(base_url, string_types) assert isinstance(timeout, int) - spark_access_token = os.environ.get('SPARK_ACCESS_TOKEN', None) + spark_access_token = os.environ.get(ACCESS_TOKEN_ENVIRONMENT_VARIABLE) access_token = access_token if access_token else spark_access_token if not access_token: error_message = "You must provide an Spark access token to " \ @@ -128,6 +138,9 @@ def __init__(self, access_token=None, base_url=DEFAULT_BASE_URL, self.teams = TeamsAPI(self._session) self.team_memberships = TeamMembershipsAPI(self._session) self.webhooks = WebhooksAPI(self._session) + self.organizations = OrganizationsAPI(self._session) + self.licenses = LicensesAPI(self._session) + self.roles = RolesAPI(self._session) self.access_tokens = AccessTokensAPI(self.base_url, timeout=timeout) @property diff --git a/ciscosparkapi/api/licenses.py b/ciscosparkapi/api/licenses.py new file mode 100644 index 0000000..45cf035 --- /dev/null +++ b/ciscosparkapi/api/licenses.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +"""Cisco Spark Licenses API wrapper. + +Classes: + License: Models a Spark License JSON object as a native Python object. + LicensesAPI: Wraps the Cisco Spark Licenses API and exposes the + API calls as Python method calls that return native Python objects. + +""" + + +from builtins import object +from six import string_types + +from ciscosparkapi.utils import generator_container +from ciscosparkapi.restsession import RestSession +from ciscosparkapi.sparkdata import SparkData + + +__author__ = "Chris Lunsford" +__author_email__ = "chrlunsf@cisco.com" +__copyright__ = "Copyright (c) 2016 Cisco Systems, Inc." +__license__ = "MIT" + + +class License(SparkData): + """Model a Spark License JSON object as a native Python object.""" + + def __init__(self, json): + """Init a new License data object from a dict or JSON string. + + Args: + json(dict, string_types): Input JSON object. + + Raises: + TypeError: If the input object is not a dictionary or string. + + """ + super(License, self).__init__(json) + + @property + def id(self): + """The unique id for the License.""" + return self._json.get('id') + + @property + def name(self): + """The name of the License.""" + return self._json.get('name') + + @property + def totalUnits(self): + """The total number of license units.""" + return self._json.get('totalUnits') + + @property + def consumedUnits(self): + """The total number of license units consumed.""" + return self._json.get('consumedUnits') + + +class LicensesAPI(object): + """Cisco Spark Licenses API wrapper. + + Wraps the Cisco Spark Licenses API and exposes the API calls as Python + method calls that return native Python objects. + + """ + + def __init__(self, session): + """Init a new LicensesAPI object with the provided RestSession. + + Args: + session(RestSession): The RESTful session object to be used for + API calls to the Cisco Spark service. + + Raises: + AssertionError: If the parameter types are incorrect. + + """ + assert isinstance(session, RestSession) + super(LicensesAPI, self).__init__() + self._session = session + + @generator_container + def list(self, orgId=None, max=None): + """List Licenses. + + Optionally filtered by Organization (orgId parameter). + + This method supports Cisco Spark's implementation of RFC5988 Web + Linking to provide pagination support. It returns a generator + container that incrementally yields all objects returned by the + query. The generator will automatically request additional 'pages' of + responses from Spark as needed until all responses have been returned. + The container makes the generator safe for reuse. A new API call will + be made, using the same parameters that were specified when the + generator was created, every time a new iterator is requested from the + container. + + Args: + orgId(string_types): Filters the returned licenses to only include + those liceses associated with the specified Organization + (orgId). + max(int): Limits the maximum number of entries returned from the + Spark service per request (page size; requesting additional + pages is handled automatically). + + Returns: + GeneratorContainer: When iterated, the GeneratorContainer, yields + the objects returned from the Cisco Spark query. + + Raises: + AssertionError: If the parameter types are incorrect. + SparkApiError: If the Cisco Spark cloud returns an error. + + """ + # Process args + assert orgId is None or isinstance(orgId, string_types) + assert max is None or isinstance(max, int) + params = {} + if orgId: + params['orgId'] = orgId + if max: + params['max'] = max + # API request - get items + items = self._session.get_items('licenses', params=params) + # Yield License objects created from the returned JSON objects + for item in items: + yield License(item) + + def get(self, licenseId): + """Get the details of a License, by id. + + Args: + licenseId(string_types): The id of the License. + + Returns: + License: With the details of the requested License. + + Raises: + AssertionError: If the parameter types are incorrect. + SparkApiError: If the Cisco Spark cloud returns an error. + + """ + # Process args + assert isinstance(licenseId, string_types) + # API request + json_obj = self._session.get('licenses/' + licenseId) + # Return a License object created from the returned JSON object + return License(json_obj) diff --git a/ciscosparkapi/api/memberships.py b/ciscosparkapi/api/memberships.py index 7e3e854..f552261 100644 --- a/ciscosparkapi/api/memberships.py +++ b/ciscosparkapi/api/memberships.py @@ -260,8 +260,8 @@ def update(self, membershipId, **update_attributes): "argument must be specified." raise ciscosparkapiException(error_message) # API request - json_obj = self._session.post('memberships/' + membershipId, - json=update_attributes) + json_obj = self._session.put('memberships/' + membershipId, + json=update_attributes) # Return a Membership object created from the response JSON data return Membership(json_obj) diff --git a/ciscosparkapi/api/organizations.py b/ciscosparkapi/api/organizations.py new file mode 100644 index 0000000..103c120 --- /dev/null +++ b/ciscosparkapi/api/organizations.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +"""Cisco Spark Organizations API wrapper. + +Classes: + Organization: Models a Spark Organization JSON object as a native Python + object. + OrganizationsAPI: Wraps the Cisco Spark Organizations API and exposes the + API calls as Python method calls that return native Python objects. + +""" + + +from builtins import object +from six import string_types + +from ciscosparkapi.utils import generator_container +from ciscosparkapi.restsession import RestSession +from ciscosparkapi.sparkdata import SparkData + + +__author__ = "Chris Lunsford" +__author_email__ = "chrlunsf@cisco.com" +__copyright__ = "Copyright (c) 2016 Cisco Systems, Inc." +__license__ = "MIT" + + +class Organization(SparkData): + """Model a Spark Organization JSON object as a native Python object.""" + + def __init__(self, json): + """Init a new Organization data object from a dict or JSON string. + + Args: + json(dict, string_types): Input JSON object. + + Raises: + TypeError: If the input object is not a dictionary or string. + + """ + super(Organization, self).__init__(json) + + @property + def id(self): + """The unique id for the Organization.""" + return self._json.get('id') + + @property + def displayName(self): + """The human-friendly display name of the Organization.""" + return self._json.get('displayName') + + @property + def created(self): + """The date and time the Organization was created.""" + return self._json.get('created') + + +class OrganizationsAPI(object): + """Cisco Spark Organizations API wrapper. + + Wraps the Cisco Spark Organizations API and exposes the API calls as Python + method calls that return native Python objects. + + """ + + def __init__(self, session): + """Init a new OrganizationsAPI object with the provided RestSession. + + Args: + session(RestSession): The RESTful session object to be used for + API calls to the Cisco Spark service. + + Raises: + AssertionError: If the parameter types are incorrect. + + """ + assert isinstance(session, RestSession) + super(OrganizationsAPI, self).__init__() + self._session = session + + @generator_container + def list(self, max=None): + """List Organizations. + + This method supports Cisco Spark's implementation of RFC5988 Web + Linking to provide pagination support. It returns a generator + container that incrementally yields all objects returned by the + query. The generator will automatically request additional 'pages' of + responses from Spark as needed until all responses have been returned. + The container makes the generator safe for reuse. A new API call will + be made, using the same parameters that were specified when the + generator was created, every time a new iterator is requested from the + container. + + Args: + max(int): Limits the maximum number of entries returned from the + Spark service per request (page size; requesting additional + pages is handled automatically). + + Returns: + GeneratorContainer: When iterated, the GeneratorContainer, yields + the objects returned from the Cisco Spark query. + + Raises: + AssertionError: If the parameter types are incorrect. + SparkApiError: If the Cisco Spark cloud returns an error. + + """ + # Process args + assert max is None or isinstance(max, int) + params = {} + if max: + params['max'] = max + # API request - get items + items = self._session.get_items('organizations', params=params) + # Yield Organization objects created from the returned JSON objects + for item in items: + yield Organization(item) + + def get(self, orgId): + """Get the details of an Organization, by id. + + Args: + orgId(string_types): The id of the Organization. + + Returns: + Organization: With the details of the requested Organization. + + Raises: + AssertionError: If the parameter types are incorrect. + SparkApiError: If the Cisco Spark cloud returns an error. + + """ + # Process args + assert isinstance(orgId, string_types) + # API request + json_obj = self._session.get('organizations/' + orgId) + # Return a Organization object created from the returned JSON object + return Organization(json_obj) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index 4ad0127..e176bec 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -41,31 +41,69 @@ def __init__(self, json): @property def id(self): - return self._json['id'] + """The person's unique ID.""" + return self._json.get('id') @property def emails(self): + """Email address(es) of the person. + + CURRENT LIMITATION: Spark (today) only allows you to provide a single + email address for a person. The list data type was selected to enable + future support for providing multiple email address. + + """ return self._json['emails'] @property def displayName(self): - return self._json['displayName'] + """Full name of the person.""" + return self._json.get('displayName') + + @property + def firstName(self): + """First name of the person.""" + return self._json.get('firstName') + + @property + def lastName(self): + """Last name of the person.""" + return self._json.get('lastName') @property def avatar(self): - return self._json['avatar'] + """URL to the person's avatar in PNG format.""" + return self._json.get('avatar') @property - def created(self): - return self._json['created'] + def orgId(self): + """ID of the organization to which this person belongs.""" + return self._json.get('orgId') @property - def lastActivity(self): - return self._json['lastActivity'] + def roles(self): + """Roles of the person.""" + return self._json.get('roles') + + @property + def licenses(self): + """Licenses allocated to the person.""" + return self._json.get('licenses') + + @property + def created(self): + """The date and time the person was created.""" + return self._json.get('created') @property def status(self): - return self._json['status'] + """The person's current status.""" + return self._json.get('status') + + @property + def lastActivity(self): + """The date and time of the person's last activity.""" + return self._json.get('lastActivity') class PeopleAPI(object): @@ -146,6 +184,103 @@ def list(self, email=None, displayName=None, max=None): for item in items: yield Person(item) + def create(self, emails, **person_attributes): + """Create a new user account for a given organization + + Only an admin can create a new user account. + + You must specify displayName and/or firstName and lastName. + + Args: + emails(list): Email address(es) of the person. (list of strings) + CURRENT LIMITATION: Spark (today) only allows you to provide a + single email address for a person. The list data type was + selected to enable future support for providing multiple email + address. + **person_attributes: + displayName(string_types): Full name of the person + firstName(string_types): First name of the person + lastName(string_types): Last name of the person + avatar(string_types): URL to the person's avatar in PNG format + orgId(string_types): ID of the organization to which this + person belongs + roles(list): Roles of the person (list of strings containing + the role IDs to be assigned to the person) + licenses(list): Licenses allocated to the person (list of + strings containing the license IDs to be allocated to the + person) + + Returns: + Person: With the details of the created person. + + Raises: + AssertionError: If the parameter types are incorrect. + ciscosparkapiException: If required parameters have been omitted. + SparkApiError: If the Cisco Spark cloud returns an error. + + """ + # Process args + assert isinstance(emails, string_types) and len(emails) == 1 + post_data = {} + post_data['emails'] = emails + post_data.update(person_attributes) + + # API request + json_obj = self._session.post('people', json=post_data) + + # Return a Room object created from the returned JSON object + return Person(json_obj) + + def update(self, personId, **person_attributes): + """Update details for a person, by ID. + + Only an admin can update a person details. + + Args: + personId(string_types): The ID of the person to be updated. + **person_attributes: + emails(list): Email address(es) of the person. (list of + strings) CURRENT LIMITATION: Spark (today) only allows you + to provide a single email address for a person. The list + data type was selected to enable future support for + providing multiple email address. + displayName(string_types): Full name of the person + firstName(string_types): First name of the person + lastName(string_types): Last name of the person + avatar(string_types): URL to the person's avatar in PNG format + orgId(string_types): ID of the organization to which this + person belongs + roles(list): Roles of the person (list of strings containing + the role IDs to be assigned to the person) + licenses(list): Licenses allocated to the person (list of + strings containing the license IDs to be allocated to the + person) + + Returns: + Person: With the updated person details. + + Raises: + AssertionError: If the parameter types are incorrect. + ciscosparkapiException: If an update attribute is not provided. + SparkApiError: If the Cisco Spark cloud returns an error. + + """ + # Process args + assert isinstance(personId, string_types) + + # Process update_attributes keyword arguments + if not person_attributes: + error_message = "At least one **update_attributes keyword " \ + "argument must be specified." + raise ciscosparkapiException(error_message) + + # API request + json_obj = self._session.put('people/' + personId, + json=person_attributes) + + # Return a Person object created from the returned JSON object + return Person(json_obj) + def get(self, personId): """Get person details, by personId. diff --git a/ciscosparkapi/api/roles.py b/ciscosparkapi/api/roles.py new file mode 100644 index 0000000..e8846b6 --- /dev/null +++ b/ciscosparkapi/api/roles.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +"""Cisco Spark Roles API wrapper. + +Classes: + Role: Models a Spark Role JSON object as a native Python object. + RolesAPI: Wraps the Cisco Spark Roles API and exposes the + API calls as Python method calls that return native Python objects. + +""" + + +from builtins import object +from six import string_types + +from ciscosparkapi.utils import generator_container +from ciscosparkapi.restsession import RestSession +from ciscosparkapi.sparkdata import SparkData + + +__author__ = "Chris Lunsford" +__author_email__ = "chrlunsf@cisco.com" +__copyright__ = "Copyright (c) 2016 Cisco Systems, Inc." +__role__ = "MIT" + + +class Role(SparkData): + """Model a Spark Role JSON object as a native Python object.""" + + def __init__(self, json): + """Init a new Role data object from a dict or JSON string. + + Args: + json(dict, string_types): Input JSON object. + + Raises: + TypeError: If the input object is not a dictionary or string. + + """ + super(Role, self).__init__(json) + + @property + def id(self): + """The unique id for the Role.""" + return self._json.get('id') + + @property + def name(self): + """The name of the Role.""" + return self._json.get('name') + + +class RolesAPI(object): + """Cisco Spark Roles API wrapper. + + Wraps the Cisco Spark Roles API and exposes the API calls as Python + method calls that return native Python objects. + + """ + + def __init__(self, session): + """Init a new RolesAPI object with the provided RestSession. + + Args: + session(RestSession): The RESTful session object to be used for + API calls to the Cisco Spark service. + + Raises: + AssertionError: If the parameter types are incorrect. + + """ + assert isinstance(session, RestSession) + super(RolesAPI, self).__init__() + self._session = session + + @generator_container + def list(self, max=None): + """List Roles. + + This method supports Cisco Spark's implementation of RFC5988 Web + Linking to provide pagination support. It returns a generator + container that incrementally yields all objects returned by the + query. The generator will automatically request additional 'pages' of + responses from Spark as needed until all responses have been returned. + The container makes the generator safe for reuse. A new API call will + be made, using the same parameters that were specified when the + generator was created, every time a new iterator is requested from the + container. + + Args: + max(int): Limits the maximum number of entries returned from the + Spark service per request (page size; requesting additional + pages is handled automatically). + + Returns: + GeneratorContainer: When iterated, the GeneratorContainer, yields + the objects returned from the Cisco Spark query. + + Raises: + AssertionError: If the parameter types are incorrect. + SparkApiError: If the Cisco Spark cloud returns an error. + + """ + # Process args + assert max is None or isinstance(max, int) + params = {} + if max: + params['max'] = max + # API request - get items + items = self._session.get_items('roles', params=params) + # Yield Role objects created from the returned JSON objects + for item in items: + yield Role(item) + + def get(self, roleId): + """Get the details of a Role, by id. + + Args: + roleId(string_types): The id of the Role. + + Returns: + Role: With the details of the requested Role. + + Raises: + AssertionError: If the parameter types are incorrect. + SparkApiError: If the Cisco Spark cloud returns an error. + + """ + # Process args + assert isinstance(roleId, string_types) + # API request + json_obj = self._session.get('roles/' + roleId) + # Return a Role object created from the returned JSON object + return Role(json_obj) diff --git a/ciscosparkapi/api/rooms.py b/ciscosparkapi/api/rooms.py index fd6cf11..09ff7cf 100644 --- a/ciscosparkapi/api/rooms.py +++ b/ciscosparkapi/api/rooms.py @@ -225,7 +225,7 @@ def update(self, roomId, **update_attributes): "argument must be specified." raise ciscosparkapiException(error_message) # API request - json_obj = self._session.post('rooms/' + roomId, json=update_attributes) + json_obj = self._session.put('rooms/' + roomId, json=update_attributes) # Return a Room object created from the response JSON data return Room(json_obj) diff --git a/ciscosparkapi/api/teammemberships.py b/ciscosparkapi/api/teammemberships.py index ce026d6..5e8fdf4 100644 --- a/ciscosparkapi/api/teammemberships.py +++ b/ciscosparkapi/api/teammemberships.py @@ -236,7 +236,7 @@ def update(self, membershipId, **update_attributes): "argument must be specified." raise ciscosparkapiException(error_message) # API request - json_obj = self._session.post('team/memberships/' + membershipId, + json_obj = self._session.put('team/memberships/' + membershipId, json=update_attributes) # Return a TeamMembership object created from the response JSON data return TeamMembership(json_obj) diff --git a/requirements.txt b/requirements.txt index a95e30d..8aff43b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,22 @@ alabaster==0.7.9 Babel==2.3.4 +-e git+https://github.com/CiscoDevNet/ciscosparkapi.git@c76abbd3826fbf36134d331e430e0ae28399ea91#egg=ciscosparkapi docutils==0.12 future==0.16.0 imagesize==0.7.1 Jinja2==2.8 MarkupSafe==0.23 +pluggy==0.4.0 +py==1.4.31 Pygments==2.1.3 +pytest==3.0.5 pytz==2016.7 requests==2.12.1 requests-toolbelt==0.7.0 six==1.10.0 snowballstemmer==1.2.1 Sphinx==1.4.9 +sphinx-rtd-theme==0.1.9 +tox==2.5.0 versioneer==0.17 +virtualenv==15.1.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/api/test_accesstokens.py b/tests/api/test_accesstokens.py new file mode 100644 index 0000000..0095b59 --- /dev/null +++ b/tests/api/test_accesstokens.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + + +import pytest + +from tests.utils import create_string + + +# Helper Functions + + + + +# pytest Fixtures + diff --git a/tests/api/test_memberships.py b/tests/api/test_memberships.py new file mode 100644 index 0000000..f85133c --- /dev/null +++ b/tests/api/test_memberships.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +"""pytest Memberships functions, fixtures and tests.""" + + +import pytest + +import ciscosparkapi + + +# Helper Functions + +def add_people_to_room(api, room, emails): + for email in emails: + api.memberships.create(room.id, personEmail=email) + + +def empty_room(api, me, room): + """Remove all memberships from a room (except the caller's membership).""" + memberships = api.memberships.list(room.id) + for membership in memberships: + if membership.personId != me.id: + api.memberships.delete(membership.id) + + +# pytest Fixtures + +@pytest.fixture(scope="session") +def group_room_with_members(api, me, group_room, email_addresses): + add_people_to_room(api, group_room, email_addresses) + yield group_room + empty_room(api, me, group_room) + + +@pytest.fixture(scope="session") +def team_room_with_members(api, me, team_room, email_addresses): + add_people_to_room(api, team_room, email_addresses) + yield team_room + empty_room(api, me, team_room) + + +@pytest.fixture +def temp_group_room_with_members(api, me, temp_group_room, email_addresses): + add_people_to_room(api, temp_group_room, email_addresses) + yield temp_group_room + empty_room(api, me, temp_group_room) + diff --git a/tests/api/test_messages.py b/tests/api/test_messages.py new file mode 100644 index 0000000..8bea049 --- /dev/null +++ b/tests/api/test_messages.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +"""pytest Messages functions, fixtures and tests.""" + + +import pytest + +import ciscosparkapi +from tests.utils import create_string + + +# Helper Functions + + + + +# pytest Fixtures + +@pytest.fixture(scope="session") +def direct_messages(api, email_addresses): + msg_text = create_string("Message") + messages = [] + for email in email_addresses: + messages.append(api.messages.create(toPersonEmail=email, + text=msg_text)) + yield messages + delete_messages(api, messages) + + +def delete_messages(api, messages): + for message in messages: + api.messages.delete(message.id) diff --git a/tests/api/test_people.py b/tests/api/test_people.py new file mode 100644 index 0000000..a399fa4 --- /dev/null +++ b/tests/api/test_people.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +"""pytest People functions, fixtures and tests.""" + + +import pytest + +import ciscosparkapi +from tests.utils import create_string + + +# Helper Functions + + + + +# pytest Fixtures + +@pytest.fixture(scope="session") +def me(api): + return api.people.me() diff --git a/tests/api/test_rooms.py b/tests/api/test_rooms.py new file mode 100644 index 0000000..09338fe --- /dev/null +++ b/tests/api/test_rooms.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +"""pytest Rooms functions, fixtures and tests.""" + +import itertools + +import pytest + +import ciscosparkapi +from tests.utils import create_string + + +# Helper Functions + +def create_room(api, title): + return api.rooms.create(title) + + +def create_team_room(api, team, room_title): + return api.rooms.create(room_title, teamId=team.id) + + +def delete_room(api, room): + api.rooms.delete(room.id) + + +def is_valid_room(room): + return isinstance(room, ciscosparkapi.Room) and room.id is not None + + +def are_valid_rooms(rooms_iterable): + rooms_are_valid = (is_valid_room(room) for room in rooms_iterable) + return all(rooms_are_valid) + + +def room_exists(api, room): + try: + api.rooms.get(room.id) + except ciscosparkapi.SparkApiError: + return False + else: + return True + + +# pytest Fixtures + +@pytest.fixture(scope="session") +def rooms_list(api): + return list(api.rooms.list()) + + +@pytest.fixture(scope="session") +def direct_rooms(api, direct_messages): + return [api.rooms.get(message.roomId) for message in direct_messages] + + +@pytest.fixture(scope="session") +def group_room(api): + room = create_room(api, create_string("Room")) + yield room + delete_room(api, room) + + +@pytest.fixture(scope="session") +def team_room(api, team): + team_room = create_team_room(api, team, create_string("Team Room")) + yield team_room + delete_room(api, team_room) + + +@pytest.fixture +def temp_group_room(api): + room = create_room(api, create_string("Room")) + yield room + if room_exists(api, room): + delete_room(api, room) + + +@pytest.fixture +def add_rooms(api): + rooms = [] + def inner(num_rooms): + for i in range(num_rooms): + rooms.append(create_room(api, create_string("Additional Room"))) + return rooms + yield inner + for room in rooms: + delete_room(api, room) + + +# Room Tests + +class TestRoomsAPI(object): + """Test RoomsAPI methods.""" + + def test_create_group_room(self, group_room): + assert is_valid_room(group_room) + + def test_create_team_room(self, team_room): + assert is_valid_room(team_room) + + def test_get_room_details(self, api, group_room): + room = api.rooms.get(group_room.id) + assert is_valid_room(room) + + def test_update_room_title(self, api, group_room): + new_title = create_string("Updated Room") + room = api.rooms.update(group_room.id, title=new_title) + assert is_valid_room(room) + assert room.title == new_title + + def test_delete_room(self, api, temp_group_room): + api.rooms.delete(temp_group_room.id) + assert not room_exists(api, temp_group_room) + + @pytest.mark.usefixtures("group_room") + def test_list_group_rooms(self, api): + group_rooms_list = list(api.rooms.list(type='group')) + assert len(group_rooms_list) > 0 + assert are_valid_rooms(group_rooms_list) + + @pytest.mark.usefixtures("team_room") + def test_list_team_rooms(self, api, team): + team_rooms_list = list(api.rooms.list(teamId=team.id)) + assert len(team_rooms_list) > 0 + assert are_valid_rooms(team_rooms_list) + + @pytest.mark.usefixtures("direct_rooms") + def test_list_direct_rooms(self, api): + direct_rooms_list = list(api.rooms.list(type='direct')) + assert len(direct_rooms_list) > 0 + assert are_valid_rooms(direct_rooms_list) + + @pytest.mark.usefixtures("group_room", "team_room", "direct_rooms") + def test_list_all_rooms(self, rooms_list): + assert len(rooms_list) > 0 + assert are_valid_rooms(rooms_list) + + @pytest.mark.usefixtures("group_room", "team_room", "direct_rooms") + def test_list_all_rooms_with_paging(self, api, rooms_list, add_rooms): + page_size = 2 + pages = 3 + num_rooms = pages * page_size + if len(rooms_list) < num_rooms: + add_rooms(num_rooms - len(rooms_list)) + rooms = api.rooms.list(max=page_size) + rooms_list = list(itertools.islice(rooms, num_rooms)) + assert len(rooms_list) == 6 + assert are_valid_rooms(rooms_list) diff --git a/tests/api/test_teammemberships.py b/tests/api/test_teammemberships.py new file mode 100644 index 0000000..69b4a4b --- /dev/null +++ b/tests/api/test_teammemberships.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +"""pytest Team Memberships functions, fixtures and tests.""" + + +import pytest + +import ciscosparkapi +from tests.utils import create_string + + +# Helper Functions + +def add_people_to_team(api, team, emails): + for email in emails: + api.team_memberships.create(team.id, personEmail=email) + + +def empty_team(api, me, team): + """Remove all memberships from a team (except the caller's membership).""" + memberships = api.team_memberships.list(team.id) + for membership in memberships: + if membership.personId != me.id: + api.team_memberships.delete(membership.id) + + +# pytest Fixtures + +@pytest.fixture(scope="session") +def team_with_members(api, me, team, email_addresses): + add_people_to_team(api, team, email_addresses) + yield team + empty_team(api, me, team) diff --git a/tests/api/test_teams.py b/tests/api/test_teams.py new file mode 100644 index 0000000..3cb78d3 --- /dev/null +++ b/tests/api/test_teams.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +"""pytest Team functions, fixtures and tests.""" + + +import pytest + +import ciscosparkapi +from tests.utils import create_string + + +# Helper Functions + +def create_team(api, name): + return api.teams.create(name) + + +def delete_team(api, team): + api.teams.delete(team.id) + + +# pytest Fixtures + +@pytest.fixture(scope="session") +def team(api): + team = create_team(api, create_string("Team")) + yield team + delete_team(api, team) diff --git a/tests/api/test_webhooks.py b/tests/api/test_webhooks.py new file mode 100644 index 0000000..6ea1a48 --- /dev/null +++ b/tests/api/test_webhooks.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +"""pytest Webhooks functions, fixtures and tests.""" + + +import pytest + +import ciscosparkapi +from tests.utils import create_string + + +# Helper Functions + + + + +# pytest Fixtures + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b110bce --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +"""pytest configuration and top-level fixtures.""" + +import pytest + + +EMAIL_ADDRESSES = [ + 'test98@cmlccie.com', + 'test99@cmlccie.com', +] + + +pytest_plugins = [ + 'tests.test_ciscosparkapi', + 'tests.api.test_accesstokens', + 'tests.api.test_memberships', + 'tests.api.test_messages', + 'tests.api.test_people', + 'tests.api.test_rooms', + 'tests.api.test_teammemberships', + 'tests.api.test_teams', + 'tests.api.test_webhooks', +] + + +# pytest Fixtures + +@pytest.fixture(scope="session") +def email_addresses(): + return EMAIL_ADDRESSES diff --git a/tests/test_ciscosparkapi.py b/tests/test_ciscosparkapi.py new file mode 100644 index 0000000..96c5ffd --- /dev/null +++ b/tests/test_ciscosparkapi.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +"""ciscosparkapi/__init__.py Fixtures & Tests""" + +import os + +import pytest + +import ciscosparkapi + + +# pytest Fixtures + +@pytest.fixture(scope="session") +def access_token(): + return os.environ.get(ciscosparkapi.ACCESS_TOKEN_ENVIRONMENT_VARIABLE) + + +@pytest.fixture +def unset_access_token(access_token): + del os.environ[ciscosparkapi.ACCESS_TOKEN_ENVIRONMENT_VARIABLE] + yield None + os.environ[ciscosparkapi.ACCESS_TOKEN_ENVIRONMENT_VARIABLE] = access_token + + +@pytest.fixture(scope="session") +def api(): + return ciscosparkapi.CiscoSparkAPI() + + +# CiscoSparkAPI Tests + +class TestCiscoSparkAPI: + """Test the CiscoSparkAPI package-level code.""" + + # Test creating CiscoSparkAPI objects + + @pytest.mark.usefixtures("unset_access_token") + def test_creating_a_new_ciscosparkapi_object_without_an_access_token(self): + with pytest.raises(ciscosparkapi.ciscosparkapiException): + ciscosparkapi.CiscoSparkAPI() + + @pytest.mark.usefixtures("unset_access_token") + def test_creating_a_new_ciscosparkapi_object_via_access_token_argument(self, access_token): + connection_object = ciscosparkapi.CiscoSparkAPI(access_token=access_token) + assert isinstance(connection_object,ciscosparkapi.CiscoSparkAPI) + + def test_creating_a_new_ciscosparkapi_object_via_environment_varable(self): + connection_object = ciscosparkapi.CiscoSparkAPI() + assert isinstance(connection_object,ciscosparkapi.CiscoSparkAPI) + + def test_default_base_url(self): + connection_object = ciscosparkapi.CiscoSparkAPI() + assert connection_object.base_url == ciscosparkapi.DEFAULT_BASE_URL + + def test_custom_base_url(self): + custom_url = "https://spark.cmlccie.com/v1/" + connection_object = ciscosparkapi.CiscoSparkAPI(base_url=custom_url) + assert connection_object.base_url == custom_url + + def test_default_timeout(self): + connection_object = ciscosparkapi.CiscoSparkAPI() + assert connection_object.timeout == ciscosparkapi.DEFAULT_TIMEOUT + + def test_custom_timeout(self): + custom_timeout = 10 + connection_object = ciscosparkapi.CiscoSparkAPI(timeout=custom_timeout) + assert connection_object.timeout == custom_timeout + + # Test creation of component API objects + + def test_people_api_object_creation(self, api): + assert isinstance(api.people, ciscosparkapi.PeopleAPI) + + def test_rooms_api_object_creation(self, api): + assert isinstance(api.rooms, ciscosparkapi.RoomsAPI) + + def test_memberships_api_object_creation(self, api): + assert isinstance(api.memberships, ciscosparkapi.MembershipsAPI) + + def test_messages_api_object_creation(self, api): + assert isinstance(api.messages, ciscosparkapi.MessagesAPI) + + def test_teams_api_object_creation(self, api): + assert isinstance(api.teams, ciscosparkapi.TeamsAPI) + + def test_team_memberships_api_object_creation(self, api): + assert isinstance(api.team_memberships, ciscosparkapi.TeamMembershipsAPI) + + def test_webhooks_api_object_creation(self, api): + assert isinstance(api.webhooks, ciscosparkapi.WebhooksAPI) + + def test_access_tokens_api_object_creation(self, api): + assert isinstance(api.access_tokens, ciscosparkapi.AccessTokensAPI) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..5f472b4 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +"""Tests helper functions and classes.""" + + +import datetime +import string + + +STRING_PREFIX = "ciscosparkapi py.test" +STRING_TEMPLATE = string.Template("$prefix $item [$datetime]") + + +# Helper Functions + +def create_string(item): + return STRING_TEMPLATE.substitute(prefix=STRING_PREFIX, + item=item, + datetime=str(datetime.datetime.now())) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..42da594 --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[tox] +envlist = py27, py35 + +[testenv] +deps = pytest +commands = py.test +passenv = SPARK_ACCESS_TOKEN