# Copyright (c) 2013 Red Hat, Inc.
# Copyright (c) 2014 Catalyst IT Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import abc
import datetime
import hashlib
import json
import math
import os
import random
import time
from unittest import mock
import uuid

import ddt
from oslo_utils import timeutils
import six
import testtools
from testtools import matchers

from zaqar.common import cache as oslo_cache
from zaqar import storage
from zaqar.storage import errors
from zaqar.storage import pipeline
from zaqar import tests as testing
from zaqar.tests import helpers


class ControllerBaseTest(testing.TestBase):
    project = 'project'
    driver_class = None
    controller_class = None
    controller_base_class = None

    def setUp(self):
        super(ControllerBaseTest, self).setUp()

        if not self.driver_class:
            self.skipTest('No driver class specified')

        if not issubclass(self.controller_class, self.controller_base_class):
            self.skipTest('{0} is not an instance of {1}. '
                          'Tests not supported'.format(
                              self.controller_class,
                              self.controller_base_class))

        oslo_cache.register_config(self.conf)
        cache = oslo_cache.get_cache(self.conf)

        pooling = 'pooling' in self.conf and self.conf.pooling
        if pooling and not self.control_driver_class:
            self.skipTest("Pooling is enabled, "
                          "but control driver class is not specified")

        self.control = self.control_driver_class(self.conf, cache)
        if not pooling:
            args = [self.conf, cache]
            if issubclass(self.driver_class, storage.DataDriverBase):
                args.append(self.control)
            self.driver = self.driver_class(*args)
        else:
            testredis = os.environ.get('ZAQAR_TEST_REDIS', 0)
            if testredis:
                uri = self.redis_url
                for i in range(4):
                    db_name = "?dbid = " + str(i)

                    # NOTE(dynarro): we need to create a unique uri.
                    new_uri = "%s/%s" % (uri, db_name)
                    self.control.pools_controller.create(six.text_type(i),
                                                         100, new_uri)
            else:
                uri = self.mongodb_url
                for i in range(4):
                    db_name = "zaqar_test_pools_" + str(i)

                    # NOTE(dynarro): we need to create a unique uri.
                    new_uri = "%s/%s" % (uri, db_name)
                    options = {'database': db_name}
                    self.control.pools_controller.create(six.text_type(i),
                                                         100, new_uri,
                                                         options=options)
            self.driver = self.driver_class(self.conf, cache, self.control)
            self.addCleanup(self.control.pools_controller.drop_all)
            self.addCleanup(self.control.catalogue_controller.drop_all)

        self._prepare_conf()

        self.addCleanup(self._purge_databases)

        if not pooling:
            self.controller = self.controller_class(self.driver)
        else:
            self.controller = self.controller_class(self.driver._pool_catalog)

        self.pipeline = pipeline.DataDriver(self.conf,
                                            self.driver,
                                            self.control)

    def _prepare_conf(self):
        """Prepare the conf before running tests

        Classes overriding this method, must use
        the `self.conf` instance and alter its state.
        """

    def _purge_databases(self):
        """Override to clean databases."""


@ddt.ddt
class QueueControllerTest(ControllerBaseTest):
    """Queue Controller base tests."""
    controller_base_class = storage.Queue

    def setUp(self):
        super(QueueControllerTest, self).setUp()
        self.queue_controller = self.pipeline.queue_controller

    @ddt.data(None, ControllerBaseTest.project)
    def test_list(self, project):
        # NOTE(kgriffs): Ensure we mix global and scoped queues
        # in order to verify that queue records are excluded that
        # are not at the same level.
        project_alt = self.project if project is None else None

        num = 15
        for queue in six.moves.xrange(num):
            queue = str(queue)
            self.controller.create(queue, project=project)
            self.controller.create(queue, project=project_alt)
            self.addCleanup(self.controller.delete,
                            queue, project=project)
            self.addCleanup(self.controller.delete,
                            queue, project=project_alt)

        interaction = self.controller.list(project=project,
                                           detailed=True)
        queues = list(next(interaction))

        self.assertTrue(all(map(lambda queue:
                                'name' in queue and
                                'metadata' in queue, queues)))
        self.assertEqual(10, len(queues))

        interaction = self.controller.list(project=project,
                                           marker=next(interaction))
        queues = list(next(interaction))

        self.assertTrue(all(map(lambda queue:
                                'name' in queue and
                                'metadata' not in queue, queues)))

        self.assertEqual(5, len(queues))

    def test_queue_lifecycle(self):
        # Test queue creation
        created = self.controller.create('test',
                                         metadata=dict(meta='test_meta'),
                                         project=self.project)
        self.assertTrue(created)

        # Test queue existence
        self.assertTrue(self.controller.exists('test', project=self.project))

        # Test queue retrieval
        interaction = self.controller.list(project=self.project)
        queue = list(next(interaction))[0]
        self.assertEqual('test', queue['name'])

        # Test queue metadata retrieval
        metadata = self.controller.get('test', project=self.project)
        self.assertEqual('test_meta', metadata['meta'])

        # Touching an existing queue does not affect metadata
        created = self.controller.create('test', project=self.project)
        self.assertFalse(created)

        metadata = self.controller.get('test', project=self.project)
        self.assertEqual('test_meta', metadata['meta'])

        # Test queue deletion
        self.controller.delete('test', project=self.project)

        # Test queue existence
        self.assertFalse(self.controller.exists('test', project=self.project))


