Hello community, here is the log from the commit of package python-rt for openSUSE:Factory checked in at 2019-12-04 13:53:16 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-rt (Old) and /work/SRC/openSUSE:Factory/.python-rt.new.4691 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-rt" Wed Dec 4 13:53:16 2019 rev:4 rq:753272 version:1.0.12 Changes: -------- --- /work/SRC/openSUSE:Factory/python-rt/python-rt.changes 2018-12-24 11:43:35.265335321 +0100 +++ /work/SRC/openSUSE:Factory/.python-rt.new.4691/python-rt.changes 2019-12-04 14:20:09.982427327 +0100 @@ -1,0 +2,13 @@ +Tue Nov 26 14:36:32 UTC 2019 - Sebastian Wagner <[email protected]> + +- update to version 1.0.12: + - Travis CI Docker tests + - RT 4.4 fixes + - Support multiline CF values in create_ticket and edit_ticket. + - Fix support for custom field names containing colons + - In search(), replace splitlines() with lines array split on \n. + - Add debug_mode flag for response logging + - Add platform independent url joining / Allow testing on Windows + - Add numerical_id to get_ticket result + +------------------------------------------------------------------- Old: ---- rt-1.0.11.tar.gz New: ---- rt-1.0.12.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-rt.spec ++++++ --- /var/tmp/diff_new_pack.eLAuX1/_old 2019-12-04 14:20:10.514427775 +0100 +++ /var/tmp/diff_new_pack.eLAuX1/_new 2019-12-04 14:20:10.518427778 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-rt # -# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2019 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,12 +19,12 @@ # Tests require internet connection %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-rt -Version: 1.0.11 +Version: 1.0.12 Release: 0 Summary: Python interface to Request Tracker API License: GPL-3.0-only Group: Development/Languages/Python -Url: https://github.com/CZ-NIC/python-rt +URL: https://github.com/CZ-NIC/python-rt Source: https://files.pythonhosted.org/packages/source/r/rt/rt-%{version}.tar.gz BuildRequires: %{python_module setuptools} BuildRequires: fdupes ++++++ rt-1.0.11.tar.gz -> rt-1.0.12.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rt-1.0.11/CHANGES new/rt-1.0.12/CHANGES --- old/rt-1.0.11/CHANGES 2018-07-16 14:39:22.000000000 +0200 +++ new/rt-1.0.12/CHANGES 2019-10-25 15:05:33.000000000 +0200 @@ -1,7 +1,20 @@ -v1.0.11, unreleased +v1.0.12, 2019-10-25 +- Travis CI Docker tests +- RT 4.4 fixes +- Support multiline CF values in create_ticket and edit_ticket. +- Fix support for custom field names containing colons +- In search(), replace splitlines() with lines array split on \n. +- Add debug_mode flag for response logging +- Add platform independent url joining / Allow testing on Windows +- Add numerical_id to get_ticket result + +v1.0.11, 2018-07-16 - Added parameter to set the content type in reply() and comment() (#12). - Added parameter Format to search() (#17). - Tests: Update to new demo instance, fixing tests. +- Tests: Disable tests in Travis, the existing test instance closed the REST interface (#28). +- Fix support for custom field names containing colons (#37). +- Fix support for custom field values containing newlines (#11). v1.0.10, 2017-02-22 - PEP8 fixes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rt-1.0.11/PKG-INFO new/rt-1.0.12/PKG-INFO --- old/rt-1.0.11/PKG-INFO 2018-07-16 14:51:02.000000000 +0200 +++ new/rt-1.0.12/PKG-INFO 2019-10-25 15:06:55.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: rt -Version: 1.0.11 +Version: 1.0.12 Summary: Python interface to Request Tracker API Home-page: https://github.com/CZ-NIC/python-rt Author: Jiri Machalek diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rt-1.0.11/rt.egg-info/PKG-INFO new/rt-1.0.12/rt.egg-info/PKG-INFO --- old/rt-1.0.11/rt.egg-info/PKG-INFO 2018-07-16 14:51:02.000000000 +0200 +++ new/rt-1.0.12/rt.egg-info/PKG-INFO 2019-10-25 15:06:55.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: rt -Version: 1.0.11 +Version: 1.0.12 Summary: Python interface to Request Tracker API Home-page: https://github.com/CZ-NIC/python-rt Author: Jiri Machalek diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rt-1.0.11/rt.py new/rt-1.0.12/rt.py --- old/rt-1.0.11/rt.py 2018-07-16 14:47:27.000000000 +0200 +++ new/rt-1.0.12/rt.py 2019-10-25 14:58:58.000000000 +0200 @@ -27,13 +27,21 @@ import os import re +import sys + import warnings +import datetime import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth from six import iteritems from six.moves import range +if sys.version_info.major == 2: + from urlparse import urljoin +else: + from urllib.parse import urljoin + __license__ = """ Copyright (C) 2012 CZ.NIC, z.s.p.o. Copyright (c) 2015 Genome Research Ltd. @@ -61,6 +69,9 @@ ALL_QUEUES = object() +DEBUG_MODE = False +""" Flag to enable debug mode for all Rt instances """ + class RtError(Exception): """ Super class of all Rt Errors """ @@ -160,18 +171,18 @@ 'deleted_link_pattern': re.compile('.* Deleted link '), 'merge_successful_pattern': re.compile('^# Merge completed.|^Merge Successful$'), 'bad_request_pattern': re.compile('.* 400 Bad Request$'), - 'user_pattern': re.compile('^# User ([0-9]*) (?:updated|created)\.$'), - 'queue_pattern': re.compile('^# Queue (\w*) (?:updated|created)\.$'), - 'ticket_created_pattern': re.compile('^# Ticket ([0-9]+) created\.$'), - 'does_not_exist_pattern': re.compile('^# (?:Queue|User|Ticket) \w* does not exist\.$'), - 'does_not_exist_pattern_bytes': re.compile(b'^# (?:Queue|User|Ticket) \w* does not exist\.$'), - 'not_related_pattern': re.compile('^# Transaction \d+ is not related to Ticket \d+'), - 'invalid_attachment_pattern_bytes': re.compile(b'^# Invalid attachment id: \d+$'), + 'user_pattern': re.compile(r'^# User ([0-9]*) (?:updated|created)\.$'), + 'queue_pattern': re.compile(r'^# Queue (\w*) (?:updated|created)\.$'), + 'ticket_created_pattern': re.compile(r'^# Ticket ([0-9]+) created\.$'), + 'does_not_exist_pattern': re.compile(r'^# (?:Queue|User|Ticket) \w* does not exist\.$'), + 'does_not_exist_pattern_bytes': re.compile(br'^# (?:Queue|User|Ticket) \w* does not exist\.$'), + 'not_related_pattern': re.compile(r'^# Transaction \d+ is not related to Ticket \d+'), + 'invalid_attachment_pattern_bytes': re.compile(br'^# Invalid attachment id: \d+$'), } def __init__(self, url, default_login=None, default_password=None, proxy=None, default_queue=DEFAULT_QUEUE, basic_auth=None, digest_auth=None, - skip_login=False, verify_cert=True): + skip_login=False, verify_cert=True, debug_mode=False): """ API initialization. :keyword url: Base URL for Request Tracker API. @@ -188,7 +199,11 @@ need to call login, because it is managed by requests library instantly. """ + # ensure trailing slash + if not url.endswith("/"): + url = url + "/" self.url = url + self.debug_mode = debug_mode self.default_login = default_login self.default_password = default_password self.default_queue = default_queue @@ -236,7 +251,7 @@ try: if (not self.login_result) and (not without_login): raise AuthorizationError('First login by calling method `login`.') - url = str(os.path.join(self.url, selector)) + url = str(urljoin(self.url, selector)) if not files: if post_data: response = self.session.post(url, data=post_data) @@ -247,6 +262,17 @@ for i, file_pair in enumerate(files): files_data['attachment_{:d}'.format(i + 1)] = file_pair response = self.session.post(url, data=post_data, files=files_data) + if self.debug_mode or DEBUG_MODE: + method = "GET" + if post_data or files: + method = "POST" + print("### {0}".format(datetime.datetime.now().isoformat())) + print("Request URL: {0}".format(url)) + print("Request method: {0}".format(method)) + print("Respone status code: {0}".format(response.status_code)) + print("Response content:") + print(response.content.decode()) + if response.status_code == 401: raise AuthorizationError('Server could not verify that you are authorized to access the requested document.') if response.status_code != 200: @@ -281,7 +307,7 @@ """ try: return int(msg.split('\n')[0].split(' ')[1]) - except: + except Exception: return None def __check_response(self, msg): @@ -496,7 +522,7 @@ raise UnexpectedMessageFormat('Missing line starting with `Requestors:`.') for i in range(req_id): if ': ' in msg[i]: - header, content = msg[i].split(': ', 1) + header, content = self.split_header(msg[i]) pairs[header.strip()] = content.strip() requestors = [msg[req_id][12:]] req_id += 1 @@ -506,7 +532,7 @@ pairs['Requestors'] = self.__normalize_list(requestors) for i in range(req_id, len(msg)): if ': ' in msg[i]: - header, content = msg[i].split(': ', 1) + header, content = self.split_header(msg[i]) pairs[header.strip()] = content.strip() if pairs: items.append(pairs) @@ -515,20 +541,30 @@ pairs['Cc'] = self.__normalize_list(pairs['Cc']) if 'AdminCc' in pairs: pairs['AdminCc'] = self.__normalize_list(pairs['AdminCc']) + + if 'id' not in pairs and not pairs['id'].startswitch('ticket/'): + raise UnexpectedMessageFormat('Response from RT didn\'t contain a valid ticket_id') + else: + pairs['numerical_id'] = pairs['id'].split('ticket/')[1] + return items elif Format == 's': items = [] - msgs = msg.splitlines()[2:] + msgs = lines[2:] for msg in msgs: - ticket_id, subject = msg.split(': ', 1) - items.append({'id': 'ticket/' + ticket_id, 'Subject': subject}) + if "" == msg: # Ignore blank line at the end + continue + ticket_id, subject = self.split_header(msg) + items.append({'id': 'ticket/' + ticket_id, 'numerical_id': ticket_id, 'Subject': subject}) return items elif Format == 'i': items = [] - msgs = msg.splitlines()[2:] + msgs = lines[2:] for msg in msgs: + if "" == msg: # Ignore blank line at the end + continue _, ticket_id = msg.split('/', 1) - items.append({'id': 'ticket/' + ticket_id}) + items.append({'id': 'ticket/' + ticket_id, 'numerical_id': ticket_id}) return items def get_ticket(self, ticket_id): @@ -540,6 +576,7 @@ *ticket_id* or None if ticket does not exist. List of keys: * id + * numerical_id * Queue * Owner * Creator @@ -575,7 +612,7 @@ raise UnexpectedMessageFormat('Missing line starting with `Requestors:`.') for i in range(req_id): if ': ' in msg[i]: - header, content = msg[i].split(': ', 1) + header, content = self.split_header(msg[i]) pairs[header.strip()] = content.strip() requestors = [msg[req_id][12:]] req_id += 1 @@ -585,7 +622,7 @@ pairs['Requestors'] = self.__normalize_list(requestors) for i in range(req_id, len(msg)): if ': ' in msg[i]: - header, content = msg[i].split(': ', 1) + header, content = self.split_header(msg[i]) pairs[header.strip()] = content.strip() if 'Cc' in pairs: @@ -593,10 +630,36 @@ if 'AdminCc' in pairs: pairs['AdminCc'] = self.__normalize_list(pairs['AdminCc']) + if 'id' not in pairs and not pairs['id'].startswitch('ticket/'): + raise UnexpectedMessageFormat('Response from RT didn\'t contain a valid ticket_id') + else: + pairs['numerical_id'] = pairs['id'].split('ticket/')[1] + return pairs else: raise UnexpectedMessageFormat('Received status code is {:d} instead of 200.'.format(status_code)) + def __ticket_post_data(self, data_source): + """Convert a dictionary of RT ticket data into a REST POST data string. + + :param data_source: Dictionary with ticket fields and values. + + :returns: Equivalent string to POST to the RT REST interface. + """ + post_data = [] + for key in data_source: + if key.startswith('CF_'): + rt_key = 'CF.{{{}}}'.format(key[3:]) + else: + rt_key = key + value = data_source[key] + if isinstance(value, (list, tuple)): + value = ', '.join(value) + value_lines = iter(value.splitlines()) + post_data.append('{}: {}'.format(rt_key, next(value_lines, ''))) + post_data.extend(' ' + line for line in value_lines) + return '\n'.join(post_data) + def create_ticket(self, Queue=None, files=[], **kwargs): """ Create new ticket and set given parameters. @@ -639,14 +702,9 @@ :returns: ID of new ticket or ``-1``, if creating failed """ - post_data = 'id: ticket/new\nQueue: {}\n'.format(Queue or self.default_queue, ) - for key in kwargs: - if key[:4] == 'Text': - post_data += "{}: {}\n".format(key, re.sub(r'\n', r'\n ', kwargs[key])) - elif key[:3] == 'CF_': - post_data += "CF.{{{}}}: {}\n".format(key[3:], kwargs[key]) - else: - post_data += "{}: {}\n".format(key, kwargs[key]) + kwargs['id'] = 'ticket/new' + kwargs['Queue'] = Queue or self.default_queue + post_data = self.__ticket_post_data(kwargs) for file_info in files: post_data += "\nAttachment: {}".format(file_info[0], ) msg = self.__request('ticket/new', post_data={'content': post_data}, files=files) @@ -676,14 +734,7 @@ Ticket with given ID does not exist or unknown parameter was set (in this case all other valid fields are changed) """ - post_data = '' - for key, value in iteritems(kwargs): - if isinstance(value, (list, tuple)): - value = ", ".join(value) - if key[:3] != 'CF_': - post_data += "{}: {}\n".format(key, value) - else: - post_data += "CF.{{{}}}: {}\n".format(key[3:], value) + post_data = self.__ticket_post_data(kwargs) msg = self.__request('ticket/{}/edit'.format(str(ticket_id)), post_data={'content': post_data}) state = msg.split('\n')[2] return self.RE_PATTERNS['update_pattern'].match(state) is not None @@ -736,7 +787,7 @@ Missing line starting with `Attachements:`.') for i in range(cont_id): if ': ' in msg[i]: - header, content = msg[i].split(': ', 1) + header, content = self.split_header(msg[i]) pairs[header.strip()] = content.strip() content = msg[cont_id][9:] cont_id += 1 @@ -746,12 +797,12 @@ pairs['Content'] = content for i in range(cont_id, atta_id): if ': ' in msg[i]: - header, content = msg[i].split(': ', 1) + header, content = self.split_header(msg[i]) pairs[header.strip()] = content.strip() attachments = [] for i in range(atta_id + 1, len(msg)): if ': ' in msg[i]: - header, content = msg[i].split(': ', 1) + header, content = self.split_header(msg[i]) attachments.append((int(header), content.strip())) pairs['Attachments'] = attachments @@ -976,8 +1027,7 @@ msg = self.__request('ticket/{}/attachments/{}'.format(str(ticket_id), str(attachment_id)), text_response=False) msg = msg.split(b'\n') - if (len(msg) > 2) and (self.RE_PATTERNS['invalid_attachment_pattern_bytes'].match(msg[2]) or self.RE_PATTERNS[ - 'does_not_exist_pattern_bytes'].match(msg[2])): + if (len(msg) > 2) and (self.RE_PATTERNS['invalid_attachment_pattern_bytes'].match(msg[2]) or self.RE_PATTERNS['does_not_exist_pattern_bytes'].match(msg[2])): return None msg = msg[2:] head_matching = [i for i, m in enumerate(msg) if self.RE_PATTERNS['headers_pattern_bytes'].match(m)] @@ -1031,8 +1081,7 @@ (str(ticket_id), str(attachment_id)), text_response=False) lines = msg.split(b'\n', 3) - if (len(lines) == 4) and (self.RE_PATTERNS['invalid_attachment_pattern_bytes'].match(lines[2]) or self.RE_PATTERNS[ - 'does_not_exist_pattern_bytes'].match(lines[2])): + if (len(lines) == 4) and (self.RE_PATTERNS['invalid_attachment_pattern_bytes'].match(lines[2]) or self.RE_PATTERNS['does_not_exist_pattern_bytes'].match(lines[2])): return None return msg[msg.find(b'\n') + 2:-3] @@ -1272,7 +1321,7 @@ i = 2 while i < len(msg): if ': ' in msg[i]: - key, link = msg[i].split(': ', 1) + key, link = self.split_header(msg[i]) links = [link.strip()] j = i + 1 pad = len(key) + 2 @@ -1405,3 +1454,19 @@ post_data = {'content': "Ticket: {}\nAction: untake".format(str(ticket_id))} msg = self.__request('ticket/{}/take'.format(str(ticket_id)), post_data=post_data) return self.__get_status_code(msg) == 200 + + @staticmethod + def split_header(line): + """ Split a header line into field name and field value. + + Note that custom fields may contain colons inside the curly braces, + so we need a special test for them. + + :param line: A message line to be split. + + :returns: (Field name, field value) tuple. + """ + match = re.match(r'^(CF\.\{.*?}): (.*)$', line) + if match: + return (match.group(1), match.group(2)) + return line.split(': ', 1) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rt-1.0.11/setup.cfg new/rt-1.0.12/setup.cfg --- old/rt-1.0.11/setup.cfg 2018-07-16 14:51:02.000000000 +0200 +++ new/rt-1.0.12/setup.cfg 2019-10-25 15:06:55.000000000 +0200 @@ -1,6 +1,10 @@ [bdist_wheel] universal = 1 +[pycodestyle] +filename = rt.py +ignore = E501 + [egg_info] tag_build = tag_date = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rt-1.0.11/setup.py new/rt-1.0.12/setup.py --- old/rt-1.0.11/setup.py 2018-07-16 14:48:00.000000000 +0200 +++ new/rt-1.0.12/setup.py 2019-10-25 15:01:01.000000000 +0200 @@ -9,7 +9,7 @@ README = open(os.path.join(here, 'README.rst')).read() setup(name='rt', - version='1.0.11', + version='1.0.12', description='Python interface to Request Tracker API', long_description=README, license='GNU General Public License (GPL)', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rt-1.0.11/test_rt.py new/rt-1.0.12/test_rt.py --- old/rt-1.0.11/test_rt.py 2018-07-16 14:48:00.000000000 +0200 +++ new/rt-1.0.12/test_rt.py 2019-10-25 14:58:58.000000000 +0200 @@ -31,17 +31,14 @@ class RtTestCase(unittest.TestCase): + rt.DEBUG_MODE = True RT_VALID_CREDENTIALS = { 'RT4.4 stable': { - 'url': 'http://demo.request-tracker.fr/REST/1.0', - 'admin': { - 'default_login': 'administrateur', - 'default_password': 'administrateur', - }, + 'url': "http://localhost:8080/REST/1.0/", 'support': { - 'default_login': 'support', - 'default_password': 'support', - } + 'default_login': 'root', + 'default_password': 'password', + }, }, # HTTP timeout # 'RT4.6 dev': { @@ -59,7 +56,7 @@ RT_INVALID_CREDENTIALS = { 'RT4.4 stable (bad credentials)': { - 'url': 'http://demo.request-tracker.fr/REST/1.0', + 'url': "http://localhost:8080/REST/1.0/", 'default_login': 'idontexist', 'default_password': 'idonthavepassword', }, @@ -67,7 +64,7 @@ RT_MISSING_CREDENTIALS = { 'RT4.4 stable (missing credentials)': { - 'url': 'http://demo.request-tracker.fr/REST/1.0', + 'url': "http://localhost:8080/REST/1.0/", }, } @@ -79,6 +76,14 @@ }, } + def _have_creds(*creds_seq): + return all(creds[name].get('url') for creds in creds_seq for name in creds) + + @unittest.skipUnless(_have_creds(RT_VALID_CREDENTIALS, + RT_INVALID_CREDENTIALS, + RT_MISSING_CREDENTIALS, + RT_BAD_URL), + "missing credentials required to run test") def test_login_and_logout(self): for name in self.RT_VALID_CREDENTIALS: tracker = rt.Rt(self.RT_VALID_CREDENTIALS[name]['url'], **self.RT_VALID_CREDENTIALS[name]['support']) @@ -95,20 +100,12 @@ tracker = rt.Rt(**params) self.assertRaises(rt.UnexpectedResponse, lambda: tracker.login()) - def check_or_create_queue(self, name): - tracker = rt.Rt(self.RT_VALID_CREDENTIALS[name]['url'], **self.RT_VALID_CREDENTIALS[name]['admin']) - tracker.login() - queue = tracker.get_queue('General') - if 'Name' not in queue: - queue_id = tracker.create_queue('General') - tracker.logout() - + @unittest.skipUnless(_have_creds(RT_VALID_CREDENTIALS), + "missing credentials required to run test") def test_ticket_operations(self): ticket_subject = 'Testing issue ' + "".join([random.choice(string.ascii_letters) for i in range(15)]) ticket_text = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' for name in ('RT4.4 stable',): - self.check_or_create_queue(name) - url = self.RT_VALID_CREDENTIALS[name]['url'] default_login = self.RT_VALID_CREDENTIALS[name]['support']['default_login'] default_password = self.RT_VALID_CREDENTIALS[name]['support']['default_password'] @@ -153,7 +150,7 @@ # get_short_history short_hist = tracker.get_short_history(ticket_id) self.assertTrue(len(short_hist) > 0, 'Empty ticket short history.') - self.assertEqual(short_hist[0][1], 'Ticket created by support') + self.assertEqual(short_hist[0][1], 'Ticket created by %s' % default_login) # create 2nd ticket ticket2_subject = 'Testing issue ' + "".join([random.choice(string.ascii_letters) for i in range(15)]) ticket2_id = tracker.create_ticket(Subject=ticket2_subject) @@ -175,21 +172,22 @@ # should provide a content type as RT 4.0 type guessing is broken (missing use statement for guess_media_type in REST.pm) self.assertTrue(tracker.reply(ticket_id, text=reply_text, files=[(attachment_name, attachment_content, 'text/plain')]), 'Reply to ticket returned False indicating error.') - at_ids = tracker.get_attachments_ids(ticket_id) - self.assertTrue(at_ids, 'Emply list with attachment ids, something went wrong.') - at_content = tracker.get_attachment_content(ticket_id, at_ids[-1]) - self.assertEqual(at_content, attachment_content, 'Recorded attachment is not equal to the original file.') # attachments list at_list = tracker.get_attachments(ticket_id) + self.assertTrue(at_list, 'Empty list with attachment ids, something went wrong.') at_names = [at[1] for at in at_list] self.assertTrue(attachment_name in at_names, 'Attachment name is not in the list of attachments.') + # get the attachment and compare it's content + at_id = at_list[at_names.index(attachment_name)][0] + at_content = tracker.get_attachment_content(ticket_id, + at_id) + self.assertEqual(at_content, attachment_content, 'Recorded attachment is not equal to the original file.') # merge tickets self.assertTrue(tracker.merge_ticket(ticket2_id, ticket_id), 'Merging tickets failed.') # delete ticket self.assertTrue(tracker.edit_ticket(ticket_id, Status='deleted'), 'Ticket delete failed.') # get user - self.assertEqual(tracker.get_user(default_login)['EmailAddress'], default_login + '@no.mail', - 'Bad user email received.') + self.assertIn('@', tracker.get_user(default_login)['EmailAddress']) if __name__ == '__main__':
