view test/test_xmlrpc.py @ 5525:bb7865241f8a

Make CSV import/export compatible across Python versions (also RDBMS journals) (issue 2550976, issue 2550975). The roundup-admin export and import commands are used for migrating between different database backends. It is desirable that they should be usable also for migrations between Python 2 and Python 3, and in some cases (e.g. with the anydbm backend) this may be required. To be usable for such migrations, the format of the generated CSV files needs to be stable, meaning the same as currently used with Python 2. The export process uses repr() to produce the fields in the CSV files and eval() to convert them back to Python data structures. repr() of strings with non-ASCII characters produces different results for Python 2 and Python 3. This patch adds repr_export and eval_import functions to roundup/anypy/strings.py which provide the required operations that are just repr() and eval() in Python 2, but are more complicated in Python 3 to use data representations compatible with Python 2. These functions are then used in the required places for export and import. repr() and eval() are also used in storing the dict of changed values in the journal for the RDBMS backends. It is similarly desirable that the database be compatible between Python 2 and Python 3, so that export and import do not need to be used for a migration between Python versions for non-anydbm back ends. Thus, this patch changes rdbms_common.py in the places involved in storing journals in the database, not just in those involved in import/export. Given this patch, import/export with non-ASCII characters appear based on some limited testing to work across Python versions, and an instance using the sqlite backend appears to be compatible between Python versions without needing import/export, *if* the sessions/otks databases (which use anydbm) are deleted when changing Python version.
author Joseph Myers <jsm@polyomino.org.uk>
date Sun, 02 Sep 2018 23:48:04 +0000
parents 7f3dfdd6a620
children 58817c3bf471
line wrap: on
line source

#
# Copyright (C) 2007 Stefan Seefeld
# All rights reserved.
# For license terms see the file COPYING.txt.
#

from __future__ import print_function
import unittest, os, shutil, errno, sys, difflib, cgi, re

from roundup.anypy import xmlrpc_
MultiCall = xmlrpc_.client.MultiCall
from roundup.cgi.exceptions import *
from roundup import init, instance, password, hyperdb, date
from roundup.xmlrpc import RoundupInstance, RoundupDispatcher
from roundup.backends import list_backends
from roundup.hyperdb import String
from roundup.cgi import TranslationService

from . import db_test_base
from .test_mysql import skip_mysql
from .test_postgresql import skip_postgresql