class MessageControllerTest(ControllerBaseTest):
    """Message Controller base tests.

    NOTE(flaper87): Implementations of this class should
    override the tearDown method in order
    to clean up storage's state.
    """
    queue_name = 'test_queue'
    controller_base_class = storage.Message

    # Specifies how often expired messages are purged, in sec.
    gc_interval = 0

    def setUp(self):
        super(MessageControllerTest, self).setUp()

        # Lets create a queue
        self.queue_controller = self.pipeline.queue_controller
        self.claim_controller = self.pipeline.claim_controller
        self.queue_controller.create(self.queue_name, project=self.project)

    def tearDown(self):
        self.queue_controller.delete(self.queue_name, project=self.project)
        super(MessageControllerTest, self).tearDown()

    def test_stats_for_empty_queue(self):
        self.addCleanup(self.queue_controller.delete, 'test',
                        project=self.project)
        created = self.queue_controller.create('test', project=self.project)
        self.assertTrue(created)

        stats = self.queue_controller.stats('test', project=self.project)
        message_stats = stats['messages']

        self.assertEqual(0, message_stats['free'])
        self.assertEqual(0, message_stats['claimed'])
        self.assertEqual(0, message_stats['total'])

        self.assertNotIn('newest', message_stats)
        self.assertNotIn('oldest', message_stats)

    def test_queue_count_on_bulk_delete(self):
        self.addCleanup(self.queue_controller.delete, 'test-queue',
                        project=self.project)
        queue_name = 'test-queue'
        client_uuid = uuid.uuid4()

        created = self.queue_controller.create(queue_name,
                                               project=self.project)
        self.assertTrue(created)

        # Create 10 messages.
        msg_keys = _insert_fixtures(self.controller, queue_name,
                                    project=self.project,
                                    client_uuid=client_uuid, num=10)

        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(10, stats['total'])

        # Delete 5 messages
        self.controller.bulk_delete(queue_name, msg_keys[0:5],
                                    self.project)
        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(5, stats['total'])

    def test_queue_count_on_bulk_delete_with_invalid_id(self):
        self.addCleanup(self.queue_controller.delete, 'test-queue',
                        project=self.project)
        queue_name = 'test-queue'
        client_uuid = uuid.uuid4()

        created = self.queue_controller.create(queue_name,
                                               project=self.project)
        self.assertTrue(created)

        # Create 10 messages.
        msg_keys = _insert_fixtures(self.controller, queue_name,
                                    project=self.project,
                                    client_uuid=client_uuid, num=10)

        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(10, stats['total'])

        # Delete 5 messages
        self.controller.bulk_delete(queue_name,
                                    msg_keys[0:5] + ['invalid'],
                                    self.project)

        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(5, stats['total'])

    def test_queue_count_on_delete(self):
        self.addCleanup(self.queue_controller.delete, 'test-queue',
                        project=self.project)
        queue_name = 'test-queue'
        client_uuid = uuid.uuid4()

        created = self.queue_controller.create(queue_name,
                                               project=self.project)
        self.assertTrue(created)

        # Create 10 messages.
        msg_keys = _insert_fixtures(self.controller, queue_name,
                                    project=self.project,
                                    client_uuid=client_uuid, num=10)

        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(10, stats['total'])

        # Delete 1 message
        self.controller.delete(queue_name, msg_keys[0], self.project)
        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(9, stats['total'])

    def test_queue_stats(self):
        # Test queue creation
        self.addCleanup(self.queue_controller.delete, 'test',
                        project=self.project)
        created = self.queue_controller.create('test',
                                               metadata=dict(meta='test_meta'),
                                               project=self.project)

        client_uuid = uuid.uuid4()
        # Test queue statistic
        _insert_fixtures(self.controller, 'test',
                         project=self.project, client_uuid=client_uuid,
                         num=6)

        # NOTE(kgriffs): We can't get around doing this, because
        # we don't know how the storage drive may be calculating
        # message timestamps (and may not be monkey-patchable).
        time.sleep(1.2)

        _insert_fixtures(self.controller, 'test',
                         project=self.project, client_uuid=client_uuid,
                         num=6)

        stats = self.queue_controller.stats('test', project=self.project)
        message_stats = stats['messages']

        self.assertEqual(12, message_stats['free'])
        self.assertEqual(0, message_stats['claimed'])
        self.assertEqual(12, message_stats['total'])

        oldest = message_stats['oldest']
        newest = message_stats['newest']

        self.assertNotEqual(oldest, newest)

        age = oldest['age']
        self.assertThat(age, matchers.GreaterThan(0))

        # NOTE(kgriffs): Ensure is different enough
        # for the next comparison to work.
        soon = timeutils.utcnow() + datetime.timedelta(seconds=60)

        for message_stat in (oldest, newest):
            created_iso = message_stat['created']
            created = timeutils.parse_isotime(created_iso)
            self.assertThat(timeutils.normalize_time(created),
                            matchers.LessThan(soon))

            self.assertIn('id', message_stat)

        self.assertThat(oldest['created'],
                        matchers.LessThan(newest['created']))

    def test_queue_count_on_claim_delete(self):
        self.addCleanup(self.queue_controller.delete, 'test-queue',
                        project=self.project)
        queue_name = 'test-queue'
        client_uuid = uuid.uuid4()

        created = self.queue_controller.create(queue_name,
                                               project=self.project)
        self.assertTrue(created)

        # Create 15 messages.
        msg_keys = _insert_fixtures(self.controller, queue_name,
                                    project=self.project,
                                    client_uuid=client_uuid, num=15)

        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(15, stats['total'])

        metadata = {'ttl': 120, 'grace': 60}
        # Claim 10 messages
        claim_id, _ = self.claim_controller.create(queue_name, metadata,
                                                   self.project)

        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(10, stats['claimed'])

        # Delete one message and ensure stats are updated even
        # thought the claim itself has not been deleted.
        self.controller.delete(queue_name, msg_keys[0],
                               self.project, claim_id)
        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(14, stats['total'])
        self.assertEqual(9, stats['claimed'])
        self.assertEqual(5, stats['free'])

        # Same thing but use bulk_delete interface
        self.controller.bulk_delete(queue_name, msg_keys[1:3],
                                    self.project)
        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']
        self.assertEqual(12, stats['total'])
        self.assertEqual(7, stats['claimed'])
        self.assertEqual(5, stats['free'])

        # Delete the claim
        self.claim_controller.delete(queue_name, claim_id,
                                     self.project)
        stats = self.queue_controller.stats(queue_name,
                                            self.project)['messages']

        self.assertEqual(0, stats['claimed'])

    def test_message_lifecycle(self):
        queue_name = self.queue_name

        message = {
            'ttl': 60,
            'body': {
                'event': 'BackupStarted',
                'backupId': 'c378813c-3f0b-11e2-ad92-7823d2b0f3ce'
            }
        }

        # Test Message Creation
        created = list(self.controller.post(queue_name, [message],
                                            project=self.project,
                                            client_uuid=uuid.uuid4()))
        self.assertEqual(1, len(created))
        message_id = created[0]

        # Test Message Get
        message_out = self.controller.get(queue_name, message_id,
                                          project=self.project)
        self.assertEqual({'id', 'body', 'ttl', 'age', 'claim_count',
                          'claim_id'}, set(message_out))
        self.assertEqual(message_id, message_out['id'])
        self.assertEqual(message['body'], message_out['body'])
        self.assertEqual(message['ttl'], message_out['ttl'])

        # Test Message Deletion
        self.controller.delete(queue_name, message_id, project=self.project)

        # Test does not exist
        with testing.expect(errors.DoesNotExist):
            self.controller.get(queue_name, message_id, project=self.project)

    def test_message_body_checksum(self):
        self.conf.enable_checksum = True
        queue_name = self.queue_name
        message = {
            'ttl': 60,
            'body': {
                'event': 'BackupStarted',
                'backupId': 'c378813c-3f0b-11e2-ad92-7823d2b0f3ce'
            }
        }

        # Test Message Creation
        created = list(self.controller.post(queue_name, [message],
                                            project=self.project,
                                            client_uuid=uuid.uuid4()))
        self.assertEqual(1, len(created))
        message_id = created[0]

        # Test Message Get
        message_out = self.controller.get(queue_name, message_id,
                                          project=self.project)
        self.assertEqual({'id', 'body', 'ttl', 'age', 'claim_count',
                          'claim_id', 'checksum'}, set(message_out))

        algorithm, checksum = message_out['checksum'].split(':')
        expected_checksum = ''
        if algorithm == 'MD5':
            md5 = hashlib.md5()
            md5.update(json.dumps(message['body']).encode('utf-8'))
            expected_checksum = md5.hexdigest()

        self.assertEqual(expected_checksum, checksum)

    def test_get_multi(self):
        client_uuid = uuid.uuid4()

        _insert_fixtures(self.controller, self.queue_name,
                         project=self.project, client_uuid=client_uuid, num=15)

        def load_messages(expected, *args, **kwargs):
            interaction = self.controller.list(*args, **kwargs)
            msgs = list(next(interaction))
            self.assertEqual(expected, len(msgs))
            return interaction

        # Test all messages, echo False and uuid
        load_messages(0, self.queue_name, project=self.project,
                      client_uuid=client_uuid)

        # Test all messages and limit
        load_messages(15, self.queue_name, project=self.project, limit=20,
                      echo=True)

        # Test default limit
        load_messages(storage.DEFAULT_MESSAGES_PER_PAGE,
                      self.queue_name, project=self.project, echo=True)

        # Test all messages, echo True, and uuid
        interaction = load_messages(10, self.queue_name, echo=True,
                                    project=self.project,
                                    client_uuid=client_uuid)

        # Test all messages, echo True, uuid and marker
        load_messages(5, self.queue_name, echo=True, project=self.project,
                      marker=next(interaction), client_uuid=client_uuid)

    def test_multi_ids(self):
        messages_in = [{'ttl': 120, 'body': 0}, {'ttl': 240, 'body': 1}]
        ids = self.controller.post(self.queue_name, messages_in,
                                   project=self.project,
                                   client_uuid=uuid.uuid4())

        messages_out = self.controller.bulk_get(self.queue_name, ids,
                                                project=self.project)

        for idx, message in enumerate(messages_out):
            self.assertEqual({'id', 'body', 'ttl', 'age', 'claim_count',
                              'claim_id'}, set(message))
            self.assertEqual(idx, message['body'])

        self.controller.bulk_delete(self.queue_name, ids,
                                    project=self.project)

        with testing.expect(StopIteration):
            result = self.controller.bulk_get(self.queue_name, ids,
                                              project=self.project)
            next(result)

    def test_claim_effects(self):
        client_uuid = uuid.uuid4()

        _insert_fixtures(self.controller, self.queue_name,
                         project=self.project, client_uuid=client_uuid, num=12)

        def list_messages(include_claimed=None):
            kwargs = {
                'project': self.project,
                'client_uuid': client_uuid,
                'echo': True,
            }

            # Properly test default value
            if include_claimed is not None:
                kwargs['include_claimed'] = include_claimed

            interaction = self.controller.list(self.queue_name, **kwargs)

            messages = next(interaction)
            return [msg['id'] for msg in messages]

        messages_before = list_messages(True)

        meta = {'ttl': 70, 'grace': 60}
        another_cid, _ = self.claim_controller.create(self.queue_name, meta,
                                                      project=self.project)

        messages_after = list_messages(True)
        self.assertEqual(messages_before, messages_after)

        messages_excluding_claimed = list_messages()
        self.assertNotEqual(messages_before, messages_excluding_claimed)
        self.assertEqual(2, len(messages_excluding_claimed))

        cid, msgs = self.claim_controller.create(self.queue_name, meta,
                                                 project=self.project)
        [msg1, msg2] = msgs

        # A wrong claim does not ensure the message deletion
        with testing.expect(errors.NotPermitted):
            self.controller.delete(self.queue_name, msg1['id'],
                                   project=self.project,
                                   claim=another_cid)

        # Make sure a message can be deleted with a claim
        self.controller.delete(self.queue_name, msg1['id'],
                               project=self.project,
                               claim=cid)

        with testing.expect(errors.DoesNotExist):
            self.controller.get(self.queue_name, msg1['id'],
                                project=self.project)

        # Make sure such a deletion is idempotent
        self.controller.delete(self.queue_name, msg1['id'],
                               project=self.project,
                               claim=cid)

        # A non-existing claim does not ensure the message deletion
        self.claim_controller.delete(self.queue_name, cid,
                                     project=self.project)

        # NOTE(kgriffs) Message is no longer claimed, but try
        # to delete it with the claim anyway. It should raise
        # an error, because the client needs a hint that
        # perhaps the claim expired before it got around to
        # trying to delete the message, which means another
        # worker could be processing this message now.
        with testing.expect(errors.NotPermitted, errors.ClaimDoesNotExist):
            self.controller.delete(self.queue_name, msg2['id'],
                                   project=self.project,
                                   claim=cid)

    @testing.is_slow(condition=lambda self: self.gc_interval > 1)
    def test_expired_messages(self):
        messages = [{'body': 3.14, 'ttl': 1}, {'body': 0.618, 'ttl': 600}]
        client_uuid = uuid.uuid4()

        [msgid_expired, msgid] = self.controller.post(self.queue_name,
                                                      messages,
                                                      project=self.project,
                                                      client_uuid=client_uuid)

        # NOTE(kgriffs): Allow for automatic GC of claims, messages
        for i in range(self.gc_interval):
            time.sleep(1)

            # NOTE(kgriffs): Some drivers require a manual GC to be
            # triggered to clean up claims and messages.
            self.driver.gc()

            try:
                self.controller.get(self.queue_name, msgid_expired,
                                    project=self.project)
            except errors.DoesNotExist:
                break
        else:
            self.fail("Didn't remove the queue")

        # Make sure expired messages not return when listing
        interaction = self.controller.list(self.queue_name,
                                           project=self.project)

        messages = list(next(interaction))
        self.assertEqual(1, len(messages))
        self.assertEqual(msgid, messages[0]['id'])

        stats = self.queue_controller.stats(self.queue_name,
                                            project=self.project)
        self.assertEqual(1, stats['messages']['free'])

        # Make sure expired messages not return when popping
        messages = self.controller.pop(self.queue_name,
                                       limit=10,
                                       project=self.project)
        self.assertEqual(1, len(messages))
        self.assertEqual(msgid, messages[0]['id'])

    def test_bad_id(self):
        # NOTE(cpp-cabrera): A malformed ID should result in an empty
        # query. Raising an exception for validating IDs makes the
        # implementation more verbose instead of taking advantage of
        # the Maybe/Optional protocol, particularly when dealing with
        # bulk operations.
        bad_message_id = 'xyz'
        self.controller.delete(self.queue_name,
                               bad_message_id,
                               project=self.project)

        with testing.expect(errors.MessageDoesNotExist):
            self.controller.get(self.queue_name,
                                bad_message_id,
                                project=self.project)

    def test_bad_claim_id(self):
        [msgid] = self.controller.post(self.queue_name,
                                       [{'body': {}, 'ttl': 10}],
                                       project=self.project,
                                       client_uuid=uuid.uuid4())

        # NOTE(kgriffs): If the client has a typo or
        # something, they will need a hint that the
        # request was invalid.
        #
        # On the other hand, if they are actually
        # probing for a vulnerability, telling them
        # the claim they requested doesn't exist should
        # be harmless.
        with testing.expect(storage.errors.ClaimDoesNotExist):
            bad_claim_id = '; DROP TABLE queues'
            self.controller.delete(self.queue_name,
                                   msgid,
                                   project=self.project,
                                   claim=bad_claim_id)

    def test_bad_marker(self):
        bad_marker = 'xyz'
        interaction = self.controller.list(self.queue_name,
                                           project=self.project,
                                           client_uuid=uuid.uuid4(),
                                           marker=bad_marker)
        messages = list(next(interaction))

        self.assertEqual([], messages)

    def test_sort_for_first(self):
        client_uuid = uuid.uuid4()

        [msgid_first] = self.controller.post(self.queue_name,
                                             [{'body': {}, 'ttl': 120}],
                                             project=self.project,
                                             client_uuid=client_uuid)

        _insert_fixtures(self.controller, self.queue_name,
                         project=self.project, client_uuid=client_uuid, num=10)

        [msgid_last] = self.controller.post(self.queue_name,
                                            [{'body': {}, 'ttl': 120}],
                                            project=self.project,
                                            client_uuid=client_uuid)

        msg_asc = self.controller.first(self.queue_name,
                                        self.project,
                                        1)
        self.assertEqual(msgid_first, msg_asc['id'])

        msg_desc = self.controller.first(self.queue_name,
                                         self.project,
                                         -1)
        self.assertEqual(msgid_last, msg_desc['id'])

    def test_get_first_with_empty_queue_exception(self):
        self.assertRaises(errors.QueueIsEmpty,
                          self.controller.first,
                          self.queue_name, project=self.project)

    def test_get_first_with_invalid_sort_option(self):
        self.assertRaises(ValueError,
                          self.controller.first,
                          self.queue_name, sort=0,
                          project=self.project)

    def test_pop_message(self):
        self.queue_controller.create(self.queue_name, project=self.project)
        messages = [
            {
                'ttl': 60,
                'body': {
                    'event': 'BackupStarted',
                    'backupId': 'c378813c-3f0b-11e2-ad92-7823d2b0f3ce',
                },
            },
            {
                'ttl': 60,
                'body': {
                    'event': 'BackupStarted',
                    'backupId': 'd378813c-3f0b-11e2-ad92-7823d2b0f3ce',
                },
            },
            {
                'ttl': 60,
                'body': {
                    'event': 'BackupStarted',
                    'backupId': 'e378813c-3f0b-11e2-ad92-7823d2b0f3ce',
                },
            },
        ]

        client_uuid = uuid.uuid1()
        self.controller.post(self.queue_name, messages, client_uuid,
                             project=self.project)

        # Test Message Pop
        popped_messages = self.controller.pop(self.queue_name,
                                              limit=1,
                                              project=self.project)

        self.assertEqual(1, len(popped_messages))

    @testtools.skip("Skip until fixed bug 1739332")
    def test_message_period(self):
        self.queue_controller.create(self.queue_name, project=self.project)
        messages = [
            {
                'ttl': 60,
                'body': {
                    'event.data': 'BackupStarted',
                    'backupId': 'c378813c-3f0b-11e2-ad92-7823d2b0f3ce',
                },
            },
        ]

        client_uuid = uuid.uuid1()
        self.controller.post(self.queue_name, messages, client_uuid,
                             project=self.project)

        stored_messages = self.controller.list(self.queue_name,
                                               project=self.project)

        self.assertItemsEqual(['event.data', 'backupId'],
                              list(next(stored_messages))[0]['body'].keys())

    def test_delete_message_from_nonexistent_queue(self):
        queue_name = 'fake_name'
        message_id = 'fake_id'
        res = self.controller.delete(queue_name, message_id,
                                     project=self.project)
        self.assertIsNone(res)

    def test_delete_messages_with_ids_from__nonexistent_queue(self):
        queue_name = 'fake_name'
        message_ids = ['fake_id1', 'fake_id2']
        res = self.controller.bulk_delete(queue_name, message_ids,
                                          project=self.project)
        self.assertIsNone(res)

    def test_get_messages_with_ids_from__nonexistent_queue(self):
        queue_name = 'fake_name'
        message_ids = ['fake_id1', 'fake_id2']
        res = self.controller.bulk_get(queue_name, message_ids,
                                       project=self.project)

        self.assertIsInstance(res, abc.Iterable)
        self.assertEqual([], list(res))


