diff --git a/.gitignore b/.gitignore index 34357dc..ba6d13d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist/ /.coverage /cover .tox +build/ diff --git a/.travis.yml b/.travis.yml index defd39f..5fd82b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: python python: - "2.7" - - "3.3" - - "3.4" + - "3.6" sudo: false branches: only: diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3e070..6c22a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,53 @@ Changelog ========= +v1.2.5 - 2019-10-23 + + * #41: Refactor tests and switch to pytest (@ArtemGordinsky) + * Fix typo in Company repr (@sjkingo) + +v1.2.4 - 2019-10-19 + + * #38: Add ticket time entry API (@smontoya) + * #39: Support binary ticket attachments (@ArtemGordinsky) + * Drop Python 3.3 from Travis CI and add 3.6 (@sjkingo) + +v1.2.3 - 2018-09-23 + + * #28: Changes from #28 create ticket with attachments (@prenit-coverfox) + * #25: Changes from #25 update_contact, restore_contact (@vdboor) + * #22: Changes from #22 pagination in list_groups(@prenit-coverfox) + +v1.2.2 - 2018-07-09 + + * #20: Changes from #20 including v1 deprecation (@prenit-coverfox) + +v1.2.1 - 2018-07-05 + + * #21: Fixed set declaration bug in base class (@sjkingo) + +v1.2.0 - 2018-06-27 + + * Filled out v1 and v2 API's and expanded test cases (@prenit-coverfox) + +v1.1.2 - 2018-06-04 + + * No code changes: fix release stuff up (@sjkingo) + +v1.1.1 - 2018-06-04 + + * #16: Add support for Role, Ticket fields and Agent API (@prenit-coverfox) + +v1.1.0 - 2018-01-08 + + * #13: Fixed group representation (@helix90) + * #14: Add support for Company API (@helix90) + * #11: Add support for creating outbound emails (@sorrison) + +v1.0.1 - 2017-08-26 + + * Allow clients to specify empty filter name (@brephophagist) + v1.0.0 - 2016-06-16 * Add support for version 2 of the Freskdesk API while maintaining support for diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5772ea2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ + +Copyright (c) 2014-2018 Sam Kingston and others. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 517f0c6..e3faeaf 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,21 @@ A library for the [Freshdesk](http://freshdesk.com/) helpdesk system for Python There is support for a limited subset of features, using either Freshdesk API v1 or v2. -Support for the v1 API includes the following features: -* Getting a [Ticket](http://freshdesk.com/api#view_a_ticket) and filtering ticket lists -* Getting a [Contact/User/Customer](http://freshdesk.com/api#view_user) -* Viewing [timesheets](http://freshdesk.com/api#view_all_time_entry) +After the deprecation of Freshdesk V1 API, library uses V2 API only. Support for the v2 API includes the following features: * [Tickets](http://developer.freshdesk.com/api/#tickets) - - [List](http://developer.freshdesk.com/api/#list_all_tickets) - [Get](http://developer.freshdesk.com/api/#view_a_ticket) - [Create](http://developer.freshdesk.com/api/#create_ticket) - [Update](http://developer.freshdesk.com/api/#update_ticket) - [Delete](http://developer.freshdesk.com/api/#delete_a_ticket) + - [Create OutBound Email](http://developer.freshdesk.com/api/#create_outbound_email) + - [List](http://developer.freshdesk.com/api/#list_all_tickets) + - [Filter](https://developer.freshdesk.com/api/#filter_tickets) + - [List Time Entries](https://developers.freshdesk.com/api/#list_all_ticket_timeentries) (as of 1.2.4) + - Custom ticket fields (as of 1.1.1) +* [Ticket Fields](http://developer.freshdesk.com/api/#ticket_fields) + - [List](http://developer.freshdesk.com/api/#list_all_ticket_fields) * [Comments](http://developer.freshdesk.com/api/#conversations) (known as Conversations in Freshdesk) - [List](http://developer.freshdesk.com/api/#list_all_ticket_notes) - [Create note](http://developer.freshdesk.com/api/#add_note_to_a_ticket) @@ -27,6 +30,25 @@ Support for the v2 API includes the following features: * [Groups](http://developer.freshdesk.com/api/#groups) - [List](http://developer.freshdesk.com/api/#list_all_groups) - [Get](http://developer.freshdesk.com/api/#view_group) +* [Contacts](http://developer.freshdesk.com/api/#contacts) + - [Get](http://developer.freshdesk.com/api/#view_contact) + - [List](http://developer.freshdesk.com/api/#list_all_contacts) + - [Create](http://developer.freshdesk.com/api/#create_contact) + - [Update](http://developer.freshdesk.com/api/#update_contact) - from 1.2.3 + - [Delete](http://developer.freshdesk.com/api/#delete_contact) + - [Restore](http://developer.freshdesk.com/api/#restore_contact) - from 1.2.3 + - [Make agent](http://developer.freshdesk.com/api/#make_agent) + +* [Company](https://developers.freshdesk.com/api/#companies) + - [Get](http://developer.freshdesk.com/api/#view_company) +* [Roles](https://developers.freshdesk.com/api/#roles) - from 1.1.1 + - [Get](http://developer.freshdesk.com/api/#view_role) + - [List](http://developer.freshdesk.com/api/#list_role) +* [Agents](https://developers.freshdesk.com/api/#agents) - from 1.1.1 + - [Get](http://developer.freshdesk.com/api/#view_agent) + - [List](http://developer.freshdesk.com/api/#list_all_agents) + - [Update](http://developer.freshdesk.com/api/#update_agent) + - [Delete](http://developer.freshdesk.com/api/#delete_agent) ## Installation @@ -48,8 +70,8 @@ The easiest way to install is from [PyPi](https://pypi.python.org/pypi/python-fr 3. Optionally, run the test suite: ``` - $ pip install nose - $ nosetests + $ pip install pytest + $ pytest ``` ## Usage @@ -65,8 +87,7 @@ without changing these. To find your API key, follow Freshdesk's step-by-step solution article [How to find your API key](https://support.freshdesk.com/support/solutions/articles/215517). -By default, API v1 is used for backwards compatibility. To specify v1 or v2 -explicitly: +By default, API v2 is used after the deprecation of Freshdesk V1 API ```python >>> a = API('company.freshdesk.com', 'q8dnkjaS554Aol21dmnas9d92', version=2) @@ -74,6 +95,20 @@ explicitly: The `API` class provides access to all the methods exposed by the Freshdesk API. +Optionally, the API v2 can be given SSL verification and/or proxy settings to obey for all requests: + +```python +>>> a = API('company.freshdesk.com', 'q8dnkjaS554Aol21dmnas9d92', verify=False) +``` + +```python +>>> proxies = { +... 'http': 'http://example.proxy:8000', +... 'https': 'https://example.proxy:8443' +... } +>>> a = API('company.freshdesk.com', 'q8dnkjaS554Aol21dmnas9d92', proxies=proxies) +``` + ### Tickets (API v2) The Ticket API is accessed by using the methods assigned to the `a.tickets` @@ -81,7 +116,7 @@ instance. Tickets are loaded as instances of the `freshdesk.v2.models.Ticket` class, and can be iterated over: ```python ->>> a.tickets.list_open_tickets() +>>> a.tickets.list_tickets() [, , ] >>> a.tickets.list_deleted_tickets() [] @@ -100,7 +135,7 @@ set([u'status', u'source_name', u'ticket_type', u'updated_at', ...]) Attributes are automatically converted to native Python objects where appropriate: ```python ->>> a.tickets.list_open_tickets()[0].created_at +>>> a.tickets.list_tickets()[0].created_at datetime.datetime(2014, 12, 5, 14, 7, 44) ``` @@ -125,6 +160,18 @@ ticket = a.tickets.create_ticket('This is a sample ticket', tags=['example']) ``` +To Create a ticket with attachments, pass a list of fully quilified file paths with key name 'attachments' + +```python +ticket = a.tickets.create_ticket('This is a sample ticket', + email='example@example.com', + description='This is the description of the ticket', + tags=['example'], + attachments=[ + '/path/to/file1', + '/path/to/file2'] + ) +``` The only positional argument is the subject, which is always required. All other values are optional named arguments, which you can find in the @@ -134,6 +181,15 @@ While all but subject are optional, you will need to specify at least one of: `requester_id`, `email`, `facebook_id`, `phone` or `twitter_id` as the requester of the ticket, or the request will fail. +You can get the list of tickets by using + +```python +ticket = a.tickets.list_tickets(filter_name=None, page=1, per_page=10) +``` + +By defauly `new_and_my_open` filter is used. If you want to list all the tickets without any filter, pass `filter_name=None`. +Pagination is supported. If `page` argument is not passed, all pages are fetched, else specified page is returned. + Updating a ticket is similar to creating a ticket. The only differences are that the ticket ID becomes the first positional argument, and subject becomes an optional named argument. @@ -156,6 +212,17 @@ To delete a ticket, just pass the ticket ID value to the `delete_ticket` method: a.tickets.delete_ticket(4) ``` +### Ticket Fields (API v2) + +To view ticket fields for your freshdesk, use the `list_ticket_fields` method, from +the ticket_fields module: + +```python +>>> a.ticket_fields.list_ticket_fields(type='default_requester') +[, ] + +``` + ### Comments (API v2) To view comments on a ticket (note or reply), use the `list_comments` method, from @@ -204,28 +271,62 @@ arguments. In both methods, the ticket ID and body must be given as positional arguments. -### Contacts/Users (API v1) +### Contacts (API v2) Freshdesk mixes up the naming of contacts and users, depending on whether they are an agent or not. `python-freshdesk` simply calls them all contacts and are represented as `Contact` instances: ```python ->>> repr(a.contacts.get_contact('1234')) +>>> repr(a.contacts.get_contact(1234)) "" ``` -### Timesheets (API v1) +Get the list of contacts using: +```python +>>> repr(a.contacts.list_contacts(page=1, per_page=10)) +[""] +``` +Pagination is supported. If `page` option is not specified, then all the pages are fetched, else specified page is returned. +Contact can be filtered using name or email by passing the filter as `email=abc@xyz.com` or `mobile=123792182138` or `state=deleted` + +Other supported methods are `create_contact`, `update_contact`, `soft_delete_contact`, `permanently_delete_contact`, `restore_contact` -You can view all timesheets: +To convert a contact to an agent, use: +```python +>>> repr(a.contacts.make_agent(1)) +[""] +``` + +### Agents (API v2) +To get an agent, use: ```python ->>> a.timesheets.get_all_timesheets() -[, >> repr(a.agents.get_agent(1234)) +"" ``` -Or filter by ticket number: +Get the list of agent using: +```python +>>> repr(a.agents.list_agents(page=1, per_page=10)) +[""] +``` +Pagination is supported. If `page` option is not specified, then all the pages are fetched, else specified page is returned. +Agent can be filtered using name or email by passing the filter as `email=abc@xyz.com` or `mobile=123792182138` +Other supported methods are `update_agent`, `delete_agent` + +### Groups (API v2) + +To get the list of groups, use: ```python ->>> a.timesheets.get_timesheet_by_ticket(4) -[] +>>> repr(a.groups.list_groups(page=1, per_page=10)) +[""] ``` +Pagination is supported. If `page` option is not specified, then all the pages are fetched, else specified page is returned. + +To get a group, use: +```python +>>> repr(a.groups.get_group(1)) +[""] +``` + diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index 1f356cc..09964d6 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.0.0' +__version__ = '1.2.5' diff --git a/freshdesk/api.py b/freshdesk/api.py index 1b76905..6745423 100644 --- a/freshdesk/api.py +++ b/freshdesk/api.py @@ -25,5 +25,23 @@ def API(domain, api_key, version=1, **kwargs): except AttributeError: pass + if version == 1: + deprecation_message = """ + Freshdesk has deprecated their V1 API from 1st July, 2018. + For more info, visit https://support.freshdesk.com/support/solutions/articles/231955-important-deprecation-of-api-v1 + + For more info about freshdesk V2 API, visit https://developers.freshdesk.com/api/ + + Now python-freshdesk library will by default return V2 API client. You need to migrate your project accordingly. + + + """ + print(deprecation_message) + version = 2 + + if version != 2: + print("Freshdesk V%d API is not released yet. Returning default V2 API client\n" % version) + + version = 2 client_class = _VERSIONS[version] return client_class(domain, api_key, **kwargs) diff --git a/freshdesk/v1/api.py b/freshdesk/v1/api.py index 5849141..1492668 100644 --- a/freshdesk/v1/api.py +++ b/freshdesk/v1/api.py @@ -1,12 +1,31 @@ import requests +import json from requests.exceptions import HTTPError -from freshdesk.v1.models import Ticket, Contact, Customer, TimeEntry +from freshdesk.v1.models import Ticket, Contact, Agent, Customer, TimeEntry class TicketAPI(object): def __init__(self, api): self._api = api + def create_ticket(self, subject, **kwargs): + url = 'helpdesk/tickets.json' + status = kwargs.get('status', 2) + priority = kwargs.get('priority', 1) + cc_emails = ','.join(kwargs.get('cc_emails', [])) + ticket_data = { + 'subject': subject, + 'status': status, + 'priority': priority, + } + ticket_data.update(kwargs) + data = { + 'helpdesk_ticket': ticket_data, + 'cc_emails': cc_emails, + } + + return Ticket(**self._api._post(url, data=data)['helpdesk_ticket']) + def get_ticket(self, ticket_id): """Fetches the ticket for the given ticket ID""" url = 'helpdesk/tickets/%d.json' % ticket_id @@ -16,14 +35,15 @@ def list_tickets(self, **kwargs): """List all tickets, optionally filtered by a view. Specify filters as keyword arguments, such as: - filter_name = one of ['all_tickets', 'new_my_open', 'spam', 'deleted'] - (defaults to 'all_tickets') + filter_name = one of ['all_tickets', 'new_my_open', 'spam', 'deleted', + None] + (defaults to 'all_tickets'; passing None uses the default) Multiple filters are AND'd together. """ filter_name = 'all_tickets' - if 'filter_name' in kwargs: + if 'filter_name' in kwargs and kwargs['filter_name'] is not None: filter_name = kwargs['filter_name'] del kwargs['filter_name'] @@ -58,10 +78,118 @@ class ContactAPI(object): def __init__(self, api): self._api = api + def list_contacts(self, **kwargs): + """ + List all contacts, optionally filtered by a query. Specify filters as + query keyword argument, such as: + + query= email is abc@xyz.com, + query= mobile is 1234567890, + query= phone is 1234567890, + + contacts can be filtered by name such as; + + letter=Prenit + + Passing None means that no named filter will be passed to + Freshdesk, which returns list of all contacts + + """ + + url = 'contacts.json?' + if 'query' in kwargs.keys(): + filter_query = kwargs.pop('query') + url = url + "query={}".format(filter_query) + + if 'state' in kwargs.keys(): + state_query = kwargs.pop('state') + url = url + "state={}".format(state_query) + + if 'letter' in kwargs.keys(): + name_query = kwargs.pop('letter') + url = url + "letter={}".format(name_query) + + contacts = self._api._get(url) + return [Contact(**c['user']) for c in contacts] + + def create_contact(self, *args, **kwargs): + """Creates a contact""" + url = 'contacts.json' + contact_data = { + 'active': True, + 'helpdesk_agent': False, + 'description': 'Freshdesk Contact' + } + contact_data.update(kwargs) + payload = { + 'user': contact_data + } + + return Contact(**self._api._post(url, data=payload)['user']) + + def make_agent(self, contact_id): + url = 'contacts/%d/make_agent.json' % contact_id + agent = self._api._put(url)['agent'] + return self._api.agents.get_agent(agent['id']) + def get_contact(self, contact_id): - url = 'contacts/%s.json' % contact_id + url = 'contacts/%d.json' % contact_id return Contact(**self._api._get(url)['user']) + def delete_contact(self, contact_id): + url = 'contacts/%d.json' % contact_id + self._api._delete(url) + + +class AgentAPI(object): + def __init__(self, api): + self._api = api + + def list_agents(self, **kwargs): + """List all agents, optionally filtered by a query. Specify filters as + query keyword argument, such as: + + query= email is abc@xyz.com, + query= mobile is 1234567890, + query= phone is 1234567890, + + agents can be filtered by state such as: + + state=active/occasional + + Passing None means that no named filter will be passed to + Freshdesk, which returns list of all agents + + """ + + url = 'agents.json?' + if 'query' in kwargs.keys(): + filter_query = kwargs.pop('query') + url = url + "query={}".format(filter_query) + + if 'state' in kwargs.keys(): + state_query = kwargs.pop('state') + url = url + "state={}".format(state_query) + + agents = self._api._get(url) + return [Agent(**a['agent']) for a in agents] + + def get_agent(self, agent_id): + """Fetches the agent for the given agent ID""" + url = 'agents/%s.json' % agent_id + return Agent(**self._api._get(url)['agent']) + + def update_agent(self, agent_id, **kwargs): + """Updates an agent""" + url = 'agents/%s.json' % agent_id + agent = self._api._put(url, data=json.dumps(kwargs))['agent'] + return Agent(**agent) + + def delete_agent(self, agent_id): + """Delete the agent for the given agent ID""" + url = 'agents/%d.json' % agent_id + self._api._delete(url) + class CustomerAPI(object): def __init__(self, api): @@ -114,24 +242,77 @@ def __init__(self, domain, api_key): .tickets: the Ticket API """ - self._api_prefix = 'http://{}/'.format(domain.rstrip('/')) - self._session = requests.Session() - self._session.auth = (api_key, 'unused_with_api_key') - self._session.headers = {'Content-Type': 'application/json'} + self._api_prefix = 'https://{}/'.format(domain.rstrip('/')) + self.auth = (api_key, 'X') + self.headers = {'Content-Type': 'application/json'} self.tickets = TicketAPI(self) self.contacts = ContactAPI(self) + self.agents = AgentAPI(self) self.timesheets = TimeAPI(self) self.customers = CustomerAPI(self) def _get(self, url, params={}): """Wrapper around request.get() to use the API prefix. Returns a JSON response.""" - r = self._session.get(self._api_prefix + url, params=params) - r.raise_for_status() - if 'Retry-After' in r.headers: + r = requests.get(self._api_prefix + url, + params=params, + headers=self.headers, + auth=self.auth, + ) + return self._action(r) + + def _post(self, url, data={}): + """Wrapper around request.post() to use the API prefix. Returns a JSON response.""" + r = requests.post(self._api_prefix + url, + data=json.dumps(data), + headers=self.headers, + auth=self.auth, + allow_redirects=False, + ) + return self._action(r) + + def _put(self, url, data={}): + """Wrapper around request.put() to use the API prefix. Returns a JSON response.""" + r = requests.put(self._api_prefix + url, + data=json.dumps(data), + headers=self.headers, + auth=self.auth, + allow_redirects=False, + ) + return self._action(r) + + def _delete(self, url): + """Wrapper around request.delete() to use the API prefix. Returns a JSON response.""" + r = requests.delete(self._api_prefix + url, + headers=self.headers, + auth=self.auth, + allow_redirects=False, + ) + return self._action(r) + + def _action(self, res): + """Returns JSON response or raise exception if errors are present""" + try: + j = res.json() + except: + res.raise_for_status() + j = {} + + if 'Retry-After' in res.headers: raise HTTPError('403 Forbidden: API rate-limit has been reached until {}.' - 'See http://freshdesk.com/api#ratelimit'.format(r.headers['Retry-After'])) - j = r.json() + 'See http://freshdesk.com/api#ratelimit'.format(res.headers['Retry-After'])) + if 'require_login' in j: raise HTTPError('403 Forbidden: API key is incorrect for this domain') - return r.json() + + if 'error' in j: + raise HTTPError('{}: {}'.format(j.get('description'), + j.get('errors'))) + + # Catch any other errors + try: + res.raise_for_status() + except Exception as e: + raise HTTPError("{}: {}".format(e, j)) + + return j diff --git a/freshdesk/v1/models.py b/freshdesk/v1/models.py index 7fb600a..ea2df76 100644 --- a/freshdesk/v1/models.py +++ b/freshdesk/v1/models.py @@ -2,9 +2,10 @@ class FreshdeskModel(object): - _keys = set() + _keys = None def __init__(self, **kwargs): + self._keys = set() if "custom_field" in kwargs.keys() and len(kwargs["custom_field"]) > 0: custom_fields = kwargs.pop("custom_field") kwargs.update(custom_fields) @@ -13,6 +14,7 @@ def __init__(self, **kwargs): k = '_' + k setattr(self, k, v) self._keys.add(k) + self.created_at = self._to_timestamp(self.created_at) self.updated_at = self._to_timestamp(self.updated_at) @@ -68,6 +70,14 @@ def __repr__(self): return ''.format(self.name) +class Agent(FreshdeskModel): + def __str__(self): + return self.user['name'] + + def __repr__(self): + return ''.format(self.id, self.user['name']) + + class TimeEntry(FreshdeskModel): def __str__(self): return str(self.id) diff --git a/freshdesk/v1/sample_json_data/contact.json b/freshdesk/v1/sample_json_data/contact.json deleted file mode 100644 index 2aacd04..0000000 --- a/freshdesk/v1/sample_json_data/contact.json +++ /dev/null @@ -1 +0,0 @@ -{"user":{"active":false,"address":null,"created_at":"2014-12-31T12:27:09+10:00","customer_id":1,"deleted":false,"description":null,"email":"rachel@freshdesk.com","external_id":null,"fb_profile_id":null,"helpdesk_agent":false,"id":5004272351,"job_title":null,"language":"en","mobile":null,"name":"Rachel","phone":null,"time_zone":"Brisbane","twitter_id":null,"updated_at":"2014-12-31T12:27:09+10:00","company_id":null,"custom_field":{}}} diff --git a/freshdesk/v1/sample_json_data/contact5004272350.json b/freshdesk/v1/sample_json_data/contact5004272350.json deleted file mode 100644 index 35da56d..0000000 --- a/freshdesk/v1/sample_json_data/contact5004272350.json +++ /dev/null @@ -1 +0,0 @@ -{"user":{"active":false,"address":null,"created_at":"2014-12-31T12:27:09+10:00","customer_id":null,"deleted":false,"description":null,"email":"william@freshdesk.com","external_id":null,"fb_profile_id":null,"helpdesk_agent":true,"id":5004272350,"job_title":"Helpdesk Engineer","language":"en","mobile":null,"name":"William","phone":null,"time_zone":"Amsterdam","twitter_id":null,"updated_at":"2014-12-31T12:27:09+10:00","company_id":null,"custom_field":{}}} diff --git a/freshdesk/v1/sample_json_data/ticket_1.json b/freshdesk/v1/sample_json_data/ticket_1.json deleted file mode 100644 index d5a2615..0000000 --- a/freshdesk/v1/sample_json_data/ticket_1.json +++ /dev/null @@ -1 +0,0 @@ -{"helpdesk_ticket":{"cc_email":{"cc_emails":[],"fwd_emails":[],"reply_cc":[]},"created_at":"2014-12-31T12:27:09+10:00","deleted":false,"delta":true,"description":"This is a sample ticket, feel free to delete it.","description_html":"\u003Cdiv\u003EThis is a sample ticket, feel free to delete it.\u003C/div\u003E","display_id":1,"due_by":"2015-01-05T12:27:09+10:00","email_config_id":null,"frDueBy":"2015-01-01T12:27:09+10:00","fr_escalated":false,"group_id":null,"id":5007268642,"isescalated":false,"notes":[{"note":{"body":"This is a reply.","body_html":"\u003Cdiv style=\"font-size: 13px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\"\u003E\n\u003Cdiv\u003E\u003C/div\u003E\n\u003Cdiv\u003E\u003Cdiv\u003EThis is a reply.\u003C/div\u003E\u003C/div\u003E\r\n\u003C/div\u003E","created_at":"2015-01-01T10:58:36+10:00","deleted":false,"id":5006713943,"incoming":false,"private":false,"source":0,"updated_at":"2015-01-01T10:58:36+10:00","user_id":5004272350,"attachments":[],"support_email":"support@pythonfreshdesk.freshdesk.com"}}],"owner_id":null,"priority":1,"requester_id":5004272351,"responder_id":5004272350,"source":2,"spam":false,"status":2,"subject":"This is a sample ticket","ticket_type":"Question","to_email":null,"trained":false,"updated_at":"2015-01-01T10:58:39+10:00","urgent":false,"status_name":"Open","requester_status_name":"Being Processed","priority_name":"Low","source_name":"Portal","requester_name":"Rachel","responder_name":"William","to_emails":null,"product_id":null,"attachments":[],"custom_field":{},"tags":[]}} diff --git a/freshdesk/v1/test.py b/freshdesk/v1/test.py deleted file mode 100644 index b1d57d2..0000000 --- a/freshdesk/v1/test.py +++ /dev/null @@ -1,244 +0,0 @@ -import datetime -import json -import re -import os.path -import responses -from unittest import TestCase - -from freshdesk.v1.api import API -from freshdesk.v1.models import Ticket, Comment, Contact, Customer, TimeEntry - - -""" -Test suite for python-freshdesk. - -We test against a dummy helpdesk created for these tests only. It is: -http://pythonfreshdesk.freshdesk.com/ -""" - -DOMAIN = 'pythonfreshdesk.freshdesk.com' -API_KEY = 'MX4CEAw4FogInimEdRW2' - - -class MockedAPI(API): - def __init__(self, *args): - self.resolver = { - re.compile(r'helpdesk/tickets/filter/all_tickets\?format=json&page=1'): self.read_test_file('all_tickets.json'), - re.compile(r'helpdesk/tickets/filter/new_my_open\?format=json&page=1'): self.read_test_file('all_tickets.json'), - re.compile(r'helpdesk/tickets/filter/spam\?format=json&page=1'): [], - re.compile(r'helpdesk/tickets/filter/deleted\?format=json&page=1'): [], - re.compile(r'helpdesk/tickets/1/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), - re.compile(r'helpdesk/tickets/1.json'): self.read_test_file('ticket_1.json'), - re.compile(r'.*&page=2'): [], - re.compile(r'contacts/5004272351.json'): self.read_test_file('contact.json'), - re.compile(r'contacts/5004272350.json'): self.read_test_file('contact5004272350.json'), - re.compile(r'customers/1.json'): self.read_test_file('customer.json'), - re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), - re.compile(r'helpdesk/time_sheets.json\?agent_id='): self.read_test_file('timeentries_ticket_1.json'), - re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), - } - super(MockedAPI, self).__init__(*args) - - def read_test_file(self, filename): - path = os.path.join(os.path.dirname(__file__), 'sample_json_data', filename) - return json.loads(open(path, 'r').read()) - - def _get(self, url, *args, **kwargs): - for pattern, j in self.resolver.items(): - if pattern.match(url): - return j - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) - - -class TestAPIClass(TestCase): - def test_api_prefix(self): - api = API('test_domain', 'test_key') - self.assertEqual(api._api_prefix, 'http://test_domain/') - api = API('test_domain/', 'test_key') - self.assertEqual(api._api_prefix, 'http://test_domain/') - - @responses.activate - def test_403_error(self): - responses.add(responses.GET, - 'http://{}/helpdesk/tickets/1.json'.format(DOMAIN), - status=403) - - api = API(DOMAIN, 'invalid_api_key') - from requests.exceptions import HTTPError - with self.assertRaises(HTTPError): - api.tickets.get_ticket(1) - - @responses.activate - def test_404_error(self): - DOMAIN_404 = 'google.com' - responses.add(responses.GET, - 'http://{}/helpdesk/tickets/1.json'.format(DOMAIN_404), - status=404) - - api = API(DOMAIN_404, 'invalid_api_key') - from requests.exceptions import HTTPError - with self.assertRaises(HTTPError): - api.tickets.get_ticket(1) - - -class TestTicket(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.ticket = cls.api.tickets.get_ticket(1) - - def test_str(self): - self.assertEqual(str(self.ticket), 'This is a sample ticket') - - def test_repr(self): - self.assertEqual(repr(self.ticket), '') - - def test_get_ticket(self): - self.assertIsInstance(self.ticket, Ticket) - self.assertEqual(self.ticket.display_id, 1) - self.assertEqual(self.ticket.subject, 'This is a sample ticket') - self.assertEqual(self.ticket.description, 'This is a sample ticket, feel free to delete it.') - - def test_ticket_priority(self): - self.assertEqual(self.ticket._priority, 1) - self.assertEqual(self.ticket.priority, 'low') - - def test_ticket_status(self): - self.assertEqual(self.ticket._status, 2) - self.assertEqual(self.ticket.status, 'open') - - def test_ticket_source(self): - self.assertEqual(self.ticket._source, 2) - self.assertEqual(self.ticket.source, 'portal') - - def test_ticket_datetime(self): - self.assertIsInstance(self.ticket.created_at, datetime.datetime) - self.assertIsInstance(self.ticket.updated_at, datetime.datetime) - - def test_all_tickets(self): - tickets = self.api.tickets.list_all_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].display_id, self.ticket.display_id) - - def test_open_tickets(self): - tickets = self.api.tickets.list_open_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].display_id, self.ticket.display_id) - - def test_deleted_tickets(self): - tickets = self.api.tickets.list_deleted_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 0) - - def test_spam_tickets(self): - tickets = self.api.tickets.list_tickets(filter_name='spam') - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 0) - - def test_default_filter_name(self): - tickets = self.api.tickets.list_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].display_id, self.ticket.display_id) - - -class TestComment(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.ticket = cls.api.tickets.get_ticket(1) - - def test_comments_list(self): - self.assertIsInstance(self.ticket.comments, list) - self.assertEqual(len(self.ticket.comments), 1) - self.assertIsInstance(self.ticket.comments[0], Comment) - - def test_comment_str(self): - self.assertEqual(str(self.ticket.comments[0]), 'This is a reply.') - - def test_comment_repr(self): - self.assertEqual(repr(self.ticket.comments[0]), '>') - - -class TestContact(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.contact = cls.api.contacts.get_contact('5004272351') - - def test_get_contact(self): - self.assertIsInstance(self.contact, Contact) - self.assertEqual(self.contact.name, 'Rachel') - self.assertEqual(self.contact.email, 'rachel@freshdesk.com') - self.assertEqual(self.contact.helpdesk_agent, False) - self.assertEqual(self.contact.customer_id, 1) - - def test_contact_datetime(self): - self.assertIsInstance(self.contact.created_at, datetime.datetime) - self.assertIsInstance(self.contact.updated_at, datetime.datetime) - - def test_contact_str(self): - self.assertEqual(str(self.contact), 'Rachel') - - def test_contact_repr(self): - self.assertEqual(repr(self.contact), '') - - -class TestCustomer(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.customer = cls.api.customers.get_customer('1') - cls.contact = cls.api.contacts.get_contact('5004272351') - - def test_customer(self): - self.assertIsInstance(self.customer, Customer) - self.assertEqual(self.customer.name, 'ACME Corp.') - self.assertEqual(self.customer.domains, 'acme.com') - self.assertEqual(self.customer.cf_custom_key, 'custom_value') - - def test_contact_datetime(self): - self.assertIsInstance(self.customer.created_at, datetime.datetime) - self.assertIsInstance(self.customer.updated_at, datetime.datetime) - - def test_contact_str(self): - self.assertEqual(str(self.customer), 'ACME Corp.') - - def test_contact_repr(self): - self.assertEqual(repr(self.customer), '') - - def test_get_customer_from_contact(self): - self.customer = self.api.customers.get_customer_from_contact(self.contact) - self.test_customer() - - -class TestTimesheets(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.timesheet = cls.api.timesheets.get_timesheet_by_ticket(1) - - def test_timesheet(self): - self.assertIsInstance(self.timesheet, type([])) - self.assertEqual(len(self.timesheet), 3) - self.assertIsInstance(self.timesheet[1], TimeEntry) - self.assertEqual(self.timesheet[1].id, 6000041896) - self.assertEqual(self.timesheet[1].note, "Foo") - self.assertEqual(self.timesheet[1].timespent, "0.33") - - def test_timesheet_str(self): - self.assertEqual(str(self.timesheet[1]), "6000041896") - - def test_timesheet_repr(self): - self.assertEqual(repr(self.timesheet[1]), '') - - def test_get_all_timesheets(self): - self.timesheet = self.api.timesheets.get_all_timesheets() - self.test_timesheet() - self.timesheet = self.api.timesheets.get_all_timesheets(filter_name="agent_id", filter_value="5004272350") - self.test_timesheet() diff --git a/freshdesk/v1/tests/__init__.py b/freshdesk/v1/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freshdesk/v1/tests/conftest.py b/freshdesk/v1/tests/conftest.py new file mode 100644 index 0000000..4b6eec4 --- /dev/null +++ b/freshdesk/v1/tests/conftest.py @@ -0,0 +1,97 @@ +import json +import os.path +import re + +import pytest + +from freshdesk.v1.api import API + +DOMAIN = 'pythonfreshdesk.freshdesk.com' +API_KEY = 'MX4CEAw4FogInimEdRW2' + + +class MockedAPI(API): + def __init__(self, *args): + self.resolver = { + 'get': { + re.compile(r'helpdesk/tickets/filter/all_tickets\?format=json&page=1'): self.read_test_file( + 'all_tickets.json'), + re.compile(r'helpdesk/tickets/filter/new_my_open\?format=json&page=1'): self.read_test_file( + 'all_tickets.json'), + re.compile(r'helpdesk/tickets/filter/spam\?format=json&page=1'): [], + re.compile(r'helpdesk/tickets/filter/deleted\?format=json&page=1'): [], + re.compile(r'helpdesk/tickets/1/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + re.compile(r'helpdesk/tickets/1.json'): self.read_test_file('ticket_1.json'), + re.compile(r'.*&page=2'): [], + re.compile(r'contacts.json'): self.read_test_file('contacts.json'), + re.compile(r'contacts/1.json'): self.read_test_file('contact.json'), + re.compile(r'contacts/1.json'): self.read_test_file('contact.json'), + re.compile(r'agents.json\?$'): self.read_test_file('agents.json'), + re.compile(r'agents/1.json$'): self.read_test_file('agent_1.json'), + re.compile(r'customers/1.json'): self.read_test_file('customer.json'), + re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + re.compile(r'helpdesk/time_sheets.json\?agent_id='): self.read_test_file('timeentries_ticket_1.json'), + re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + }, + 'post': { + re.compile(r'helpdesk/tickets.json'): self.read_test_file('ticket_1.json'), + re.compile(r'contacts.json'): self.read_test_file('contact.json'), + re.compile(r'agents/1.json$'): self.read_test_file('agent_1.json'), + }, + 'put': { + re.compile(r'contacts/1/make_agent.json'): self.read_test_file('agent_1.json'), + re.compile(r'agents/1.json$'): self.read_test_file('agent_1_updated.json'), + }, + 'delete': { + re.compile(r'helpdesk/tickets/1.json'): None, + re.compile(r'contacts/1.json'): None, + re.compile(r'agents/1.json$'): None, + } + } + + super(MockedAPI, self).__init__(*args) + + def read_test_file(self, filename): + path = os.path.join(os.path.dirname(__file__), 'sample_json_data', filename) + return json.loads(open(path, 'r').read()) + + def _get(self, url, *args, **kwargs): + for pattern, j in self.resolver['get'].items(): + if pattern.match(url): + return j + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) + + def _post(self, url, *args, **kwargs): + for pattern, data in self.resolver['post'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_post() has no pattern for \'{}\''.format(url)) + + def _put(self, url, *args, **kwargs): + for pattern, data in self.resolver['put'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_put() has no pattern for \'{}\''.format(url)) + + def _delete(self, url, *args, **kwargs): + for pattern, data in self.resolver['delete'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_delete() has no pattern for \'{}\''.format(url)) + + +@pytest.fixture() +def api(): + return MockedAPI(DOMAIN, API_KEY) diff --git a/freshdesk/v1/tests/sample_json_data/agent_1.json b/freshdesk/v1/tests/sample_json_data/agent_1.json new file mode 100644 index 0000000..0c01eef --- /dev/null +++ b/freshdesk/v1/tests/sample_json_data/agent_1.json @@ -0,0 +1,36 @@ +{ + "agent":{ + "active_since":null, + "available":true, + "created_at":"2015-01-02T22:56:39-10:00", + "id":1, + "occasional":false, + "points":0, + "scoreboard_level_id":1, + "signature":null, + "signature_html":"\u003Cp\u003E\u003Cbr\u003E\u003C/p\u003E\r\n", + "ticket_permission":1, + "updated_at":"2015-01-04T23:09:52-10:00", + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-30T18:49:39-10:00", + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":true, + "id":1, + "job_title":"Agent", + "language":"en", + "mobile":1234, + "name":"Rachel", + "phone":5678, + "time_zone":"Chennai", + "twitter_id":null, + "updated_at":"2015-01-04T23:09:51-10:00" + }, + "user_id":1 + } +} diff --git a/freshdesk/v1/tests/sample_json_data/agent_1_updated.json b/freshdesk/v1/tests/sample_json_data/agent_1_updated.json new file mode 100644 index 0000000..e2e8a9b --- /dev/null +++ b/freshdesk/v1/tests/sample_json_data/agent_1_updated.json @@ -0,0 +1,36 @@ +{ + "agent":{ + "active_since":null, + "available":true, + "created_at":"2015-01-02T22:56:39-10:00", + "id":1, + "occasional":true, + "points":0, + "scoreboard_level_id":1, + "signature":null, + "signature_html":"\u003Cp\u003E\u003Cbr\u003E\u003C/p\u003E\r\n", + "ticket_permission":1, + "updated_at":"2015-01-04T23:09:52-10:00", + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-30T18:49:39-10:00", + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":true, + "id":1, + "job_title":"Agent", + "language":"en", + "mobile":1234, + "name":"Updated Name", + "phone":5678, + "time_zone":"Chennai", + "twitter_id":null, + "updated_at":"2015-01-04T23:09:51-10:00" + }, + "user_id":1 + } +} diff --git a/freshdesk/v1/tests/sample_json_data/agents.json b/freshdesk/v1/tests/sample_json_data/agents.json new file mode 100644 index 0000000..a513fe6 --- /dev/null +++ b/freshdesk/v1/tests/sample_json_data/agents.json @@ -0,0 +1,74 @@ +[ + { + "agent":{ + "active_since":null, + "available":true, + "created_at":"2015-01-02T22:56:39-10:00", + "id":1, + "occasional":false, + "points":0, + "scoreboard_level_id":1, + "signature":null, + "signature_html":"\u003Cp\u003E\u003Cbr\u003E\u003C/p\u003E\r\n", + "ticket_permission":1, + "updated_at":"2015-01-04T23:09:52-10:00", + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-30T18:49:39-10:00", + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":true, + "id":1, + "job_title":"Agent", + "language":"en", + "mobile":1234, + "name":"Rachel", + "phone":5678, + "time_zone":"Chennai", + "twitter_id":null, + "updated_at":"2015-01-04T23:09:51-10:00" + }, + "user_id":1 + } + }, + { + "agent":{ + "active_since":null, + "available":true, + "created_at":"2015-01-02T22:56:39-10:00", + "id":2, + "occasional":false, + "points":0, + "scoreboard_level_id":1, + "signature":null, + "signature_html":"\u003Cp\u003E\u003Cbr\u003E\u003C/p\u003E\r\n", + "ticket_permission":1, + "updated_at":"2015-01-04T23:09:52-10:00", + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-30T18:49:39-10:00", + "deleted":false, + "description":null, + "email":"sam@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":true, + "id":2, + "job_title":"Agent", + "language":"en", + "mobile":"", + "name":"Sam", + "phone":"", + "time_zone":"Chennai", + "twitter_id":null, + "updated_at":"2015-01-04T23:09:51-10:00" + }, + "user_id":2 + } + } +] diff --git a/freshdesk/v1/sample_json_data/all_tickets.json b/freshdesk/v1/tests/sample_json_data/all_tickets.json similarity index 100% rename from freshdesk/v1/sample_json_data/all_tickets.json rename to freshdesk/v1/tests/sample_json_data/all_tickets.json diff --git a/freshdesk/v1/tests/sample_json_data/contact.json b/freshdesk/v1/tests/sample_json_data/contact.json new file mode 100644 index 0000000..388f973 --- /dev/null +++ b/freshdesk/v1/tests/sample_json_data/contact.json @@ -0,0 +1,27 @@ +{ + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-31T12:27:09+10:00", + "customer_id":1, + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":false, + "id":1, + "job_title":null, + "language":"en", + "mobile":null, + "name":"Rachel", + "phone":null, + "time_zone":"Brisbane", + "twitter_id":null, + "updated_at":"2014-12-31T12:27:09+10:00", + "company_id":null, + "custom_field":{ + + } + } +} diff --git a/freshdesk/v1/tests/sample_json_data/contacts.json b/freshdesk/v1/tests/sample_json_data/contacts.json new file mode 100644 index 0000000..5565162 --- /dev/null +++ b/freshdesk/v1/tests/sample_json_data/contacts.json @@ -0,0 +1,56 @@ +[ + { + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-31T12:27:09+10:00", + "customer_id":1, + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":false, + "id":1, + "job_title":null, + "language":"en", + "mobile":null, + "name":"Rachel", + "phone":null, + "time_zone":"Brisbane", + "twitter_id":null, + "updated_at":"2014-12-31T12:27:09+10:00", + "company_id":null, + "custom_field":{ + + } + } + }, + { + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-31T12:27:09+10:00", + "customer_id":2, + "deleted":false, + "description":null, + "email":"sam@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":false, + "id":2, + "job_title":null, + "language":"en", + "mobile":null, + "name":"Sam", + "phone":null, + "time_zone":"Brisbane", + "twitter_id":null, + "updated_at":"2014-12-31T12:27:09+10:00", + "company_id":null, + "custom_field":{ + + } + } + } +] diff --git a/freshdesk/v1/sample_json_data/customer.json b/freshdesk/v1/tests/sample_json_data/customer.json similarity index 100% rename from freshdesk/v1/sample_json_data/customer.json rename to freshdesk/v1/tests/sample_json_data/customer.json diff --git a/freshdesk/v1/tests/sample_json_data/ticket_1.json b/freshdesk/v1/tests/sample_json_data/ticket_1.json new file mode 100644 index 0000000..ba27666 --- /dev/null +++ b/freshdesk/v1/tests/sample_json_data/ticket_1.json @@ -0,0 +1,79 @@ +{ + "helpdesk_ticket": { + "cc_email": { + "cc_emails": [ + "test2@example.com" + ], + "fwd_emails": [ + + ], + "reply_cc": [ + + ] + }, + "created_at": "2014-12-31T12:27:09+10:00", + "deleted": false, + "delta": true, + "description": "This is a sample ticket, feel free to delete it.", + "description_html": "
This is a sample ticket, feel free to delete it.<\/div>", + "display_id": 1, + "due_by": "2015-01-05T12:27:09+10:00", + "email_config_id": null, + "frDueBy": "2015-01-01T12:27:09+10:00", + "fr_escalated": false, + "group_id": null, + "id": 5007268642, + "isescalated": false, + "notes": [ + { + "note": { + "body": "This is a reply.", + "body_html": "
\n
<\/div>\n
This is a reply.<\/div><\/div>\r\n<\/div>", + "created_at": "2015-01-01T10:58:36+10:00", + "deleted": false, + "id": 5006713943, + "incoming": false, + "private": false, + "source": 0, + "updated_at": "2015-01-01T10:58:36+10:00", + "user_id": 5004272350, + "attachments": [ + + ], + "support_email": "support@pythonfreshdesk.freshdesk.com" + } + } + ], + "owner_id": null, + "priority": 1, + "requester_id": 5004272351, + "responder_id": 5004272350, + "source": 2, + "spam": false, + "status": 2, + "subject": "This is a sample ticket", + "ticket_type": "Question", + "to_email": null, + "trained": false, + "updated_at": "2015-01-01T10:58:39+10:00", + "urgent": false, + "status_name": "Open", + "requester_status_name": "Being Processed", + "priority_name": "Low", + "source_name": "Portal", + "requester_name": "Rachel", + "responder_name": "William", + "to_emails": null, + "product_id": null, + "attachments": [ + + ], + "custom_field": { + + }, + "tags": [ + "foo", + "bar" + ] + } +} diff --git a/freshdesk/v1/sample_json_data/timeentries_ticket_1.json b/freshdesk/v1/tests/sample_json_data/timeentries_ticket_1.json similarity index 100% rename from freshdesk/v1/sample_json_data/timeentries_ticket_1.json rename to freshdesk/v1/tests/sample_json_data/timeentries_ticket_1.json diff --git a/freshdesk/v1/tests/test_agent.py b/freshdesk/v1/tests/test_agent.py new file mode 100644 index 0000000..4667fa2 --- /dev/null +++ b/freshdesk/v1/tests/test_agent.py @@ -0,0 +1,77 @@ +import datetime +import json +import os.path + +import pytest + +from freshdesk.v1.models import Agent + + +@pytest.fixture +def agent(api): + return api.agents.get_agent(1) + + +@pytest.fixture +def agent_json(): + return json.loads(open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'agent_1.json')).read()) + + +def test_str(agent): + assert str(agent) == 'Rachel' + + +def test_repr(agent): + assert repr(agent) == '' + + +def test_list_agents(api, agent): + agents = api.agents.list_agents() + assert isinstance(agents, list) + assert len(agents) == 2 + assert agents[0].id == agent.id + + +def test_get_agent(agent): + assert isinstance(agent, Agent) + assert agent.id == 1 + assert agent.user['name'] == 'Rachel' + assert agent.user['email'] == 'rachel@freshdesk.com' + assert agent.user['mobile'] == 1234 + assert agent.user['phone'] == 5678 + assert agent.occasional is False + + +def test_update_agent(api): + values = { + 'occasional': True, + 'contact': { + 'name': 'Updated Name' + } + } + agent = api.agents.update_agent(1, **values) + + assert agent.occasional is True + assert agent.user['name'] == 'Updated Name' + + +def test_delete_agent(api): + assert api.agents.delete_agent(1) is None + + +def test_agent_name(agent): + assert agent.user['name'] == 'Rachel' + + +def test_agent_mobile(agent): + assert agent.user['mobile'] == 1234 + + +def test_agent_state(agent): + assert agent.available is True + assert agent.occasional is False + + +def test_agent_datetime(agent): + assert isinstance(agent.created_at, datetime.datetime) + assert isinstance(agent.updated_at, datetime.datetime) diff --git a/freshdesk/v1/tests/test_api_class.py b/freshdesk/v1/tests/test_api_class.py new file mode 100644 index 0000000..936edee --- /dev/null +++ b/freshdesk/v1/tests/test_api_class.py @@ -0,0 +1,37 @@ +import pytest +import responses + +from freshdesk.v1.api import API +from freshdesk.v1.tests.conftest import DOMAIN + + +def test_api_prefix(): + api = API('test_domain', 'test_key') + assert api._api_prefix == 'https://test_domain/' + api = API('test_domain/', 'test_key') + assert api._api_prefix == 'https://test_domain/' + + +@responses.activate +def test_403_error(): + responses.add(responses.GET, + 'https://{}/helpdesk/tickets/1.json'.format(DOMAIN), + status=403) + + api = API(DOMAIN, 'invalid_api_key') + from requests.exceptions import HTTPError + with pytest.raises(HTTPError): + api.tickets.get_ticket(1) + + +@responses.activate +def test_404_error(): + DOMAIN_404 = 'google.com' + responses.add(responses.GET, + 'https://{}/helpdesk/tickets/1.json'.format(DOMAIN_404), + status=404) + + api = API(DOMAIN_404, 'invalid_api_key') + from requests.exceptions import HTTPError + with pytest.raises(HTTPError): + api.tickets.get_ticket(1) diff --git a/freshdesk/v1/tests/test_comment.py b/freshdesk/v1/tests/test_comment.py new file mode 100644 index 0000000..b323b09 --- /dev/null +++ b/freshdesk/v1/tests/test_comment.py @@ -0,0 +1,22 @@ +import pytest + +from freshdesk.v1.models import Comment + + +@pytest.fixture +def ticket(api): + return api.tickets.get_ticket(1) + + +def test_comments_list(ticket): + assert isinstance(ticket.comments, list) + assert len(ticket.comments) == 1 + assert isinstance(ticket.comments[0], Comment) + + +def test_comment_str(ticket): + assert str(ticket.comments[0]) == 'This is a reply.' + + +def test_comment_repr(ticket): + assert repr(ticket.comments[0]) == '>' diff --git a/freshdesk/v1/tests/test_contact.py b/freshdesk/v1/tests/test_contact.py new file mode 100644 index 0000000..7718744 --- /dev/null +++ b/freshdesk/v1/tests/test_contact.py @@ -0,0 +1,68 @@ +import datetime + +import pytest + +from freshdesk.v1.models import Contact, Agent + + +@pytest.fixture +def contact(api): + return api.contacts.get_contact(1) + + +def test_get_contact(contact): + assert isinstance(contact, Contact) + assert contact.name == 'Rachel' + assert contact.email == 'rachel@freshdesk.com' + assert contact.helpdesk_agent is False + assert contact.customer_id == 1 + + +def test_list_contacts(api, contact): + contacts = api.contacts.list_contacts() + assert isinstance(contacts, list) + assert len(contacts) == 2 + assert isinstance(contacts[0], Contact) + assert contacts[0].id == contact.id + assert contacts[0].email == contact.email + assert contacts[0].name == contact.name + + +def test_create_contact(api): + contact_data = { + 'name': 'Rachel', + 'email': 'rachel@freshdesk.com' + } + contact = api.contacts.create_contact(contact_data) + assert isinstance(contact, Contact) + assert contact.id == contact.id + assert contact.email == contact.email + assert contact.name == contact.name + + +def test_make_agent(api, contact): + agent = api.contacts.make_agent(contact.id) + assert isinstance(agent, Agent) + assert agent.available is True + assert agent.occasional is False + assert agent.id == 1 + assert agent.user_id == contact.id + assert agent.user['email'] == contact.email + assert agent.user['name'] == contact.name + + +def test_delete_contact(api): + assert api.contacts.delete_contact(1) is None + + +def test_contact_datetime(contact): + assert isinstance(contact.created_at, datetime.datetime) + assert isinstance(contact.updated_at, datetime.datetime) + + +def test_contact_str(contact): + assert str(contact) == 'Rachel' + + +def test_contact_repr(contact): + assert repr(contact) == '' diff --git a/freshdesk/v1/tests/test_customer.py b/freshdesk/v1/tests/test_customer.py new file mode 100644 index 0000000..1744a60 --- /dev/null +++ b/freshdesk/v1/tests/test_customer.py @@ -0,0 +1,40 @@ +import datetime + +import pytest + +from freshdesk.v1.models import Customer + + +@pytest.fixture +def customer(api): + return api.customers.get_customer(1) + + +@pytest.fixture +def contact(api): + return api.contacts.get_contact(1) + + +def test_customer(customer): + assert isinstance(customer, Customer) + assert customer.name == 'ACME Corp.' + assert customer.domains == 'acme.com' + assert customer.cf_custom_key == 'custom_value' + + +def test_contact_datetime(customer): + assert isinstance(customer.created_at, datetime.datetime) + assert isinstance(customer.updated_at, datetime.datetime) + + +def test_contact_str(customer): + assert str(customer) == 'ACME Corp.' + + +def test_contact_repr(customer): + assert repr(customer) == '' + + +def test_get_customer_from_contact(api, contact): + customer = api.customers.get_customer_from_contact(contact) + test_customer(customer) diff --git a/freshdesk/v1/tests/test_ticket.py b/freshdesk/v1/tests/test_ticket.py new file mode 100644 index 0000000..88dbd71 --- /dev/null +++ b/freshdesk/v1/tests/test_ticket.py @@ -0,0 +1,109 @@ +import datetime +import json +import os.path + +import pytest + +from freshdesk.v1.models import Ticket + + +@pytest.fixture +def ticket(api): + return api.tickets.get_ticket(1) + + +@pytest.fixture +def ticket_json(): + return json.loads(open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'ticket_1.json')).read()) + + +def test_str(ticket): + assert str(ticket) == 'This is a sample ticket' + + +def test_repr(ticket): + assert repr(ticket) == '' + + +def test_create_ticket(api): + ticket = api.tickets.create_ticket('This is a sample ticket', + description='This is a sample ticket, feel free to delete it.', + email='test@example.com', + priority=1, status=2, + tags=['foo', 'bar'], + cc_emails=['test2@example.com']) + assert isinstance(ticket, Ticket) + assert ticket.subject == 'This is a sample ticket' + assert ticket.description, 'This is a sample ticket == feel free to delete it.' + assert ticket.priority == 'low' + assert ticket.status == 'open' + assert ticket.cc_email['cc_emails'] == ['test2@example.com'] + assert 'foo' in ticket.tags + assert 'bar' in ticket.tags + + +def test_get_ticket(ticket): + assert isinstance(ticket, Ticket) + assert ticket.display_id == 1 + assert ticket.subject == 'This is a sample ticket' + assert ticket.description, 'This is a sample ticket == feel free to delete it.' + + +def test_ticket_priority(ticket): + assert ticket._priority == 1 + assert ticket.priority == 'low' + + +def test_ticket_status(ticket): + assert ticket._status == 2 + assert ticket.status == 'open' + + +def test_ticket_source(ticket): + assert ticket._source == 2 + assert ticket.source == 'portal' + + +def test_ticket_datetime(ticket): + assert isinstance(ticket.created_at, datetime.datetime) + assert isinstance(ticket.updated_at, datetime.datetime) + + +def test_all_tickets(api, ticket): + tickets = api.tickets.list_all_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].display_id == ticket.display_id + + +def test_open_tickets(api, ticket): + tickets = api.tickets.list_open_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].display_id == ticket.display_id + + +def test_deleted_tickets(api): + tickets = api.tickets.list_deleted_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 0 + + +def test_spam_tickets(api): + tickets = api.tickets.list_tickets(filter_name='spam') + assert isinstance(tickets, list) + assert len(tickets) == 0 + + +def test_default_filter_name(api, ticket): + tickets = api.tickets.list_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].display_id == ticket.display_id + + +def test_none_filter_name(api, ticket): + tickets = api.tickets.list_tickets(filter_name=None) + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].display_id == ticket.display_id diff --git a/freshdesk/v1/tests/test_timesheets.py b/freshdesk/v1/tests/test_timesheets.py new file mode 100644 index 0000000..c9ff246 --- /dev/null +++ b/freshdesk/v1/tests/test_timesheets.py @@ -0,0 +1,32 @@ +import pytest + +from freshdesk.v1.models import TimeEntry + + +@pytest.fixture +def timesheet(api): + return api.timesheets.get_timesheet_by_ticket(1) + + +def test_timesheet(timesheet): + assert isinstance(timesheet, type([])) + assert len(timesheet) == 3 + assert isinstance(timesheet[1], TimeEntry) + assert timesheet[1].id == 6000041896 + assert timesheet[1].note == "Foo" + assert timesheet[1].timespent == "0.33" + + +def test_timesheet_str(timesheet): + assert str(timesheet[1]) == "6000041896" + + +def test_timesheet_repr(timesheet): + assert repr(timesheet[1]) == '' + + +def test_get_all_timesheets(api): + timesheet = api.timesheets.get_all_timesheets() + test_timesheet(timesheet) + timesheet = api.timesheets.get_all_timesheets(filter_name="agent_id", filter_value="5004272350") + test_timesheet(timesheet) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index a376380..e14a540 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -1,7 +1,13 @@ -import requests -from requests.exceptions import HTTPError import json -from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group + +import requests +from requests import HTTPError + +from freshdesk.v2.errors import ( + FreshdeskAccessDenied, FreshdeskBadRequest, FreshdeskError, FreshdeskNotFound, FreshdeskRateLimited, + FreshdeskServerError, FreshdeskUnauthorized, +) +from freshdesk.v2.models import Agent, Comment, Company, Contact, Customer, Group, Role, Ticket, TicketField, TimeEntry class TicketAPI(object): @@ -15,7 +21,13 @@ def get_ticket(self, ticket_id): return Ticket(**ticket) def create_ticket(self, subject, **kwargs): - """Creates a ticket""" + """ + Creates a ticket + To create ticket with attachments, + pass a key 'attachments' with value as list of fully qualified file paths in string format. + ex: attachments = ('/path/to/attachment1', '/path/to/attachment2') + """ + url = 'tickets' status = kwargs.get('status', 2) priority = kwargs.get('priority', 1) @@ -25,6 +37,37 @@ def create_ticket(self, subject, **kwargs): 'priority': priority, } data.update(kwargs) + if 'attachments' in data: + ticket = self._create_ticket_with_attachment(url, data) + return Ticket(**ticket) + + ticket = self._api._post(url, data=json.dumps(data)) + return Ticket(**ticket) + + def _create_ticket_with_attachment(self, url, data): + attachments = data['attachments'] + del data['attachments'] + multipart_data = [] + + for attachment in attachments: + file_name = attachment.split("/")[-1:][0] + multipart_data.append(('attachments[]', (file_name, open(attachment, 'rb'), None))) + + ticket = self._api._post(url, data=data, files=multipart_data) + return ticket + + def create_outbound_email(self, subject, description, email, email_config_id, **kwargs): + """Creates an outbound email""" + url = 'tickets/outbound_email' + priority = kwargs.get('priority', 1) + data = { + 'subject': subject, + 'description': description, + 'priority': priority, + 'email': email, + 'email_config_id': email_config_id, + } + data.update(kwargs) ticket = self._api._post(url, data=json.dumps(data)) return Ticket(**ticket) @@ -43,8 +86,12 @@ def list_tickets(self, **kwargs): """List all tickets, optionally filtered by a view. Specify filters as keyword arguments, such as: - filter_name = one of ['new_and_my_open', 'watching', 'spam', 'deleted'] + filter_name = one of ['new_and_my_open', 'watching', 'spam', 'deleted', + None] (defaults to 'new_and_my_open') + Passing None means that no named filter will be passed to + Freshdesk, which mimics the behavior of the 'all_tickets' filter + in v1 of the API. Multiple filters are AND'd together. """ @@ -54,17 +101,22 @@ def list_tickets(self, **kwargs): filter_name = kwargs['filter_name'] del kwargs['filter_name'] - url = 'tickets?filter=%s' % filter_name - page = 1 - per_page = 100 + url = 'tickets' + if filter_name is not None: + url += '?filter=%s&' % filter_name + else: + url += '?' + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 100 if not 'per_page' in kwargs else kwargs['per_page'] tickets = [] - # Skip pagination by looping over each page and adding tickets + # Skip pagination by looping over each page and adding tickets if 'page' key is not in kwargs. + # else return the requested page and break the loop while True: - this_page = self._api._get(url + '&page=%d&per_page=%d' + this_page = self._api._get(url + 'page=%d&per_page=%d' % (page, per_page), kwargs) tickets += this_page - if len(this_page) < per_page: + if len(this_page) < per_page or 'page' in kwargs: break page += 1 @@ -82,6 +134,29 @@ def list_deleted_tickets(self): """Lists all deleted tickets.""" return self.list_tickets(filter_name='deleted') + def filter_tickets(self, query, **kwargs): + """Filter tickets by a given query string. The query string must be in + the format specified in the API documentation at: + https://developer.freshdesk.com/api/#filter_tickets + + query = "(ticket_field:integer OR ticket_field:'string') AND ticket_field:boolean" + """ + url = 'search/tickets?' + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 30 + + tickets = [] + while True: + this_page = self._api._get(url + 'page=%d&query=%s' + % (page, repr(query)), kwargs) + this_page = this_page['results'] + tickets += this_page + if len(this_page) < per_page or page == 10 or 'page' in kwargs: + break + page += 1 + + return [Ticket(**t) for t in tickets] + class CommentAPI(object): def __init__(self, api): @@ -111,12 +186,21 @@ class GroupAPI(object): def __init__(self, api): self._api = api - def list_groups(self): - url = 'groups' + def list_groups(self, **kwargs): + url = 'groups?' + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 100 if not 'per_page' in kwargs else kwargs['per_page'] + groups = [] - for g in self._api._get(url): - groups.append(Group(**g)) - return groups + while True: + this_page = self._api._get(url + 'page=%d&per_page=%d' + % (page, per_page), kwargs) + groups += this_page + if len(this_page) < per_page or 'page' in kwargs: + break + page += 1 + + return [Group(**g) for g in groups] def get_group(self, group_id): url = 'groups/%s' % group_id @@ -127,10 +211,88 @@ class ContactAPI(object): def __init__(self, api): self._api = api + def list_contacts(self, **kwargs): + """ + List all contacts, optionally filtered by a query. Specify filters as + query keyword argument, such as: + + email=abc@xyz.com, + mobile=1234567890, + phone=1234567890, + + contacts can be filtered by state and company_id such as: + + state=[blocked/deleted/unverified/verified] + company_id=1234 + + contacts updated after a timestamp can be filtered such as; + + _updated_since=2018-01-19T02:00:00Z + + Passing None means that no named filter will be passed to + Freshdesk, which returns list of all contacts + + """ + + url = 'contacts?' + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 100 if not 'per_page' in kwargs else kwargs['per_page'] + + contacts = [] + + # Skip pagination by looping over each page and adding tickets if 'page' key is not in kwargs. + # else return the requested page and break the loop + while True: + this_page = self._api._get(url + 'page=%d&per_page=%d' + % (page, per_page), kwargs) + contacts += this_page + if len(this_page) < per_page or 'page' in kwargs: + break + + page += 1 + + return [Contact(**c) for c in contacts] + + def create_contact(self, *args, **kwargs): + """Creates a contact""" + url = 'contacts' + data = { + 'view_all_tickets': False, + 'description': 'Freshdesk Contact' + } + data.update(kwargs) + return Contact(**self._api._post(url, data=json.dumps(data))) + def get_contact(self, contact_id): - url = 'contacts/%s' % contact_id + url = 'contacts/%d' % contact_id return Contact(**self._api._get(url)) + def update_contact(self, contact_id, **data): + url = 'contacts/%d' % contact_id + return Contact(**self._api._put(url, data=json.dumps(data))) + + def soft_delete_contact(self, contact_id): + url = 'contacts/%d' % contact_id + self._api._delete(url) + + def restore_contact(self, contact_id): + url = 'contacts/%d/restore' % contact_id + self._api._put(url) + + def permanently_delete_contact(self, contact_id, force=True): + url = 'contacts/%d/hard_delete?force=%r' % (contact_id, force) + self._api._delete(url) + + def make_agent(self, contact_id, **kwargs): + url = 'contacts/%d/make_agent' % contact_id + data = { + 'occasional': False, + 'ticket_scope': 2, + } + data.update(kwargs) + contact = self._api._put(url, data=json.dumps(data)) + return self._api.agents.get_agent(contact['agent']['id']) + class CustomerAPI(object): def __init__(self, api): @@ -144,8 +306,126 @@ def get_customer_from_contact(self, contact): return self.get_customer(contact.customer_id) +class CompanyAPI(object): + def __init__(self, api): + self._api = api + + def get_company(self, company_id): + url = 'companies/%s' % company_id + return Company(**self._api._get(url)) + + +class RoleAPI(object): + def __init__(self, api): + self._api = api + + def list_roles(self): + url = 'roles' + roles = [] + for r in self._api._get(url): + roles.append(Role(**r)) + return roles + + def get_role(self, role_id): + url = 'roles/%s' % role_id + return Role(**self._api._get(url)) + +class TimeEntryAPI(object): + def __init__(self, api): + self._api = api + + def list_time_entries(self, ticket_id=None): + url = 'tickets/time_entries' + if ticket_id is not None: + url = 'tickets/%d/time_entries' % ticket_id + timeEntries = [] + for r in self._api._get(url): + timeEntries.append(TimeEntry(**r)) + return timeEntries + + def get_role(self, role_id): + url = 'roles/%s' % role_id + return Role(**self._api._get(url)) + +class TicketFieldAPI(object): + def __init__(self, api): + self._api = api + + def list_ticket_fields(self, **kwargs): + url = 'ticket_fields' + ticket_fields = [] + + if kwargs.has_key('type'): + url = "{}?type={}".format(url, kwargs['type']) + + for tf in self._api._get(url): + ticket_fields.append(TicketField(**tf)) + return ticket_fields + + +class AgentAPI(object): + def __init__(self, api): + self._api = api + + def list_agents(self, **kwargs): + """List all agents, optionally filtered by a view. Specify filters as + keyword arguments, such as: + + { + email='abc@xyz.com', + phone=873902, + mobile=56523, + state='fulltime' + } + + Passing None means that no named filter will be passed to + Freshdesk, which returns list of all agents + + Multiple filters are AND'd together. + """ + + url = 'agents?' + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 100 if not 'per_page' in kwargs else kwargs['per_page'] + + agents = [] + + # Skip pagination by looping over each page and adding tickets if 'page' key is not in kwargs. + # else return the requested page and break the loop + while True: + this_page = self._api._get(url + 'page=%d&per_page=%d' + % (page, per_page), kwargs) + agents += this_page + if len(this_page) < per_page or 'page' in kwargs: + break + page += 1 + + return [Agent(**a) for a in agents] + + def get_agent(self, agent_id): + """Fetches the agent for the given agent ID""" + url = 'agents/%s' % agent_id + return Agent(**self._api._get(url)) + + def update_agent(self, agent_id, **kwargs): + """Updates an agent""" + url = 'agents/%s' % agent_id + agent = self._api._put(url, data=json.dumps(kwargs)) + return Agent(**agent) + + def delete_agent(self, agent_id): + """Delete the agent for the given agent ID""" + url = 'agents/%d' % agent_id + self._api._delete(url) + + def currently_authenticated_agent(self): + """Fetches currently logged in agent""" + url = 'agents/me' + return Agent(**self._api._get(url)) + + class API(object): - def __init__(self, domain, api_key): + def __init__(self, domain, api_key, verify=True, proxies=None): """Creates a wrapper to perform API actions. Arguments: @@ -159,13 +439,19 @@ def __init__(self, domain, api_key): self._api_prefix = 'https://{}/api/v2/'.format(domain.rstrip('/')) self._session = requests.Session() self._session.auth = (api_key, 'unused_with_api_key') + self._session.verify = verify + self._session.proxies = proxies self._session.headers = {'Content-Type': 'application/json'} self.tickets = TicketAPI(self) self.comments = CommentAPI(self) self.contacts = ContactAPI(self) + self.companies = CompanyAPI(self) self.groups = GroupAPI(self) self.customers = CustomerAPI(self) + self.agents = AgentAPI(self) + self.roles = RoleAPI(self) + self.ticket_fields = TicketFieldAPI(self) if domain.find('freshdesk.com') < 0: raise AttributeError('Freshdesk v2 API works only via Freshdesk' @@ -175,19 +461,33 @@ def __init__(self, domain, api_key): def _action(self, req): try: j = req.json() - except: - req.raise_for_status() + except ValueError: j = {} - if 'error' in j: - raise HTTPError('{}: {}'.format(j.get('description'), - j.get('errors'))) + error_message = 'Freshdesk Request Failed' + if 'errors' in j: + error_message = '{}: {}'.format(j.get('description'), j.get('errors')) + + if req.status_code == 400: + raise FreshdeskBadRequest(error_message) + elif req.status_code == 401: + raise FreshdeskUnauthorized(error_message) + elif req.status_code == 403: + raise FreshdeskAccessDenied(error_message) + elif req.status_code == 404: + raise FreshdeskNotFound(error_message) + elif req.status_code == 429: + raise FreshdeskRateLimited( + '429 Rate Limit Exceeded: API rate-limit has been reached until {} seconds. See ' + 'http://freshdesk.com/api#ratelimit'.format(req.headers.get('Retry-After'))) + elif 500 < req.status_code < 600: + raise FreshdeskServerError('{}: Server Error'.format(req.status_code)) # Catch any other errors try: req.raise_for_status() - except Exception as e: - raise HTTPError("{}: {}".format(e, j)) + except HTTPError as e: + raise FreshdeskError("{}: {}".format(e, j)) return j @@ -196,9 +496,13 @@ def _get(self, url, params={}): req = self._session.get(self._api_prefix + url, params=params) return self._action(req) - def _post(self, url, data={}): + def _post(self, url, data={}, **kwargs): """Wrapper around request.post() to use the API prefix. Returns a JSON response.""" - req = self._session.post(self._api_prefix + url, data=data) + if 'files' in kwargs: + req = self._session.post(self._api_prefix + url, auth=self._session.auth, data=data, **kwargs) + return self._action(req) + + req = self._session.post(self._api_prefix + url, data=data, **kwargs) return self._action(req) def _put(self, url, data={}): diff --git a/freshdesk/v2/errors.py b/freshdesk/v2/errors.py new file mode 100644 index 0000000..964241d --- /dev/null +++ b/freshdesk/v2/errors.py @@ -0,0 +1,33 @@ +from requests import HTTPError + + +class FreshdeskError(HTTPError): + """ + Base error class. + + Subclassing HTTPError to avoid breaking existing code that expects only HTTPErrors. + """ + + +class FreshdeskBadRequest(FreshdeskError): + """Most 40X and 501 status codes""" + + +class FreshdeskUnauthorized(FreshdeskError): + """401 Unauthorized""" + + +class FreshdeskAccessDenied(FreshdeskError): + """403 Forbidden""" + + +class FreshdeskNotFound(FreshdeskError): + """404""" + + +class FreshdeskRateLimited(FreshdeskError): + """429 Rate Limit Reached""" + + +class FreshdeskServerError(FreshdeskError): + """50X errors""" diff --git a/freshdesk/v2/models.py b/freshdesk/v2/models.py index c3ddd8f..c5a1ecd 100644 --- a/freshdesk/v2/models.py +++ b/freshdesk/v2/models.py @@ -2,9 +2,11 @@ class FreshdeskModel(object): - _keys = set() + _keys = None def __init__(self, **kwargs): + self._keys = set() + if "custom_field" in kwargs.keys() and len(kwargs["custom_field"]) > 0: custom_fields = kwargs.pop("custom_field") kwargs.update(custom_fields) @@ -22,6 +24,13 @@ def _to_timestamp(self, timestamp_str): return dateutil.parser.parse(timestamp_str) +class TicketField(FreshdeskModel): + def __str__(self): + return self.name + + def __repr__(self): + return ''.format(self.name, self.description) + class Ticket(FreshdeskModel): def __str__(self): return self.subject @@ -51,7 +60,7 @@ def source(self): class Group(FreshdeskModel): def __str__(self): - return self.body + return self.name def __repr__(self): return ''.format(self.name) @@ -93,3 +102,26 @@ def __str__(self): def __repr__(self): return ''.format(self.name) + +class Company(FreshdeskModel): + def __str__(self): + return self.name + + def __repr__(self): + return ''.format(self.name) + +class Agent(FreshdeskModel): + def __str__(self): + return self.contact['name'] + + def __repr__(self): + return ''.format(self.id, self.contact['name']) + + +class Role(FreshdeskModel): + def __str__(self): + return self.name + + def __repr__(self): + return ''.format(self.name) + diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py deleted file mode 100644 index b6aa20d..0000000 --- a/freshdesk/v2/test.py +++ /dev/null @@ -1,323 +0,0 @@ -import datetime -import json -import re -import os.path -import responses -from unittest import TestCase - -from freshdesk.v2.api import API -from freshdesk.v2.models import Ticket, Comment, Contact, Customer, Group - -""" -Test suite for python-freshdesk. - -We test against a dummy helpdesk created for these tests only. It is: -https://pythonfreshdesk.freshdesk.com/ -""" - -DOMAIN = 'pythonfreshdesk.freshdesk.com' -API_KEY = 'MX4CEAw4FogInimEdRW2' - - -class MockedAPI(API): - def __init__(self, *args): - self.resolver = { - re.compile(r'tickets\?filter=new_and_my_open&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?filter=deleted&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?filter=spam&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?filter=watching&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets/1$'): self.read_test_file('ticket_1.json'), - re.compile(r'tickets/1/conversations'): self.read_test_file('conversations.json'), - re.compile(r'contacts/1$'): self.read_test_file('contact.json'), - re.compile(r'customers/1$'): self.read_test_file('customer.json'), - re.compile(r'groups$'): self.read_test_file('groups.json'), - re.compile(r'groups/1$'): self.read_test_file('group_1.json'), - } - super(MockedAPI, self).__init__(*args) - - def read_test_file(self, filename): - path = os.path.join(os.path.dirname(__file__), 'sample_json_data', filename) - return json.loads(open(path, 'r').read()) - - def _get(self, url, *args, **kwargs): - for pattern, data in self.resolver.items(): - if pattern.match(url): - return data - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) - - -class TestAPIClass(TestCase): - - def test_custom_cname(self): - with self.assertRaises(AttributeError): - API('custom_cname_domain', 'invalid_api_key') - - def test_api_prefix(self): - api = API('test_domain.freshdesk.com', 'test_key') - self.assertEqual(api._api_prefix, - 'https://test_domain.freshdesk.com/api/v2/') - api = API('test_domain.freshdesk.com/', 'test_key') - self.assertEqual(api._api_prefix, - 'https://test_domain.freshdesk.com/api/v2/') - - @responses.activate - def test_403_error(self): - responses.add(responses.GET, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=403) - - api = API('pythonfreshdesk.freshdesk.com', 'invalid_api_key') - from requests.exceptions import HTTPError - with self.assertRaises(HTTPError): - api.tickets.get_ticket(1) - - -class TestTicket(TestCase): - - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.ticket = cls.api.tickets.get_ticket(1) - cls.ticket_json = json.loads(open(os.path.join(os.path.dirname(__file__), - 'sample_json_data', - 'ticket_1.json')).read()) - - def test_str(self): - self.assertEqual(str(self.ticket), 'This is a sample ticket') - - def test_repr(self): - self.assertEqual(repr(self.ticket), '') - - def test_get_ticket(self): - self.assertIsInstance(self.ticket, Ticket) - self.assertEqual(self.ticket.id, 1) - self.assertEqual(self.ticket.subject, 'This is a sample ticket') - self.assertEqual(self.ticket.description_text, 'This is a sample ticket, feel free to delete it.') - self.assertEqual(self.ticket.cc_emails, ['test2@example.com']) - self.assertIn('foo', self.ticket.tags) - self.assertIn('bar', self.ticket.tags) - - @responses.activate - def test_create_ticket(self): - responses.add(responses.POST, - 'https://{}/api/v2/tickets'.format(DOMAIN), - status=200, content_type='application/json', - json=self.ticket_json) - - ticket = self.api.tickets.create_ticket('This is a sample ticket', - description='This is a sample ticket, feel free to delete it.', - email='test@example.com', - priority=1, status=2, - tags=['foo', 'bar'], - cc_emails=['test2@example.com']) - self.assertIsInstance(ticket, Ticket) - self.assertEqual(ticket.subject, 'This is a sample ticket') - self.assertEqual(ticket.description_text, 'This is a sample ticket, feel free to delete it.') - self.assertEqual(ticket.priority, 'low') - self.assertEqual(ticket.status, 'open') - self.assertEqual(ticket.cc_emails, ['test2@example.com']) - self.assertIn('foo', ticket.tags) - self.assertIn('bar', ticket.tags) - - @responses.activate - def test_update_ticket(self): - j = self.ticket_json.copy() - values = { - 'subject': 'Test subject update', - 'priority': 3, - 'status': 4, - 'tags': ['hello', 'world'] - } - j.update(values) - - responses.add(responses.GET, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=200, content_type='application/json', json=j) - - responses.add(responses.PUT, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=200, content_type='application/json', json=j) - - ticket = self.api.tickets.update_ticket(j['id'], **values) - self.assertEqual(ticket.subject, 'Test subject update') - self.assertEqual(ticket.status, 'resolved') - self.assertEqual(ticket.priority, 'high') - self.assertIn('hello', ticket.tags) - self.assertIn('world', ticket.tags) - - @responses.activate - def test_delete_ticket(self): - responses.add(responses.DELETE, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=204) - self.api.tickets.delete_ticket(1) - - def test_ticket_priority(self): - self.assertEqual(self.ticket._priority, 1) - self.assertEqual(self.ticket.priority, 'low') - - def test_ticket_status(self): - self.assertEqual(self.ticket._status, 2) - self.assertEqual(self.ticket.status, 'open') - - def test_ticket_source(self): - self.assertEqual(self.ticket._source, 2) - self.assertEqual(self.ticket.source, 'portal') - - def test_ticket_datetime(self): - self.assertIsInstance(self.ticket.created_at, datetime.datetime) - self.assertIsInstance(self.ticket.updated_at, datetime.datetime) - - def test_new_and_my_open_tickets(self): - tickets = self.api.tickets.list_new_and_my_open_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].id, self.ticket.id) - - def test_deleted_tickets(self): - tickets = self.api.tickets.list_deleted_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - - def test_watched_tickets(self): - tickets = self.api.tickets.list_watched_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].id, self.ticket.id) - - def test_spam_tickets(self): - tickets = self.api.tickets.list_tickets(filter_name='spam') - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - - def test_default_filter_name(self): - tickets = self.api.tickets.list_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].id, self.ticket.id) - - -class TestComment(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.comments = cls.api.comments.list_comments(1) - cls.comments_json = json.loads(open(os.path.join( - os.path.dirname(__file__), - 'sample_json_data', - 'conversations.json')).read()) - - def test_comments_list(self): - self.assertIsInstance(self.comments, list) - self.assertEqual(len(self.comments), 2) - self.assertIsInstance(self.comments[0], Comment) - - def test_comment_str(self): - self.assertEqual(str(self.comments[0]), 'This is a private note') - - def test_comment_repr(self): - self.assertEqual(repr(self.comments[0]), '') - - @responses.activate - def test_create_note(self): - responses.add(responses.POST, - 'https://{}/api/v2/tickets/1/notes'.format(DOMAIN), - status=200, content_type='application/json', - json=self.comments_json[0]) - - comment = self.api.comments.create_note(1, 'This is a private note') - self.assertIsInstance(comment, Comment) - self.assertEqual(comment.body_text, 'This is a private note') - self.assertEqual(comment.source, 'note') - - @responses.activate - def test_create_reply(self): - responses.add(responses.POST, - 'https://{}/api/v2/tickets/1/reply'.format(DOMAIN), - status=200, content_type='application/json', - json=self.comments_json[1]) - - comment = self.api.comments.create_reply(1, 'This is a reply') - self.assertIsInstance(comment, Comment) - self.assertEqual(comment.body_text, 'This is a reply') - self.assertEqual(comment.source, 'reply') - - -class TestContact(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.contact = cls.api.contacts.get_contact('1') - - def test_get_contact(self): - self.assertIsInstance(self.contact, Contact) - self.assertEqual(self.contact.name, 'Rachel') - self.assertEqual(self.contact.email, 'rachel@freshdesk.com') - self.assertEqual(self.contact.helpdesk_agent, False) - self.assertEqual(self.contact.customer_id, 1) - - def test_contact_datetime(self): - self.assertIsInstance(self.contact.created_at, datetime.datetime) - self.assertIsInstance(self.contact.updated_at, datetime.datetime) - - def test_contact_str(self): - self.assertEqual(str(self.contact), 'Rachel') - - def test_contact_repr(self): - self.assertEqual(repr(self.contact), '') - - -class TestCustomer(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.customer = cls.api.customers.get_customer('1') - cls.contact = cls.api.contacts.get_contact('1') - - def test_customer(self): - self.assertIsInstance(self.customer, Customer) - self.assertEqual(self.customer.name, 'ACME Corp.') - self.assertEqual(self.customer.domains, 'acme.com') - self.assertEqual(self.customer.cf_custom_key, 'custom_value') - - def test_contact_datetime(self): - self.assertIsInstance(self.customer.created_at, datetime.datetime) - self.assertIsInstance(self.customer.updated_at, datetime.datetime) - - def test_contact_str(self): - self.assertEqual(str(self.customer), 'ACME Corp.') - - def test_contact_repr(self): - self.assertEqual(repr(self.customer), '') - - def test_get_customer_from_contact(self): - self.customer = self.api.customers.get_customer_from_contact(self.contact) - self.test_customer() - - -class TestGroup(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.group = cls.api.groups.get_group(1) - - def test_list_groups(self): - groups = self.api.groups.list_groups() - self.assertIsInstance(groups, list) - self.assertEqual(len(groups), 2) - self.assertEqual(groups[0].id, self.group.id) - - def test_group(self): - self.assertIsInstance(self.group, Group) - self.assertEqual(self.group.name, 'Entertainers') - self.assertEqual(self.group.description, 'Singers dancers and stand up comedians') - - def test_group_datetime(self): - self.assertIsInstance(self.group.created_at, datetime.datetime) - self.assertIsInstance(self.group.updated_at, datetime.datetime) - - def test_group_repr(self): - self.assertEqual(repr(self.group), '') diff --git a/freshdesk/v2/tests/__init__.py b/freshdesk/v2/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freshdesk/v2/tests/conftest.py b/freshdesk/v2/tests/conftest.py new file mode 100644 index 0000000..cbbe82b --- /dev/null +++ b/freshdesk/v2/tests/conftest.py @@ -0,0 +1,107 @@ +import json +import os.path +import re + +import pytest + +from freshdesk.v2.api import API + +DOMAIN = 'pythonfreshdesk.freshdesk.com' +API_KEY = 'MX4CEAw4FogInimEdRW2' + + +class MockedAPI(API): + def __init__(self, *args): + self.resolver = { + 'get': { + re.compile(r'tickets\?filter=new_and_my_open&page=1&per_page=100'): self.read_test_file( + 'all_tickets.json'), + re.compile(r'tickets\?filter=deleted&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?filter=spam&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?filter=watching&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets/1$'): self.read_test_file('ticket_1.json'), + re.compile(r'tickets/1/conversations'): self.read_test_file('conversations.json'), + re.compile(r'contacts\?page=1&per_page=100$'): self.read_test_file('contacts.json'), + re.compile(r'contacts/1$'): self.read_test_file('contact.json'), + re.compile(r'customers/1$'): self.read_test_file('customer.json'), + re.compile(r'groups\?page=1&per_page=100$'): self.read_test_file('groups.json'), + re.compile(r'groups/1$'): self.read_test_file('group_1.json'), + re.compile(r'roles$'): self.read_test_file('roles.json'), + re.compile(r'roles/1$'): self.read_test_file('role_1.json'), + re.compile(r'agents\?email=abc@xyz.com&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?mobile=1234&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?phone=5678&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?state=fulltime&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?page=1&per_page=100'): self.read_test_file('agents.json'), + re.compile(r'agents/1$'): self.read_test_file('agent_1.json'), + re.compile(r'search/tickets\?page=1&query="tag:\'mytag\'"'): self.read_test_file('search_tickets.json'), + }, + 'post': { + re.compile(r'tickets$'): self.read_test_file('ticket_1.json'), + re.compile(r'tickets/outbound_email$'): self.read_test_file('outbound_email_1.json'), + re.compile(r'tickets/1/notes$'): self.read_test_file('note_1.json'), + re.compile(r'tickets/1/reply$'): self.read_test_file('reply_1.json'), + re.compile(r'contacts$'): self.read_test_file('contact.json'), + }, + 'put': { + re.compile(r'tickets/1$'): self.read_test_file('ticket_1_updated.json'), + re.compile(r'contacts/1$'): self.read_test_file('contact_updated.json'), + re.compile(r'contacts/1/restore$'): self.read_test_file('contact.json'), + re.compile(r'contacts/1/make_agent$'): self.read_test_file('contact_1_agent.json'), + re.compile(r'agents/1$'): self.read_test_file('agent_1_updated.json'), + }, + 'delete': { + re.compile(r'tickets/1$'): None, + re.compile(r'agents/1$'): None, + re.compile(r'contacts/1$'): None, + re.compile(r'contacts/1/hard_delete\?force=True$'): None, + } + } + + super(MockedAPI, self).__init__(*args) + + def read_test_file(self, filename): + path = os.path.join(os.path.dirname(__file__), 'sample_json_data', filename) + return json.loads(open(path, 'r').read()) + + def _get(self, url, *args, **kwargs): + for pattern, data in self.resolver['get'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) + + def _post(self, url, *args, **kwargs): + for pattern, data in self.resolver['post'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_post() has no pattern for \'{}\''.format(url)) + + def _put(self, url, *args, **kwargs): + for pattern, data in self.resolver['put'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_put() has no pattern for \'{}\''.format(url)) + + def _delete(self, url, *args, **kwargs): + for pattern, data in self.resolver['delete'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_delete() has no pattern for \'{}\''.format(url)) + + +@pytest.fixture() +def api(): + return MockedAPI(DOMAIN, API_KEY) diff --git a/freshdesk/v2/tests/sample_json_data/agent_1.json b/freshdesk/v2/tests/sample_json_data/agent_1.json new file mode 100644 index 0000000..685f47d --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/agent_1.json @@ -0,0 +1,23 @@ +{ + "available":true, + "occasional":false, + "signature":null, + "id":1, + "ticket_scope":1, + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-18T16:18:05Z", + "available_since":null, + "contact":{ + "active":true, + "email":"rachel@freshdesk.com", + "job_title":null, + "language":"en", + "last_login_at":"2015-08-21T14:54:46+05:30", + "mobile":1234, + "name":"Rachel", + "phone":5678, + "time_zone":"Chennai", + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-25T08:50:20Z" + } +} diff --git a/freshdesk/v2/tests/sample_json_data/agent_1_updated.json b/freshdesk/v2/tests/sample_json_data/agent_1_updated.json new file mode 100644 index 0000000..6b74842 --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/agent_1_updated.json @@ -0,0 +1,23 @@ +{ + "available":true, + "occasional":true, + "signature":null, + "id":1, + "ticket_scope":1, + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-18T16:18:05Z", + "available_since":null, + "contact":{ + "active":true, + "email":"abc@xyz.com", + "job_title":null, + "language":"en", + "last_login_at":"2015-08-21T14:54:46+05:30", + "mobile":1234, + "name":"Updated Name", + "phone":5678, + "time_zone":"Chennai", + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-25T08:50:20Z" + } +} diff --git a/freshdesk/v2/tests/sample_json_data/agents.json b/freshdesk/v2/tests/sample_json_data/agents.json new file mode 100644 index 0000000..d7bcc22 --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/agents.json @@ -0,0 +1,48 @@ +[ + { + "available":true, + "occasional":false, + "signature":null, + "id":1, + "ticket_scope":1, + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-18T16:18:05Z", + "available_since":null, + "contact":{ + "active":true, + "email":"abc@xyz.com", + "job_title":null, + "language":"en", + "last_login_at":"2015-08-21T14:54:46+05:30", + "mobile":1234, + "name":"Support", + "phone":5678, + "time_zone":"Chennai", + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-25T08:50:20Z" + } + }, + { + "available":true, + "occasional":false, + "signature":null, + "id":432, + "ticket_scope":1, + "created_at":"2015-08-28T11:47:58Z", + "updated_at":"2015-08-28T11:47:58Z", + "available_since":null, + "contact":{ + "active":false, + "email":"superman@freshdesk.com", + "job_title":"Journalist", + "language":"en", + "last_login_at":null, + "mobile":null, + "name":"Clark Kent", + "phone":null, + "time_zone":"Chennai", + "created_at":"2015-08-28T09:08:16Z", + "updated_at":"2015-08-28T11:47:58Z" + } + } +] diff --git a/freshdesk/v2/sample_json_data/all_tickets.json b/freshdesk/v2/tests/sample_json_data/all_tickets.json similarity index 98% rename from freshdesk/v2/sample_json_data/all_tickets.json rename to freshdesk/v2/tests/sample_json_data/all_tickets.json index 1caab2d..2ab7178 100644 --- a/freshdesk/v2/sample_json_data/all_tickets.json +++ b/freshdesk/v2/tests/sample_json_data/all_tickets.json @@ -38,6 +38,7 @@ "responder_name": "Sam Kingston", "to_emails": null, "product_id": null, + "tags": ["mytag"], "custom_field": {} } ] diff --git a/freshdesk/v2/tests/sample_json_data/attachment.txt b/freshdesk/v2/tests/sample_json_data/attachment.txt new file mode 100644 index 0000000..47f20bb --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/attachment.txt @@ -0,0 +1 @@ +test attachment file diff --git a/freshdesk/v2/sample_json_data/contact.json b/freshdesk/v2/tests/sample_json_data/contact.json similarity index 100% rename from freshdesk/v2/sample_json_data/contact.json rename to freshdesk/v2/tests/sample_json_data/contact.json diff --git a/freshdesk/v2/tests/sample_json_data/contact_1_agent.json b/freshdesk/v2/tests/sample_json_data/contact_1_agent.json new file mode 100644 index 0000000..3090b05 --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/contact_1_agent.json @@ -0,0 +1,39 @@ +{ + "active": false, + "address": null, + "created_at": "2014-12-31T12:27:09+10:00", + "customer_id": 1, + "deleted": false, + "description": null, + "email": "rachel@freshdesk.com", + "external_id": null, + "fb_profile_id": null, + "helpdesk_agent": false, + "id": 1, + "job_title": null, + "language": "en", + "mobile": null, + "name": "Rachel", + "phone": null, + "time_zone": "Brisbane", + "twitter_id": null, + "updated_at": "2014-12-31T12:27:09+10:00", + "company_id": null, + "custom_field": {}, + "agent": { + "available_since": null, + "available": true, + "occasional": false, + "signature": null, + "group_ids" : [ + 1 + ], + "role_ids" : [ + 1 + ], + "id": 1, + "ticket_scope": 1, + "created_at": "2015-08-28T11:47:58Z", + "updated_at": "2015-08-28T11:47:58Z" + } +} diff --git a/freshdesk/v2/tests/sample_json_data/contact_updated.json b/freshdesk/v2/tests/sample_json_data/contact_updated.json new file mode 100644 index 0000000..e907408 --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/contact_updated.json @@ -0,0 +1,23 @@ +{ + "active": false, + "address": null, + "created_at": "2014-12-31T12:27:09+10:00", + "customer_id": 1, + "deleted": false, + "description": null, + "email": "rachel@freshdesk.com", + "external_id": null, + "fb_profile_id": null, + "helpdesk_agent": false, + "id": 1, + "job_title": null, + "language": "en", + "mobile": null, + "name": "New Name", + "phone": null, + "time_zone": "Brisbane", + "twitter_id": null, + "updated_at": "2014-12-31T12:27:09+10:00", + "company_id": null, + "custom_field": {} +} diff --git a/freshdesk/v2/tests/sample_json_data/contacts.json b/freshdesk/v2/tests/sample_json_data/contacts.json new file mode 100644 index 0000000..615af7e --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/contacts.json @@ -0,0 +1,49 @@ +[ + { + "active": false, + "address": null, + "created_at": "2014-12-31T12:27:09+10:00", + "customer_id": 1, + "deleted": false, + "description": null, + "email": "rachel@freshdesk.com", + "external_id": null, + "fb_profile_id": null, + "helpdesk_agent": false, + "id": 1, + "job_title": null, + "language": "en", + "mobile": null, + "name": "Rachel", + "phone": null, + "time_zone": "Brisbane", + "twitter_id": null, + "updated_at": "2014-12-31T12:27:09+10:00", + "company_id": null, + "custom_field": {} + }, + { + "active": false, + "address": null, + "created_at": "2014-12-31T12:27:09+10:00", + "customer_id": 1, + "deleted": false, + "description": null, + "email": "rachel@freshdesk.com", + "external_id": null, + "fb_profile_id": null, + "helpdesk_agent": false, + "id": 1, + "job_title": null, + "language": "en", + "mobile": null, + "name": "Rachel", + "phone": null, + "time_zone": "Brisbane", + "twitter_id": null, + "updated_at": "2014-12-31T12:27:09+10:00", + "company_id": null, + "custom_field": {} + } + +] diff --git a/freshdesk/v2/sample_json_data/conversations.json b/freshdesk/v2/tests/sample_json_data/conversations.json similarity index 100% rename from freshdesk/v2/sample_json_data/conversations.json rename to freshdesk/v2/tests/sample_json_data/conversations.json diff --git a/freshdesk/v2/sample_json_data/customer.json b/freshdesk/v2/tests/sample_json_data/customer.json similarity index 100% rename from freshdesk/v2/sample_json_data/customer.json rename to freshdesk/v2/tests/sample_json_data/customer.json diff --git a/freshdesk/v2/sample_json_data/group_1.json b/freshdesk/v2/tests/sample_json_data/group_1.json similarity index 100% rename from freshdesk/v2/sample_json_data/group_1.json rename to freshdesk/v2/tests/sample_json_data/group_1.json diff --git a/freshdesk/v2/sample_json_data/groups.json b/freshdesk/v2/tests/sample_json_data/groups.json similarity index 100% rename from freshdesk/v2/sample_json_data/groups.json rename to freshdesk/v2/tests/sample_json_data/groups.json diff --git a/freshdesk/v2/tests/sample_json_data/note_1.json b/freshdesk/v2/tests/sample_json_data/note_1.json new file mode 100644 index 0000000..6f8e3dc --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/note_1.json @@ -0,0 +1,18 @@ +{ + "body": "
This is a private note
", + "body_text": "This is a private note", + "id": 1, + "incoming": false, + "private": true, + "user_id": 1, + "support_email": null, + "source": 2, + "ticket_id": 1, + "to_emails": [], + "from_email": null, + "cc_emails": null, + "bcc_emails": null, + "created_at": "2016-05-19T05:36:12Z", + "updated_at": "2016-05-19T05:36:12Z", + "attachments": [] +} diff --git a/freshdesk/v2/tests/sample_json_data/outbound_email_1.json b/freshdesk/v2/tests/sample_json_data/outbound_email_1.json new file mode 100644 index 0000000..4be3f39 --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/outbound_email_1.json @@ -0,0 +1,42 @@ +{ + "cc_emails": [ + "test2@example.com" + ], + "fwd_emails": [], + "reply_cc_emails": [ + "test2@example.com" + ], + "email_config_id": 5000054536, + "fr_escalated": false, + "group_id": null, + "priority": 1, + "priority_name": "Low", + "requester_id": 6012691534, + "responder_id": 6010396333, + "source": 2, + "spam": false, + "status": 2, + "subject": "This is a sample outbound email", + "type": "Question", + "company_id": null, + "id": 1, + "to_emails": null, + "product_id": null, + "created_at": "2016-05-18T04:36:04Z", + "updated_at": "2016-05-18T23:42:50Z", + "due_by": "2016-05-18T23:36:04Z", + "fr_due_by": "2016-05-18T05:36:04Z", + "is_escalated": true, + "description_text": "This is a sample outbound email, feel free to delete it.", + "description": "
This is a sample outbound email, feel free to delete it.
", + "custom_fields": { + "parent_or_child": null, + "parent_ticket_id": null, + "child_ticket_id": null + }, + "tags": [ + "foo", + "bar" + ], + "attachments": [] +} diff --git a/freshdesk/v2/tests/sample_json_data/reply_1.json b/freshdesk/v2/tests/sample_json_data/reply_1.json new file mode 100644 index 0000000..bc785cf --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/reply_1.json @@ -0,0 +1,20 @@ +{ + "body": "
This is a reply
", + "body_text": "This is a reply", + "id": 2, + "incoming": false, + "private": false, + "user_id": 1, + "support_email": null, + "source": 0, + "ticket_id": 1, + "to_emails": [ + "test@example.com" + ], + "from_email": "Rachel ", + "cc_emails": null, + "bcc_emails": null, + "created_at": "2016-05-19T05:36:14Z", + "updated_at": "2016-05-19T05:36:14Z", + "attachments": [] +} diff --git a/freshdesk/v2/tests/sample_json_data/role_1.json b/freshdesk/v2/tests/sample_json_data/role_1.json new file mode 100644 index 0000000..39297ea --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/role_1.json @@ -0,0 +1,11 @@ +{ + "id": 1, + "name": "Agent", + "description": "Can log, view, reply, update and resolve tickets and manage contacts.", + "business_hour_id": null, + "escalate_to": 1, + "unassigned_for": "30m", + "auto_ticket_assign": true, + "created_at": "2014-01-08T07:53:41+05:30", + "updated_at": "2014-01-08T07:53:41+05:30" +} diff --git a/freshdesk/v2/tests/sample_json_data/roles.json b/freshdesk/v2/tests/sample_json_data/roles.json new file mode 100644 index 0000000..2888503 --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/roles.json @@ -0,0 +1,24 @@ +[ + { + "id": 1, + "name": "Agent", + "description": "Can log, view, reply, update and resolve tickets and manage contacts.", + "business_hour_id": null, + "escalate_to": 1, + "unassigned_for": "30m", + "auto_ticket_assign": true, + "created_at": "2014-01-08T07:53:41+05:30", + "updated_at": "2014-01-08T07:53:41+05:30" + }, + { + "id": 2, + "name": "Administrator", + "description": "Can configure all features through the Admin tab, but is restricted from viewing Account or Billing related information.", + "business_hour_id": null, + "escalate_to": 1, + "unassigned_for": "30m", + "auto_ticket_assign": true, + "created_at": "2014-01-08T07:53:41+05:30", + "updated_at": "2014-01-08T07:53:41+05:30" + } +] diff --git a/freshdesk/v2/tests/sample_json_data/search_tickets.json b/freshdesk/v2/tests/sample_json_data/search_tickets.json new file mode 100644 index 0000000..dbb5f2d --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/search_tickets.json @@ -0,0 +1,90 @@ +{ + "total": 2, + "results": [ + { + "cc_email": { + "cc_emails": [], + "fwd_emails": [], + "reply_cc": [] + }, + "created_at": "2014-12-31T12:27:09+10:00", + "deleted": false, + "delta": true, + "description": "This is a sample ticket, feel free to delete it.", + "description_html": "
This is a sample ticket, feel free to delete it.
", + "id": 1, + "due_by": "2015-01-05T12:27:09+10:00", + "email_config_id": null, + "frDueBy": "2015-01-01T12:27:09+10:00", + "fr_escalated": false, + "group_id": null, + "isescalated": false, + "owner_id": null, + "priority": 1, + "requester_id": 5004272351, + "responder_id": 5004272350, + "source": 2, + "spam": false, + "status": 2, + "subject": "This is a sample ticket", + "ticket_type": "Question", + "to_email": null, + "trained": false, + "updated_at": "2015-01-01T10:58:39+10:00", + "urgent": false, + "status_name": "Open", + "requester_status_name": "Being Processed", + "priority_name": "Low", + "source_name": "Portal", + "requester_name": "Rachel", + "responder_name": "Sam Kingston", + "to_emails": null, + "product_id": null, + "tags": ["mytag"], + "custom_field": {} + }, + { + "cc_email": { + "cc_emails": [], + "fwd_emails": [], + "reply_cc": [] + }, + "created_at": "2020-01-01T16:21:45+10:00", + "deleted": false, + "delta": true, + "description": "This is another ticket.", + "description_html": "
This is a another ticket.
", + "id": 1, + "due_by": "2020-01-02T16:21:45+10:00", + "email_config_id": null, + "frDueBy": "2020-01-02T16:21:45+10:00", + "fr_escalated": false, + "group_id": null, + "isescalated": false, + "owner_id": null, + "priority": 1, + "requester_id": 5004272351, + "responder_id": 5004272350, + "source": 2, + "spam": false, + "status": 2, + "subject": "This is another ticket", + "ticket_type": "Question", + "to_email": null, + "trained": false, + "updated_at": "2020-01-01T17:02:12+10:00", + "urgent": false, + "status_name": "Open", + "requester_status_name": "Being Processed", + "priority_name": "Low", + "source_name": "Portal", + "requester_name": "Rachel", + "responder_name": "Sam Kingston", + "to_emails": null, + "product_id": null, + "tags": ["mytag", "myothertag"], + "custom_field": {} + } + + ] +} diff --git a/freshdesk/v2/tests/sample_json_data/ticket_1.json b/freshdesk/v2/tests/sample_json_data/ticket_1.json new file mode 100644 index 0000000..cc97a19 --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/ticket_1.json @@ -0,0 +1,52 @@ +{ + "cc_emails": [ + "test2@example.com" + ], + "fwd_emails": [], + "reply_cc_emails": [ + "test2@example.com" + ], + "email_config_id": null, + "fr_escalated": false, + "group_id": null, + "priority": 1, + "priority_name": "Low", + "requester_id": 6012691534, + "responder_id": 6010396333, + "source": 2, + "spam": false, + "status": 2, + "subject": "This is a sample ticket", + "type": "Question", + "company_id": null, + "id": 1, + "to_emails": null, + "product_id": null, + "created_at": "2016-05-18T04:36:04Z", + "updated_at": "2016-05-18T23:42:50Z", + "due_by": "2016-05-18T23:36:04Z", + "fr_due_by": "2016-05-18T05:36:04Z", + "is_escalated": true, + "description_text": "This is a sample ticket, feel free to delete it.", + "description": "
This is a sample ticket, feel free to delete it.
", + "custom_fields": { + "parent_or_child": null, + "parent_ticket_id": null, + "child_ticket_id": null + }, + "tags": [ + "foo", + "bar" + ], + "attachments": [ + { + "id":1, + "content_type":"text/plain", + "file_size":44115, + "name":"attachment.txt", + "attachment_url":"https://cdn.freshdesk.com/data/helpdesk/attachments/production/4004881085/original/attachment.txt", + "created_at":"2014-07-28T16:20:03+05:30", + "updated_at":"2014-07-28T16:20:03+05:30" + } + ] +} diff --git a/freshdesk/v2/sample_json_data/ticket_1.json b/freshdesk/v2/tests/sample_json_data/ticket_1_updated.json similarity index 90% rename from freshdesk/v2/sample_json_data/ticket_1.json rename to freshdesk/v2/tests/sample_json_data/ticket_1_updated.json index 8ea487a..ccb5747 100644 --- a/freshdesk/v2/sample_json_data/ticket_1.json +++ b/freshdesk/v2/tests/sample_json_data/ticket_1_updated.json @@ -9,14 +9,14 @@ "email_config_id": null, "fr_escalated": false, "group_id": null, - "priority": 1, + "priority": 3, "priority_name": "Low", "requester_id": 6012691534, "responder_id": 6010396333, "source": 2, "spam": false, - "status": 2, - "subject": "This is a sample ticket", + "status": 4, + "subject": "Test subject update", "type": "Question", "company_id": null, "id": 1, @@ -35,8 +35,8 @@ "child_ticket_id": null }, "tags": [ - "foo", - "bar" + "hello", + "world" ], "attachments": [] } diff --git a/freshdesk/v2/tests/test_agent.py b/freshdesk/v2/tests/test_agent.py new file mode 100644 index 0000000..d635910 --- /dev/null +++ b/freshdesk/v2/tests/test_agent.py @@ -0,0 +1,77 @@ +import datetime +import json +import os.path + +import pytest + +from freshdesk.v2.models import Agent + + +@pytest.fixture +def agent(api): + return api.agents.get_agent(1) + + +@pytest.fixture +def agent_json(): + return json.loads(open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'agent_1.json')).read()) + + +def test_str(agent): + assert str(agent) == 'Rachel' + + +def test_repr(agent): + assert repr(agent) == '' + + +def test_get_agent(agent): + assert isinstance(agent, Agent) + assert agent.id == 1 + assert agent.contact['name'] == 'Rachel' + assert agent.contact['email'] == 'rachel@freshdesk.com' + assert agent.contact['mobile'] == 1234 + assert agent.contact['phone'] == 5678 + assert agent.occasional is False + + +def test_update_agent(api): + values = { + 'occasional': True, + 'contact': { + 'name': 'Updated Name' + } + } + agent = api.agents.update_agent(1, **values) + + assert agent.occasional is True + assert agent.contact['name'] == 'Updated Name' + + +def test_delete_agent(api): + assert api.agents.delete_agent(1) is None + + +def test_agent_name(agent): + assert agent.contact['name'] == 'Rachel' + + +def test_agent_mobile(agent): + assert agent.contact['mobile'] == 1234 + + +def test_agent_state(agent): + assert agent.available is True + assert agent.occasional is False + + +def test_agent_datetime(agent): + assert isinstance(agent.created_at, datetime.datetime) + assert isinstance(agent.updated_at, datetime.datetime) + + +def test_none_filter_name(api, agent): + agents = api.agents.list_agents() + assert isinstance(agents, list) + assert len(agents) == 2 + assert agents[0].id == agent.id diff --git a/freshdesk/v2/tests/test_api_class.py b/freshdesk/v2/tests/test_api_class.py new file mode 100644 index 0000000..e128d9e --- /dev/null +++ b/freshdesk/v2/tests/test_api_class.py @@ -0,0 +1,38 @@ +import pytest +import responses + +from freshdesk.v2.api import API +from freshdesk.v2.errors import ( + FreshdeskBadRequest, FreshdeskAccessDenied, FreshdeskNotFound, FreshdeskError, + FreshdeskRateLimited, + FreshdeskServerError, +) +from freshdesk.v2.tests.conftest import DOMAIN + + +def test_custom_cname(): + with pytest.raises(AttributeError): + API('custom_cname_domain', 'invalid_api_key') + + +def test_api_prefix(): + api = API('test_domain.freshdesk.com', 'test_key') + assert api._api_prefix == 'https://test_domain.freshdesk.com/api/v2/' + api = API('test_domain.freshdesk.com/', 'test_key') + assert api._api_prefix == 'https://test_domain.freshdesk.com/api/v2/' + + +@responses.activate +@pytest.mark.parametrize( + ('status_code', 'exception'), + [(400, FreshdeskBadRequest), (403, FreshdeskAccessDenied), (404, FreshdeskNotFound), + (418, FreshdeskError), (429, FreshdeskRateLimited), (502, FreshdeskServerError)] +) +def test_errors(status_code, exception): + responses.add(responses.GET, + 'https://{}/api/v2/tickets/1'.format(DOMAIN), + status=status_code) + + api = API('pythonfreshdesk.freshdesk.com', 'test_key') + with pytest.raises(exception): + api.tickets.get_ticket(1) diff --git a/freshdesk/v2/tests/test_comment.py b/freshdesk/v2/tests/test_comment.py new file mode 100644 index 0000000..b841941 --- /dev/null +++ b/freshdesk/v2/tests/test_comment.py @@ -0,0 +1,36 @@ +import pytest + +from freshdesk.v2.models import Comment + + +@pytest.fixture +def comments(api): + return api.comments.list_comments(1) + + +def test_comments_list(comments): + assert isinstance(comments, list) + assert len(comments) == 2 + assert isinstance(comments[0], Comment) + + +def test_comment_str(comments): + assert str(comments[0]) == 'This is a private note' + + +def test_comment_repr(comments): + assert repr(comments[0]) == '' + + +def test_create_note(api): + comment = api.comments.create_note(1, 'This is a private note') + assert isinstance(comment, Comment) + assert comment.body_text == 'This is a private note' + assert comment.source == 'note' + + +def test_create_reply(api): + comment = api.comments.create_reply(1, 'This is a reply') + assert isinstance(comment, Comment) + assert comment.body_text == 'This is a reply' + assert comment.source == 'reply' diff --git a/freshdesk/v2/tests/test_contact.py b/freshdesk/v2/tests/test_contact.py new file mode 100644 index 0000000..73d6980 --- /dev/null +++ b/freshdesk/v2/tests/test_contact.py @@ -0,0 +1,83 @@ +import datetime + +import pytest + +from freshdesk.v2.models import Contact, Agent + + +@pytest.fixture +def contact(api): + return api.contacts.get_contact(1) + + +def test_get_contact(contact): + assert isinstance(contact, Contact) + assert contact.name == 'Rachel' + assert contact.email == 'rachel@freshdesk.com' + assert contact.helpdesk_agent is False + assert contact.customer_id == 1 + + +def test_list_contact(api, contact): + contacts = api.contacts.list_contacts() + assert isinstance(contacts, list) + assert isinstance(contacts[0], Contact) + assert len(contacts) == 2 + assert contacts[0].__dict__ == contact.__dict__ + + +def test_create_contact(api): + contact_data = { + 'name': 'Rachel', + 'email': 'rachel@freshdesk.com' + } + contact = api.contacts.create_contact(contact_data) + assert isinstance(contact, Contact) + assert contact.email == contact.email + assert contact.name == contact.name + + +def test_update_contact(api): + contact_data = { + 'name': 'New Name' + } + contact = api.contacts.update_contact(1, **contact_data) + assert isinstance(contact, Contact) + assert contact.name == 'New Name' + + +def test_soft_delete_contact(api): + assert api.contacts.soft_delete_contact(1) is None + + +def test_permanently_delete_contact(api): + assert api.contacts.permanently_delete_contact(1) is None + + +def test_restore_contact(api): + api.contacts.restore_contact(1) + contact = api.contacts.get_contact(1) + assert isinstance(contact, Contact) + assert contact.deleted is False + + +def test_make_agent(api, contact): + agent = api.contacts.make_agent(contact.id) + assert isinstance(agent, Agent) + assert agent.available is True + assert agent.occasional is False + assert agent.contact['email'] == contact.email + assert agent.contact['name'] == contact.name + + +def test_contact_datetime(contact): + assert isinstance(contact.created_at, datetime.datetime) + assert isinstance(contact.updated_at, datetime.datetime) + + +def test_contact_str(contact): + assert str(contact) == 'Rachel' + + +def test_contact_repr(contact): + assert repr(contact) == '' diff --git a/freshdesk/v2/tests/test_customer.py b/freshdesk/v2/tests/test_customer.py new file mode 100644 index 0000000..1964d69 --- /dev/null +++ b/freshdesk/v2/tests/test_customer.py @@ -0,0 +1,40 @@ +import datetime + +import pytest + +from freshdesk.v2.models import Customer + + +@pytest.fixture +def customer(api): + return api.customers.get_customer('1') + + +@pytest.fixture +def contact(api): + return api.contacts.get_contact(1) + + +def test_customer(customer): + assert isinstance(customer, Customer) + assert customer.name == 'ACME Corp.' + assert customer.domains == 'acme.com' + assert customer.cf_custom_key == 'custom_value' + + +def test_customer_datetime(customer): + assert isinstance(customer.created_at, datetime.datetime) + assert isinstance(customer.updated_at, datetime.datetime) + + +def test_customer_str(customer): + assert str(customer) == 'ACME Corp.' + + +def test_customer_repr(customer): + assert repr(customer) == '' + + +def test_get_customer_from_contact(api, contact): + customer = api.customers.get_customer_from_contact(contact) + test_customer(customer) diff --git a/freshdesk/v2/tests/test_group.py b/freshdesk/v2/tests/test_group.py new file mode 100644 index 0000000..931d3b2 --- /dev/null +++ b/freshdesk/v2/tests/test_group.py @@ -0,0 +1,36 @@ +import datetime + +import pytest + +from freshdesk.v2.models import Group + + +@pytest.fixture +def group(api): + return api.groups.get_group(1) + + +def test_list_groups(api, group): + groups = api.groups.list_groups() + assert isinstance(groups, list) + assert len(groups) == 2 + assert groups[0].id == group.id + + +def test_group(group): + assert isinstance(group, Group) + assert group.name == 'Entertainers' + assert group.description == 'Singers dancers and stand up comedians' + + +def test_group_datetime(group): + assert isinstance(group.created_at, datetime.datetime) + assert isinstance(group.updated_at, datetime.datetime) + + +def test_group_str(group): + assert str(group) == 'Entertainers' + + +def test_group_repr(group): + assert repr(group) == '' diff --git a/freshdesk/v2/tests/test_role.py b/freshdesk/v2/tests/test_role.py new file mode 100644 index 0000000..32879ac --- /dev/null +++ b/freshdesk/v2/tests/test_role.py @@ -0,0 +1,32 @@ +import datetime + +import pytest + +from freshdesk.v2.models import Role + + +@pytest.fixture +def role(api): + return api.roles.get_role(1) + + +def test_list_roles(api, role): + roles = api.roles.list_roles() + assert isinstance(roles, list) + assert len(roles) == 2 + assert roles[0].id == role.id + + +def test_role(role): + assert isinstance(role, Role) + assert role.name == 'Agent' + assert role.description, 'Can log, view, reply == update and resolve tickets and manage contacts.' + + +def test_group_datetime(role): + assert isinstance(role.created_at, datetime.datetime) + assert isinstance(role.updated_at, datetime.datetime) + + +def test_group_repr(role): + assert repr(role) == '' diff --git a/freshdesk/v2/tests/test_ticket.py b/freshdesk/v2/tests/test_ticket.py new file mode 100644 index 0000000..2df331e --- /dev/null +++ b/freshdesk/v2/tests/test_ticket.py @@ -0,0 +1,178 @@ +import datetime +import json +import os.path + +import pytest + +from freshdesk.v2.models import Ticket + + +@pytest.fixture +def ticket(api): + return api.tickets.get_ticket(1) + + +@pytest.fixture +def ticket_json(): + return json.loads(open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'ticket_1.json')).read()) + + +@pytest.fixture +def outbound_email_json(api): + return json.loads( + open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'outbound_email_1.json')).read()) + + +def test_str(ticket): + assert str(ticket) == 'This is a sample ticket' + + +def test_repr(ticket): + assert repr(ticket) == '' + + +def test_get_ticket(ticket): + assert isinstance(ticket, Ticket) + assert ticket.id == 1 + assert ticket.subject == 'This is a sample ticket' + assert ticket.description_text, 'This is a sample ticket == feel free to delete it.' + assert ticket.cc_emails == ['test2@example.com'] + assert 'foo' in ticket.tags + assert 'bar' in ticket.tags + + +def test_create_ticket(api): + attachment_path = os.path.join(os.path.dirname(__file__), 'sample_json_data', 'attachment.txt') + ticket = api.tickets.create_ticket('This is a sample ticket', + description='This is a sample ticket, feel free to delete it.', + email='test@example.com', + priority=1, status=2, + tags=['foo', 'bar'], + cc_emails=['test2@example.com'], + attachments=(attachment_path,)) + assert isinstance(ticket, Ticket) + assert ticket.subject == 'This is a sample ticket' + assert ticket.description_text, 'This is a sample ticket == feel free to delete it.' + assert ticket.priority == 'low' + assert ticket.status == 'open' + assert ticket.cc_emails == ['test2@example.com'] + assert 'foo' in ticket.tags + assert 'bar' in ticket.tags + + +def test_create_outbound_email(api, outbound_email_json): + j = outbound_email_json.copy() + email = 'test@example.com' + subject = 'This is a sample outbound email' + description = 'This is a sample outbound email, feel free to delete it.' + email_config_id = 5000054536 + values = { + 'status': 5, + 'priority': 1, + 'tags': ['foo', 'bar'], + 'cc_emails': ['test2@example.com'] + } + + email = api.tickets.create_outbound_email( + subject, + description, + email, + email_config_id, + **values + ) + + assert email.description_text == j['description_text'] + assert email._priority == j['priority'] + assert email._status == j['status'] + assert email.cc_emails == j['cc_emails'] + assert 'foo' in email.tags + assert 'bar' in email.tags + + +def test_update_ticket(api, ticket_json): + j = ticket_json.copy() + values = { + 'subject': 'Test subject update', + 'priority': 3, + 'status': 4, + 'tags': ['hello', 'world'] + } + j.update(values) + + ticket = api.tickets.update_ticket(j['id'], **values) + assert ticket.subject == 'Test subject update' + assert ticket.status == 'resolved' + assert ticket.priority == 'high' + assert 'hello' in ticket.tags + assert 'world' in ticket.tags + + +def test_delete_ticket(api): + assert api.tickets.delete_ticket(1) is None + + +def test_ticket_priority(ticket): + assert ticket._priority == 1 + assert ticket.priority == 'low' + + +def test_ticket_status(ticket): + assert ticket._status == 2 + assert ticket.status == 'open' + + +def test_ticket_source(ticket): + assert ticket._source == 2 + assert ticket.source == 'portal' + + +def test_ticket_datetime(ticket): + assert isinstance(ticket.created_at, datetime.datetime) + assert isinstance(ticket.updated_at, datetime.datetime) + + +def test_new_and_my_open_tickets(api, ticket): + tickets = api.tickets.list_new_and_my_open_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].id == ticket.id + + +def test_deleted_tickets(api): + tickets = api.tickets.list_deleted_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + + +def test_watched_tickets(api, ticket): + tickets = api.tickets.list_watched_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].id == ticket.id + + +def test_spam_tickets(api): + tickets = api.tickets.list_tickets(filter_name='spam') + assert isinstance(tickets, list) + assert len(tickets) == 1 + + +def test_default_filter_name(api, ticket): + tickets = api.tickets.list_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].id == ticket.id + + +def test_none_filter_name(api, ticket): + tickets = api.tickets.list_tickets(filter_name=None) + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].id == ticket.id + + +def test_filter_query(api, ticket): + tickets = api.tickets.filter_tickets(query="tag:'mytag'") + assert isinstance(tickets, list) + assert len(tickets) == 2 + assert 'mytag' in tickets[0].tags diff --git a/test-requirements.txt b/test-requirements.txt index 8474bc2..2b13f10 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,6 @@ coveralls -nose +pytest +pytest-cov python-dateutil requests responses diff --git a/test.sh b/test.sh index e46d1ff..6768c51 100755 --- a/test.sh +++ b/test.sh @@ -1,2 +1,2 @@ #!/bin/sh -nosetests --with-coverage --cover-package=freshdesk +pytest --cov=freshdesk diff --git a/tox.ini b/tox.ini index eb3ae4c..7374ad1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pep8,py27,py35 +envlist = pep8,py27,py36 minversion = 1.6 [testenv] @@ -12,8 +12,9 @@ deps = coverage responses mock - nose -commands = nosetests -v --with-coverage --cover-package=freshdesk + pytest + pytest-cov +commands = pytest -v --cov=freshdesk [testenv:venv] commands = {posargs}