class XmlrpcTest(object):

    backend = None

    def setUp(self):
        self.dirname = '_test_xmlrpc'
        # set up and open a tracker
        self.instance = db_test_base.setupTracker(self.dirname, self.backend)

        # open the database
        self.db = self.instance.open('admin')

        print("props_only default", self.db.security.get_props_only_default())

        # Get user id (user4 maybe). Used later to get data from db.
        self.joeid = 'user' + self.db.user.create(username='joe',
            password=password.Password('random'), address='random@home.org',
            realname='Joe Random', roles='User')

        self.db.commit()
        self.db.close()
        self.db = self.instance.open('joe')

        self.db.tx_Source = 'web'

        self.db.issue.addprop(tx_Source=hyperdb.String())
        self.db.msg.addprop(tx_Source=hyperdb.String())

        self.db.post_init()

        thisdir = os.path.dirname(__file__)
        vars = {}
        exec(compile(open(os.path.join(thisdir,
                                       "tx_Source_detector.py")).read(),
                     os.path.join(thisdir, "tx_Source_detector.py"), 'exec'),
             vars)
        vars['init'](self.db)

        self.server = RoundupInstance(self.db, self.instance.actions, None)

    def tearDown(self):
        self.db.close()
        try:
            shutil.rmtree(self.dirname)
        except OSError as error:
            if error.errno not in (errno.ENOENT, errno.ESRCH): raise

    def testAccess(self):
        # Retrieve all three users.
        results = self.server.list('user', 'id')
        self.assertEqual(len(results), 3)

        # Obtain data for 'joe'.
        results = self.server.display(self.joeid)
        self.assertEqual(results['username'], 'joe')
        self.assertEqual(results['realname'], 'Joe Random')

    def testChange(self):
        # Reset joe's 'realname'.
        results = self.server.set(self.joeid, 'realname=Joe Doe')
        results = self.server.display(self.joeid, 'realname')
        self.assertEqual(results['realname'], 'Joe Doe')

        # check we can't change admin's details
        self.assertRaises(Unauthorised, self.server.set, 'user1', 'realname=Joe Doe')

    def testCreate(self):
        results = self.server.create('issue', 'title=foo')
        issueid = 'issue' + results
        results = self.server.display(issueid, 'title')
        self.assertEqual(results['title'], 'foo')
        self.assertEqual(self.db.issue.get('1', "tx_Source"), 'web')

    def testFileCreate(self):
        results = self.server.create('file', 'content=hello\r\nthere')
        fileid = 'file' + results
        results = self.server.display(fileid, 'content')
        self.assertEqual(results['content'], 'hello\r\nthere')

    def testSchema(self):
        schema={'status': [('name', '<roundup.hyperdb.String>'),
                           ('order', '<roundup.hyperdb.Number>')],
                'keyword': [('name', '<roundup.hyperdb.String>')],
                'priority': [('name', '<roundup.hyperdb.String>'),
                             ('order', '<roundup.hyperdb.Number>')],
                'user': [('address', '<roundup.hyperdb.String>'),
                         ('alternate_addresses', '<roundup.hyperdb.String>'),
                         ('organisation', '<roundup.hyperdb.String>'),
                         ('password', '<roundup.hyperdb.Password>'),
                         ('phone', '<roundup.hyperdb.String>'),
                         ('queries', '<roundup.hyperdb.Multilink to "query">'),
                         ('realname', '<roundup.hyperdb.String>'),
                         ('roles', '<roundup.hyperdb.String>'),
                         ('timezone', '<roundup.hyperdb.String>'),
                         ('username', '<roundup.hyperdb.String>')],
                'file': [('content', '<roundup.hyperdb.String>'),
                         ('name', '<roundup.hyperdb.String>'),
                         ('type', '<roundup.hyperdb.String>')],
                'msg': [('author', '<roundup.hyperdb.Link to "user">'),
                        ('content', '<roundup.hyperdb.String>'),
                        ('date', '<roundup.hyperdb.Date>'),
                        ('files', '<roundup.hyperdb.Multilink to "file">'),
                        ('inreplyto', '<roundup.hyperdb.String>'),
                        ('messageid', '<roundup.hyperdb.String>'),
                        ('recipients', '<roundup.hyperdb.Multilink to "user">'),
                        ('summary', '<roundup.hyperdb.String>'),
                        ('tx_Source', '<roundup.hyperdb.String>'),
                        ('type', '<roundup.hyperdb.String>')],
                'query': [('klass', '<roundup.hyperdb.String>'),
                          ('name', '<roundup.hyperdb.String>'),
                          ('private_for', '<roundup.hyperdb.Link to "user">'),
                          ('url', '<roundup.hyperdb.String>')],
                'issue': [('assignedto', '<roundup.hyperdb.Link to "user">'),
                          ('files', '<roundup.hyperdb.Multilink to "file">'),
                          ('keyword', '<roundup.hyperdb.Multilink to "keyword">'),
                          ('messages', '<roundup.hyperdb.Multilink to "msg">'), 
                          ('nosy', '<roundup.hyperdb.Multilink to "user">'),
                          ('priority', '<roundup.hyperdb.Link to "priority">'),
                          ('status', '<roundup.hyperdb.Link to "status">'),
                          ('superseder', '<roundup.hyperdb.Multilink to "issue">'),
                          ('title', '<roundup.hyperdb.String>'),
                          ('tx_Source', '<roundup.hyperdb.String>')]}

        results = self.server.schema()
        self.assertEqual(results, schema)

    def testLookup(self):
        self.assertRaises(KeyError, self.server.lookup, 'user', '1')
        results = self.server.lookup('user', 'admin')
        self.assertEqual(results, '1')

    def testAction(self):
        # As this action requires special previledges, we temporarily switch
        # to 'admin'
        self.db.setCurrentUser('admin')
        users_before = self.server.list('user')
        try:
            tmp = 'user' + self.db.user.create(username='tmp')
            self.server.action('retire', tmp)
        finally:
            self.db.setCurrentUser('joe')
        users_after = self.server.list('user')
        self.assertEqual(users_before, users_after)

        # test a bogus action
        with self.assertRaises(Exception) as cm:
            self.server.action('bogus')
        print(cm.exception)
        self.assertEqual(cm.exception.args[0],
                         'action "bogus" is not supported ')

    def testAuthDeniedEdit(self):
        # Wrong permissions (caught by roundup security module).
        self.assertRaises(Unauthorised, self.server.set,
                          'user1', 'realname=someone')

    def testAuthDeniedCreate(self):
        self.assertRaises(Unauthorised, self.server.create,
                          'user', {'username': 'blah'})

    def testAuthAllowedEdit(self):
        self.db.setCurrentUser('admin')
        try:
            try:
                self.server.set('user2', 'realname=someone')
            except Unauthorised as err:
                self.fail('raised %s'%err)
        finally:
            self.db.setCurrentUser('joe')

    def testAuthAllowedCreate(self):
        self.db.setCurrentUser('admin')
        try:
            try:
                self.server.create('user', 'username=blah')
            except Unauthorised as err:
                self.fail('raised %s'%err)
        finally:
            self.db.setCurrentUser('joe')

    def testAuthFilter(self):
        # this checks if we properly check for search permissions
        self.db.security.permissions = {}
        # self.db.security.set_props_only_default(props_only=False)
        self.db.security.addRole(name='User')
        self.db.security.addRole(name='Project')
        self.db.security.addPermissionToRole('User', 'Web Access')
        self.db.security.addPermissionToRole('Project', 'Web Access')
        # Allow viewing keyword
        p = self.db.security.addPermission(name='View', klass='keyword')
        print("View keyword class: %r"%p)
        self.db.security.addPermissionToRole('User', p)
        # Allow viewing interesting things (but not keyword) on issue
        # But users might only view issues where they are on nosy
        # (so in the real world the check method would be better)
        p = self.db.security.addPermission(name='View', klass='issue',
            properties=("title", "status"), check=lambda x,y,z: True)
        print("View keyword class w/ props: %r"%p)
        self.db.security.addPermissionToRole('User', p)
        # Allow role "Project" access to whole issue
        p = self.db.security.addPermission(name='View', klass='issue')
        self.db.security.addPermissionToRole('Project', p)
        # Allow all access to status:
        p = self.db.security.addPermission(name='View', klass='status')
        self.db.security.addPermissionToRole('User', p)
        self.db.security.addPermissionToRole('Project', p)

        keyword = self.db.keyword
        status = self.db.status
        issue = self.db.issue

        d1 = keyword.create(name='d1')
        d2 = keyword.create(name='d2')
        open = status.create(name='open')
        closed = status.create(name='closed')
        issue.create(title='i1', status=open, keyword=[d2])
        issue.create(title='i2', status=open, keyword=[d1])
        issue.create(title='i2', status=closed, keyword=[d1])

        chef = self.db.user.create(username = 'chef', roles='User, Project')
        joe  = self.db.user.lookup('joe')

        # Conditionally allow view of whole issue (check is False here,
        # this might check for keyword owner in the real world)
        p = self.db.security.addPermission(name='View', klass='issue',
            check=lambda x,y,z: False)
        print("View issue class: %r"%p)
        self.db.security.addPermissionToRole('User', p)
        # Allow user to search for issue.status
        p = self.db.security.addPermission(name='Search', klass='issue',
            properties=("status",))
        print("View Search class w/ props: %r"%p)
        self.db.security.addPermissionToRole('User', p)

        keyw = {'keyword':self.db.keyword.lookup('d1')}
        stat = {'status':self.db.status.lookup('open')}
        keygroup = keysort = [('+', 'keyword')]
        self.db.commit()

        # Filter on keyword ignored for role 'User':
        r = self.server.filter('issue', None, keyw)
        self.assertEqual(r, ['1', '2', '3'])
        # Filter on status works for all:
        r = self.server.filter('issue', None, stat)
        self.assertEqual(r, ['1', '2'])
        # Sorting and grouping for class User fails:
        r = self.server.filter('issue', None, {}, sort=keysort)
        self.assertEqual(r, ['1', '2', '3'])
        r = self.server.filter('issue', None, {}, group=keygroup)
        self.assertEqual(r, ['1', '2', '3'])

        self.db.close()
        self.db = self.instance.open('chef')
        self.db.tx_Source = 'web'

        self.db.issue.addprop(tx_Source=hyperdb.String())
        self.db.msg.addprop(tx_Source=hyperdb.String())
        self.db.post_init()

        self.server = RoundupInstance(self.db, self.instance.actions, None)

        # Filter on keyword works for role 'Project':
        r = self.server.filter('issue', None, keyw)
        self.assertEqual(r, ['2', '3'])
        # Filter on status works for all:
        r = self.server.filter('issue', None, stat)
        self.assertEqual(r, ['1', '2'])
        # Sorting and grouping for class Project works:
        r = self.server.filter('issue', None, {}, sort=keysort)
        self.assertEqual(r, ['2', '3', '1'])
        r = self.server.filter('issue', None, {}, group=keygroup)
        self.assertEqual(r, ['2', '3', '1'])

    def testMulticall(self):
        translator = TranslationService.get_translation(
            language=self.instance.config["TRACKER_LANGUAGE"],
            tracker_home=self.instance.config["TRACKER_HOME"])
        self.server = RoundupDispatcher(self.db, self.instance.actions,
            translator, allow_none = True)
        class S:
            multicall=self.server.funcs['system.multicall']
        self.server.system = S()
        self.db.issue.create(title='i1')
        self.db.issue.create(title='i2')
        m = MultiCall(self.server)
        m.display('issue1')
        m.display('issue2')
        result = m()
        results = [
            {'files': [], 'status': '1', 'tx_Source': 'web',
             'keyword': [], 'title': 'i1', 'nosy': [], 'messages': [],
             'priority': None, 'assignedto': None, 'superseder': []},
            {'files': [], 'status': '1', 'tx_Source': 'web',
             'keyword': [], 'title': 'i2', 'nosy': [], 'messages': [],
             'priority': None, 'assignedto': None, 'superseder': []}]
        for n, r in enumerate(result):
            self.assertEqual(r, results[n])


class anydbmXmlrpcTest(XmlrpcTest, unittest.TestCase):
    backend = 'anydbm'


@skip_mysql
class mysqlXmlrpcTest(XmlrpcTest, unittest.TestCase):
    backend = 'mysql'


class sqliteXmlrpcTest(XmlrpcTest, unittest.TestCase):
    backend = 'sqlite'


@skip_postgresql
class postgresqlXmlrpcTest(XmlrpcTest, unittest.TestCase):
    backend = 'postgresql'

Roundup Issue Tracker: http://roundup-tracker.org/