class ClaimControllerTest(ControllerBaseTest):
    """Claim Controller base tests.

    NOTE(flaper87): Implementations of this class should
    override the tearDown method in order
    to clean up storage's state.
    """
    queue_name = 'test_queue'
    controller_base_class = storage.Claim

    def setUp(self):
        super(ClaimControllerTest, self).setUp()

        # Lets create a queue
        self.queue_controller = self.pipeline.queue_controller
        self.message_controller = self.pipeline.message_controller
        self.queue_controller.create(self.queue_name, project=self.project)

    def tearDown(self):
        self.queue_controller.delete(self.queue_name, project=self.project)
        super(ClaimControllerTest, self).tearDown()

    def test_claim_lifecycle(self):
        _insert_fixtures(self.message_controller, self.queue_name,
                         project=self.project, client_uuid=uuid.uuid4(),
                         num=20)

        meta = {'ttl': 70, 'grace': 30}

        # Make sure create works
        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project,
                                                    limit=15)

        messages = list(messages)
        self.assertEqual(15, len(messages))

        # Ensure Queue stats
        countof = self.queue_controller.stats(self.queue_name,
                                              project=self.project)
        self.assertEqual(15, countof['messages']['claimed'])
        self.assertEqual(5, countof['messages']['free'])
        self.assertEqual(20, countof['messages']['total'])

        # Make sure get works
        claim, messages2 = self.controller.get(self.queue_name, claim_id,
                                               project=self.project)

        messages2 = list(messages2)
        self.assertEqual(15, len(messages2))
        for msg1, msg2 in zip(messages, messages2):
            self.assertEqual(msg1['body'], msg2['body'])
            self.assertEqual(msg1['claim_id'], msg2['claim_id'])
            self.assertEqual(msg1['id'], msg2['id'])
            self.assertEqual(msg1['ttl'], msg2['ttl'])
        self.assertEqual(70, claim['ttl'])
        self.assertEqual(claim_id, claim['id'])

        new_meta = {'ttl': 100, 'grace': 60}
        self.controller.update(self.queue_name, claim_id,
                               new_meta, project=self.project)

        # Make sure update works
        claim, messages2 = self.controller.get(self.queue_name, claim_id,
                                               project=self.project)

        messages2 = list(messages2)
        self.assertEqual(15, len(messages2))

        # TODO(zyuan): Add some tests to ensure the ttl is
        # extended/not-extended.
        for msg1, msg2 in zip(messages, messages2):
            self.assertEqual(msg1['body'], msg2['body'])

        self.assertEqual(new_meta['ttl'], claim['ttl'])
        self.assertEqual(claim_id, claim['id'])

        # Make sure delete works
        self.controller.delete(self.queue_name, claim_id,
                               project=self.project)

        self.assertRaises(errors.ClaimDoesNotExist,
                          self.controller.get, self.queue_name,
                          claim_id, project=self.project)

    def test_claim_create_default_limit_multi(self):
        num_claims = 5
        num_messages = storage.DEFAULT_MESSAGES_PER_CLAIM * num_claims

        # NOTE(kgriffs): + 1 on num_messages to check for off-by-one error
        _insert_fixtures(self.message_controller, self.queue_name,
                         project=self.project, client_uuid=uuid.uuid4(),
                         num=num_messages + 1)

        meta = {'ttl': 70, 'grace': 30}
        total_claimed = 0

        for _ in range(num_claims):
            claim_id, messages = self.controller.create(
                self.queue_name, meta, project=self.project)

            messages = list(messages)
            num_claimed = len(messages)
            self.assertEqual(storage.DEFAULT_MESSAGES_PER_CLAIM,
                             num_claimed)

            total_claimed += num_claimed

        self.assertEqual(num_messages, total_claimed)

    def test_extend_lifetime(self):
        _insert_fixtures(self.message_controller, self.queue_name,
                         project=self.project, client_uuid=uuid.uuid4(),
                         num=20, ttl=120)

        meta = {'ttl': 777, 'grace': 0}

        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)

        for message in messages:
            self.assertEqual(777, message['ttl'])

    def test_extend_lifetime_with_grace_1(self):
        _insert_fixtures(self.message_controller, self.queue_name,
                         project=self.project, client_uuid=uuid.uuid4(),
                         num=20, ttl=120)

        meta = {'ttl': 777, 'grace': 23}

        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)

        for message in messages:
            self.assertEqual(800, message['ttl'])

    def test_extend_lifetime_with_grace_2(self):
        _insert_fixtures(self.message_controller, self.queue_name,
                         project=self.project, client_uuid=uuid.uuid4(),
                         num=20, ttl=120)

        meta = {'ttl': 121, 'grace': 22}

        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)

        for message in messages:
            self.assertEqual(143, message['ttl'])

    def test_do_not_extend_lifetime(self):
        _insert_fixtures(self.message_controller, self.queue_name,
                         project=self.project, client_uuid=uuid.uuid4(),
                         num=20, ttl=120)

        # Choose a ttl that is less than the message's current TTL
        meta = {'ttl': 60, 'grace': 30}

        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)

        for message in messages:
            self.assertEqual(120, message['ttl'])

    def test_expired_claim(self):
        meta = {'ttl': 1, 'grace': 60}

        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)
        time.sleep(1)

        with testing.expect(errors.DoesNotExist):
            self.controller.get(self.queue_name, claim_id,
                                project=self.project)

        with testing.expect(errors.DoesNotExist):
            self.controller.update(self.queue_name, claim_id,
                                   meta, project=self.project)

    def test_delete_message_expired_claim(self):
        meta = {'ttl': 2, 'grace': 2}
        new_messages = [{'ttl': 60, 'body': {}},
                        {'ttl': 60, 'body': {}},
                        {'ttl': 60, 'body': {}}]

        self.message_controller.post(self.queue_name, new_messages,
                                     client_uuid=str(uuid.uuid1()),
                                     project=self.project)

        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)

        now = timeutils.utcnow_ts()
        timeutils_utcnow = 'oslo_utils.timeutils.utcnow_ts'

        with mock.patch(timeutils_utcnow) as mock_utcnow:
            mock_utcnow.return_value = now + 2

            messages = [msg['id'] for msg in messages]
            self.message_controller.delete(self.queue_name,
                                           messages.pop(),
                                           project=self.project)

            self.message_controller.bulk_delete(self.queue_name,
                                                messages,
                                                project=self.project)

    def test_illformed_id(self):
        # any ill-formed IDs should be regarded as non-existing ones.

        self.controller.delete(self.queue_name,
                               'illformed',
                               project=self.project)

        with testing.expect(errors.DoesNotExist):
            self.controller.get(self.queue_name,
                                'illformed',
                                project=self.project)

        with testing.expect(errors.DoesNotExist):
            self.controller.update(self.queue_name,
                                   'illformed',
                                   {'ttl': 40},
                                   project=self.project)

    def test_dead_letter_queue(self):
        DLQ_name = "DLQ"
        meta = {'ttl': 3, 'grace': 3}
        self.queue_controller.create("DLQ", project=self.project)
        # Set dead letter queeu metadata
        metadata = {"_max_claim_count": 2,
                    "_dead_letter_queue": DLQ_name,
                    "_dead_letter_queue_messages_ttl": 9999}
        self.queue_controller.set_metadata(self.queue_name,
                                           metadata,
                                           project=self.project)

        new_messages = [{'ttl': 3600, 'body': {"key": "value"}}]

        self.message_controller.post(self.queue_name, new_messages,
                                     client_uuid=str(uuid.uuid1()),
                                     project=self.project)

        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)
        self.assertIsNotNone(claim_id)
        self.assertEqual(1, len(list(messages)))
        time.sleep(5)
        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)
        self.assertIsNotNone(claim_id)
        messages = list(messages)
        self.assertEqual(1, len(messages))
        time.sleep(5)
        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)
        self.assertIsNone(claim_id)
        self.assertEqual(0, len(list(messages)))

        DLQ_messages = self.message_controller.list(DLQ_name,
                                                    project=self.project,
                                                    include_claimed=True)
        expected_msg = list(next(DLQ_messages))[0]
        self.assertEqual(9999, expected_msg["ttl"])
        self.assertEqual({"key": "value"}, expected_msg["body"])

    def test_delay_queue(self):
        meta = {'ttl': 1, 'grace': 0}
        # Set default message delay for queue.
        metadata = {'_default_message_delay': 2}
        self.queue_controller.set_metadata(self.queue_name,
                                           metadata,
                                           project=self.project)
        # Scenarios 1: Normal message sent to delayed queue
        new_messages = [{'ttl': 3600, 'body': {'key': 'value'}}]
        queue_meta = self.queue_controller.get_metadata(self.queue_name,
                                                        project=self.project)
        delay = queue_meta.get('_default_message_delay', 0)
        for msg in new_messages:
            if delay and 'delay' not in new_messages:
                msg['delay'] = delay
        ids = self.message_controller.post(self.queue_name, new_messages,
                                           client_uuid=str(uuid.uuid1()),
                                           project=self.project)
        interaction = self.message_controller.list(self.queue_name,
                                                   project=self.project,
                                                   include_delayed=True)
        delay_messages = list(next(interaction))
        self.assertEqual(1, len(list(delay_messages)))

        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)
        self.assertIsNone(claim_id)
        self.assertEqual(0, len(list(messages)))
        time.sleep(3)
        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)
        self.assertIsNotNone(claim_id)
        self.assertEqual(1, len(list(messages)))
        time.sleep(1)
        self.message_controller.delete(self.queue_name, ids[0],
                                       project=self.project)

        # Scenarios 2: Delay message sent to delayed queue
        new_messages = [{'ttl': 3600, 'delay': 1,
                         'body': {'key': 'value'}}]

        ids = self.message_controller.post(self.queue_name, new_messages,
                                           client_uuid=str(uuid.uuid1()),
                                           project=self.project)
        interaction = self.message_controller.list(self.queue_name,
                                                   project=self.project,
                                                   include_delayed=True)
        delay_messages = list(next(interaction))
        self.assertEqual(1, len(list(delay_messages)))

        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)
        self.assertIsNone(claim_id)
        self.assertEqual(0, len(list(messages)))
        time.sleep(1)
        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)
        self.assertIsNotNone(claim_id)
        self.assertEqual(1, len(list(messages)))
        time.sleep(1)
        self.message_controller.delete(self.queue_name, ids[0],
                                       project=self.project)

        # Scenarios 3: Message sent to normal queue
        new_messages = [{'ttl': 3600, 'body': {'key': 'value'}}]
        self.queue_controller.set_metadata(self.queue_name,
                                           {},
                                           project=self.project)
        ids = self.message_controller.post(self.queue_name, new_messages,
                                           client_uuid=str(uuid.uuid1()),
                                           project=self.project)
        interaction = self.message_controller.list(self.queue_name,
                                                   project=self.project,
                                                   include_delayed=True)
        delay_messages = list(next(interaction))
        self.assertEqual(1, len(list(delay_messages)))
        claim_id, messages = self.controller.create(self.queue_name, meta,
                                                    project=self.project)
        self.assertIsNotNone(claim_id)
        self.assertEqual(1, len(list(messages)))
        time.sleep(1)
        self.message_controller.delete(self.queue_name, ids[0],
                                       project=self.project)


