Exemple #1
0
class ProductMilestoneTestCase(MilestoneTestCase, MultiproductTestCase):
    def setUp(self):
        self.global_env = self._setup_test_env(create_folder=True)
        self._upgrade_mp(self.global_env)
        self._setup_test_log(self.global_env)
        self._load_product_from_data(self.global_env, self.default_product)

        self.env = ProductEnvironment(self.global_env, self.default_product)
        self._load_default_data(self.env)

    def tearDown(self):
        shutil.rmtree(self.global_env.path)
        self.global_env.reset_db()
        self.env = self.global_env = None

    def test_update_milestone(self):

        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")

        milestone = Milestone(self.env, 'Test')
        t1 = datetime(2001, 01, 01, tzinfo=utc)
        t2 = datetime(2002, 02, 02, tzinfo=utc)
        milestone.due = t1
        milestone.completed = t2
        milestone.description = 'Foo bar'
        milestone.update()

        self.assertEqual(
            [('Test', to_utimestamp(t1), to_utimestamp(t2), 'Foo bar', 
                    self.default_product)],
            self.env.db_query("SELECT * FROM milestone WHERE name='Test'"))
Exemple #2
0
class EnvironmentUpgradeTestCase(unittest.TestCase):
    def setUp(self, options=()):
        env_path = tempfile.mkdtemp(prefix='bh-product-tempenv-')
        self.env = Environment(env_path, create=True, options=options)
        DummyPlugin.version = 1

    def tearDown(self):
        shutil.rmtree(self.env.path)

    def test_can_upgrade_environment_with_multi_product_disabled(self):
        self.env.upgrade()

        # Multiproduct was not enabled so multiproduct tables should not exist
        for table in BLOODHOUND_TABLES:
            with self.assertFailsWithMissingTable():
                self.env.db_direct_query("SELECT * FROM %s" % table)

        for table in TABLES_WITH_PRODUCT_FIELD:
            with self.assertFailsWithMissingColumn():
                self.env.db_direct_query("SELECT product FROM %s" % table)

    def test_upgrade_creates_multi_product_tables_and_adds_product_column(self):
        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            for table in BLOODHOUND_TABLES:
                db("SELECT * FROM %s" % table)

            for table in TABLES_WITH_PRODUCT_FIELD:
                db("SELECT product FROM %s" % table)

    def test_upgrade_creates_default_product(self):
        self._enable_multiproduct()
        self.env.upgrade()

        products = Product.select(self.env)
        self.assertEqual(len(products), 1)

    def test_upgrade_moves_tickets_and_related_objects_to_default_prod(self):
        self._add_custom_field('custom_field')
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO ticket (id) VALUES (1)""")
            db("""INSERT INTO attachment (type, id, filename)
                       VALUES ('ticket', '1', '')""")
            db("""INSERT INTO ticket_custom (ticket, name, value)
                       VALUES (1, 'custom_field', '42')""")
            db("""INSERT INTO ticket_change (ticket, time, field)
                       VALUES (1, 42, 'summary')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('@'):
            ticket = Ticket(self.env, 1)
            attachments = list(Attachment.select(self.env,
                                                 ticket.resource.realm,
                                                 ticket.resource.id))
            self.assertEqual(len(attachments), 1)
            self.assertEqual(ticket['custom_field'], '42')
            changes = ticket.get_changelog()
            self.assertEqual(len(changes), 3)

    def test_upgrade_moves_custom_wikis_to_default_product(self):
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO wiki (name, version) VALUES ('MyPage', 1)""")
            db("""INSERT INTO attachment (type, id, filename)
                         VALUES ('wiki', 'MyPage', '')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            self.assertEqual(
                len(db("""SELECT * FROM wiki WHERE product='@'""")), 1)
            self.assertEqual(
                len(db("""SELECT * FROM attachment
                           WHERE product='@'
                             AND type='wiki'""")), 1)

    def test_upgrade_moves_system_wikis_to_products(self):
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO wiki (name, version) VALUES ('WikiStart', 1)""")
            db("""INSERT INTO attachment (type, id, filename)
                         VALUES ('wiki', 'WikiStart', '')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            self.assertEqual(
                len(db("""SELECT * FROM wiki WHERE product='@'""")), 1)
            self.assertEqual(
                len(db("""SELECT * FROM attachment
                           WHERE product='@'
                             AND type='wiki'""")), 1)
            self.assertEqual(
                len(db("""SELECT * FROM wiki WHERE product=''""")), 0)
            self.assertEqual(
                len(db("""SELECT * FROM attachment
                           WHERE product=''
                             AND type='wiki'""")), 0)

    def test_upgrade_copies_content_of_system_tables_to_all_products(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._update_db_version(db, 1)
            for i in range(1, 6):
                db("""INSERT INTO bloodhound_product (prefix, name)
                           VALUES ('p%d', 'Product 1')""" % i)
            for table in ('component', 'milestone', 'enum', 'version',
                          'permission', 'report'):
                db("""DELETE FROM %s""" % table)
            db("""INSERT INTO component (name) VALUES ('foobar')""")
            db("""INSERT INTO milestone (name) VALUES ('foobar')""")
            db("""INSERT INTO version (name) VALUES ('foobar')""")
            db("""INSERT INTO enum (type, name) VALUES ('a', 'b')""")
            db("""INSERT INTO permission VALUES ('x', 'TICKET_VIEW')""")
            db("""INSERT INTO report (title) VALUES ('x')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            for table in ('component', 'milestone', 'version', 'enum',
                          'report'):
                rows = db("SELECT * FROM %s" % table)
                self.assertEqual(
                    len(rows), 6,
                    "Wrong number of lines in %s (%d instead of %d)\n%s"
                    % (table, len(rows), 6, rows))
            for table in ('permission',):
                # Permissions also hold rows for global product.
                rows = db("SELECT * FROM %s WHERE username='******'" % table)
                self.assertEqual(
                    len(rows), 7,
                    "Wrong number of lines in %s (%d instead of %d)\n%s"
                    % (table, len(rows), 7, rows))

    def test_upgrading_database_moves_attachment_to_correct_product(self):
        ticket = self.insert_ticket('ticket')
        wiki = self.insert_wiki('MyWiki')
        attachment = self._create_file_with_content('Hello World!')
        self.add_attachment(ticket.resource, attachment)
        self.add_attachment(wiki.resource, attachment)

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('@'):
            attachments = list(
                Attachment.select(self.env, 'ticket', ticket.id))
            attachments.extend(
                Attachment.select(self.env, 'wiki', wiki.name))
        self.assertEqual(len(attachments), 2)
        for attachment in attachments:
            self.assertEqual(attachment.open().read(), 'Hello World!')

    def test_can_upgrade_database_with_ticket_attachment_with_text_ids(self):
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO attachment (id, type, filename)
                       VALUES ('abc', 'ticket', '')""")

        self._enable_multiproduct()
        self.env.upgrade()

    def test_can_upgrade_database_with_orphaned_attachments(self):
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO attachment (id, type, filename)
                       VALUES ('5', 'ticket', '')""")
            db("""INSERT INTO attachment (id, type, filename)
                       VALUES ('MyWiki', 'wiki', '')""")

        self._enable_multiproduct()
        self.env.upgrade()

    def test_can_upgrade_multi_product_from_v1(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._update_db_version(db, 1)

            db("""INSERT INTO bloodhound_product (prefix, name)
                       VALUES ('p1', 'Product 1')""")
            db("""INSERT INTO ticket (id, product)
                       VALUES (1, 'Product 1')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('p1'):
            Ticket(self.env, 1)

    def test_can_upgrade_multi_product_from_v2(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._replace_product_on_ticket_with_product_prefix(db)
            mp._update_db_version(db, 2)

            db("""INSERT INTO bloodhound_product (prefix, name)
                       VALUES ('p1', 'Product 1')""")
            db("""INSERT INTO ticket (id, product)
                       VALUES (1, 'p1')""")
            db("""INSERT INTO ticket (id)
                       VALUES (2)""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('p1'):
            Ticket(self.env, 1)
        with self.product('@'):
            Ticket(self.env, 2)

    def test_upgrade_plugin(self):
        self._enable_component(DummyPlugin)
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            db("SELECT v1 FROM dummy_table")
            with self.assertFailsWithMissingColumn():
                db("SELECT v2 FROM dummy_table")

        DummyPlugin.version = 2
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            db("SELECT v2 FROM dummy_table")

    def test_upgrade_plugin_to_multiproduct(self):
        self._enable_multiproduct()
        self._enable_component(DummyPlugin)
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            db("SELECT * FROM dummy_table")
            db("""SELECT * FROM "@_dummy_table" """)

    def test_upgrade_existing_plugin_to_multiproduct(self):
        self._enable_component(DummyPlugin)
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            with self.assertFailsWithMissingTable():
                db("""SELECT * FROM "@_dummy_table" """)

        self._enable_multiproduct()
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            db("SELECT * FROM dummy_table")
            db("""SELECT * FROM "@_dummy_table" """)

    def test_upgrading_existing_plugin_leaves_data_in_global_env(self):
        DummyPlugin.version = 2
        self._enable_component(DummyPlugin)
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            for i in range(5):
                db("INSERT INTO dummy_table (v1) VALUES ('%d')" % i)
            self.assertEqual(
                len(db("SELECT * FROM dummy_table")), 5)

        self._enable_multiproduct()
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            self.assertEqual(
                len(db('SELECT * FROM "dummy_table"')), 5)
            self.assertEqual(
                len(db('SELECT * FROM "@_dummy_table"')), 0)

    def test_creating_new_product_calls_environment_created(self):
        self._enable_component(DummyPlugin)
        self._enable_multiproduct()
        self.env.upgrade()

        prod = Product(self.env)
        prod.update_field_dict(dict(prefix='p1'))
        ProductEnvironment(self.env, prod, create=True)
        with self.env.db_direct_transaction as db:
            db('SELECT * FROM "p1_dummy_table"')

    def test_migrating_to_multiproduct_with_custom_default_prefix(self):
        ticket = self.insert_ticket('ticket')

        self.env.config.set('multiproduct', 'default_product_prefix', 'xxx')
        self._enable_multiproduct()
        self.env.upgrade()

        products = Product.select(self.env)
        self.assertEqual(len(products), 1)
        self.assertEqual(products[0].prefix, 'xxx')

    def test_migration_to_multiproduct_preserves_ticket_ids(self):
        for ticket_id in (1, 3, 5, 7):
            with self.env.db_transaction as db:
                cursor = db.cursor()
                cursor.execute("INSERT INTO ticket (id) VALUES (%i)" % ticket_id)
                db.update_sequence(cursor, 'ticket')

        self._enable_multiproduct()
        self.env.upgrade()

        for ticket_id in (1, 3, 5, 7):
            with self.product('@'):
                ticket = Ticket(self.env, ticket_id)
            self.assertEqual(ticket.id, ticket_id)

    def test_can_insert_tickets_after_upgrade(self):
        t1 = Ticket(self.env)
        t1.summary = "test"
        t1.insert()
        self.assertEqual(t1.id, 1)

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('@'):
            ticket = Ticket(self.env)
            ticket.summary = 'test'
            ticket.insert()
            self.assertEqual(ticket.id, 2)

    def test_can_insert_tickets_with_same_id_to_different_products(self):
        self._enable_multiproduct()
        self.env.upgrade()

        self.env.db_transaction("INSERT INTO ticket (id, summary)"
                                "            VALUES (1, 'first product')")
        t1 = Ticket(self.env, 1)

        with self.product('@'):
            self.env.db_transaction("INSERT INTO ticket (id, summary)"
                                    "            VALUES (1, 'second product')")
            t2 = Ticket(self.env, 1)

        self.assertEqual(t1.id, t2.id)
        self.assertNotEqual(t1['summary'], t2['summary'])

    def test_batch_ticket_insert_after_upgrade(self):
        self._enable_multiproduct()
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            db("""CREATE TABLE "@_tmp" (summary text, product text)""")
            for summary in "abcdef":
                db("""INSERT INTO "@_tmp" VALUES ('%s', '@')""" % (summary,))

        with self.product('@'):
            with self.env.db_transaction as db:
                db("""INSERT INTO ticket (summary) SELECT summary FROM tmp""")



    def _enable_multiproduct(self):
        self._update_config('components', 'multiproduct.*', 'enabled')

    def _add_custom_field(self, field_name):
        self._update_config('ticket-custom', field_name, 'text')

    def _enable_component(self, cls):
        self._update_config(
            'components',
            '%s.%s' % (cls.__module__, cls.__name__),
            'enabled'
        )

    def _update_config(self, section, key, value):
        self.env.config.set(section, key, value)
        self.env.config.save()
        self.env = Environment(self.env.path)

    def _create_file_with_content(self, content):
        filename = str(uuid.uuid4())[:6]
        path = os.path.join(self.env.path, filename)
        with open(path, 'wb') as f:
            f.write(content)
        return path

    @contextmanager
    def assertFailsWithMissingTable(self):
        with self.assertRaises(self.env.db_exc.OperationalError) as cm:
            yield
        self.assertIn('no such table', str(cm.exception))

    @contextmanager
    def assertFailsWithMissingColumn(self):
        with self.assertRaises(self.env.db_exc.OperationalError) as cm:
            yield
        self.assertIn('no such column', str(cm.exception))

    def create_ticket(self, summary, **kw):
        ticket = Ticket(self.env)
        ticket["summary"] = summary
        for k, v in kw.items():
            ticket[k] = v
        return ticket

    def insert_ticket(self, summary, **kw):
        """Helper for inserting a ticket into the database"""
        ticket = self.create_ticket(summary, **kw)
        ticket.insert()
        return ticket

    def create_wiki(self, name, text,  **kw):
        page = WikiPage(self.env, name)
        page.text = text
        for k, v in kw.items():
            page[k] = v
        return page

    def insert_wiki(self, name, text = None, **kw):
        text = text or "Dummy text"
        page = self.create_wiki(name, text, **kw)
        page.save("dummy author", "dummy comment", "::1")
        return page

    def add_attachment(self, resource, path):
        resource = '%s:%s' % (resource.realm, resource.id)
        AttachmentAdmin(self.env)._do_add(resource, path)

    @contextmanager
    def product(self, prefix):
        old_env = self.env
        self.env = ProductEnvironment(self.env, prefix)
        yield
        self.env = old_env
Exemple #3
0
class ProductMilestoneTestCase(MilestoneTestCase, MultiproductTestCase):
    def setUp(self):
        self.global_env = self._setup_test_env(create_folder=True)
        self._upgrade_mp(self.global_env)
        self._setup_test_log(self.global_env)
        self._load_product_from_data(self.global_env, self.default_product)

        self.env = ProductEnvironment(self.global_env, self.default_product)
        self._load_default_data(self.env)

    def tearDown(self):
        shutil.rmtree(self.global_env.path)
        self.global_env.reset_db()
        self.env = self.global_env = None

    @unittest.skipUnless(threading, 'Threading required for test')
    def test_milestone_threads(self):
        """ Ensure that in threaded (e.g. mod_wsgi) situations, we get
        an accurate list of milestones from Milestone.list

        The basic strategy is:
            thread-1 requests a list of milestones
            thread-2 adds a milestone
            thread-1 requests a new list of milestones
        To pass, thread-1 should have a list of milestones that matches
        those that are in the database.
        """
        lock = threading.RLock()
        results = []
        # two events to coordinate the workers and ensure that the threads
        # alternate appropriately
        e1 = threading.Event()
        e2 = threading.Event()

        def task(add):
            """the thread task - either we are discovering or adding events"""
            with lock:
                env = ProductEnvironment(self.global_env,
                                         self.default_product)
                if add:
                    name = 'milestone_from_' + threading.current_thread().name
                    milestone = Milestone(env)
                    milestone.name = name
                    milestone.insert()
                else:
                    # collect the names of milestones reported by Milestone and
                    # directly from the db - as sets to ease comparison later
                    results.append({
                        'from_t': set([m.name for m in Milestone.select(env)]),
                        'from_db': set(
                            [v[0] for v in self.env.db_query(
                                "SELECT name FROM milestone")])})

        def worker1():
            """ check milestones in this thread twice either side of ceding
            control to worker2
            """
            task(False)
            e1.set()
            e2.wait()
            task(False)

        def worker2():
            """ adds a milestone when worker1 allows us to then cede control
            back to worker1
            """
            e1.wait()
            task(True)
            e2.set()

        t1, t2 = [threading.Thread(target=f) for f in (worker1, worker2)]
        t1.start()
        t2.start()
        t1.join()
        t2.join()

        r = results[-1]  # note we only care about the final result
        self.assertEqual(r['from_t'], r['from_db'])

    def test_update_milestone(self):

        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")

        milestone = Milestone(self.env, 'Test')
        t1 = datetime(2001, 01, 01, tzinfo=utc)
        t2 = datetime(2002, 02, 02, tzinfo=utc)
        milestone.due = t1
        milestone.completed = t2
        milestone.description = 'Foo bar'
        milestone.update()

        self.assertEqual(
            [('Test', to_utimestamp(t1), to_utimestamp(t2), 'Foo bar',
                    self.default_product)],
            self.env.db_query("SELECT * FROM milestone WHERE name='Test'"))
Exemple #4
0
class ProductMilestoneTestCase(MilestoneTestCase, MultiproductTestCase):
    def setUp(self):
        self.global_env = self._setup_test_env(create_folder=True)
        self._upgrade_mp(self.global_env)
        self._setup_test_log(self.global_env)
        self._load_product_from_data(self.global_env, self.default_product)

        self.env = ProductEnvironment(self.global_env, self.default_product)
        self._load_default_data(self.env)

    def tearDown(self):
        shutil.rmtree(self.global_env.path)
        self.global_env.reset_db()
        self.env = self.global_env = None

    @unittest.skipUnless(threading, 'Threading required for test')
    def test_milestone_threads(self):
        """ Ensure that in threaded (e.g. mod_wsgi) situations, we get
        an accurate list of milestones from Milestone.list

        The basic strategy is:
            thread-1 requests a list of milestones
            thread-2 adds a milestone
            thread-1 requests a new list of milestones
        To pass, thread-1 should have a list of milestones that matches
        those that are in the database.
        """
        lock = threading.RLock()
        results = []
        # two events to coordinate the workers and ensure that the threads
        # alternate appropriately
        e1 = threading.Event()
        e2 = threading.Event()

        def task(add):
            """the thread task - either we are discovering or adding events"""
            with lock:
                env = ProductEnvironment(self.global_env, self.default_product)
                if add:
                    name = 'milestone_from_' + threading.current_thread().name
                    milestone = Milestone(env)
                    milestone.name = name
                    milestone.insert()
                else:
                    # collect the names of milestones reported by Milestone and
                    # directly from the db - as sets to ease comparison later
                    results.append({
                        'from_t':
                        set([m.name for m in Milestone.select(env)]),
                        'from_db':
                        set([
                            v[0] for v in self.env.db_query(
                                "SELECT name FROM milestone")
                        ])
                    })

        def worker1():
            """ check milestones in this thread twice either side of ceding
            control to worker2
            """
            task(False)
            e1.set()
            e2.wait()
            task(False)

        def worker2():
            """ adds a milestone when worker1 allows us to then cede control
            back to worker1
            """
            e1.wait()
            task(True)
            e2.set()

        t1, t2 = [threading.Thread(target=f) for f in (worker1, worker2)]
        t1.start()
        t2.start()
        t1.join()
        t2.join()

        r = results[-1]  # note we only care about the final result
        self.assertEqual(r['from_t'], r['from_db'])

    def test_update_milestone(self):

        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")

        milestone = Milestone(self.env, 'Test')
        t1 = datetime(2001, 01, 01, tzinfo=utc)
        t2 = datetime(2002, 02, 02, tzinfo=utc)
        milestone.due = t1
        milestone.completed = t2
        milestone.description = 'Foo bar'
        milestone.update()

        self.assertEqual(
            [('Test', to_utimestamp(t1), to_utimestamp(t2), 'Foo bar',
              self.default_product)],
            self.env.db_query("SELECT * FROM milestone WHERE name='Test'"))
Exemple #5
0
class EnvironmentUpgradeTestCase(unittest.TestCase):
    def setUp(self, options=()):
        env_path = tempfile.mkdtemp(prefix='bh-product-tempenv-')
        self.env = Environment(env_path, create=True, options=options)
        DummyPlugin.version = 1

    def tearDown(self):
        shutil.rmtree(self.env.path)

    def test_can_upgrade_environment_with_multi_product_disabled(self):
        self.env.upgrade()

        # Multiproduct was not enabled so multiproduct tables should not exist
        for table in BLOODHOUND_TABLES:
            with self.assertFailsWithMissingTable():
                self.env.db_direct_query("SELECT * FROM %s" % table)

        for table in TABLES_WITH_PRODUCT_FIELD:
            with self.assertFailsWithMissingColumn():
                self.env.db_direct_query("SELECT product FROM %s" % table)

    def test_upgrade_creates_multi_product_tables_and_adds_product_column(
            self):
        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            for table in BLOODHOUND_TABLES:
                db("SELECT * FROM %s" % table)

            for table in TABLES_WITH_PRODUCT_FIELD:
                db("SELECT product FROM %s" % table)

    def test_upgrade_creates_default_product(self):
        self._enable_multiproduct()
        self.env.upgrade()

        products = Product.select(self.env)
        self.assertEqual(len(products), 1)

    def test_upgrade_moves_tickets_and_related_objects_to_default_prod(self):
        self._add_custom_field('custom_field')
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO ticket (id) VALUES (1)""")
            db("""INSERT INTO attachment (type, id, filename)
                       VALUES ('ticket', '1', '')""")
            db("""INSERT INTO ticket_custom (ticket, name, value)
                       VALUES (1, 'custom_field', '42')""")
            db("""INSERT INTO ticket_change (ticket, time, field)
                       VALUES (1, 42, 'summary')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('@'):
            ticket = Ticket(self.env, 1)
            attachments = list(
                Attachment.select(self.env, ticket.resource.realm,
                                  ticket.resource.id))
            self.assertEqual(len(attachments), 1)
            self.assertEqual(ticket['custom_field'], '42')
            changes = ticket.get_changelog()
            self.assertEqual(len(changes), 3)

    def test_upgrade_moves_custom_wikis_to_default_product(self):
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO wiki (name, version) VALUES ('MyPage', 1)""")
            db("""INSERT INTO attachment (type, id, filename)
                         VALUES ('wiki', 'MyPage', '')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            self.assertEqual(
                len(db("""SELECT * FROM wiki WHERE product='@'""")), 1)
            self.assertEqual(
                len(
                    db("""SELECT * FROM attachment
                           WHERE product='@'
                             AND type='wiki'""")), 1)

    def test_upgrade_moves_system_wikis_to_products(self):
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO wiki (name, version) VALUES ('WikiStart', 1)""")
            db("""INSERT INTO attachment (type, id, filename)
                         VALUES ('wiki', 'WikiStart', '')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            self.assertEqual(
                len(db("""SELECT * FROM wiki WHERE product='@'""")), 1)
            self.assertEqual(
                len(
                    db("""SELECT * FROM attachment
                           WHERE product='@'
                             AND type='wiki'""")), 1)
            self.assertEqual(
                len(db("""SELECT * FROM wiki WHERE product=''""")), 0)
            self.assertEqual(
                len(
                    db("""SELECT * FROM attachment
                           WHERE product=''
                             AND type='wiki'""")), 0)

    def test_upgrade_copies_content_of_system_tables_to_all_products(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._update_db_version(db, 1)
            for i in range(1, 6):
                db("""INSERT INTO bloodhound_product (prefix, name)
                           VALUES ('p%d', 'Product 1')""" % i)
            for table in ('component', 'milestone', 'enum', 'version',
                          'permission', 'report'):
                db("""DELETE FROM %s""" % table)
            db("""INSERT INTO component (name) VALUES ('foobar')""")
            db("""INSERT INTO milestone (name) VALUES ('foobar')""")
            db("""INSERT INTO version (name) VALUES ('foobar')""")
            db("""INSERT INTO enum (type, name) VALUES ('a', 'b')""")
            db("""INSERT INTO permission VALUES ('x', 'TICKET_VIEW')""")
            db("""INSERT INTO report (title) VALUES ('x')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            for table in ('component', 'milestone', 'version', 'enum',
                          'report'):
                rows = db("SELECT * FROM %s" % table)
                self.assertEqual(
                    len(rows), 6,
                    "Wrong number of lines in %s (%d instead of %d)\n%s" %
                    (table, len(rows), 6, rows))
            for table in ('permission', ):
                # Permissions also hold rows for global product.
                rows = db("SELECT * FROM %s WHERE username='******'" % table)
                self.assertEqual(
                    len(rows), 7,
                    "Wrong number of lines in %s (%d instead of %d)\n%s" %
                    (table, len(rows), 7, rows))

    def test_upgrading_database_moves_attachment_to_correct_product(self):
        ticket = self.insert_ticket('ticket')
        wiki = self.insert_wiki('MyWiki')
        attachment = self._create_file_with_content('Hello World!')
        self.add_attachment(ticket.resource, attachment)
        self.add_attachment(wiki.resource, attachment)

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('@'):
            attachments = list(Attachment.select(self.env, 'ticket',
                                                 ticket.id))
            attachments.extend(Attachment.select(self.env, 'wiki', wiki.name))
        self.assertEqual(len(attachments), 2)
        for attachment in attachments:
            self.assertEqual(attachment.open().read(), 'Hello World!')

    def test_can_upgrade_database_with_ticket_attachment_with_text_ids(self):
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO attachment (id, type, filename)
                       VALUES ('abc', 'ticket', '')""")

        self._enable_multiproduct()
        self.env.upgrade()

    def test_can_upgrade_database_with_orphaned_attachments(self):
        with self.env.db_direct_transaction as db:
            db("""INSERT INTO attachment (id, type, filename)
                       VALUES ('5', 'ticket', '')""")
            db("""INSERT INTO attachment (id, type, filename)
                       VALUES ('MyWiki', 'wiki', '')""")

        self._enable_multiproduct()
        self.env.upgrade()

    def test_can_upgrade_multi_product_from_v1(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._update_db_version(db, 1)

            db("""INSERT INTO bloodhound_product (prefix, name)
                       VALUES ('p1', 'Product 1')""")
            db("""INSERT INTO ticket (id, product)
                       VALUES (1, 'Product 1')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('p1'):
            Ticket(self.env, 1)

    def test_can_upgrade_multi_product_from_v2(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._replace_product_on_ticket_with_product_prefix(db)
            mp._update_db_version(db, 2)

            db("""INSERT INTO bloodhound_product (prefix, name)
                       VALUES ('p1', 'Product 1')""")
            db("""INSERT INTO ticket (id, product)
                       VALUES (1, 'p1')""")
            db("""INSERT INTO ticket (id)
                       VALUES (2)""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('p1'):
            Ticket(self.env, 1)
        with self.product('@'):
            Ticket(self.env, 2)

    def test_upgrade_plugin(self):
        self._enable_component(DummyPlugin)
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            db("SELECT v1 FROM dummy_table")
            with self.assertFailsWithMissingColumn():
                db("SELECT v2 FROM dummy_table")

        DummyPlugin.version = 2
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            db("SELECT v2 FROM dummy_table")

    def test_upgrade_plugin_to_multiproduct(self):
        self._enable_multiproduct()
        self._enable_component(DummyPlugin)
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            db("SELECT * FROM dummy_table")
            db("""SELECT * FROM "@_dummy_table" """)

    def test_upgrade_existing_plugin_to_multiproduct(self):
        self._enable_component(DummyPlugin)
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            with self.assertFailsWithMissingTable():
                db("""SELECT * FROM "@_dummy_table" """)

        self._enable_multiproduct()
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            db("SELECT * FROM dummy_table")
            db("""SELECT * FROM "@_dummy_table" """)

    def test_upgrading_existing_plugin_leaves_data_in_global_env(self):
        DummyPlugin.version = 2
        self._enable_component(DummyPlugin)
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            for i in range(5):
                db("INSERT INTO dummy_table (v1) VALUES ('%d')" % i)
            self.assertEqual(len(db("SELECT * FROM dummy_table")), 5)

        self._enable_multiproduct()
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            self.assertEqual(len(db('SELECT * FROM "dummy_table"')), 5)
            self.assertEqual(len(db('SELECT * FROM "@_dummy_table"')), 0)

    def test_creating_new_product_calls_environment_created(self):
        self._enable_component(DummyPlugin)
        self._enable_multiproduct()
        self.env.upgrade()

        prod = Product(self.env)
        prod.update_field_dict(dict(prefix='p1'))
        ProductEnvironment(self.env, prod, create=True)
        with self.env.db_direct_transaction as db:
            db('SELECT * FROM "p1_dummy_table"')

    def test_migrating_to_multiproduct_with_custom_default_prefix(self):
        ticket = self.insert_ticket('ticket')

        self.env.config.set('multiproduct', 'default_product_prefix', 'xxx')
        self._enable_multiproduct()
        self.env.upgrade()

        products = Product.select(self.env)
        self.assertEqual(len(products), 1)
        self.assertEqual(products[0].prefix, 'xxx')

    def test_migration_to_multiproduct_preserves_ticket_ids(self):
        for ticket_id in (1, 3, 5, 7):
            with self.env.db_transaction as db:
                cursor = db.cursor()
                cursor.execute("INSERT INTO ticket (id) VALUES (%i)" %
                               ticket_id)
                db.update_sequence(cursor, 'ticket')

        self._enable_multiproduct()
        self.env.upgrade()

        for ticket_id in (1, 3, 5, 7):
            with self.product('@'):
                ticket = Ticket(self.env, ticket_id)
            self.assertEqual(ticket.id, ticket_id)

    def test_can_insert_tickets_after_upgrade(self):
        t1 = Ticket(self.env)
        t1.summary = "test"
        t1.insert()
        self.assertEqual(t1.id, 1)

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('@'):
            ticket = Ticket(self.env)
            ticket.summary = 'test'
            ticket.insert()
            self.assertEqual(ticket.id, 2)

    def test_can_insert_tickets_with_same_id_to_different_products(self):
        self._enable_multiproduct()
        self.env.upgrade()

        self.env.db_transaction("INSERT INTO ticket (id, summary)"
                                "            VALUES (1, 'first product')")
        t1 = Ticket(self.env, 1)

        with self.product('@'):
            self.env.db_transaction("INSERT INTO ticket (id, summary)"
                                    "            VALUES (1, 'second product')")
            t2 = Ticket(self.env, 1)

        self.assertEqual(t1.id, t2.id)
        self.assertNotEqual(t1['summary'], t2['summary'])

    def test_batch_ticket_insert_after_upgrade(self):
        self._enable_multiproduct()
        self.env.upgrade()
        with self.env.db_direct_transaction as db:
            db("""CREATE TABLE "@_tmp" (summary text, product text)""")
            for summary in "abcdef":
                db("""INSERT INTO "@_tmp" VALUES ('%s', '@')""" % (summary, ))

        with self.product('@'):
            with self.env.db_transaction as db:
                db("""INSERT INTO ticket (summary) SELECT summary FROM tmp""")

    def _enable_multiproduct(self):
        self._update_config('components', 'multiproduct.*', 'enabled')

    def _add_custom_field(self, field_name):
        self._update_config('ticket-custom', field_name, 'text')

    def _enable_component(self, cls):
        self._update_config('components',
                            '%s.%s' % (cls.__module__, cls.__name__),
                            'enabled')

    def _update_config(self, section, key, value):
        self.env.config.set(section, key, value)
        self.env.config.save()
        self.env = Environment(self.env.path)

    def _create_file_with_content(self, content):
        filename = str(uuid.uuid4())[:6]
        path = os.path.join(self.env.path, filename)
        with open(path, 'wb') as f:
            f.write(content)
        return path

    @contextmanager
    def assertFailsWithMissingTable(self):
        with self.assertRaises(self.env.db_exc.OperationalError) as cm:
            yield
        self.assertIn('no such table', str(cm.exception))

    @contextmanager
    def assertFailsWithMissingColumn(self):
        with self.assertRaises(self.env.db_exc.OperationalError) as cm:
            yield
        self.assertIn('no such column', str(cm.exception))

    def create_ticket(self, summary, **kw):
        ticket = Ticket(self.env)
        ticket["summary"] = summary
        for k, v in kw.items():
            ticket[k] = v
        return ticket

    def insert_ticket(self, summary, **kw):
        """Helper for inserting a ticket into the database"""
        ticket = self.create_ticket(summary, **kw)
        ticket.insert()
        return ticket

    def create_wiki(self, name, text, **kw):
        page = WikiPage(self.env, name)
        page.text = text
        for k, v in kw.items():
            page[k] = v
        return page

    def insert_wiki(self, name, text=None, **kw):
        text = text or "Dummy text"
        page = self.create_wiki(name, text, **kw)
        page.save("dummy author", "dummy comment", "::1")
        return page

    def add_attachment(self, resource, path):
        resource = '%s:%s' % (resource.realm, resource.id)
        AttachmentAdmin(self.env)._do_add(resource, path)

    @contextmanager
    def product(self, prefix):
        old_env = self.env
        self.env = ProductEnvironment(self.env, prefix)
        yield
        self.env = old_env