@ddt.ddt
class SubscriptionControllerTest(ControllerBaseTest):
    """Subscriptions Controller base tests.

    """
    queue_name = 'test_queue'
    controller_base_class = storage.Subscription

    def setUp(self):
        super(SubscriptionControllerTest, self).setUp()
        self.subscription_controller = self.driver.subscription_controller
        self.queue_controller = self.driver.queue_controller

        self.source = self.queue_name
        self.subscriber = 'http://trigger.me'
        self.ttl = 600
        self.options = {'uri': 'http://fake.com'}

    def tearDown(self):
        self.queue_controller.delete(self.queue_name, project=self.project)
        super(SubscriptionControllerTest, self).tearDown()

    # NOTE(Eva-i): this method helps to test cases when the queue is
    # pre-created and when it's not.
    def _precreate_queue(self, precreate_queue):
        if precreate_queue:
            # Let's create a queue as the source of subscription
            self.queue_controller.create(self.queue_name, project=self.project)

    @ddt.data(True, False)
    def test_list(self, precreate_queue):
        self._precreate_queue(precreate_queue)
        for s in six.moves.xrange(15):
            subscriber = 'http://fake_{0}'.format(s)
            s_id = self.subscription_controller.create(
                self.source,
                subscriber,
                self.ttl,
                self.options,
                project=self.project)
            self.addCleanup(self.subscription_controller.delete, self.source,
                            s_id, self.project)

        added_age = 1
        time.sleep(added_age)
        interaction = self.subscription_controller.list(self.source,
                                                        project=self.project)
        subscriptions = list(next(interaction))

        self.assertTrue(all(map(lambda s:
                                'source' in s and 'subscriber' in s,
                                subscriptions)))
        self.assertEqual(10, len(subscriptions))
        self.assertLessEqual(added_age, math.ceil(subscriptions[2]['age']))

        interaction = (self.subscription_controller.list(self.source,
                                                         project=self.project,
                       marker=next(interaction)))
        subscriptions = list(next(interaction))

        self.assertTrue(all(map(lambda s:
                                'source' in s and 'subscriber' in s,
                                subscriptions)))
        self.assertEqual(5, len(subscriptions))

    def test_small_list(self):
        subscriber = 'http://fake'
        s_id = self.subscription_controller.create(
            self.source,
            subscriber,
            self.ttl,
            self.options,
            project=self.project)
        self.addCleanup(self.subscription_controller.delete, self.source,
                        s_id, self.project)

        interaction = self.subscription_controller.list(self.source,
                                                        project=self.project)
        subscriptions = list(next(interaction))
        marker = next(interaction)

        self.assertEqual(1, len(subscriptions))

        interaction = (self.subscription_controller.list(self.source,
                                                         project=self.project,
                       marker=marker))
        subscriptions = list(next(interaction))

        self.assertEqual([], subscriptions)

    @ddt.data(True, False)
    def test_get_raises_if_subscription_does_not_exist(self, precreate_queue):
        self._precreate_queue(precreate_queue)
        self.assertRaises(errors.SubscriptionDoesNotExist,
                          self.subscription_controller.get,
                          self.queue_name,
                          'notexists',
                          project=self.project)

    @ddt.data(True, False)
    def test_lifecycle(self, precreate_queue):
        self._precreate_queue(precreate_queue)
        s_id = self.subscription_controller.create(self.source,
                                                   self.subscriber,
                                                   self.ttl,
                                                   self.options,
                                                   project=self.project)
        added_age = 2
        time.sleep(added_age)
        subscription = self.subscription_controller.get(self.queue_name,
                                                        s_id,
                                                        self.project)

        self.assertEqual(self.source, subscription['source'])
        self.assertEqual(self.subscriber, subscription['subscriber'])
        self.assertEqual(self.ttl, subscription['ttl'])
        self.assertEqual(self.options, subscription['options'])
        self.assertLessEqual(added_age, math.ceil(subscription['age']))

        exist = self.subscription_controller.exists(self.queue_name,
                                                    s_id,
                                                    self.project)

        self.assertTrue(exist)

        self.subscription_controller.update(self.queue_name,
                                            s_id,
                                            project=self.project,
                                            subscriber='http://a.com',
                                            options={'funny': 'no'}
                                            )

        updated = self.subscription_controller.get(self.queue_name,
                                                   s_id,
                                                   self.project)

        self.assertEqual('http://a.com', updated['subscriber'])
        self.assertEqual({'funny': 'no'}, updated['options'])

        self.subscription_controller.delete(self.queue_name,
                                            s_id, project=self.project)
        self.assertRaises(errors.SubscriptionDoesNotExist,
                          self.subscription_controller.get,
                          self.queue_name, s_id)

    @ddt.data(True, False)
    def test_create_existed(self, precreate_queue):
        self._precreate_queue(precreate_queue)
        s_id = self.subscription_controller.create(
            self.source,
            self.subscriber,
            self.ttl,
            self.options,
            project=self.project)
        self.addCleanup(self.subscription_controller.delete, self.source, s_id,
                        self.project)
        self.assertIsNotNone(s_id)

        s_id = self.subscription_controller.create(self.source,
                                                   self.subscriber,
                                                   self.ttl,
                                                   self.options,
                                                   project=self.project)
        self.assertIsNone(s_id)

    def test_create_existed_and_get_correct_id(self):
        queue_name1 = 'test_queue1'
        queue_name2 = 'test_queue2'
        subscriber = 'http://fake'
        self.queue_controller.create(queue_name1, project=self.project)
        self.queue_controller.create(queue_name2, project=self.project)

        self.subscription_controller.create(queue_name1,
                                            subscriber,
                                            self.ttl,
                                            self.options,
                                            project=self.project)

        s_id_2 = self.subscription_controller.create(queue_name2,
                                                     subscriber,
                                                     self.ttl,
                                                     self.options,
                                                     project=self.project)

        s_id_3 = self.subscription_controller.create(queue_name2,
                                                     subscriber,
                                                     self.ttl,
                                                     self.options,
                                                     project=self.project)
        self.assertIsNone(s_id_3)

        s_id = self.subscription_controller. \
            get_with_subscriber(queue_name2,
                                subscriber,
                                project=self.project)['id']

        self.assertEqual(str(s_id_2), s_id)

    def test_get_update_delete_on_non_existing_queue(self):
        self._precreate_queue(precreate_queue=True)
        s_id = self.subscription_controller.create(
            self.source,
            self.subscriber,
            self.ttl,
            self.options,
            project=self.project)
        self.addCleanup(self.subscription_controller.delete, self.source, s_id,
                        self.project)
        self.assertIsNotNone(s_id)
        non_existing_queue = "fake_name"
        # get
        self.assertRaises(errors.SubscriptionDoesNotExist,
                          self.subscription_controller.get,
                          non_existing_queue, s_id, project=self.project)
        # update
        body = {
            "subscriber": self.subscriber,
            "ttl": self.ttl,
            "options": self.options
        }
        self.assertRaises(errors.SubscriptionDoesNotExist,
                          self.subscription_controller.update,
                          non_existing_queue, s_id, project=self.project,
                          **body)
        # delete
        self.subscription_controller.delete(non_existing_queue, s_id,
                                            project=self.project)
        s_id = self.subscription_controller.get(self.queue_name, s_id,
                                                project=self.project)
        self.assertIsNotNone(s_id)

    def test_nonexist_source(self):
        try:
            s_id = self.subscription_controller.create('fake_queue_name',
                                                       self.subscriber,
                                                       self.ttl,
                                                       self.options,
                                                       self.project)
        except Exception:
            self.fail("Subscription controller should not raise an exception "
                      "in case of non-existing queue.")
        self.addCleanup(self.subscription_controller.delete, 'fake_queue_name',
                        s_id, self.project)

    @ddt.data(True, False)
    def test_update_raises_if_try_to_update_to_existing_subscription(
            self,
            precreate_queue):
        self._precreate_queue(precreate_queue)
        # create two subscriptions: fake_0 and fake_1
        ids = []
        for s in six.moves.xrange(2):
            subscriber = 'http://fake_{0}'.format(s)
            s_id = self.subscription_controller.create(
                self.source,
                subscriber,
                self.ttl,
                self.options,
                project=self.project)
            self.addCleanup(self.subscription_controller.delete, self.source,
                            s_id, self.project)
            ids.append(s_id)
        # update fake_0 to fake_2, success
        update_fields = {
            'subscriber': 'http://fake_2'
        }
        self.subscription_controller.update(self.queue_name,
                                            ids[0],
                                            project=self.project,
                                            **update_fields)
        # update fake_1 to fake_2, raise error
        self.assertRaises(errors.SubscriptionAlreadyExists,
                          self.subscription_controller.update,
                          self.queue_name,
                          ids[1],
                          project=self.project,
                          **update_fields)

    @ddt.data(True, False)
    def test_update_raises_if_subscription_does_not_exist(self,
                                                          precreate_queue):
        self._precreate_queue(precreate_queue)
        update_fields = {
            'subscriber': 'http://fake'
        }
        self.assertRaises(errors.SubscriptionDoesNotExist,
                          self.subscription_controller.update,
                          self.queue_name,
                          'notexists',
                          project=self.project,
                          **update_fields)

    def test_confirm(self):
        s_id = self.subscription_controller.create(self.source,
                                                   self.subscriber,
                                                   self.ttl,
                                                   self.options,
                                                   project=self.project)
        self.addCleanup(self.subscription_controller.delete, self.source,
                        s_id, self.project)
        subscription = self.subscription_controller.get(self.source, s_id,
                                                        project=self.project)
        self.assertFalse(subscription['confirmed'])

        self.subscription_controller.confirm(self.source, s_id,
                                             project=self.project,
                                             confirmed=True)
        subscription = self.subscription_controller.get(self.source, s_id,
                                                        project=self.project)
        self.assertTrue(subscription['confirmed'])

        self.subscription_controller.confirm(self.source, s_id,
                                             project=self.project,
                                             confirmed=False)
        subscription = self.subscription_controller.get(self.source, s_id,
                                                        project=self.project)
        self.assertFalse(subscription['confirmed'])

    def test_confirm_with_nonexist_subscription(self):
        s_id = 'fake-id'
        self.assertRaises(errors.SubscriptionDoesNotExist,
                          self.subscription_controller.confirm,
                          self.source, s_id, project=self.project,
                          confirmed=True
                          )


class PoolsControllerTest(ControllerBaseTest):
    """Pools Controller base tests.

    NOTE(flaper87): Implementations of this class should
    override the tearDown method in order
    to clean up storage's state.
    """
    controller_base_class = storage.PoolsBase

    def setUp(self):
        super(PoolsControllerTest, self).setUp()
        self.pools_controller = self.driver.pools_controller

        # Let's create one pool
        self.pool = str(uuid.uuid1())
        self.pool1 = str(uuid.uuid1())
        self.flavor = str(uuid.uuid1())
        self.uri = str(uuid.uuid1())
        self.pools_controller.create(self.pool1, 100, self.uri,
                                     flavor=self.flavor, options={})
        self.flavors_controller = self.driver.flavors_controller

    def tearDown(self):
        self.pools_controller.drop_all()
        super(PoolsControllerTest, self).tearDown()

    def test_create_succeeds(self):
        self.pools_controller.create(str(uuid.uuid1()),
                                     100, 'localhost:13124',
                                     options={})

    def test_create_replaces_on_duplicate_insert(self):
        name = str(uuid.uuid1())
        self.pools_controller.create(name,
                                     100, 'localhost:76553',
                                     options={})
        self.pools_controller.create(name,
                                     111, 'localhost:758353',
                                     options={})
        entry = self.pools_controller.get(name)
        self._pool_expects(entry, xname=name, xweight=111,
                           xlocation='localhost:758353')

    def _pool_expects(self, pool, xname, xweight, xlocation):
        self.assertIn('name', pool)
        self.assertEqual(xname, pool['name'])
        self.assertIn('weight', pool)
        self.assertEqual(xweight, pool['weight'])
        self.assertIn('uri', pool)
        self.assertEqual(xlocation, pool['uri'])

    def test_get_returns_expected_content(self):
        res = self.pools_controller.get(self.pool1)
        self._pool_expects(res, self.pool1, 100, self.uri)
        self.assertNotIn('options', res)

    def test_detailed_get_returns_expected_content(self):
        res = self.pools_controller.get(self.pool1, detailed=True)
        self.assertIn('options', res)
        self.assertEqual({}, res['options'])

    def test_get_raises_if_not_found(self):
        self.assertRaises(errors.PoolDoesNotExist,
                          self.pools_controller.get, 'notexists')

    def test_exists(self):
        self.assertTrue(self.pools_controller.exists(self.pool1))
        self.assertFalse(self.pools_controller.exists('notexists'))

    def test_update_raises_assertion_error_on_bad_fields(self):
        self.assertRaises(AssertionError, self.pools_controller.update,
                          self.pool)

    def test_update_works(self):
        # NOTE(flaper87): This may fail for redis. Create
        # a dummy store for tests.
        self.uri3 = str(uuid.uuid1())
        self.pools_controller.update(self.pool1, weight=101,
                                     uri=self.uri3,
                                     options={'a': 1})
        res = self.pools_controller.get(self.pool1, detailed=True)
        self._pool_expects(res, self.pool1, 101, self.uri3)
        self.assertEqual({'a': 1}, res['options'])

    def test_delete_works(self):
        # self.pools_controller.delete(self.pool)
        # (gengchc): Remove the flavor from pool, then testcase cleanup pool
        self.pools_controller.update(self.pool1, flavor="")
        self.pools_controller.delete(self.pool1)
        self.assertFalse(self.pools_controller.exists(self.pool1))

    def test_delete_nonexistent_is_silent(self):
        self.pools_controller.delete('nonexisting')

    def test_drop_all_leads_to_empty_listing(self):
        self.pools_controller.drop_all()
        cursor = self.pools_controller.list()
        pools = next(cursor)
        self.assertRaises(StopIteration, next, pools)

    def test_listing_simple(self):
        # (gengchc): Remove the flavor from pool, then testcase cleanup pool
        self.pools_controller.update(self.pool1, flavor="")
        self.pools_controller._drop_all()
        pools = []
        marker = ''
        for i in range(15):
            n = str(uuid.uuid4())
            w = random.randint(1, 100)
            pools.append({'n': n, 'w': w, 'u': str(i)})

            # Keep the max name as marker
            if n > marker:
                marker = n

            self.pools_controller.create(n, w, str(i), options={})

        # Get the target pool
        def _pool(name):
            pool = [p for p in pools if p['n'] == name]
            self.assertEqual(1, len(pool))

            pool = pool[0]
            n = pool['n']
            w = pool['w']
            u = pool['u']

            return n, w, u

        def get_res(**kwargs):
            cursor = self.pools_controller.list(**kwargs)
            res = list(next(cursor))
            marker = next(cursor)
            # TODO(jeffrey4l): marker should exist
            self.assertTrue(marker)
            return res

        res = get_res()
        self.assertEqual(10, len(res))
        for entry in res:
            n, w, u = _pool(entry['name'])

            self._pool_expects(entry, n, w, u)
            self.assertNotIn('options', entry)

        res = get_res(limit=5)
        self.assertEqual(5, len(res))

        res = get_res(limit=0)
        self.assertEqual(15, len(res))

        next_name = marker + 'n'
        self.pools_controller.create(next_name, 123, '123', options={})
        res = get_res(marker=marker)
        self._pool_expects(res[0], next_name, 123, '123')
        self.pools_controller.delete(next_name)

        res = get_res(detailed=True)
        self.assertEqual(10, len(res))
        for entry in res:
            n, w, u = _pool(entry['name'])

            self._pool_expects(entry, n, w, u)
            self.assertIn('options', entry)
            self.assertEqual({}, entry['options'])


class CatalogueControllerTest(ControllerBaseTest):
    controller_base_class = storage.CatalogueBase

    def setUp(self):
        super(CatalogueControllerTest, self).setUp()
        self.controller = self.driver.catalogue_controller
        self.pool_ctrl = self.driver.pools_controller
        self.queue = six.text_type(uuid.uuid4())
        self.project = six.text_type(uuid.uuid4())

        self.pool = str(uuid.uuid1())
        self.flavor = str(uuid.uuid1())
        self.uri = str(uuid.uuid1())
        self.uri1 = str(uuid.uuid1())
        self.pool_ctrl.create(self.pool, 100, self.uri,
                              flavor=self.flavor, options={})
        self.addCleanup(self.pool_ctrl.delete, self.pool)
        self.pool1 = str(uuid.uuid1())
        self.pool_ctrl.create(self.pool1, 100, self.uri1,
                              flavor=self.flavor, options={})
        self.addCleanup(self.pool_ctrl.delete, self.pool1)

    def tearDown(self):
        self.pool_ctrl.update(self.pool, flavor="")
        self.pool_ctrl.update(self.pool1, flavor="")
        self.pool_ctrl.drop_all()
        self.controller.drop_all()
        super(CatalogueControllerTest, self).tearDown()

    def _check_structure(self, entry):
        self.assertIn('queue', entry)
        self.assertIn('project', entry)
        self.assertIn('pool', entry)
        self.assertIsInstance(entry['queue'], six.text_type)
        self.assertIsInstance(entry['project'], six.text_type)
        self.assertIsInstance(entry['pool'], six.text_type)

    def _check_value(self, entry, xqueue, xproject, xpool):
        self.assertEqual(xqueue, entry['queue'])
        self.assertEqual(xproject, entry['project'])
        self.assertEqual(xpool, entry['pool'])

    def test_catalogue_entry_life_cycle(self):
        queue = self.queue
        project = self.project

        # check listing is initially empty
        for p in self.controller.list(project):
            self.fail('There should be no entries at this time')

        # create a listing, check its length
        with helpers.pool_entries(self.controller,
                                  self.pool_ctrl, 10) as expect:
            project = expect[0][0]
            xs = list(self.controller.list(project))
            self.assertEqual(10, len(xs))

        # create, check existence, delete
        with helpers.pool_entry(self.controller, project, queue, self.pool):
            self.assertTrue(self.controller.exists(project, queue))

        # verify it no longer exists
        self.assertFalse(self.controller.exists(project, queue))

        # verify it isn't listable
        self.assertEqual(0, len(list(self.controller.list(project))))

    def test_list(self):
        with helpers.pool_entries(self.controller,
                                  self.pool_ctrl, 10) as expect:
            values = zip(self.controller.list(u'_'), expect)
            for e, x in values:
                p, q, s = x
                self._check_structure(e)
                self._check_value(e, xqueue=q, xproject=p, xpool=s)

    def test_update(self):
        p2 = u'b'
        # NOTE(gengchc2): Remove [group=self.pool_group] in
        # it can be tested for redis as management.
        self.pool_ctrl.create(p2, 100, '127.0.0.1',
                              options={})
        self.addCleanup(self.pool_ctrl.delete, p2)

        with helpers.pool_entry(self.controller, self.project,
                                self.queue, self.pool) as expect:
            p, q, s = expect
            self.controller.update(p, q, pool=p2)
            entry = self.controller.get(p, q)
            self._check_value(entry, xqueue=q, xproject=p, xpool=p2)

    def test_update_raises_when_entry_does_not_exist(self):
        e = self.assertRaises(errors.QueueNotMapped,
                              self.controller.update,
                              'p', 'q', 'a')
        self.assertIn('queue q for project p', str(e))

    def test_get(self):
        with helpers.pool_entry(self.controller,
                                self.project,
                                self.queue, self.pool) as expect:
            p, q, s = expect
            e = self.controller.get(p, q)
            self._check_value(e, xqueue=q, xproject=p, xpool=s)

    def test_get_raises_if_does_not_exist(self):
        with helpers.pool_entry(self.controller,
                                self.project,
                                self.queue, u'a') as expect:
            p, q, _ = expect
            self.assertRaises(errors.QueueNotMapped,
                              self.controller.get,
                              p, 'non_existing')
            self.assertRaises(errors.QueueNotMapped,
                              self.controller.get,
                              'non_existing', q)
            self.assertRaises(errors.QueueNotMapped,
                              self.controller.get,
                              'non_existing', 'non_existing')

    def test_exists(self):
        with helpers.pool_entry(self.controller,
                                self.project,
                                self.queue, self.pool) as expect:
            p, q, _ = expect
            self.assertTrue(self.controller.exists(p, q))
            self.assertFalse(self.controller.exists('nada', 'not_here'))

    def test_insert(self):
        q1 = six.text_type(uuid.uuid1())
        q2 = six.text_type(uuid.uuid1())
        self.controller.insert(self.project, q1, u'a')
        self.controller.insert(self.project, q2, u'a')


# NOTE(gengchc2): Unittest for new flavor configure scenario.
class FlavorsControllerTest1(ControllerBaseTest):
    """Flavors Controller base tests.

    NOTE(flaper87): Implementations of this class should
    override the tearDown method in order
    to clean up storage's state.
    """
    controller_base_class = storage.FlavorsBase

    def setUp(self):
        super(FlavorsControllerTest1, self).setUp()
        self.pools_controller = self.driver.pools_controller
        self.flavors_controller = self.driver.flavors_controller

        # Let's create one pool
        self.pool = str(uuid.uuid1())
        self.flavor = 'durable'
        self.uri = str(uuid.uuid1())
        self.pools_controller.create(self.pool, 100, self.uri,
                                     flavor=self.flavor, options={})
        self.addCleanup(self.pools_controller.delete, self.pool)

    def tearDown(self):
        self.pools_controller.update(self.pool, flavor="")
        self.pools_controller.drop_all()
        self.flavors_controller.drop_all()
        super(FlavorsControllerTest1, self).tearDown()

    def test_create_succeeds(self):
        self.flavors_controller.create(self.flavor,
                                       project=self.project,
                                       capabilities={})

    def _flavors_expects(self, flavor, xname, xproject):
        self.assertIn('name', flavor)
        self.assertEqual(xname, flavor['name'])
        self.assertNotIn('project', flavor)

    def test_create_replaces_on_duplicate_insert(self):
        name = str(uuid.uuid1())
        self.flavors_controller.create(name,
                                       project=self.project,
                                       capabilities={})
        entry = self.flavors_controller.get(name, self.project)
        self._flavors_expects(entry, name, self.project)
        new_capabilities = {'fifo': False}
        self.flavors_controller.create(name,
                                       project=self.project,
                                       capabilities=new_capabilities)
        entry = self.flavors_controller.get(name, project=self.project,
                                            detailed=True)
        self._flavors_expects(entry, name, self.project)
        self.assertEqual(new_capabilities, entry['capabilities'])

    def test_get_returns_expected_content(self):
        name = 'durable'
        capabilities = {'fifo': True}
        self.flavors_controller.create(name,
                                       project=self.project,
                                       capabilities=capabilities)
        res = self.flavors_controller.get(name, project=self.project)
        self._flavors_expects(res, name, self.project)
        self.assertNotIn('capabilities', res)

    def test_detailed_get_returns_expected_content(self):
        name = 'durable'
        capabilities = {'fifo': True}
        self.flavors_controller.create(name,
                                       project=self.project,
                                       capabilities=capabilities)
        res = self.flavors_controller.get(name, project=self.project,
                                          detailed=True)
        self._flavors_expects(res, name, self.project)
        self.assertIn('capabilities', res)
        self.assertEqual(capabilities, res['capabilities'])

    def test_get_raises_if_not_found(self):
        self.assertRaises(errors.FlavorDoesNotExist,
                          self.flavors_controller.get, 'notexists')

    def test_exists(self):
        self.flavors_controller.create('exists',
                                       project=self.project,
                                       capabilities={})
        self.assertTrue(self.flavors_controller.exists('exists',
                                                       project=self.project))
        self.assertFalse(self.flavors_controller.exists('notexists',
                                                        project=self.project))

    def test_update_raises_assertion_error_on_bad_fields(self):
        self.assertRaises(AssertionError, self.pools_controller.update,
                          self.flavor)

    def test_update_works(self):
        name = 'yummy'
        self.flavors_controller.create(name,
                                       project=self.project,
                                       capabilities={})

        res = self.flavors_controller.get(name, project=self.project,
                                          detailed=True)

        p = 'olympic'
        flavor = name
        self.uri2 = str(uuid.uuid1())
        self.pools_controller.create(p, 100, self.uri2,
                                     flavor=flavor, options={})
        self.addCleanup(self.pools_controller.delete, p)

        new_capabilities = {'fifo': False}
        self.flavors_controller.update(name, project=self.project,
                                       capabilities={'fifo': False})
        res = self.flavors_controller.get(name, project=self.project,
                                          detailed=True)
        self._flavors_expects(res, name, self.project)
        self.assertEqual(new_capabilities, res['capabilities'])
        self.pools_controller.update(p, flavor="")

    def test_delete_works(self):
        name = 'puke'
        self.flavors_controller.create(name,
                                       project=self.project,
                                       capabilities={})
        self.flavors_controller.delete(name, project=self.project)
        self.assertFalse(self.flavors_controller.exists(name))

    def test_delete_nonexistent_is_silent(self):
        self.flavors_controller.delete('nonexisting')

    def test_drop_all_leads_to_empty_listing(self):
        self.flavors_controller.drop_all()
        cursor = self.flavors_controller.list()
        flavors = next(cursor)
        self.assertRaises(StopIteration, next, flavors)
        self.assertFalse(next(cursor))

    def test_listing_simple(self):
        name_gen = lambda i: chr(ord('A') + i)
        for i in range(15):
            pool = str(i)
            flavor = name_gen(i)
            uri = 'localhost:2701' + pool
            self.pools_controller.create(pool, 100, uri,
                                         flavor=flavor, options={})
            self.addCleanup(self.pools_controller.delete, pool)

            self.flavors_controller.create(flavor, project=self.project,
                                           capabilities={})

        def get_res(**kwargs):
            cursor = self.flavors_controller.list(project=self.project,
                                                  **kwargs)
            res = list(next(cursor))
            marker = next(cursor)
            self.assertTrue(marker)
            return res

        res = get_res()
        self.assertEqual(10, len(res))
        for i, entry in enumerate(res):
            self._flavors_expects(entry, name_gen(i), self.project)
            self.assertNotIn('capabilities', entry)

        res = get_res(limit=5)
        self.assertEqual(5, len(res))

        res = get_res(marker=name_gen(3))
        self._flavors_expects(res[0], name_gen(4), self.project)

        res = get_res(detailed=True)
        self.assertEqual(10, len(res))
        for i, entry in enumerate(res):
            self._flavors_expects(entry, name_gen(i), self.project)
            self.assertIn('capabilities', entry)
            self.assertEqual({}, entry['capabilities'])
        # (gengchc): Remove the flavor from pool, then testcase cleanup pools
        for i in range(15):
            pool = str(i)
            self.pools_controller.update(pool, flavor="")


def _insert_fixtures(controller, queue_name, project=None,
                     client_uuid=None, num=4, ttl=120):
    def messages():
        for n in six.moves.xrange(num):
            yield {
                'ttl': ttl,
                'body': {
                    'event': 'Event number {0}'.format(n)
                }}

    return controller.post(queue_name, messages(),
                           project=project, client_uuid=client_uuid)
