# Copyright 2012 OpenStack Foundation.
# All Rights Reserved.
#
#    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.

import datetime
import hashlib
import http.client as http
import os
import requests
from unittest import mock
import uuid

from castellan.common import exception as castellan_exception
import glance_store as store
from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import fixture
import testtools
import webob
import webob.exc

import glance.api.v2.image_actions
import glance.api.v2.images
from glance.common import exception
from glance.common import store_utils
from glance.common import timeutils
from glance import domain
import glance.notifier
import glance.schema
from glance.tests.unit import base
from glance.tests.unit.keymgr import fake as fake_keymgr
import glance.tests.unit.utils as unit_test_utils
from glance.tests.unit.v2 import test_tasks_resource
import glance.tests.utils as test_utils

CONF = cfg.CONF

DATETIME = datetime.datetime(2012, 5, 16, 15, 27, 36, 325355)
ISOTIME = '2012-05-16T15:27:36Z'


BASE_URI = unit_test_utils.BASE_URI


UUID1 = 'c80a1a6c-bd1f-41c5-90ee-81afedb1d58d'
UUID2 = 'a85abd86-55b3-4d5b-b0b4-5d0a6e6042fc'
UUID3 = '971ec09a-8067-4bc8-a91f-ae3557f1c4c7'
UUID4 = '6bbe7cc2-eae7-4c0f-b50d-a7160b0c6a86'
UUID5 = '13c58ac4-210d-41ab-8cdb-1adfe4610019'
UUID6 = '6d33fd0f-2438-4419-acd0-ce1d452c97a0'
UUID7 = '75ddbc84-9427-4f3b-8d7d-b0fd0543d9a8'

TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
TENANT2 = '2c014f32-55eb-467d-8fcb-4bd706012f81'
TENANT3 = '5a3e60e8-cfa9-4a9e-a90a-62b42cea92b8'
TENANT4 = 'c6c87f25-8a94-47ed-8c83-053c25f42df4'

CHKSUM = '93264c3edf5972c9f1cb309543d38a5c'
CHKSUM1 = '43254c3edf6972c9f1cb309543d38a8c'

FAKEHASHALGO = 'fake-name-for-sha512'
MULTIHASH1 = hashlib.sha512(b'glance').hexdigest()
MULTIHASH2 = hashlib.sha512(b'image_service').hexdigest()


TASK_ID_1 = 'b3006bd0-461e-4228-88ea-431c14e918b4'
TASK_ID_2 = '07b6b562-6770-4c8b-a649-37a515144ce9'
TASK_ID_3 = '72d16bb6-4d70-48a5-83fe-14bb842dc737'


def _db_fixture(id, **kwargs):
    obj = {
        'id': id,
        'name': None,
        'visibility': 'shared',
        'properties': {},
        'checksum': None,
        'os_hash_algo': FAKEHASHALGO,
        'os_hash_value': None,
        'owner': None,
        'status': 'queued',
        'tags': [],
        'size': None,
        'virtual_size': None,
        'locations': [],
        'protected': False,
        'disk_format': None,
        'container_format': None,
        'deleted': False,
        'min_ram': None,
        'min_disk': None,
    }
    obj.update(kwargs)
    return obj


def _db_task_fixtures(task_id, **kwargs):
    default_datetime = timeutils.utcnow()
    obj = {
        'id': task_id,
        'status': kwargs.get('status', 'pending'),
        'type': 'import',
        'input': kwargs.get('input', {}),
        'result': None,
        'owner': None,
        'image_id': kwargs.get('image_id'),
        'user_id': kwargs.get('user_id'),
        'request_id': kwargs.get('request_id'),
        'message': None,
        'expires_at': default_datetime + datetime.timedelta(days=365),
        'created_at': default_datetime,
        'updated_at': default_datetime,
        'deleted_at': None,
        'deleted': False
    }
    obj.update(kwargs)
    return obj


def _domain_fixture(id, **kwargs):
    properties = {
        'image_id': id,
        'name': None,
        'visibility': 'private',
        'checksum': None,
        'os_hash_algo': None,
        'os_hash_value': None,
        'owner': None,
        'status': 'queued',
        'size': None,
        'virtual_size': None,
        'locations': [],
        'protected': False,
        'disk_format': None,
        'container_format': None,
        'min_ram': None,
        'min_disk': None,
        'tags': [],
    }
    properties.update(kwargs)
    return glance.domain.Image(**properties)


def _db_image_member_fixture(image_id, member_id, **kwargs):
    obj = {
        'image_id': image_id,
        'member': member_id,
    }
    obj.update(kwargs)
    return obj


class FakeImage(object):
    def __init__(self, id=None, status='active', container_format='ami',
                 disk_format='ami', locations=None):
        self.id = id or UUID4
        self.status = status
        self.container_format = container_format
        self.disk_format = disk_format
        self.locations = locations
        self.owner = unit_test_utils.TENANT1
        self.created_at = ''
        self.updated_at = ''
        self.min_disk = ''
        self.min_ram = ''
        self.protected = False
        self.checksum = ''
        self.os_hash_algo = ''
        self.os_hash_value = ''
        self.size = 0
        self.virtual_size = 0
        self.visibility = 'public'
        self.os_hidden = False
        self.name = 'foo'
        self.tags = []
        self.extra_properties = {}
        self.member = self.owner

        # NOTE(danms): This fixture looks more like the db object than
        # the proxy model. This needs fixing all through the tests
        # below.
        self.image_id = self.id


class TestImagesController(base.IsolatedUnitTest):

    def setUp(self):
        super(TestImagesController, self).setUp()
        self.db = unit_test_utils.FakeDB(initialize=False)
        self.policy = unit_test_utils.FakePolicyEnforcer()
        self.notifier = unit_test_utils.FakeNotifier()
        self.store = unit_test_utils.FakeStoreAPI()
        for i in range(1, 4):
            self.store.data['%s/fake_location_%i' % (BASE_URI, i)] = ('Z', 1)
        self.store_utils = unit_test_utils.FakeStoreUtils(self.store)
        self._create_images()
        self._create_image_members()
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                self.policy,
                                                                self.notifier,
                                                                self.store)
        self.action_controller = (glance.api.v2.image_actions.
                                  ImageActionsController(self.db,
                                                         self.policy,
                                                         self.notifier,
                                                         self.store))
        self.controller.gateway.store_utils = self.store_utils
        self.controller._key_manager = fake_keymgr.fake_api()
        store.create_stores()

    def _create_images(self):
        self.images = [
            _db_fixture(UUID1, owner=TENANT1, checksum=CHKSUM,
                        os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
                        name='1', size=256, virtual_size=1024,
                        visibility='public',
                        locations=[{'url': '%s/%s' % (BASE_URI, UUID1),
                                    'metadata': {}, 'status': 'active'}],
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        created_at=DATETIME,
                        updated_at=DATETIME),
            _db_fixture(UUID2, owner=TENANT1, checksum=CHKSUM1,
                        os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH2,
                        name='2', size=512, virtual_size=2048,
                        visibility='public',
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        tags=['redhat', '64bit', 'power'],
                        properties={'hypervisor_type': 'kvm', 'foo': 'bar',
                                    'bar': 'foo'},
                        created_at=DATETIME + datetime.timedelta(seconds=1),
                        updated_at=DATETIME + datetime.timedelta(seconds=1)),
            _db_fixture(UUID3, owner=TENANT3, checksum=CHKSUM1,
                        os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH2,
                        name='3', size=512, virtual_size=2048,
                        visibility='public', tags=['windows', '64bit', 'x86'],
                        created_at=DATETIME + datetime.timedelta(seconds=2),
                        updated_at=DATETIME + datetime.timedelta(seconds=2)),
            _db_fixture(UUID4, owner=TENANT4, name='4',
                        size=1024, virtual_size=3072,
                        created_at=DATETIME + datetime.timedelta(seconds=3),
                        updated_at=DATETIME + datetime.timedelta(seconds=3)),
        ]
        [self.db.image_create(None, image) for image in self.images]

        # Create tasks associated with image
        self.tasks = [
            _db_task_fixtures(
                TASK_ID_1, image_id=UUID1, status='completed',
                input={
                    "image_id": UUID1,
                    "import_req": {
                        "method": {
                            "name": "glance-direct"
                        },
                        "backend": ["fake-store"]
                    },
                },
                user_id='fake-user-id',
                request_id='fake-request-id',
            ),
            _db_task_fixtures(
                TASK_ID_2, image_id=UUID1, status='completed',
                input={
                    "image_id": UUID1,
                    "import_req": {
                        "method": {
                            "name": "copy-image"
                        },
                        "all_stores": True,
                        "all_stores_must_succeed": False,
                        "backend": ["fake-store", "fake_store_1"]
                    },
                },
                user_id='fake-user-id',
                request_id='fake-request-id',
            ),
            _db_task_fixtures(
                TASK_ID_3, status='completed',
                input={
                    "image_id": UUID2,
                    "import_req": {
                        "method": {
                            "name": "glance-direct"
                        },
                        "backend": ["fake-store"]
                    },
                },
            ),
        ]
        [self.db.task_create(None, task) for task in self.tasks]

        self.db.image_tag_set_all(None, UUID1, ['ping', 'pong'])

    def _create_image_members(self):
        self.image_members = [
            _db_image_member_fixture(UUID4, TENANT2),
            _db_image_member_fixture(UUID4, TENANT3,
                                     status='accepted'),
        ]
        [self.db.image_member_create(None, image_member)
            for image_member in self.image_members]

    def test_index(self):
        self.config(limit_param_default=1, api_limit_max=3)
        request = unit_test_utils.get_fake_request()
        output = self.controller.index(request)
        self.assertEqual(1, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID3])
        self.assertEqual(expected, actual)

    def test_index_member_status_accepted(self):
        self.config(limit_param_default=5, api_limit_max=5)
        request = unit_test_utils.get_fake_request(tenant=TENANT2)
        output = self.controller.index(request)
        self.assertEqual(3, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID1, UUID2, UUID3])
        # can see only the public image
        self.assertEqual(expected, actual)

        request = unit_test_utils.get_fake_request(tenant=TENANT3)
        output = self.controller.index(request)
        self.assertEqual(4, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID1, UUID2, UUID3, UUID4])
        self.assertEqual(expected, actual)

    def test_index_admin(self):
        request = unit_test_utils.get_fake_request(is_admin=True)
        output = self.controller.index(request)
        self.assertEqual(4, len(output['images']))

    def test_index_admin_deleted_images_hidden(self):
        request = unit_test_utils.get_fake_request(is_admin=True)
        self.controller.delete(request, UUID1)
        output = self.controller.index(request)
        self.assertEqual(3, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID2, UUID3, UUID4])
        self.assertEqual(expected, actual)

    def test_index_return_parameters(self):
        self.config(limit_param_default=1, api_limit_max=3)
        request = unit_test_utils.get_fake_request()
        output = self.controller.index(request, marker=UUID3, limit=1,
                                       sort_key=['created_at'],
                                       sort_dir=['desc'])
        self.assertEqual(1, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID2])
        self.assertEqual(actual, expected)
        self.assertEqual(UUID2, output['next_marker'])

    def test_index_next_marker(self):
        self.config(limit_param_default=1, api_limit_max=3)
        request = unit_test_utils.get_fake_request()
        output = self.controller.index(request, marker=UUID3, limit=2)
        self.assertEqual(2, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID2, UUID1])
        self.assertEqual(expected, actual)
        self.assertEqual(UUID1, output['next_marker'])

    def test_index_no_next_marker(self):
        self.config(limit_param_default=1, api_limit_max=3)
        request = unit_test_utils.get_fake_request()
        output = self.controller.index(request, marker=UUID1, limit=2)
        self.assertEqual(0, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([])
        self.assertEqual(expected, actual)
        self.assertNotIn('next_marker', output)

    def test_index_marker_would_be_disallowed(self):
        self.config(limit_param_default=1, api_limit_max=10)
        request = unit_test_utils.get_fake_request(is_admin=True)

        def fake_enforce(context, action, target=None, **kw):
            assert target is not None
            if target['project_id'] != TENANT1:
                raise exception.Forbidden()

        # As admin, list three images. By default, this should leave
        # us on UUID3 (and as next_marker), which is owned by TENANT3
        output = self.controller.index(request, sort_dir=['asc'], limit=3)
        self.assertEqual(UUID3, output['next_marker'])
        self.assertEqual(3, len(output['images']))

        # Now sub in our fake policy that restricts us to TENANT1 images only.
        # Even though we list with limit=3, we should only get two images back,
        # and our next_marker should be UUID2 because we couldn't see UUID3.
        with mock.patch.object(self.controller.policy, 'enforce',
                               new=fake_enforce):
            output = self.controller.index(request, sort_dir=['asc'], limit=3)
        self.assertEqual(UUID2, output['next_marker'])
        self.assertEqual(2, len(output['images']))

    def test_index_with_id_filter(self):
        request = unit_test_utils.get_fake_request('/images?id=%s' % UUID1)
        output = self.controller.index(request, filters={'id': UUID1})
        self.assertEqual(1, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID1])
        self.assertEqual(expected, actual)

    def test_index_with_invalid_hidden_filter(self):
        request = unit_test_utils.get_fake_request('/images?os_hidden=abcd')
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.controller.index, request,
                          filters={'os_hidden': 'abcd'})

    def test_index_with_checksum_filter_single_image(self):
        req = unit_test_utils.get_fake_request('/images?checksum=%s' % CHKSUM)
        output = self.controller.index(req, filters={'checksum': CHKSUM})
        self.assertEqual(1, len(output['images']))
        actual = list([image.image_id for image in output['images']])
        expected = [UUID1]
        self.assertEqual(expected, actual)

    def test_index_with_checksum_filter_multiple_images(self):
        req = unit_test_utils.get_fake_request('/images?checksum=%s' % CHKSUM1)
        output = self.controller.index(req, filters={'checksum': CHKSUM1})
        self.assertEqual(2, len(output['images']))
        actual = list([image.image_id for image in output['images']])
        expected = [UUID3, UUID2]
        self.assertEqual(expected, actual)

    def test_index_with_non_existent_checksum(self):
        req = unit_test_utils.get_fake_request('/images?checksum=236231827')
        output = self.controller.index(req, filters={'checksum': '236231827'})
        self.assertEqual(0, len(output['images']))

    def test_index_with_os_hash_value_filter_single_image(self):
        req = unit_test_utils.get_fake_request(
            '/images?os_hash_value=%s' % MULTIHASH1)
        output = self.controller.index(req,
                                       filters={'os_hash_value': MULTIHASH1})
        self.assertEqual(1, len(output['images']))
        actual = list([image.image_id for image in output['images']])
        expected = [UUID1]
        self.assertEqual(expected, actual)

    def test_index_with_os_hash_value_filter_multiple_images(self):
        req = unit_test_utils.get_fake_request(
            '/images?os_hash_value=%s' % MULTIHASH2)
        output = self.controller.index(req,
                                       filters={'os_hash_value': MULTIHASH2})
        self.assertEqual(2, len(output['images']))
        actual = list([image.image_id for image in output['images']])
        expected = [UUID3, UUID2]
        self.assertEqual(expected, actual)

    def test_index_with_non_existent_os_hash_value(self):
        fake_hash_value = hashlib.sha512(b'not_used_in_fixtures').hexdigest()
        req = unit_test_utils.get_fake_request(
            '/images?os_hash_value=%s' % fake_hash_value)
        output = self.controller.index(req,
                                       filters={'checksum': fake_hash_value})
        self.assertEqual(0, len(output['images']))

    def test_index_size_max_filter(self):
        request = unit_test_utils.get_fake_request('/images?size_max=512')
        output = self.controller.index(request, filters={'size_max': 512})
        self.assertEqual(3, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID1, UUID2, UUID3])
        self.assertEqual(expected, actual)

    def test_index_size_min_filter(self):
        request = unit_test_utils.get_fake_request('/images?size_min=512')
        output = self.controller.index(request, filters={'size_min': 512})
        self.assertEqual(2, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID2, UUID3])
        self.assertEqual(expected, actual)

    def test_index_size_range_filter(self):
        path = '/images?size_min=512&size_max=512'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       filters={'size_min': 512,
                                                'size_max': 512})
        self.assertEqual(2, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID2, UUID3])
        self.assertEqual(expected, actual)

    def test_index_virtual_size_max_filter(self):
        ref = '/images?virtual_size_max=2048'
        request = unit_test_utils.get_fake_request(ref)
        output = self.controller.index(request,
                                       filters={'virtual_size_max': 2048})
        self.assertEqual(3, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID1, UUID2, UUID3])
        self.assertEqual(expected, actual)

    def test_index_virtual_size_min_filter(self):
        ref = '/images?virtual_size_min=2048'
        request = unit_test_utils.get_fake_request(ref)
        output = self.controller.index(request,
                                       filters={'virtual_size_min': 2048})
        self.assertEqual(2, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID2, UUID3])
        self.assertEqual(expected, actual)

    def test_index_virtual_size_range_filter(self):
        path = '/images?virtual_size_min=512&virtual_size_max=2048'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       filters={'virtual_size_min': 2048,
                                                'virtual_size_max': 2048})
        self.assertEqual(2, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID2, UUID3])
        self.assertEqual(expected, actual)

    def test_index_with_invalid_max_range_filter_value(self):
        request = unit_test_utils.get_fake_request('/images?size_max=blah')
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.controller.index,
                          request,
                          filters={'size_max': 'blah'})

    def test_index_with_filters_return_many(self):
        path = '/images?status=queued'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request, filters={'status': 'queued'})
        self.assertEqual(1, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID3])
        self.assertEqual(expected, actual)

    def test_index_with_nonexistent_name_filter(self):
        request = unit_test_utils.get_fake_request('/images?name=%s' % 'blah')
        images = self.controller.index(request,
                                       filters={'name': 'blah'})['images']
        self.assertEqual(0, len(images))

    def test_index_with_non_default_is_public_filter(self):
        private_uuid = str(uuid.uuid4())
        new_image = _db_fixture(private_uuid,
                                visibility='private',
                                owner=TENANT3)
        self.db.image_create(None, new_image)

        path = '/images?visibility=private'
        request = unit_test_utils.get_fake_request(path, is_admin=True)
        output = self.controller.index(request,
                                       filters={'visibility': 'private'})
        self.assertEqual(1, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([private_uuid])
        self.assertEqual(expected, actual)

        path = '/images?visibility=shared'
        request = unit_test_utils.get_fake_request(path, is_admin=True)
        output = self.controller.index(request,
                                       filters={'visibility': 'shared'})
        self.assertEqual(1, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID4])
        self.assertEqual(expected, actual)

    def test_index_with_many_filters(self):
        url = '/images?status=queued&name=3'
        request = unit_test_utils.get_fake_request(url)
        output = self.controller.index(request,
                                       filters={
                                           'status': 'queued',
                                           'name': '3',
                                       })
        self.assertEqual(1, len(output['images']))
        actual = set([image.image_id for image in output['images']])
        expected = set([UUID3])
        self.assertEqual(expected, actual)

    def test_index_with_marker(self):
        self.config(limit_param_default=1, api_limit_max=3)
        path = '/images'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request, marker=UUID3)
        actual = set([image.image_id for image in output['images']])
        self.assertEqual(1, len(actual))
        self.assertIn(UUID2, actual)

    def test_index_with_limit(self):
        path = '/images'
        limit = 2
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request, limit=limit)
        actual = set([image.image_id for image in output['images']])
        self.assertEqual(limit, len(actual))
        self.assertIn(UUID3, actual)
        self.assertIn(UUID2, actual)

    def test_index_greater_than_limit_max(self):
        self.config(limit_param_default=1, api_limit_max=3)
        path = '/images'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request, limit=4)
        actual = set([image.image_id for image in output['images']])
        self.assertEqual(3, len(actual))
        self.assertNotIn(output['next_marker'], output)

    def test_index_default_limit(self):
        self.config(limit_param_default=1, api_limit_max=3)
        path = '/images'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request)
        actual = set([image.image_id for image in output['images']])
        self.assertEqual(1, len(actual))

    def test_index_with_sort_dir(self):
        path = '/images'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request, sort_dir=['asc'], limit=3)
        actual = [image.image_id for image in output['images']]
        self.assertEqual(3, len(actual))
        self.assertEqual(UUID1, actual[0])
        self.assertEqual(UUID2, actual[1])
        self.assertEqual(UUID3, actual[2])

    def test_index_with_sort_key(self):
        path = '/images'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request, sort_key=['created_at'],
                                       limit=3)
        actual = [image.image_id for image in output['images']]
        self.assertEqual(3, len(actual))
        self.assertEqual(UUID3, actual[0])
        self.assertEqual(UUID2, actual[1])
        self.assertEqual(UUID1, actual[2])

    def test_index_with_multiple_sort_keys(self):
        path = '/images'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       sort_key=['created_at', 'name'],
                                       limit=3)
        actual = [image.image_id for image in output['images']]
        self.assertEqual(3, len(actual))
        self.assertEqual(UUID3, actual[0])
        self.assertEqual(UUID2, actual[1])
        self.assertEqual(UUID1, actual[2])

    def test_index_with_marker_not_found(self):
        fake_uuid = str(uuid.uuid4())
        path = '/images'
        request = unit_test_utils.get_fake_request(path)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.controller.index, request, marker=fake_uuid)

    def test_index_invalid_sort_key(self):
        path = '/images'
        request = unit_test_utils.get_fake_request(path)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.controller.index, request, sort_key=['foo'])

    def test_index_zero_images(self):
        self.db.reset()
        request = unit_test_utils.get_fake_request()
        output = self.controller.index(request)
        self.assertEqual([], output['images'])

    def test_index_with_tags(self):
        path = '/images?tag=64bit'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request, filters={'tags': ['64bit']})
        actual = [image.tags for image in output['images']]
        self.assertEqual(2, len(actual))
        self.assertIn('64bit', actual[0])
        self.assertIn('64bit', actual[1])

    def test_index_with_multi_tags(self):
        path = '/images?tag=power&tag=64bit'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       filters={'tags': ['power', '64bit']})
        actual = [image.tags for image in output['images']]
        self.assertEqual(1, len(actual))
        self.assertIn('64bit', actual[0])
        self.assertIn('power', actual[0])

    def test_index_with_multi_tags_and_nonexistent(self):
        path = '/images?tag=power&tag=fake'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       filters={'tags': ['power', 'fake']})
        actual = [image.tags for image in output['images']]
        self.assertEqual(0, len(actual))

    def test_index_with_tags_and_properties(self):
        path = '/images?tag=64bit&hypervisor_type=kvm'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       filters={'tags': ['64bit'],
                                                'hypervisor_type': 'kvm'})
        tags = [image.tags for image in output['images']]
        properties = [image.extra_properties for image in output['images']]
        self.assertEqual(len(tags), len(properties))
        self.assertIn('64bit', tags[0])
        self.assertEqual('kvm', properties[0]['hypervisor_type'])

    def test_index_with_multiple_properties(self):
        path = '/images?foo=bar&hypervisor_type=kvm'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       filters={'foo': 'bar',
                                                'hypervisor_type': 'kvm'})
        properties = [image.extra_properties for image in output['images']]
        self.assertEqual('kvm', properties[0]['hypervisor_type'])
        self.assertEqual('bar', properties[0]['foo'])

    def test_index_with_core_and_extra_property(self):
        path = '/images?disk_format=raw&foo=bar'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       filters={'foo': 'bar',
                                                'disk_format': 'raw'})
        properties = [image.extra_properties for image in output['images']]
        self.assertEqual(1, len(output['images']))
        self.assertEqual('raw', output['images'][0].disk_format)
        self.assertEqual('bar', properties[0]['foo'])

    def test_index_with_nonexistent_properties(self):
        path = '/images?abc=xyz&pudding=banana'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       filters={'abc': 'xyz',
                                                'pudding': 'banana'})
        self.assertEqual(0, len(output['images']))

    def test_index_with_non_existent_tags(self):
        path = '/images?tag=fake'
        request = unit_test_utils.get_fake_request(path)
        output = self.controller.index(request,
                                       filters={'tags': ['fake']})
        actual = [image.tags for image in output['images']]
        self.assertEqual(0, len(actual))

    def test_show(self):
        request = unit_test_utils.get_fake_request()
        output = self.controller.show(request, image_id=UUID2)
        self.assertEqual(UUID2, output.image_id)
        self.assertEqual('2', output.name)

    def test_show_deleted_properties(self):
        """Ensure that the api filters out deleted image properties."""

        # get the image properties into the odd state
        image = {
            'id': str(uuid.uuid4()),
            'status': 'active',
            'properties': {'poo': 'bear'},
        }
        self.db.image_create(None, image)
        self.db.image_update(None, image['id'],
                             {'properties': {'yin': 'yang'}},
                             purge_props=True)

        request = unit_test_utils.get_fake_request()
        output = self.controller.show(request, image['id'])
        self.assertEqual('yang', output.extra_properties['yin'])

    def test_show_non_existent(self):
        request = unit_test_utils.get_fake_request()
        image_id = str(uuid.uuid4())
        self.assertRaises(webob.exc.HTTPNotFound,
                          self.controller.show, request, image_id)

    def test_show_deleted_image_admin(self):
        request = unit_test_utils.get_fake_request(is_admin=True)
        self.controller.delete(request, UUID1)
        self.assertRaises(webob.exc.HTTPNotFound,
                          self.controller.show, request, UUID1)

    def test_show_not_allowed(self):
        request = unit_test_utils.get_fake_request()
        self.assertEqual(TENANT1, request.context.project_id)
        self.assertRaises(webob.exc.HTTPNotFound,
                          self.controller.show, request, UUID4)

    def test_show_not_allowed_by_policy(self):
        # Use admin so that we get past the check buried in the DB and
        # only hit the policy check we are mocking.
        request = unit_test_utils.get_fake_request(is_admin=True)
        with mock.patch.object(self.controller.policy, 'enforce') as mock_enf:
            mock_enf.side_effect = webob.exc.HTTPForbidden()
            # Make sure we get NotFound instead of Forbidden
            exc = self.assertRaises(webob.exc.HTTPNotFound,
                                    self.controller.show, request, UUID4)
        # Make sure we did not leak details of the original Forbidden
        # error into the NotFound returned to the client.
        self.assertEqual('The resource could not be found.', str(exc))

    def test_get_task_info(self):
        request = unit_test_utils.get_fake_request()
        output = self.controller.get_task_info(request, image_id=UUID1)
        # NOTE Here we have only tasks associated with the image and not
        # other task which has not stored image_id, user_id and
        # request_id in tasks database table.
        self.assertEqual(2, len(output['tasks']))
        for task in output['tasks']:
            self.assertEqual(UUID1, task['image_id'])
            self.assertEqual('fake-user-id', task['user_id'])
            self.assertEqual('fake-request-id', task['request_id'])

    def test_get_task_info_no_tasks(self):
        request = unit_test_utils.get_fake_request()
        output = self.controller.get_task_info(request, image_id=UUID2)
        self.assertEqual([], output['tasks'])

    def test_get_task_info_raises_not_found(self):
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPNotFound,
                          self.controller.get_task_info, request,
                          'fake-image-id')

    def test_image_import_raises_conflict_if_container_format_is_none(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(container_format=None)
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'glance-direct'}})

    def test_image_import_raises_conflict_if_disk_format_is_none(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(disk_format=None)
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'glance-direct'}})

    def test_image_import_raises_conflict(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='queued')
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'glance-direct'}})

    def test_image_import_raises_conflict_for_web_download(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage()
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'web-download'}})

    def test_image_import_raises_conflict_for_invalid_status_change(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage()
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'glance-direct'}})

    @mock.patch('glance.db.simple.api.image_set_property_atomic')
    @mock.patch('glance.api.common.get_thread_pool')
    def test_image_import_raises_bad_request(self, mock_gpt, mock_spa):
        request = unit_test_utils.get_fake_request()
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='uploading')
            # NOTE(abhishekk): Due to
            # https://bugs.launchpad.net/glance/+bug/1712463 taskflow is not
            # executing. Once it is fixed instead of mocking spawn method
            # we should mock execute method of _ImportToStore task.
            mock_gpt.return_value.spawn.side_effect = ValueError
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'glance-direct'}})
            self.assertTrue(mock_gpt.return_value.spawn.called)

    def test_image_import_invalid_uri_filtering(self):
        request = unit_test_utils.get_fake_request()
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='queued')
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'web-download',
                                          'uri': 'fake_uri'}})

    def test_image_import_raises_bad_request_for_glance_download_missing_input(
            self):
        request = unit_test_utils.get_fake_request()
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='queued')
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'glance-download'}})

    def test_image_import_raise_bad_request_wrong_id_for_glance_download(
            self):
        request = unit_test_utils.get_fake_request()
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='queued')
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'glance-download',
                                          'glance_image_id': 'fake_id',
                                          'glance_region': 'REGION4'}})

    @mock.patch.object(glance.domain.TaskFactory, 'new_task')
    @mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
    def test_image_import_add_default_service_endpoint_for_glance_download(
            self, mock_get, mock_nt):
        request = unit_test_utils.get_fake_request()
        mock_get.return_value = FakeImage(status='queued')
        body = {'method': {'name': 'glance-download',
                           'glance_image_id': UUID4,
                           'glance_region': 'REGION2'}}
        self.controller.import_image(request, UUID4, body)
        expected_req = {'method': {'name': 'glance-download',
                                   'glance_image_id': UUID4,
                                   'glance_region': 'REGION2',
                                   'glance_service_interface': 'public'}}
        self.assertEqual(expected_req,
                         mock_nt.call_args.kwargs['task_input']['import_req'])

    @mock.patch('glance.context.get_ksa_client')
    def test_image_import_proxies(self, mock_client):
        # Make sure that we proxy to the remote side when we need to
        self.config(
            worker_self_reference_url='http://glance-worker2.openstack.org')
        request = unit_test_utils.get_fake_request(
            '/v2/images/%s/import' % UUID4)
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='uploading')
            mock_get.return_value.extra_properties['os_glance_stage_host'] = (
                'https://glance-worker1.openstack.org')
            remote_hdrs = {'x-openstack-request-id': 'remote-req'}
            mock_resp = mock.MagicMock(location='/target',
                                       status_code=202,
                                       reason='Thanks',
                                       headers=remote_hdrs)
            mock_client.return_value.post.return_value = mock_resp
            r = self.controller.import_image(
                request, UUID4,
                {'method': {'name': 'glance-direct'}})

            # Make sure we returned the ID like expected normally
            self.assertEqual(UUID4, r)

            # Make sure we called the expected remote URL and passed
            # the body.
            mock_client.return_value.post.assert_called_once_with(
                ('https://glance-worker1.openstack.org'
                 '/v2/images/%s/import') % UUID4,
                json={'method': {'name': 'glance-direct'}},
                timeout=60)

            # Make sure the remote request-id is returned to us
            self.assertEqual('remote-req', request.context.request_id)

    @mock.patch('glance.context.get_ksa_client')
    def test_image_delete_proxies(self, mock_client):
        # Make sure that we proxy to the remote side when we need to
        self.config(
            worker_self_reference_url='http://glance-worker2.openstack.org')
        request = unit_test_utils.get_fake_request(
            '/v2/images/%s' % UUID4, method='DELETE')
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='uploading')
            mock_get.return_value.extra_properties['os_glance_stage_host'] = (
                'https://glance-worker1.openstack.org')
            remote_hdrs = {'x-openstack-request-id': 'remote-req'}
            mock_resp = mock.MagicMock(location='/target',
                                       status_code=202,
                                       reason='Thanks',
                                       headers=remote_hdrs)
            mock_client.return_value.delete.return_value = mock_resp
            self.controller.delete(request, UUID4)

            # Make sure we called the expected remote URL and passed
            # the body.
            mock_client.return_value.delete.assert_called_once_with(
                ('https://glance-worker1.openstack.org'
                 '/v2/images/%s') % UUID4,
                json=None, timeout=60)

    @mock.patch('glance.context.get_ksa_client')
    def test_image_import_proxies_error(self, mock_client):
        # Make sure that errors from the remote worker are proxied to our
        # client with the proper code and message
        self.config(
            worker_self_reference_url='http://glance-worker2.openstack.org')
        request = unit_test_utils.get_fake_request(
            '/v2/images/%s/import' % UUID4)
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='uploading')
            mock_get.return_value.extra_properties['os_glance_stage_host'] = (
                'https://glance-worker1.openstack.org')
            mock_resp = mock.MagicMock(location='/target',
                                       status_code=456,
                                       reason='No thanks')
            mock_client.return_value.post.return_value = mock_resp
            exc = self.assertRaises(webob.exc.HTTPError,
                                    self.controller.import_image,
                                    request, UUID4,
                                    {'method': {'name': 'glance-direct'}})
            self.assertEqual('456 No thanks', exc.status)
            mock_client.return_value.post.assert_called_once_with(
                ('https://glance-worker1.openstack.org'
                 '/v2/images/%s/import') % UUID4,
                json={'method': {'name': 'glance-direct'}},
                timeout=60)

    @mock.patch('glance.context.get_ksa_client')
    def test_image_delete_proxies_error(self, mock_client):
        # Make sure that errors from the remote worker are proxied to our
        # client with the proper code and message
        self.config(
            worker_self_reference_url='http://glance-worker2.openstack.org')
        request = unit_test_utils.get_fake_request(
            '/v2/images/%s' % UUID4, method='DELETE')
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='uploading')
            mock_get.return_value.extra_properties['os_glance_stage_host'] = (
                'https://glance-worker1.openstack.org')
            remote_hdrs = {'x-openstack-request-id': 'remote-req'}
            mock_resp = mock.MagicMock(location='/target',
                                       status_code=456,
                                       reason='No thanks',
                                       headers=remote_hdrs)
            mock_client.return_value.delete.return_value = mock_resp
            exc = self.assertRaises(webob.exc.HTTPError,
                                    self.controller.delete, request, UUID4)
            self.assertEqual('456 No thanks', exc.status)

            # Make sure we called the expected remote URL and passed
            # the body.
            mock_client.return_value.delete.assert_called_once_with(
                ('https://glance-worker1.openstack.org'
                 '/v2/images/%s') % UUID4,
                json=None, timeout=60)

    @mock.patch('glance.context.get_ksa_client')
    @mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
    @mock.patch.object(glance.notifier.ImageRepoProxy, 'remove')
    def test_image_delete_deletes_locally_on_error(self, mock_remove, mock_get,
                                                   mock_client):
        # Make sure that if the proxy delete fails due to a connection error
        # that we continue with the delete ourselves.
        self.config(
            worker_self_reference_url='http://glance-worker2.openstack.org')
        request = unit_test_utils.get_fake_request(
            '/v2/images/%s' % UUID4, method='DELETE')

        image = FakeImage(status='uploading')
        mock_get.return_value = image
        image.extra_properties['os_glance_stage_host'] = (
            'https://glance-worker1.openstack.org')
        image.delete = mock.MagicMock()

        mock_client.return_value.delete.side_effect = (
            requests.exceptions.ConnectTimeout)

        self.controller.delete(request, UUID4)

        # Make sure we called delete on our image
        mock_get.return_value.delete.assert_called_once_with()
        mock_remove.assert_called_once_with(image)

        # Make sure we called the expected remote URL and passed
        # the body.
        mock_client.return_value.delete.assert_called_once_with(
            ('https://glance-worker1.openstack.org'
             '/v2/images/%s') % UUID4,
            json=None, timeout=60)

    @mock.patch('glance.context.get_ksa_client')
    def test_image_import_no_proxy_non_direct(self, mock_client):
        # Make sure that we won't take the proxy path for import methods
        # other than glance-direct
        self.config(
            worker_self_reference_url='http://glance-worker2.openstack.org')
        request = unit_test_utils.get_fake_request(
            '/v2/images/%s/import' % UUID4)
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='queued')
            mock_get.return_value.extra_properties['os_glance_stage_host'] = (
                'https://glance-worker1.openstack.org')
            # This will fail validation after the point at which we would
            # have proxied to the remote side, just to avoid task setup.
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.controller.import_image,
                              request, UUID4,
                              {'method': {'name': 'web-download',
                                          'url': 'not-a-url'}})
            # Make sure we did not try to proxy this web-download request
            mock_client.return_value.post.assert_not_called()

    def test_create(self):
        request = unit_test_utils.get_fake_request()
        image = {'name': 'image-1'}
        output = self.controller.create(request, image=image,
                                        extra_properties={},
                                        tags=[])
        self.assertEqual('image-1', output.name)
        self.assertEqual({}, output.extra_properties)
        self.assertEqual(set([]), output.tags)
        self.assertEqual('shared', output.visibility)
        output_logs = self.notifier.get_logs()
        self.assertEqual(1, len(output_logs))
        output_log = output_logs[0]
        self.assertEqual('INFO', output_log['notification_type'])
        self.assertEqual('image.create', output_log['event_type'])
        self.assertEqual('image-1', output_log['payload']['name'])

    def test_create_disabled_notification(self):
        self.config(disabled_notifications=["image.create"])
        request = unit_test_utils.get_fake_request()
        image = {'name': 'image-1'}
        output = self.controller.create(request, image=image,
                                        extra_properties={},
                                        tags=[])
        self.assertEqual('image-1', output.name)
        self.assertEqual({}, output.extra_properties)
        self.assertEqual(set([]), output.tags)
        self.assertEqual('shared', output.visibility)
        output_logs = self.notifier.get_logs()
        self.assertEqual(0, len(output_logs))

    def test_create_with_properties(self):
        request = unit_test_utils.get_fake_request()
        image_properties = {'foo': 'bar'}
        image = {'name': 'image-1'}
        output = self.controller.create(request, image=image,
                                        extra_properties=image_properties,
                                        tags=[])
        self.assertEqual('image-1', output.name)
        self.assertEqual(image_properties, output.extra_properties)
        self.assertEqual(set([]), output.tags)
        self.assertEqual('shared', output.visibility)
        output_logs = self.notifier.get_logs()
        self.assertEqual(1, len(output_logs))
        output_log = output_logs[0]
        self.assertEqual('INFO', output_log['notification_type'])
        self.assertEqual('image.create', output_log['event_type'])
        self.assertEqual('image-1', output_log['payload']['name'])

    def test_create_with_too_many_properties(self):
        self.config(image_property_quota=1)
        request = unit_test_utils.get_fake_request()
        image_properties = {'foo': 'bar', 'foo2': 'bar'}
        image = {'name': 'image-1'}
        self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
                          self.controller.create, request,
                          image=image,
                          extra_properties=image_properties,
                          tags=[])

    def test_create_with_bad_min_disk_size(self):
        request = unit_test_utils.get_fake_request()
        image = {'min_disk': -42, 'name': 'image-1'}
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.controller.create, request,
                          image=image,
                          extra_properties={},
                          tags=[])

    def test_create_with_bad_min_ram_size(self):
        request = unit_test_utils.get_fake_request()
        image = {'min_ram': -42, 'name': 'image-1'}
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.controller.create, request,
                          image=image,
                          extra_properties={},
                          tags=[])

    def test_create_public_image_as_admin(self):
        request = unit_test_utils.get_fake_request()
        image = {'name': 'image-1', 'visibility': 'public'}
        output = self.controller.create(request, image=image,
                                        extra_properties={}, tags=[])
        self.assertEqual('public', output.visibility)
        output_logs = self.notifier.get_logs()
        self.assertEqual(1, len(output_logs))
        output_log = output_logs[0]
        self.assertEqual('INFO', output_log['notification_type'])
        self.assertEqual('image.create', output_log['event_type'])
        self.assertEqual(output.image_id, output_log['payload']['id'])

    def test_create_dup_id(self):
        request = unit_test_utils.get_fake_request()
        image = {'image_id': UUID4}

        self.assertRaises(webob.exc.HTTPConflict,
                          self.controller.create,
                          request,
                          image=image,
                          extra_properties={},
                          tags=[])

    def test_create_duplicate_tags(self):
        request = unit_test_utils.get_fake_request()
        tags = ['ping', 'ping']
        output = self.controller.create(request, image={},
                                        extra_properties={}, tags=tags)
        self.assertEqual(set(['ping']), output.tags)
        output_logs = self.notifier.get_logs()
        self.assertEqual(1, len(output_logs))
        output_log = output_logs[0]
        self.assertEqual('INFO', output_log['notification_type'])
        self.assertEqual('image.create', output_log['event_type'])
        self.assertEqual(output.image_id, output_log['payload']['id'])

    def test_create_with_too_many_tags(self):
        self.config(image_tag_quota=1)
        request = unit_test_utils.get_fake_request()
        tags = ['ping', 'pong']
        self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
                          self.controller.create,
                          request, image={}, extra_properties={},
                          tags=tags)

    def test_create_with_owner_non_admin(self):
        enforcer = unit_test_utils.enforcer_from_rules({
            "add_image": "role:member,reader",
        })
        request = unit_test_utils.get_fake_request()
        request.context.is_admin = False
        image = {'owner': '12345'}
        self.controller.policy = enforcer
        self.assertRaises(webob.exc.HTTPForbidden,
                          self.controller.create,
                          request, image=image, extra_properties={},
                          tags=[])

        enforcer = unit_test_utils.enforcer_from_rules({
            "add_image": "'{0}':%(owner)s".format(TENANT1),
        })
        request = unit_test_utils.get_fake_request()
        request.context.is_admin = False
        image = {'owner': TENANT1}
        self.controller.policy = enforcer
        output = self.controller.create(request, image=image,
                                        extra_properties={}, tags=[])
        self.assertEqual(TENANT1, output.owner)

    def test_create_with_owner_admin(self):
        request = unit_test_utils.get_fake_request()
        request.context.is_admin = True
        image = {'owner': '12345'}
        output = self.controller.create(request, image=image,
                                        extra_properties={}, tags=[])
        self.assertEqual('12345', output.owner)

    def test_create_with_duplicate_location(self):
        request = unit_test_utils.get_fake_request()
        location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        image = {'name': 'image-1', 'locations': [location, location]}
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
                          request, image=image, extra_properties={},
                          tags=[])

    def test_create_unexpected_property(self):
        request = unit_test_utils.get_fake_request()
        image_properties = {'unexpected': 'unexpected'}
        image = {'name': 'image-1'}
        with mock.patch.object(domain.ImageFactory, 'new_image',
                               side_effect=TypeError):
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.controller.create, request, image=image,
                              extra_properties=image_properties, tags=[])

    def test_create_reserved_property(self):
        request = unit_test_utils.get_fake_request()
        image_properties = {'reserved': 'reserved'}
        image = {'name': 'image-1'}
        with mock.patch.object(domain.ImageFactory, 'new_image',
                               side_effect=exception.ReservedProperty(
                                   property='reserved')):
            self.assertRaises(webob.exc.HTTPForbidden,
                              self.controller.create, request, image=image,
                              extra_properties=image_properties, tags=[])

    def test_create_readonly_property(self):
        request = unit_test_utils.get_fake_request()
        image_properties = {'readonly': 'readonly'}
        image = {'name': 'image-1'}
        with mock.patch.object(domain.ImageFactory, 'new_image',
                               side_effect=exception.ReadonlyProperty(
                                   property='readonly')):
            self.assertRaises(webob.exc.HTTPForbidden,
                              self.controller.create, request, image=image,
                              extra_properties=image_properties, tags=[])

    def test_update_no_changes(self):
        request = unit_test_utils.get_fake_request()
        output = self.controller.update(request, UUID1, changes=[])
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(output.created_at, output.updated_at)
        self.assertEqual(2, len(output.tags))
        self.assertIn('ping', output.tags)
        self.assertIn('pong', output.tags)
        output_logs = self.notifier.get_logs()
        # NOTE(markwash): don't send a notification if nothing is updated
        self.assertEqual(0, len(output_logs))

    def test_update_queued_image_with_hidden(self):
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['os_hidden'], 'value': 'true'}]
        image = self.controller.update(request, UUID1, changes=changes)
        self.assertTrue(image.os_hidden)

    def test_update_with_bad_min_disk(self):
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['min_disk'], 'value': -42}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes=changes)

    def test_update_with_bad_min_ram(self):
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['min_ram'], 'value': -42}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes=changes)

    def test_update_image_doesnt_exist(self):
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
                          request, str(uuid.uuid4()), changes=[])

    def test_update_deleted_image_admin(self):
        request = unit_test_utils.get_fake_request(is_admin=True)
        self.controller.delete(request, UUID1)
        self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
                          request, UUID1, changes=[])

    def test_update_with_too_many_properties(self):
        self.config(show_multiple_locations=True)
        self.config(user_storage_quota='1')
        new_location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
                          self.controller.update,
                          request, UUID1, changes=changes)

    def test_update_replace_base_attribute(self):
        self.db.image_update(None, UUID1, {'properties': {'foo': 'bar'}})
        request = unit_test_utils.get_fake_request()
        request.context.is_admin = True
        changes = [{'op': 'replace', 'path': ['name'], 'value': 'fedora'},
                   {'op': 'replace', 'path': ['owner'], 'value': TENANT3}]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual('fedora', output.name)
        self.assertEqual(TENANT3, output.owner)
        self.assertEqual({'foo': 'bar'}, output.extra_properties)
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_replace_onwer_non_admin(self):
        request = unit_test_utils.get_fake_request()
        request.context.is_admin = False
        changes = [{'op': 'replace', 'path': ['owner'], 'value': TENANT3}]
        self.assertRaises(webob.exc.HTTPForbidden,
                          self.controller.update, request, UUID1, changes)

    def test_update_replace_tags(self):
        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'replace', 'path': ['tags'], 'value': ['king', 'kong']},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(2, len(output.tags))
        self.assertIn('king', output.tags)
        self.assertIn('kong', output.tags)
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_replace_property(self):
        request = unit_test_utils.get_fake_request()
        properties = {'foo': 'bar', 'snitch': 'golden'}
        self.db.image_update(None, UUID1, {'properties': properties})

        output = self.controller.show(request, UUID1)
        self.assertEqual('bar', output.extra_properties['foo'])
        self.assertEqual('golden', output.extra_properties['snitch'])

        changes = [
            {'op': 'replace', 'path': ['foo'], 'value': 'baz'},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual('baz', output.extra_properties['foo'])
        self.assertEqual('golden', output.extra_properties['snitch'])
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_add_too_many_properties(self):
        self.config(image_property_quota=1)
        request = unit_test_utils.get_fake_request()

        changes = [
            {'op': 'add', 'path': ['foo'], 'value': 'baz'},
            {'op': 'add', 'path': ['snitch'], 'value': 'golden'},
        ]
        self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
                          self.controller.update, request,
                          UUID1, changes)

    def test_update_reserved_not_counted_in_quota(self):
        self.config(image_property_quota=1)
        request = unit_test_utils.get_fake_request()
        self.db.image_update(None, UUID1, {'properties': {
            'os_glance_foo': '123', 'os_glance_bar': 456}})

        changes = [
            {'op': 'add', 'path': ['foo'], 'value': 'baz'},
        ]
        # Should succeed
        self.controller.update(request, UUID1, changes)

        changes = [
            {'op': 'add', 'path': ['snitch'], 'value': 'golden'},
        ]
        # Should fail, over quota
        self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
                          self.controller.update, request,
                          UUID1, changes)

    def test_update_add_and_remove_too_many_properties(self):
        request = unit_test_utils.get_fake_request()

        changes = [
            {'op': 'add', 'path': ['foo'], 'value': 'baz'},
            {'op': 'add', 'path': ['snitch'], 'value': 'golden'},
        ]
        self.controller.update(request, UUID1, changes)
        self.config(image_property_quota=1)

        # We must remove two properties to avoid being
        # over the limit of 1 property
        changes = [
            {'op': 'remove', 'path': ['foo']},
            {'op': 'add', 'path': ['fizz'], 'value': 'buzz'},
        ]
        self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
                          self.controller.update, request,
                          UUID1, changes)

    def test_update_add_unlimited_properties(self):
        self.config(image_property_quota=-1)
        request = unit_test_utils.get_fake_request()
        output = self.controller.show(request, UUID1)

        changes = [{'op': 'add',
                    'path': ['foo'],
                    'value': 'bar'}]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_format_properties(self):
        statuses_for_immutability = ['active', 'saving', 'killed']
        request = unit_test_utils.get_fake_request(roles=['admin'],
                                                   is_admin=True)
        for status in statuses_for_immutability:
            image = {
                'id': str(uuid.uuid4()),
                'status': status,
                'disk_format': 'ari',
                'container_format': 'ari',
            }
            self.db.image_create(None, image)
            changes = [
                {'op': 'replace', 'path': ['disk_format'], 'value': 'ami'},
            ]
            self.assertRaises(webob.exc.HTTPForbidden,
                              self.controller.update,
                              request, image['id'], changes)
            changes = [
                {'op': 'replace',
                 'path': ['container_format'],
                 'value': 'ami'},
            ]
            self.assertRaises(webob.exc.HTTPForbidden,
                              self.controller.update,
                              request, image['id'], changes)
        self.db.image_update(None, image['id'], {'status': 'queued'})

        changes = [
            {'op': 'replace', 'path': ['disk_format'], 'value': 'raw'},
            {'op': 'replace', 'path': ['container_format'], 'value': 'bare'},
        ]
        resp = self.controller.update(request, image['id'], changes)
        self.assertEqual('raw', resp.disk_format)
        self.assertEqual('bare', resp.container_format)

    def test_update_remove_property_while_over_limit(self):
        """Ensure that image properties can be removed.

        Image properties should be able to be removed as long as the image has
        fewer than the limited number of image properties after the
        transaction.

        """
        request = unit_test_utils.get_fake_request()

        changes = [
            {'op': 'add', 'path': ['foo'], 'value': 'baz'},
            {'op': 'add', 'path': ['snitch'], 'value': 'golden'},
            {'op': 'add', 'path': ['fizz'], 'value': 'buzz'},
        ]
        self.controller.update(request, UUID1, changes)
        self.config(image_property_quota=1)

        # We must remove two properties to avoid being
        # over the limit of 1 property
        changes = [
            {'op': 'remove', 'path': ['foo']},
            {'op': 'remove', 'path': ['snitch']},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(1, len(output.extra_properties))
        self.assertEqual('buzz', output.extra_properties['fizz'])
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_add_and_remove_property_under_limit(self):
        """Ensure that image properties can be removed.

        Image properties should be able to be added and removed simultaneously
        as long as the image has fewer than the limited number of image
        properties after the transaction.

        """
        request = unit_test_utils.get_fake_request()

        changes = [
            {'op': 'add', 'path': ['foo'], 'value': 'baz'},
            {'op': 'add', 'path': ['snitch'], 'value': 'golden'},
        ]
        self.controller.update(request, UUID1, changes)
        self.config(image_property_quota=1)

        # We must remove two properties to avoid being
        # over the limit of 1 property
        changes = [
            {'op': 'remove', 'path': ['foo']},
            {'op': 'remove', 'path': ['snitch']},
            {'op': 'add', 'path': ['fizz'], 'value': 'buzz'},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(1, len(output.extra_properties))
        self.assertEqual('buzz', output.extra_properties['fizz'])
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_replace_missing_property(self):
        request = unit_test_utils.get_fake_request()

        changes = [
            {'op': 'replace', 'path': 'foo', 'value': 'baz'},
        ]
        self.assertRaises(webob.exc.HTTPConflict,
                          self.controller.update, request, UUID1, changes)

    def test_prop_protection_with_create_and_permitted_role(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties={},
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member'])
        changes = [
            {'op': 'add', 'path': ['x_owner_foo'], 'value': 'bar'},
        ]
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertEqual('bar', output.extra_properties['x_owner_foo'])

    def test_prop_protection_with_update_and_permitted_policy(self):
        self.set_property_protections(use_policies=True)
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        request = unit_test_utils.get_fake_request(roles=['spl_role', 'admin'])
        image = {'name': 'image-1'}
        extra_props = {'spl_creator_policy': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        self.assertEqual('bar',
                         created_image.extra_properties['spl_creator_policy'])

        another_request = unit_test_utils.get_fake_request(roles=['spl_role'])
        changes = [
            {'op': 'replace', 'path': ['spl_creator_policy'], 'value': 'par'},
        ]
        enforcer = unit_test_utils.enforcer_from_rules({
            "get_image": "",
            "modify_image": "role:spl_role"
        })

        self.controller.policy = enforcer
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          another_request, created_image.image_id, changes)
        enforcer = unit_test_utils.enforcer_from_rules({
            "get_image": "",
            "modify_image": "role:admin"
        })
        self.controller.policy = enforcer
        another_request = unit_test_utils.get_fake_request(roles=['admin'])
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertEqual('par',
                         output.extra_properties['spl_creator_policy'])

    def test_prop_protection_with_create_with_patch_and_policy(self):
        self.set_property_protections(use_policies=True)
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        request = unit_test_utils.get_fake_request(roles=['spl_role', 'admin'])
        image = {'name': 'image-1'}
        extra_props = {'spl_default_policy': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['fake_role'])
        changes = [
            {'op': 'add', 'path': ['spl_creator_policy'], 'value': 'bar'},
        ]
        enforcer = unit_test_utils.enforcer_from_rules({
            "get_image": "",
            "modify_image": "role:fake_role"
        })

        self.controller.policy = enforcer
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          another_request, created_image.image_id, changes)
        enforcer = unit_test_utils.enforcer_from_rules({
            "get_image": "",
            "modify_image": "role:member"
        })
        self.controller.policy = enforcer
        another_request = unit_test_utils.get_fake_request(roles=['member',
                                                                  'spl_role'])
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertEqual('bar',
                         output.extra_properties['spl_creator_policy'])

    def test_prop_protection_with_create_and_unpermitted_role(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties={},
                                               tags=[])
        roles = ['fake_member']
        enforcer = unit_test_utils.enforcer_from_rules({
            "get_image": "",
            "modify_image": "role:fake_member"
        })
        self.controller.policy = enforcer
        another_request = unit_test_utils.get_fake_request(roles=roles)
        changes = [
            {'op': 'add', 'path': ['x_owner_foo'], 'value': 'bar'},
        ]
        self.assertRaises(webob.exc.HTTPForbidden,
                          self.controller.update, another_request,
                          created_image.image_id, changes)

    def test_prop_protection_with_show_and_permitted_role(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_owner_foo': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['reader',
                                                                  'member'])
        output = self.controller.show(another_request, created_image.image_id)
        self.assertEqual('bar', output.extra_properties['x_owner_foo'])

    def test_prop_protection_with_show_and_unpermitted_role(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['member'])
        image = {'name': 'image-1'}
        extra_props = {'x_owner_foo': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['reader',
                                                                  'fake_role'])
        output = self.controller.show(another_request, created_image.image_id)
        self.assertRaises(KeyError, output.extra_properties.__getitem__,
                          'x_owner_foo')

    def test_prop_protection_with_update_and_permitted_role(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_owner_foo': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member'])
        changes = [
            {'op': 'replace', 'path': ['x_owner_foo'], 'value': 'baz'},
        ]
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertEqual('baz', output.extra_properties['x_owner_foo'])

    def test_prop_protection_with_update_and_unpermitted_role(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_owner_foo': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        enforcer = unit_test_utils.enforcer_from_rules({
            "get_image": "",
            "modify_image": "role:fake_role"
        })
        self.controller.policy = enforcer
        another_request = unit_test_utils.get_fake_request(roles=['fake_role'])
        changes = [
            {'op': 'replace', 'path': ['x_owner_foo'], 'value': 'baz'},
        ]
        self.assertRaises(webob.exc.HTTPConflict, self.controller.update,
                          another_request, created_image.image_id, changes)

    def test_prop_protection_with_delete_and_permitted_role(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_owner_foo': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member'])
        changes = [
            {'op': 'remove', 'path': ['x_owner_foo']}
        ]
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertRaises(KeyError, output.extra_properties.__getitem__,
                          'x_owner_foo')

    def test_prop_protection_with_delete_and_unpermitted_role(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_owner_foo': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        enforcer = unit_test_utils.enforcer_from_rules({
            "get_image": "",
            "modify_image": "role:fake_role"
        })
        self.controller.policy = enforcer
        another_request = unit_test_utils.get_fake_request(roles=['fake_role'])
        changes = [
            {'op': 'remove', 'path': ['x_owner_foo']}
        ]
        self.assertRaises(webob.exc.HTTPConflict, self.controller.update,
                          another_request, created_image.image_id, changes)

    def test_create_protected_prop_case_insensitive(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties={},
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member'])
        changes = [
            {'op': 'add', 'path': ['x_case_insensitive'], 'value': '1'},
        ]
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertEqual('1', output.extra_properties['x_case_insensitive'])

    def test_read_protected_prop_case_insensitive(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_case_insensitive': '1'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['reader',
                                                                  'member'])
        output = self.controller.show(another_request, created_image.image_id)
        self.assertEqual('1', output.extra_properties['x_case_insensitive'])

    def test_update_protected_prop_case_insensitive(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_case_insensitive': '1'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member'])
        changes = [
            {'op': 'replace', 'path': ['x_case_insensitive'], 'value': '2'},
        ]
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertEqual('2', output.extra_properties['x_case_insensitive'])

    def test_delete_protected_prop_case_insensitive(self):
        enforcer = glance.api.policy.Enforcer(
            suppress_deprecation_warnings=True)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                enforcer,
                                                                self.notifier,
                                                                self.store)
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_case_insensitive': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member'])
        changes = [
            {'op': 'remove', 'path': ['x_case_insensitive']}
        ]
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertRaises(KeyError, output.extra_properties.__getitem__,
                          'x_case_insensitive')

    def test_create_non_protected_prop(self):
        """Property marked with special char @ creatable by an unknown role"""
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_all_permitted_1': '1'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        self.assertEqual('1',
                         created_image.extra_properties['x_all_permitted_1'])
        another_request = unit_test_utils.get_fake_request(roles=['joe_soap'])
        extra_props = {'x_all_permitted_2': '2'}
        created_image = self.controller.create(another_request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        self.assertEqual('2',
                         created_image.extra_properties['x_all_permitted_2'])

    def test_read_non_protected_prop(self):
        """Property marked with special char @ readable by an unknown role"""
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_all_permitted': '1'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['joe_soap'])
        output = self.controller.show(another_request, created_image.image_id)
        self.assertEqual('1', output.extra_properties['x_all_permitted'])

    def test_update_non_protected_prop(self):
        """Property marked with special char @ updatable by an unknown role"""
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_all_permitted': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member',
                                                                  'joe_soap'])
        changes = [
            {'op': 'replace', 'path': ['x_all_permitted'], 'value': 'baz'},
        ]
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertEqual('baz', output.extra_properties['x_all_permitted'])

    def test_delete_non_protected_prop(self):
        """Property marked with special char @ deletable by an unknown role"""
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_all_permitted': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member'])
        changes = [
            {'op': 'remove', 'path': ['x_all_permitted']}
        ]
        output = self.controller.update(another_request,
                                        created_image.image_id, changes)
        self.assertRaises(KeyError, output.extra_properties.__getitem__,
                          'x_all_permitted')

    def test_create_locked_down_protected_prop(self):
        """Property marked with special char ! creatable by no one"""
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties={},
                                               tags=[])
        roles = ['fake_member']
        another_request = unit_test_utils.get_fake_request(roles=roles)
        changes = [
            {'op': 'add', 'path': ['x_none_permitted'], 'value': 'bar'},
        ]
        self.assertRaises(webob.exc.HTTPForbidden,
                          self.controller.update, another_request,
                          created_image.image_id, changes)

    def test_read_locked_down_protected_prop(self):
        """Property marked with special char ! readable by no one"""
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['member'])
        image = {'name': 'image-1'}
        extra_props = {'x_none_read': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['fake_role'])
        output = self.controller.show(another_request, created_image.image_id)
        self.assertRaises(KeyError, output.extra_properties.__getitem__,
                          'x_none_read')

    def test_update_locked_down_protected_prop(self):
        """Property marked with special char ! updatable by no one"""
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_none_update': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member',
                                                                  'fake_role'])
        changes = [
            {'op': 'replace', 'path': ['x_none_update'], 'value': 'baz'},
        ]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          another_request, created_image.image_id, changes)

    def test_delete_locked_down_protected_prop(self):
        """Property marked with special char ! deletable by no one"""
        self.set_property_protections()
        request = unit_test_utils.get_fake_request(roles=['admin'])
        image = {'name': 'image-1'}
        extra_props = {'x_none_delete': 'bar'}
        created_image = self.controller.create(request, image=image,
                                               extra_properties=extra_props,
                                               tags=[])
        another_request = unit_test_utils.get_fake_request(roles=['member',
                                                                  'fake_role'])
        changes = [
            {'op': 'remove', 'path': ['x_none_delete']}
        ]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          another_request, created_image.image_id, changes)

    def test_update_replace_locations_non_empty(self):
        self.config(show_multiple_locations=True)
        new_location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['locations'],
                    'value': [new_location]}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes)

    def test_update_replace_locations_metadata_update(self):
        self.config(show_multiple_locations=True)
        location = {'url': '%s/%s' % (BASE_URI, UUID1),
                    'metadata': {'a': 1}}
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['locations'],
                    'value': [location]}]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual({'a': 1}, output.locations[0]['metadata'])

    def test_locations_actions_with_locations_invisible(self):
        self.config(show_multiple_locations=False)
        new_location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['locations'],
                    'value': [new_location]}]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          request, UUID1, changes)

    def test_update_replace_locations_invalid(self):
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['locations'], 'value': []}]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          request, UUID1, changes)

    def test_update_add_property(self):
        request = unit_test_utils.get_fake_request()

        changes = [
            {'op': 'add', 'path': ['foo'], 'value': 'baz'},
            {'op': 'add', 'path': ['snitch'], 'value': 'golden'},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual('baz', output.extra_properties['foo'])
        self.assertEqual('golden', output.extra_properties['snitch'])
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_add_base_property_json_schema_version_4(self):
        request = unit_test_utils.get_fake_request()
        changes = [{
            'json_schema_version': 4, 'op': 'add',
            'path': ['name'], 'value': 'fedora'
        }]
        self.assertRaises(webob.exc.HTTPConflict, self.controller.update,
                          request, UUID1, changes)

    def test_update_add_extra_property_json_schema_version_4(self):
        self.db.image_update(None, UUID1, {'properties': {'foo': 'bar'}})
        request = unit_test_utils.get_fake_request()
        changes = [{
            'json_schema_version': 4, 'op': 'add',
            'path': ['foo'], 'value': 'baz'
        }]
        self.assertRaises(webob.exc.HTTPConflict, self.controller.update,
                          request, UUID1, changes)

    def test_update_add_base_property_json_schema_version_10(self):
        request = unit_test_utils.get_fake_request()
        changes = [{
            'json_schema_version': 10, 'op': 'add',
            'path': ['name'], 'value': 'fedora'
        }]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual('fedora', output.name)

    def test_update_add_extra_property_json_schema_version_10(self):
        self.db.image_update(None, UUID1, {'properties': {'foo': 'bar'}})
        request = unit_test_utils.get_fake_request()
        changes = [{
            'json_schema_version': 10, 'op': 'add',
            'path': ['foo'], 'value': 'baz'
        }]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual({'foo': 'baz'}, output.extra_properties)

    def test_update_add_property_already_present_json_schema_version_4(self):
        request = unit_test_utils.get_fake_request()
        properties = {'foo': 'bar'}
        self.db.image_update(None, UUID1, {'properties': properties})

        output = self.controller.show(request, UUID1)
        self.assertEqual('bar', output.extra_properties['foo'])

        changes = [
            {'json_schema_version': 4, 'op': 'add',
             'path': ['foo'], 'value': 'baz'},
        ]
        self.assertRaises(webob.exc.HTTPConflict,
                          self.controller.update, request, UUID1, changes)

    def test_update_add_property_already_present_json_schema_version_10(self):
        request = unit_test_utils.get_fake_request()
        properties = {'foo': 'bar'}
        self.db.image_update(None, UUID1, {'properties': properties})

        output = self.controller.show(request, UUID1)
        self.assertEqual('bar', output.extra_properties['foo'])

        changes = [
            {'json_schema_version': 10, 'op': 'add',
             'path': ['foo'], 'value': 'baz'},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual({'foo': 'baz'}, output.extra_properties)

    def test_update_add_locations(self):
        self.config(show_multiple_locations=True)
        new_location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(2, len(output.locations))
        self.assertEqual(new_location, output.locations[1])

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_replace_locations_on_queued(self,
                                         mock_get_size,
                                         mock_get_size_uri,
                                         mock_set_acls,
                                         mock_check_loc,
                                         mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='queued',
                        checksum=None,
                        os_hash_algo=None,
                        os_hash_value=None),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        new_location1 = {'url': '%s/fake_location_1' % BASE_URI,
                         'metadata': {},
                         'validation_data': {'checksum': CHKSUM,
                                             'os_hash_algo': 'sha512',
                                             'os_hash_value': MULTIHASH1}}
        new_location2 = {'url': '%s/fake_location_2' % BASE_URI,
                         'metadata': {},
                         'validation_data': {'checksum': CHKSUM,
                                             'os_hash_algo': 'sha512',
                                             'os_hash_value': MULTIHASH1}}
        changes = [{'op': 'replace', 'path': ['locations'],
                    'value': [new_location1, new_location2]}]
        output = self.controller.update(request, image_id, changes)
        self.assertEqual(image_id, output.image_id)
        self.assertEqual(2, len(output.locations))
        self.assertEqual(new_location1['url'], output.locations[0]['url'])
        self.assertEqual(new_location2['url'], output.locations[1]['url'])
        self.assertEqual('active', output.status)
        self.assertEqual(CHKSUM, output.checksum)
        self.assertEqual('sha512', output.os_hash_algo)
        self.assertEqual(MULTIHASH1, output.os_hash_value)

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_replace_locations_identify_associated_store(
            self, mock_get_size, mock_get_size_uri, mock_set_acls,
            mock_check_loc, mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        self.config(enabled_backends={'fake-store': 'http'})
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='queued',
                        checksum=None,
                        os_hash_algo=None,
                        os_hash_value=None),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        new_location1 = {'url': '%s/fake_location_1' % BASE_URI,
                         'metadata': {},
                         'validation_data': {'checksum': CHKSUM,
                                             'os_hash_algo': 'sha512',
                                             'os_hash_value': MULTIHASH1}}
        new_location2 = {'url': '%s/fake_location_2' % BASE_URI,
                         'metadata': {},
                         'validation_data': {'checksum': CHKSUM,
                                             'os_hash_algo': 'sha512',
                                             'os_hash_value': MULTIHASH1}}
        changes = [{'op': 'replace', 'path': ['locations'],
                    'value': [new_location1, new_location2]}]

        with mock.patch.object(store_utils,
                               '_get_store_id_from_uri') as mock_store:
            mock_store.return_value = 'fake-store'
            # ensure location metadata is updated
            new_location1['metadata']['store'] = 'fake-store'
            new_location1['metadata']['store'] = 'fake-store'

            output = self.controller.update(request, image_id, changes)
            self.assertEqual(2, len(output.locations))
            self.assertEqual(image_id, output.image_id)
            self.assertEqual(new_location1, output.locations[0])
            self.assertEqual(new_location2, output.locations[1])
            self.assertEqual('active', output.status)
            self.assertEqual(CHKSUM, output.checksum)
            self.assertEqual('sha512', output.os_hash_algo)
            self.assertEqual(MULTIHASH1, output.os_hash_value)

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_replace_locations_unknon_locations(
            self, mock_get_size, mock_get_size_uri, mock_set_acls,
            mock_check_loc, mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        self.config(enabled_backends={'fake-store': 'http'})
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='queued',
                        checksum=None,
                        os_hash_algo=None,
                        os_hash_value=None),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        new_location1 = {'url': 'unknown://whocares',
                         'metadata': {},
                         'validation_data': {'checksum': CHKSUM,
                                             'os_hash_algo': 'sha512',
                                             'os_hash_value': MULTIHASH1}}
        new_location2 = {'url': 'unknown://whatever',
                         'metadata': {'store': 'unkstore'},
                         'validation_data': {'checksum': CHKSUM,
                                             'os_hash_algo': 'sha512',
                                             'os_hash_value': MULTIHASH1}}
        changes = [{'op': 'replace', 'path': ['locations'],
                    'value': [new_location1, new_location2]}]

        output = self.controller.update(request, image_id, changes)
        self.assertEqual(2, len(output.locations))
        self.assertEqual(image_id, output.image_id)
        self.assertEqual('active', output.status)
        self.assertEqual(CHKSUM, output.checksum)
        self.assertEqual('sha512', output.os_hash_algo)
        self.assertEqual(MULTIHASH1, output.os_hash_value)
        # ensure location metadata is same
        self.assertEqual(new_location1, output.locations[0])
        self.assertEqual(new_location2, output.locations[1])

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_add_location_new_validation_data_on_active(self,
                                                        mock_get_size,
                                                        mock_get_size_uri,
                                                        mock_set_acls,
                                                        mock_check_loc,
                                                        mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        checksum=None,
                        os_hash_algo=None,
                        os_hash_value=None),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        new_location = {'url': '%s/fake_location_1' % BASE_URI,
                        'metadata': {},
                        'validation_data': {'checksum': CHKSUM,
                                            'os_hash_algo': 'sha512',
                                            'os_hash_value': MULTIHASH1}}
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        self.assertRaisesRegex(
            webob.exc.HTTPConflict,
            "may only be provided when image status is 'queued'",
            self.controller.update,
            request, image_id, changes)

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_replace_locations_different_validation_data(self,
                                                         mock_get_size,
                                                         mock_get_size_uri,
                                                         mock_set_acls,
                                                         mock_check_loc,
                                                         mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        checksum=CHKSUM,
                        os_hash_algo='sha512',
                        os_hash_value=MULTIHASH1),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        new_location = {'url': '%s/fake_location_1' % BASE_URI,
                        'metadata': {},
                        'validation_data': {'checksum': CHKSUM1,
                                            'os_hash_algo': 'sha512',
                                            'os_hash_value': MULTIHASH2}}
        changes = [{'op': 'replace', 'path': ['locations'],
                    'value': [new_location]}]
        self.assertRaisesRegex(
            webob.exc.HTTPConflict,
            "already set with a different value",
            self.controller.update,
            request, image_id, changes)

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def _test_add_location_on_queued(self,
                                     visibility,
                                     mock_get_size,
                                     mock_get_size_uri,
                                     mock_set_acls,
                                     mock_check_loc,
                                     mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1, checksum=CHKSUM,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        visibility=visibility,
                        status='queued'),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        new_location = {'url': '%s/fake_location_1' % BASE_URI,
                        'metadata': {}}
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        output = self.controller.update(request, image_id, changes)
        self.assertEqual(image_id, output.image_id)
        self.assertEqual(1, len(output.locations))
        self.assertEqual(new_location, output.locations[0])
        self.assertEqual('active', output.status)
        self.assertEqual(visibility, output.visibility)
        mock_set_acls.assert_called_once()

    def test_add_location_on_queued_shared(self):
        self._test_add_location_on_queued('shared')

    def test_add_location_on_queued_community(self):
        self._test_add_location_on_queued('community')

    def test_add_location_on_queued_public(self):
        self._test_add_location_on_queued('public')

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_add_location_identify_associated_store(
            self, mock_get_size, mock_get_size_uri, mock_set_acls,
            mock_check_loc, mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        self.config(enabled_backends={'fake-store': 'http'})
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1, checksum=CHKSUM,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='queued'),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        new_location = {'url': '%s/fake_location_1' % BASE_URI,
                        'metadata': {}}
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        with mock.patch.object(store_utils,
                               '_get_store_id_from_uri') as mock_store:
            mock_store.return_value = 'fake-store'
            output = self.controller.update(request, image_id, changes)

            self.assertEqual(image_id, output.image_id)
            self.assertEqual(1, len(output.locations))
            self.assertEqual('active', output.status)
            # ensure location metadata is updated
            new_location['metadata']['store'] = 'fake-store'
            self.assertEqual(new_location, output.locations[0])

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_add_location_unknown_locations(
            self, mock_get_size, mock_get_size_uri, mock_set_acls,
            mock_check_loc, mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        self.config(enabled_backends={'fake-store': 'http'})
        image_id = str(uuid.uuid4())

        self.images = [
            _db_fixture(image_id, owner=TENANT1, checksum=CHKSUM,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='queued'),
        ]
        self.db.image_create(None, self.images[0])

        new_location = {'url': 'unknown://whocares', 'metadata': {}}
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]

        output = self.controller.update(request, image_id, changes)

        self.assertEqual(image_id, output.image_id)
        self.assertEqual('active', output.status)
        self.assertEqual(1, len(output.locations))
        # ensure location metadata is same
        self.assertEqual(new_location, output.locations[0])

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_add_location_invalid_validation_data(self,
                                                  mock_get_size,
                                                  mock_get_size_uri,
                                                  mock_set_acls,
                                                  mock_check_loc,
                                                  mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1,
                        checksum=None,
                        os_hash_algo=None,
                        os_hash_value=None,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='queued'),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()

        location = {
            'url': '%s/fake_location_1' % BASE_URI,
            'metadata': {},
            'validation_data': {}
        }
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': location}]

        changes[0]['value']['validation_data'] = {
            'checksum': 'something the same length as md5',
            'os_hash_algo': 'sha512',
            'os_hash_value': MULTIHASH1,
        }
        self.assertRaisesRegex(
            webob.exc.HTTPConflict,
            'checksum .* is not a valid hexadecimal value',
            self.controller.update,
            request, image_id, changes)

        changes[0]['value']['validation_data'] = {
            'checksum': '0123456789abcdef',
            'os_hash_algo': 'sha512',
            'os_hash_value': MULTIHASH1,
        }
        self.assertRaisesRegex(
            webob.exc.HTTPConflict,
            'checksum .* is not the correct size',
            self.controller.update,
            request, image_id, changes)

        changes[0]['value']['validation_data'] = {
            'checksum': CHKSUM,
            'os_hash_algo': 'sha256',
            'os_hash_value': MULTIHASH1,
        }
        self.assertRaisesRegex(
            webob.exc.HTTPConflict,
            'os_hash_algo must be sha512',
            self.controller.update,
            request, image_id, changes)

        changes[0]['value']['validation_data'] = {
            'checksum': CHKSUM,
            'os_hash_algo': 'sha512',
            'os_hash_value': 'not a hex value',
        }
        self.assertRaisesRegex(
            webob.exc.HTTPConflict,
            'os_hash_value .* is not a valid hexadecimal value',
            self.controller.update,
            request, image_id, changes)

        changes[0]['value']['validation_data'] = {
            'checksum': CHKSUM,
            'os_hash_algo': 'sha512',
            'os_hash_value': '0123456789abcdef',
        }
        self.assertRaisesRegex(
            webob.exc.HTTPConflict,
            'os_hash_value .* is not the correct size for sha512',
            self.controller.update,
            request, image_id, changes)

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_add_location_same_validation_data(self,
                                               mock_get_size,
                                               mock_get_size_uri,
                                               mock_set_acls,
                                               mock_check_loc,
                                               mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        image_id = str(uuid.uuid4())
        os_hash_value = '6513f21e44aa3da349f248188a44bc304a3653a04122d8fb45' \
                        '35423c8e1d14cd6a153f735bb0982e2161b5b5186106570c17' \
                        'a9e58b64dd39390617cd5a350f78'
        self.images = [
            _db_fixture(image_id, owner=TENANT1,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        checksum='checksum1',
                        os_hash_algo='sha512',
                        os_hash_value=os_hash_value),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        new_location = {'url': '%s/fake_location_1' % BASE_URI,
                        'metadata': {},
                        'validation_data': {'checksum': 'checksum1',
                                            'os_hash_algo': 'sha512',
                                            'os_hash_value': os_hash_value}}
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        output = self.controller.update(request, image_id, changes)
        self.assertEqual(image_id, output.image_id)
        self.assertEqual(1, len(output.locations))
        self.assertEqual(new_location, output.locations[0])
        self.assertEqual('active', output.status)

    @mock.patch.object(glance.quota, '_calc_required_size')
    @mock.patch.object(glance.location, '_check_image_location')
    @mock.patch.object(glance.location.ImageRepoProxy, '_set_acls')
    @mock.patch.object(store, 'get_size_from_uri_and_backend')
    @mock.patch.object(store, 'get_size_from_backend')
    def test_add_location_different_validation_data(self,
                                                    mock_get_size,
                                                    mock_get_size_uri,
                                                    mock_set_acls,
                                                    mock_check_loc,
                                                    mock_calc):
        mock_calc.return_value = 1
        mock_get_size.return_value = 1
        mock_get_size_uri.return_value = 1
        self.config(show_multiple_locations=True)
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        checksum=CHKSUM,
                        os_hash_algo='sha512',
                        os_hash_value=MULTIHASH1),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        new_location = {'url': '%s/fake_location_1' % BASE_URI,
                        'metadata': {},
                        'validation_data': {'checksum': CHKSUM1,
                                            'os_hash_algo': 'sha512',
                                            'os_hash_value': MULTIHASH2}}
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        self.assertRaisesRegex(
            webob.exc.HTTPConflict,
            "already set with a different value",
            self.controller.update,
            request, image_id, changes)

    def _test_update_locations_status(self, image_status, update):
        self.config(show_multiple_locations=True)
        self.images = [
            _db_fixture('1', owner=TENANT1, checksum=CHKSUM,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status=image_status),
        ]
        request = unit_test_utils.get_fake_request()
        if image_status == 'deactivated':
            self.db.image_create(request.context, self.images[0])
        else:
            self.db.image_create(None, self.images[0])
        new_location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        changes = [{'op': update, 'path': ['locations', '-'],
                    'value': new_location}]
        self.assertRaises(webob.exc.HTTPConflict,
                          self.controller.update, request, '1', changes)

    def test_location_add_not_permitted_status_saving(self):
        self._test_update_locations_status('saving', 'add')

    def test_location_add_not_permitted_status_deactivated(self):
        self._test_update_locations_status('deactivated', 'add')

    def test_location_add_not_permitted_status_deleted(self):
        self._test_update_locations_status('deleted', 'add')

    def test_location_add_not_permitted_status_pending_delete(self):
        self._test_update_locations_status('pending_delete', 'add')

    def test_location_add_not_permitted_status_killed(self):
        self._test_update_locations_status('killed', 'add')

    def test_location_add_not_permitted_status_importing(self):
        self._test_update_locations_status('importing', 'add')

    def test_location_add_not_permitted_status_uploading(self):
        self._test_update_locations_status('uploading', 'add')

    def test_location_remove_not_permitted_status_saving(self):
        self._test_update_locations_status('saving', 'remove')

    def test_location_remove_not_permitted_status_deactivated(self):
        self._test_update_locations_status('deactivated', 'remove')

    def test_location_remove_not_permitted_status_deleted(self):
        self._test_update_locations_status('deleted', 'remove')

    def test_location_remove_not_permitted_status_pending_delete(self):
        self._test_update_locations_status('pending_delete', 'remove')

    def test_location_remove_not_permitted_status_killed(self):
        self._test_update_locations_status('killed', 'remove')

    def test_location_remove_not_permitted_status_queued(self):
        self._test_update_locations_status('queued', 'remove')

    def test_location_remove_not_permitted_status_importing(self):
        self._test_update_locations_status('importing', 'remove')

    def test_location_remove_not_permitted_status_uploading(self):
        self._test_update_locations_status('uploading', 'remove')

    def test_location_replace_not_permitted_status_saving(self):
        self._test_update_locations_status('saving', 'replace')

    def test_location_replace_not_permitted_status_deactivated(self):
        self._test_update_locations_status('deactivated', 'replace')

    def test_location_replace_not_permitted_status_deleted(self):
        self._test_update_locations_status('deleted', 'replace')

    def test_location_replace_not_permitted_status_pending_delete(self):
        self._test_update_locations_status('pending_delete', 'replace')

    def test_location_replace_not_permitted_status_killed(self):
        self._test_update_locations_status('killed', 'replace')

    def test_location_replace_not_permitted_status_importing(self):
        self._test_update_locations_status('importing', 'replace')

    def test_location_replace_not_permitted_status_uploading(self):
        self._test_update_locations_status('uploading', 'replace')

    def test_update_add_locations_insertion(self):
        self.config(show_multiple_locations=True)
        new_location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'add', 'path': ['locations', '0'],
                    'value': new_location}]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(2, len(output.locations))
        self.assertEqual(new_location, output.locations[0])

    def test_update_add_locations_list(self):
        self.config(show_multiple_locations=True)
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': {'url': 'foo', 'metadata': {}}}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes)

    def test_update_add_locations_invalid(self):
        self.config(show_multiple_locations=True)
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': {'url': 'unknow://foo', 'metadata': {}}}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes)

        changes = [{'op': 'add', 'path': ['locations', None],
                    'value': {'url': 'unknow://foo', 'metadata': {}}}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes)

    def test_update_add_duplicate_locations(self):
        self.config(show_multiple_locations=True)
        new_location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(2, len(output.locations))
        self.assertEqual(new_location, output.locations[1])

        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes)

    def test_update_add_too_many_locations(self):
        self.config(show_multiple_locations=True)
        self.config(image_location_quota=1)
        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_1' % BASE_URI,
                       'metadata': {}}},
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_2' % BASE_URI,
                       'metadata': {}}},
        ]
        self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
                          self.controller.update, request,
                          UUID1, changes)

    def test_update_add_and_remove_too_many_locations(self):
        self.config(show_multiple_locations=True)
        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_1' % BASE_URI,
                       'metadata': {}}},
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_2' % BASE_URI,
                       'metadata': {}}},
        ]
        self.controller.update(request, UUID1, changes)
        self.config(image_location_quota=1)

        # We must remove two properties to avoid being
        # over the limit of 1 property
        changes = [
            {'op': 'remove', 'path': ['locations', '0']},
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_3' % BASE_URI,
                       'metadata': {}}},
        ]
        self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
                          self.controller.update, request,
                          UUID1, changes)

    def test_update_add_unlimited_locations(self):
        self.config(show_multiple_locations=True)
        self.config(image_location_quota=-1)
        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_1' % BASE_URI,
                       'metadata': {}}},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_remove_location_while_over_limit(self):
        """Ensure that image locations can be removed.

        Image locations should be able to be removed as long as the image has
        fewer than the limited number of image locations after the
        transaction.
        """
        self.config(show_multiple_locations=True)
        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_1' % BASE_URI,
                       'metadata': {}}},
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_2' % BASE_URI,
                       'metadata': {}}},
        ]
        self.controller.update(request, UUID1, changes)
        self.config(image_location_quota=1)
        self.config(show_multiple_locations=True)

        # We must remove two locations to avoid being over
        # the limit of 1 location
        changes = [
            {'op': 'remove', 'path': ['locations', '0']},
            {'op': 'remove', 'path': ['locations', '0']},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(1, len(output.locations))
        self.assertIn('fake_location_2', output.locations[0]['url'])
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_add_and_remove_location_under_limit(self):
        """Ensure that image locations can be removed.

        Image locations should be able to be added and removed simultaneously
        as long as the image has fewer than the limited number of image
        locations after the transaction.
        """
        self.mock_object(store, 'get_size_from_backend',
                         unit_test_utils.fake_get_size_from_backend)
        self.config(show_multiple_locations=True)
        request = unit_test_utils.get_fake_request()

        changes = [
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_1' % BASE_URI,
                       'metadata': {}}},
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_2' % BASE_URI,
                       'metadata': {}}},
        ]
        self.controller.update(request, UUID1, changes)
        self.config(image_location_quota=2)

        # We must remove two properties to avoid being
        # over the limit of 1 property
        changes = [
            {'op': 'remove', 'path': ['locations', '0']},
            {'op': 'remove', 'path': ['locations', '0']},
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location_3' % BASE_URI,
                       'metadata': {}}},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(2, len(output.locations))
        self.assertIn('fake_location_3', output.locations[1]['url'])
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_remove_base_property(self):
        self.db.image_update(None, UUID1, {'properties': {'foo': 'bar'}})
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'remove', 'path': ['name']}]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          request, UUID1, changes)

    def test_update_remove_property(self):
        request = unit_test_utils.get_fake_request()
        properties = {'foo': 'bar', 'snitch': 'golden'}
        self.db.image_update(None, UUID1, {'properties': properties})

        output = self.controller.show(request, UUID1)
        self.assertEqual('bar', output.extra_properties['foo'])
        self.assertEqual('golden', output.extra_properties['snitch'])

        changes = [
            {'op': 'remove', 'path': ['snitch']},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual({'foo': 'bar'}, output.extra_properties)
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_remove_missing_property(self):
        request = unit_test_utils.get_fake_request()

        changes = [
            {'op': 'remove', 'path': ['foo']},
        ]
        self.assertRaises(webob.exc.HTTPConflict,
                          self.controller.update, request, UUID1, changes)

    def test_update_remove_location(self):
        self.config(show_multiple_locations=True)
        self.mock_object(store, 'get_size_from_backend',
                         unit_test_utils.fake_get_size_from_backend)

        request = unit_test_utils.get_fake_request()
        new_location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        self.controller.update(request, UUID1, changes)
        changes = [{'op': 'remove', 'path': ['locations', '0']}]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(1, len(output.locations))
        self.assertEqual('active', output.status)

    def test_update_remove_location_invalid_pos(self):
        self.config(show_multiple_locations=True)
        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location' % BASE_URI,
                       'metadata': {}}}]
        self.controller.update(request, UUID1, changes)
        changes = [{'op': 'remove', 'path': ['locations', None]}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes)
        changes = [{'op': 'remove', 'path': ['locations', '-1']}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes)
        changes = [{'op': 'remove', 'path': ['locations', '99']}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes)
        changes = [{'op': 'remove', 'path': ['locations', 'x']}]
        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
                          request, UUID1, changes)

    def test_update_remove_location_store_exception(self):
        self.config(show_multiple_locations=True)

        def fake_delete_image_location_from_backend(self, *args, **kwargs):
            raise Exception('fake_backend_exception')

        self.mock_object(self.store_utils,
                         'delete_image_location_from_backend',
                         fake_delete_image_location_from_backend)

        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'add', 'path': ['locations', '-'],
             'value': {'url': '%s/fake_location' % BASE_URI,
                       'metadata': {}}}]
        self.controller.update(request, UUID1, changes)
        changes = [{'op': 'remove', 'path': ['locations', '0']}]
        self.assertRaises(webob.exc.HTTPInternalServerError,
                          self.controller.update, request, UUID1, changes)

    def test_update_multiple_changes(self):
        request = unit_test_utils.get_fake_request()
        properties = {'foo': 'bar', 'snitch': 'golden'}
        self.db.image_update(None, UUID1, {'properties': properties})

        changes = [
            {'op': 'replace', 'path': ['min_ram'], 'value': 128},
            {'op': 'replace', 'path': ['foo'], 'value': 'baz'},
            {'op': 'remove', 'path': ['snitch']},
            {'op': 'add', 'path': ['kb'], 'value': 'dvorak'},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(UUID1, output.image_id)
        self.assertEqual(128, output.min_ram)
        self.addDetail('extra_properties',
                       testtools.content.json_content(
                           jsonutils.dumps(output.extra_properties)))
        self.assertEqual(2, len(output.extra_properties))
        self.assertEqual('baz', output.extra_properties['foo'])
        self.assertEqual('dvorak', output.extra_properties['kb'])
        self.assertNotEqual(output.created_at, output.updated_at)

    def test_update_invalid_operation(self):
        request = unit_test_utils.get_fake_request()
        change = {'op': 'test', 'path': 'options', 'value': 'puts'}
        try:
            self.controller.update(request, UUID1, [change])
        except AttributeError:
            pass  # AttributeError is the desired behavior
        else:
            self.fail('Failed to raise AssertionError on %s' % change)

    def test_update_duplicate_tags(self):
        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'replace', 'path': ['tags'], 'value': ['ping', 'ping']},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual(1, len(output.tags))
        self.assertIn('ping', output.tags)
        output_logs = self.notifier.get_logs()
        self.assertEqual(1, len(output_logs))
        output_log = output_logs[0]
        self.assertEqual('INFO', output_log['notification_type'])
        self.assertEqual('image.update', output_log['event_type'])
        self.assertEqual(UUID1, output_log['payload']['id'])

    def test_update_disabled_notification(self):
        self.config(disabled_notifications=["image.update"])
        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'replace', 'path': ['name'], 'value': 'Ping Pong'},
        ]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual('Ping Pong', output.name)
        output_logs = self.notifier.get_logs()
        self.assertEqual(0, len(output_logs))

    def test_delete(self):
        request = unit_test_utils.get_fake_request()
        self.assertIn('%s/%s' % (BASE_URI, UUID1), self.store.data)
        try:
            self.controller.delete(request, UUID1)
            output_logs = self.notifier.get_logs()
            self.assertEqual(1, len(output_logs))
            output_log = output_logs[0]
            self.assertEqual('INFO', output_log['notification_type'])
            self.assertEqual("image.delete", output_log['event_type'])
        except Exception as e:
            self.fail("Delete raised exception: %s" % e)

        deleted_img = self.db.image_get(request.context, UUID1,
                                        force_show_deleted=True)
        self.assertTrue(deleted_img['deleted'])
        self.assertEqual('deleted', deleted_img['status'])
        self.assertNotIn('%s/%s' % (BASE_URI, UUID1), self.store.data)

    def test_delete_not_allowed_by_policy(self):
        request = unit_test_utils.get_fake_request()
        with mock.patch.object(self.controller.policy, 'enforce') as mock_enf:
            mock_enf.side_effect = webob.exc.HTTPForbidden()
            exc = self.assertRaises(webob.exc.HTTPNotFound,
                                    self.controller.delete, request, UUID1)
            self.assertTrue(mock_enf.called)
        # Make sure we did not leak details of the original Forbidden
        # error into the NotFound returned to the client.
        self.assertEqual('The resource could not be found.', str(exc))

        # Now reject the delete_image call, but allow get_image to ensure that
        # we properly see a Forbidden result.
        with mock.patch.object(self.controller.policy, 'enforce') as mock_enf:
            mock_enf.side_effect = [webob.exc.HTTPForbidden(),
                                    lambda *a: None]
            exc = self.assertRaises(webob.exc.HTTPForbidden,
                                    self.controller.delete, request, UUID1)
            self.assertTrue(mock_enf.called)

    @mock.patch.object(store, 'get_store_from_store_identifier')
    @mock.patch.object(store.location, 'get_location_from_uri_and_backend')
    @mock.patch.object(store_utils, 'get_dir_separator')
    def test_verify_staging_data_deleted_on_image_delete(
            self, mock_get_dir_separator, mock_location,
            mock_store):
        self.config(enabled_backends={'fake-store': 'file'})
        fake_staging_store = mock.Mock()
        mock_store.return_value = fake_staging_store
        mock_get_dir_separator.return_value = (
            "/", "/tmp/os_glance_staging_store")
        image_id = str(uuid.uuid4())
        self.images = [
            _db_fixture(image_id, owner=TENANT1,
                        name='1',
                        disk_format='raw',
                        container_format='bare',
                        status='importing',
                        checksum=None,
                        os_hash_algo=None,
                        os_hash_value=None),
        ]
        self.db.image_create(None, self.images[0])
        request = unit_test_utils.get_fake_request()
        try:
            self.controller.delete(request, image_id)
            self.assertEqual(1, mock_store.call_count)
            mock_store.assert_called_once_with("os_glance_staging_store")
            self.assertEqual(1, mock_location.call_count)
            fake_staging_store.delete.assert_called_once()
        except Exception as e:
            self.fail("Delete raised exception: %s" % e)

        deleted_img = self.db.image_get(request.context, image_id,
                                        force_show_deleted=True)
        self.assertTrue(deleted_img['deleted'])
        self.assertEqual('deleted', deleted_img['status'])

    def test_delete_with_tags(self):
        request = unit_test_utils.get_fake_request()
        changes = [
            {'op': 'replace', 'path': ['tags'],
             'value': ['many', 'cool', 'new', 'tags']},
        ]
        self.controller.update(request, UUID1, changes)
        self.assertIn('%s/%s' % (BASE_URI, UUID1), self.store.data)
        self.controller.delete(request, UUID1)
        output_logs = self.notifier.get_logs()

        # Get `delete` event from logs
        output_delete_logs = [output_log for output_log in output_logs
                              if output_log['event_type'] == 'image.delete']

        self.assertEqual(1, len(output_delete_logs))
        output_log = output_delete_logs[0]

        self.assertEqual('INFO', output_log['notification_type'])

        deleted_img = self.db.image_get(request.context, UUID1,
                                        force_show_deleted=True)
        self.assertTrue(deleted_img['deleted'])
        self.assertEqual('deleted', deleted_img['status'])
        self.assertNotIn('%s/%s' % (BASE_URI, UUID1), self.store.data)

    def test_delete_disabled_notification(self):
        self.config(disabled_notifications=["image.delete"])
        request = unit_test_utils.get_fake_request()
        self.assertIn('%s/%s' % (BASE_URI, UUID1), self.store.data)
        try:
            self.controller.delete(request, UUID1)
            output_logs = self.notifier.get_logs()
            self.assertEqual(0, len(output_logs))
        except Exception as e:
            self.fail("Delete raised exception: %s" % e)

        deleted_img = self.db.image_get(request.context, UUID1,
                                        force_show_deleted=True)
        self.assertTrue(deleted_img['deleted'])
        self.assertEqual('deleted', deleted_img['status'])
        self.assertNotIn('%s/%s' % (BASE_URI, UUID1), self.store.data)

    def test_delete_queued_updates_status(self):
        """Ensure status of queued image is updated (LP bug #1048851)"""
        request = unit_test_utils.get_fake_request(is_admin=True)
        image = self.db.image_create(request.context, {'status': 'queued'})
        image_id = image['id']
        self.controller.delete(request, image_id)

        image = self.db.image_get(request.context, image_id,
                                  force_show_deleted=True)
        self.assertTrue(image['deleted'])
        self.assertEqual('deleted', image['status'])

    def test_delete_queued_updates_status_delayed_delete(self):
        """Ensure status of queued image is updated (LP bug #1048851).

        Must be set to 'deleted' when delayed_delete isenabled.
        """
        self.config(delayed_delete=True)

        request = unit_test_utils.get_fake_request(is_admin=True)
        image = self.db.image_create(request.context, {'status': 'queued'})
        image_id = image['id']
        self.controller.delete(request, image_id)

        image = self.db.image_get(request.context, image_id,
                                  force_show_deleted=True)
        self.assertTrue(image['deleted'])
        self.assertEqual('deleted', image['status'])

    def test_delete_not_in_store(self):
        request = unit_test_utils.get_fake_request()
        self.assertIn('%s/%s' % (BASE_URI, UUID1), self.store.data)
        for k in self.store.data:
            if UUID1 in k:
                del self.store.data[k]
                break

        self.controller.delete(request, UUID1)
        deleted_img = self.db.image_get(request.context, UUID1,
                                        force_show_deleted=True)
        self.assertTrue(deleted_img['deleted'])
        self.assertEqual('deleted', deleted_img['status'])
        self.assertNotIn('%s/%s' % (BASE_URI, UUID1), self.store.data)

    def test_delayed_delete(self):
        self.config(delayed_delete=True)
        request = unit_test_utils.get_fake_request()
        self.assertIn('%s/%s' % (BASE_URI, UUID1), self.store.data)

        self.controller.delete(request, UUID1)
        deleted_img = self.db.image_get(request.context, UUID1,
                                        force_show_deleted=True)
        self.assertTrue(deleted_img['deleted'])
        self.assertEqual('pending_delete', deleted_img['status'])
        self.assertIn('%s/%s' % (BASE_URI, UUID1), self.store.data)

    def test_delete_non_existent(self):
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
                          request, str(uuid.uuid4()))

    def test_delete_already_deleted_image_admin(self):
        request = unit_test_utils.get_fake_request(is_admin=True)
        self.controller.delete(request, UUID1)
        self.assertRaises(webob.exc.HTTPNotFound,
                          self.controller.delete, request, UUID1)

    def test_delete_not_allowed(self):
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
                          request, UUID4)

    def test_delete_in_use(self):
        def fake_safe_delete_from_backend(self, *args, **kwargs):
            raise store.exceptions.InUseByStore()
        self.mock_object(self.store_utils, 'safe_delete_from_backend',
                         fake_safe_delete_from_backend)
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPConflict, self.controller.delete,
                          request, UUID1)

    def test_delete_has_snapshot(self):
        def fake_safe_delete_from_backend(self, *args, **kwargs):
            raise store.exceptions.HasSnapshot()
        self.mock_object(self.store_utils, 'safe_delete_from_backend',
                         fake_safe_delete_from_backend)
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPConflict, self.controller.delete,
                          request, UUID1)

    def test_delete_to_unallowed_status(self):
        # from deactivated to pending-delete
        self.config(delayed_delete=True)
        request = unit_test_utils.get_fake_request(is_admin=True)
        self.action_controller.deactivate(request, UUID1)

        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
                          request, UUID1)

    def test_delete_uploading_status_image(self):
        """Ensure uploading image is deleted (LP bug #1733289)
        Ensure image stuck in uploading state is deleted (LP bug #1836140)
        """
        request = unit_test_utils.get_fake_request(is_admin=True)
        image = self.db.image_create(request.context, {'status': 'uploading'})
        image_id = image['id']
        with mock.patch.object(os.path, 'exists') as mock_exists:
            mock_exists.return_value = True
            with mock.patch.object(os, "unlink") as mock_unlik:
                self.controller.delete(request, image_id)

                self.assertEqual(1, mock_exists.call_count)
                self.assertEqual(1, mock_unlik.call_count)

        # Ensure that image is deleted
        image = self.db.image_get(request.context, image_id,
                                  force_show_deleted=True)
        self.assertTrue(image['deleted'])
        self.assertEqual('deleted', image['status'])

    def test_deletion_of_staging_data_failed(self):
        """Ensure uploading image is deleted (LP bug #1733289)
        Ensure image stuck in uploading state is deleted (LP bug #1836140)
        """
        request = unit_test_utils.get_fake_request(is_admin=True)
        image = self.db.image_create(request.context, {'status': 'uploading'})
        image_id = image['id']
        with mock.patch.object(os.path, 'exists') as mock_exists:
            mock_exists.return_value = False
            with mock.patch.object(os, "unlink") as mock_unlik:
                self.controller.delete(request, image_id)

                self.assertEqual(1, mock_exists.call_count)
                self.assertEqual(0, mock_unlik.call_count)

        # Ensure that image is deleted
        image = self.db.image_get(request.context, image_id,
                                  force_show_deleted=True)
        self.assertTrue(image['deleted'])
        self.assertEqual('deleted', image['status'])

    def test_delete_from_store_no_multistore(self):
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPNotFound,
                          self.controller.delete_from_store, request,
                          "the IDs should", "not matter")

    def test_index_with_invalid_marker(self):
        fake_uuid = str(uuid.uuid4())
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.controller.index, request, marker=fake_uuid)

    def test_invalid_locations_op_pos(self):
        pos = self.controller._get_locations_op_pos(None, 2, True)
        self.assertIsNone(pos)
        pos = self.controller._get_locations_op_pos('1', None, True)
        self.assertIsNone(pos)

    @mock.patch('glance.db.simple.api.image_set_property_atomic')
    @mock.patch.object(glance.notifier.TaskFactoryProxy, 'new_task')
    @mock.patch.object(glance.domain.TaskExecutorFactory, 'new_task_executor')
    @mock.patch('glance.api.common.get_thread_pool')
    @mock.patch('glance.quota.keystone.enforce_image_size_total')
    def test_image_import(self, mock_enforce, mock_gtp, mock_nte, mock_nt,
                          mock_spa):
        request = unit_test_utils.get_fake_request()
        image = FakeImage(status='uploading')
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = image
            output = self.controller.import_image(
                request, UUID4, {'method': {'name': 'glance-direct'}})

        self.assertEqual(UUID4, output)

        # Make sure we checked quota
        mock_enforce.assert_called_once_with(request.context,
                                             request.context.project_id)

        # Make sure we set the lock on the image
        mock_spa.assert_called_once_with(UUID4, 'os_glance_import_task',
                                         mock_nt.return_value.task_id)

        # Make sure we grabbed a thread pool, and that we asked it
        # to spawn the task's run method with it.
        mock_gtp.assert_called_once_with('tasks_pool')
        mock_gtp.return_value.spawn.assert_called_once_with(
            mock_nt.return_value.run, mock_nte.return_value)

    @mock.patch.object(glance.domain.TaskFactory, 'new_task')
    @mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
    def test_image_import_not_allowed(self, mock_get, mock_new_task):
        # NOTE(danms): FakeImage is owned by utils.TENANT1. Try to do the
        # import as TENANT2 and we should get an HTTPForbidden
        enforcer = unit_test_utils.enforcer_from_rules({
            "get_image": "",
            "modify_image": "'{0}':%(owner)s".format(TENANT2)
        })
        request = unit_test_utils.get_fake_request()
        self.controller.policy = enforcer
        mock_get.return_value = FakeImage(status='uploading')
        self.assertRaises(webob.exc.HTTPForbidden,
                          self.controller.import_image,
                          request, UUID4, {'method': {'name':
                                                      'glance-direct'}})
        # NOTE(danms): Make sure we failed early and never even created
        # a task
        mock_new_task.assert_not_called()

    @mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
    @mock.patch('glance.quota.keystone.enforce_image_size_total')
    def test_image_import_quota_fail(self, mock_enforce, mock_get):
        request = unit_test_utils.get_fake_request()
        mock_get.return_value = FakeImage(status='uploading')
        mock_enforce.side_effect = exception.LimitExceeded('test')
        self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
                          self.controller.import_image,
                          request, UUID4,
                          {'method': {'name': 'glance-direct'}})

    @mock.patch('glance.db.simple.api.image_set_property_atomic')
    @mock.patch('glance.context.RequestContext.elevated')
    @mock.patch.object(glance.domain.TaskFactory, 'new_task')
    @mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
    def test_image_import_copy_allowed_by_policy(self, mock_get,
                                                 mock_new_task,
                                                 mock_elevated,
                                                 mock_spa,
                                                 allowed=True):
        # NOTE(danms): FakeImage is owned by utils.TENANT1. Try to do the
        # import as TENANT2, but with a policy exception
        request = unit_test_utils.get_fake_request(tenant=TENANT2)
        mock_get.return_value = FakeImage(status='active', locations=[])

        self.policy.rules = {'copy_image': allowed}

        req_body = {'method': {'name': 'copy-image'},
                    'stores': ['cheap']}

        with mock.patch.object(
                self.controller.gateway,
                'get_task_executor_factory',
                side_effect=self.controller.gateway.get_task_executor_factory
        ) as mock_tef:
            self.controller.import_image(request, UUID4, req_body)
            # Make sure we passed an admin context to our task executor factory
            mock_tef.assert_called_once_with(
                request.context,
                admin_context=mock_elevated.return_value)

        expected_input = {'image_id': UUID4,
                          'import_req': mock.ANY,
                          'backend': mock.ANY}
        mock_new_task.assert_called_with(task_type='api_image_import',
                                         owner=TENANT2,
                                         task_input=expected_input,
                                         image_id=UUID4,
                                         user_id=request.context.user_id,
                                         request_id=request.context.request_id)

    def test_image_import_copy_not_allowed_by_policy(self):
        # Make sure that if the policy check fails, we fail a copy-image with
        # Forbidden
        self.assertRaises(webob.exc.HTTPForbidden,
                          self.test_image_import_copy_allowed_by_policy,
                          allowed=False)

    @mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
    def test_image_import_locked(self, mock_get):
        task = test_tasks_resource._db_fixture(test_tasks_resource.UUID1,
                                               status='pending')
        self.db.task_create(None, task)
        image = FakeImage(status='uploading')
        # Image is locked with a valid task that has not aged out, so
        # the lock will not be busted.
        image.extra_properties['os_glance_import_task'] = task['id']
        mock_get.return_value = image

        request = unit_test_utils.get_fake_request(tenant=TENANT1)
        req_body = {'method': {'name': 'glance-direct'}}

        exc = self.assertRaises(webob.exc.HTTPConflict,
                                self.controller.import_image,
                                request, UUID1, req_body)
        self.assertEqual('Image has active task', str(exc))

    @mock.patch('glance.db.simple.api.image_set_property_atomic')
    @mock.patch('glance.db.simple.api.image_delete_property_atomic')
    @mock.patch.object(glance.notifier.TaskFactoryProxy, 'new_task')
    @mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
    def test_image_import_locked_by_reaped_task(self, mock_get, mock_nt,
                                                mock_dpi, mock_spi):
        image = FakeImage(status='uploading')
        # Image is locked by some other task that TaskRepo will not find
        image.extra_properties['os_glance_import_task'] = 'missing'
        mock_get.return_value = image

        request = unit_test_utils.get_fake_request(tenant=TENANT1)
        req_body = {'method': {'name': 'glance-direct'}}

        mock_nt.return_value.task_id = 'mytask'
        self.controller.import_image(request, UUID1, req_body)

        # We should have atomically deleted the missing task lock
        mock_dpi.assert_called_once_with(image.id, 'os_glance_import_task',
                                         'missing')
        # We should have atomically grabbed the lock with our task id
        mock_spi.assert_called_once_with(image.id, 'os_glance_import_task',
                                         'mytask')

    @mock.patch.object(glance.notifier.ImageRepoProxy, 'save')
    @mock.patch('glance.db.simple.api.image_set_property_atomic')
    @mock.patch('glance.db.simple.api.image_delete_property_atomic')
    @mock.patch.object(glance.notifier.TaskFactoryProxy, 'new_task')
    @mock.patch.object(glance.notifier.ImageRepoProxy, 'get')
    def test_image_import_locked_by_bustable_task(self, mock_get, mock_nt,
                                                  mock_dpi, mock_spi,
                                                  mock_save,
                                                  task_status='processing'):
        if task_status == 'processing':
            # NOTE(danms): Only set task_input on one of the tested
            # states to make sure we don't choke on a task without
            # some of the data set yet.
            task_input = {'backend': ['store2']}
        else:
            task_input = {}
        task = test_tasks_resource._db_fixture(
            test_tasks_resource.UUID1,
            status=task_status,
            input=task_input)
        self.db.task_create(None, task)
        image = FakeImage(status='uploading')
        # Image is locked by a task in 'processing' state
        image.extra_properties['os_glance_import_task'] = task['id']
        image.extra_properties['os_glance_importing_to_stores'] = 'store2'
        mock_get.return_value = image

        request = unit_test_utils.get_fake_request(tenant=TENANT1)
        req_body = {'method': {'name': 'glance-direct'}}

        # Task has only been running for ten minutes
        time_fixture = fixture.TimeFixture(task['updated_at'] +
                                           datetime.timedelta(minutes=10))
        self.useFixture(time_fixture)

        mock_nt.return_value.task_id = 'mytask'

        # Task holds the lock, API refuses to bust it
        self.assertRaises(webob.exc.HTTPConflict,
                          self.controller.import_image,
                          request, UUID1, req_body)
        mock_dpi.assert_not_called()
        mock_spi.assert_not_called()
        mock_nt.assert_not_called()

        # Fast forward to 90 minutes from now
        time_fixture.advance_time_delta(datetime.timedelta(minutes=90))
        self.controller.import_image(request, UUID1, req_body)

        # API deleted the other task's lock and locked it for us
        mock_dpi.assert_called_once_with(image.id, 'os_glance_import_task',
                                         task['id'])
        mock_spi.assert_called_once_with(image.id, 'os_glance_import_task',
                                         'mytask')

        # If we stored task_input with information about the stores
        # and thus triggered the cleanup code, make sure that cleanup
        # happened here.
        if task_status == 'processing':
            self.assertNotIn('store2',
                             image.extra_properties[
                                 'os_glance_importing_to_stores'])

    def test_image_import_locked_by_bustable_terminal_task_failure(self):
        # Make sure we don't fail with a task status transition error
        self.test_image_import_locked_by_bustable_task(task_status='failure')

    def test_image_import_locked_by_bustable_terminal_task_success(self):
        # Make sure we don't fail with a task status transition error
        self.test_image_import_locked_by_bustable_task(task_status='success')

    def test_cleanup_stale_task_progress(self):
        img_repo = mock.MagicMock()
        image = mock.MagicMock()
        task = mock.MagicMock()

        # No backend info from the old task, means no action
        task.task_input = {}
        image.extra_properties = {}
        self.controller._cleanup_stale_task_progress(img_repo, image, task)
        img_repo.save.assert_not_called()

        # If we have info but no stores, no action
        task.task_input = {'backend': []}
        self.controller._cleanup_stale_task_progress(img_repo, image, task)
        img_repo.save.assert_not_called()

        # If task had stores, but image does not have those stores in
        # the lists, no action
        task.task_input = {'backend': ['store1', 'store2']}
        self.controller._cleanup_stale_task_progress(img_repo, image, task)
        img_repo.save.assert_not_called()

        # If the image has stores in the lists, but not the ones we care
        # about, make sure they are not disturbed
        image.extra_properties = {'os_glance_failed_import': 'store3'}
        self.controller._cleanup_stale_task_progress(img_repo, image, task)
        img_repo.save.assert_not_called()

        # Only if the image has stores that relate to our old task should
        # take action, and only on those stores.
        image.extra_properties = {
            'os_glance_importing_to_stores': 'foo,store1,bar',
            'os_glance_failed_import': 'foo,store2,bar',
        }
        self.controller._cleanup_stale_task_progress(img_repo, image, task)
        img_repo.save.assert_called_once_with(image)
        self.assertEqual({'os_glance_importing_to_stores': 'foo,bar',
                          'os_glance_failed_import': 'foo,bar'},
                         image.extra_properties)

    def test_bust_import_lock_race_to_delete(self):
        image_repo = mock.MagicMock()
        task_repo = mock.MagicMock()
        image = mock.MagicMock()
        task = mock.MagicMock(id='foo')
        # Simulate a race where we tried to bust a specific lock and
        # someone else already had, and/or re-locked it
        image_repo.delete_property_atomic.side_effect = exception.NotFound
        self.assertRaises(exception.Conflict,
                          self.controller._bust_import_lock,
                          image_repo, task_repo,
                          image, task, task.id)

    def test_enforce_lock_log_not_bustable(self, task_status='processing'):
        task = test_tasks_resource._db_fixture(
            test_tasks_resource.UUID1,
            status=task_status)
        self.db.task_create(None, task)
        request = unit_test_utils.get_fake_request(tenant=TENANT1)
        image = FakeImage()
        image.extra_properties['os_glance_import_task'] = task['id']

        # Freeze time to make this repeatable
        time_fixture = fixture.TimeFixture(task['updated_at'] +
                                           datetime.timedelta(minutes=55))
        self.useFixture(time_fixture)

        expected_expire = 300
        if task_status == 'pending':
            # NOTE(danms): Tasks in 'pending' get double the expiry time,
            # so we'd be expecting an extra hour here.
            expected_expire += 3600

        with mock.patch.object(glance.api.v2.images, 'LOG') as mock_log:
            self.assertRaises(exception.Conflict,
                              self.controller._enforce_import_lock,
                              request, image)
            mock_log.warning.assert_called_once_with(
                'Image %(image)s has active import task %(task)s in '
                'status %(status)s; lock remains valid for %(expire)i '
                'more seconds',
                {'image': image.id,
                 'task': task['id'],
                 'status': task_status,
                 'expire': expected_expire})

    def test_enforce_lock_pending_takes_longer(self):
        self.test_enforce_lock_log_not_bustable(task_status='pending')

    def test_delete_encryption_key_no_encryption_key(self):
        request = unit_test_utils.get_fake_request()
        fake_encryption_key = self.controller._key_manager.store(
            request.context, mock.Mock())
        image = _domain_fixture(
            UUID2, name='image-2', owner=TENANT2,
            checksum='ca425b88f047ce8ec45ee90e813ada91',
            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
            created_at=DATETIME, updated_at=DATETIME, size=1024,
            virtual_size=3072, extra_properties={})
        self.controller._delete_encryption_key(request.context, image)
        # Make sure the encryption key is still there
        key = self.controller._key_manager.get(request.context,
                                               fake_encryption_key)
        self.assertEqual(fake_encryption_key, key._id)

    def test_delete_encryption_key_no_deletion_policy(self):
        request = unit_test_utils.get_fake_request()
        fake_encryption_key = self.controller._key_manager.store(
            request.context, mock.Mock())
        props = {
            'cinder_encryption_key_id': fake_encryption_key,
        }
        image = _domain_fixture(
            UUID2, name='image-2', owner=TENANT2,
            checksum='ca425b88f047ce8ec45ee90e813ada91',
            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
            created_at=DATETIME, updated_at=DATETIME, size=1024,
            virtual_size=3072, extra_properties=props)
        self.controller._delete_encryption_key(request.context, image)
        # Make sure the encryption key is still there
        key = self.controller._key_manager.get(request.context,
                                               fake_encryption_key)
        self.assertEqual(fake_encryption_key, key._id)

    def test_delete_encryption_key_do_not_delete(self):
        request = unit_test_utils.get_fake_request()
        fake_encryption_key = self.controller._key_manager.store(
            request.context, mock.Mock())
        props = {
            'cinder_encryption_key_id': fake_encryption_key,
            'cinder_encryption_key_deletion_policy': 'do_not_delete',
        }
        image = _domain_fixture(
            UUID2, name='image-2', owner=TENANT2,
            checksum='ca425b88f047ce8ec45ee90e813ada91',
            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
            created_at=DATETIME, updated_at=DATETIME, size=1024,
            virtual_size=3072, extra_properties=props)
        self.controller._delete_encryption_key(request.context, image)
        # Make sure the encryption key is still there
        key = self.controller._key_manager.get(request.context,
                                               fake_encryption_key)
        self.assertEqual(fake_encryption_key, key._id)

    def test_delete_encryption_key_forbidden(self):
        request = unit_test_utils.get_fake_request()
        fake_encryption_key = self.controller._key_manager.store(
            request.context, mock.Mock())
        props = {
            'cinder_encryption_key_id': fake_encryption_key,
            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
        }
        image = _domain_fixture(
            UUID2, name='image-2', owner=TENANT2,
            checksum='ca425b88f047ce8ec45ee90e813ada91',
            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
            created_at=DATETIME, updated_at=DATETIME, size=1024,
            virtual_size=3072, extra_properties=props)
        with mock.patch.object(self.controller._key_manager, 'delete',
                               side_effect=castellan_exception.Forbidden):
            self.controller._delete_encryption_key(request.context, image)
        # Make sure the encryption key is still there
        key = self.controller._key_manager.get(request.context,
                                               fake_encryption_key)
        self.assertEqual(fake_encryption_key, key._id)

    def test_delete_encryption_key_not_found(self):
        request = unit_test_utils.get_fake_request()
        fake_encryption_key = self.controller._key_manager.store(
            request.context, mock.Mock())
        props = {
            'cinder_encryption_key_id': fake_encryption_key,
            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
        }
        image = _domain_fixture(
            UUID2, name='image-2', owner=TENANT2,
            checksum='ca425b88f047ce8ec45ee90e813ada91',
            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
            created_at=DATETIME, updated_at=DATETIME, size=1024,
            virtual_size=3072, extra_properties=props)
        with mock.patch.object(self.controller._key_manager, 'delete',
                               side_effect=castellan_exception.ManagedObjectNotFoundError):  # noqa
            self.controller._delete_encryption_key(request.context, image)
        # Make sure the encryption key is still there
        key = self.controller._key_manager.get(request.context,
                                               fake_encryption_key)
        self.assertEqual(fake_encryption_key, key._id)

    def test_delete_encryption_key_error(self):
        request = unit_test_utils.get_fake_request()
        fake_encryption_key = self.controller._key_manager.store(
            request.context, mock.Mock())
        props = {
            'cinder_encryption_key_id': fake_encryption_key,
            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
        }
        image = _domain_fixture(
            UUID2, name='image-2', owner=TENANT2,
            checksum='ca425b88f047ce8ec45ee90e813ada91',
            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
            created_at=DATETIME, updated_at=DATETIME, size=1024,
            virtual_size=3072, extra_properties=props)
        with mock.patch.object(self.controller._key_manager, 'delete',
                               side_effect=castellan_exception.KeyManagerError):  # noqa
            self.controller._delete_encryption_key(request.context, image)
        # Make sure the encryption key is still there
        key = self.controller._key_manager.get(request.context,
                                               fake_encryption_key)
        self.assertEqual(fake_encryption_key, key._id)

    def test_delete_encryption_key(self):
        request = unit_test_utils.get_fake_request()
        fake_encryption_key = self.controller._key_manager.store(
            request.context, mock.Mock())
        props = {
            'cinder_encryption_key_id': fake_encryption_key,
            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
        }
        image = _domain_fixture(
            UUID2, name='image-2', owner=TENANT2,
            checksum='ca425b88f047ce8ec45ee90e813ada91',
            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
            created_at=DATETIME, updated_at=DATETIME, size=1024,
            virtual_size=3072, extra_properties=props)
        self.controller._delete_encryption_key(request.context, image)
        # Make sure the encryption key is gone
        self.assertRaises(KeyError,
                          self.controller._key_manager.get,
                          request.context, fake_encryption_key)

    def test_delete_no_encryption_key_id(self):
        request = unit_test_utils.get_fake_request()
        extra_props = {
            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
        }
        created_image = self.controller.create(request,
                                               image={'name': 'image-1'},
                                               extra_properties=extra_props,
                                               tags=[])
        image_id = created_image.image_id
        self.controller.delete(request, image_id)
        # Ensure that image is deleted
        image = self.db.image_get(request.context, image_id,
                                  force_show_deleted=True)
        self.assertTrue(image['deleted'])
        self.assertEqual('deleted', image['status'])

    def test_delete_invalid_encryption_key_id(self):
        request = unit_test_utils.get_fake_request()
        extra_props = {
            'cinder_encryption_key_id': 'invalid',
            'cinder_encryption_key_deletion_policy': 'on_image_deletion',
        }
        created_image = self.controller.create(request,
                                               image={'name': 'image-1'},
                                               extra_properties=extra_props,
                                               tags=[])
        image_id = created_image.image_id
        self.controller.delete(request, image_id)
        # Ensure that image is deleted
        image = self.db.image_get(request.context, image_id,
                                  force_show_deleted=True)
        self.assertTrue(image['deleted'])
        self.assertEqual('deleted', image['status'])

    def test_delete_invalid_encryption_key_deletion_policy(self):
        request = unit_test_utils.get_fake_request()
        extra_props = {
            'cinder_encryption_key_deletion_policy': 'invalid',
        }
        created_image = self.controller.create(request,
                                               image={'name': 'image-1'},
                                               extra_properties=extra_props,
                                               tags=[])
        image_id = created_image.image_id
        self.controller.delete(request, image_id)
        # Ensure that image is deleted
        image = self.db.image_get(request.context, image_id,
                                  force_show_deleted=True)
        self.assertTrue(image['deleted'])
        self.assertEqual('deleted', image['status'])


class TestImagesControllerPolicies(base.IsolatedUnitTest):

    def setUp(self):
        super(TestImagesControllerPolicies, self).setUp()
        self.db = unit_test_utils.FakeDB()
        self.policy = unit_test_utils.FakePolicyEnforcer()
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                self.policy)
        store = unit_test_utils.FakeStoreAPI()
        self.store_utils = unit_test_utils.FakeStoreUtils(store)

    def test_index_unauthorized(self):
        rules = {"get_images": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.index,
                          request)

    def test_show_unauthorized(self):
        # Make sure that if policy says we can't see the image that we get a
        # NotFound result instead of a Forbidden one.
        rules = {"get_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
                          request, image_id=UUID2)

    def test_create_image_unauthorized(self):
        rules = {"add_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        image = {'name': 'image-1'}
        extra_properties = {}
        tags = []
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.create,
                          request, image, extra_properties, tags)

    def test_create_public_image_unauthorized(self):
        rules = {"publicize_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        image = {'name': 'image-1', 'visibility': 'public'}
        extra_properties = {}
        tags = []
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.create,
                          request, image, extra_properties, tags)

    def test_create_community_image_unauthorized(self):
        rules = {"communitize_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        image = {'name': 'image-c1', 'visibility': 'community'}
        extra_properties = {}
        tags = []
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.create,
                          request, image, extra_properties, tags)

    def test_update_unauthorized(self):
        rules = {"modify_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['name'], 'value': 'image-2'}]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          request, UUID1, changes)

    def test_update_publicize_image_unauthorized(self):
        rules = {"publicize_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['visibility'],
                    'value': 'public'}]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          request, UUID1, changes)

    def test_update_communitize_image_unauthorized(self):
        rules = {"communitize_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['visibility'],
                    'value': 'community'}]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          request, UUID1, changes)

    def test_update_depublicize_image_unauthorized(self):
        rules = {"publicize_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['visibility'],
                    'value': 'private'}]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual('private', output.visibility)

    def test_update_decommunitize_image_unauthorized(self):
        rules = {"communitize_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['visibility'],
                    'value': 'private'}]
        output = self.controller.update(request, UUID1, changes)
        self.assertEqual('private', output.visibility)

    def test_update_get_image_location_unauthorized(self):
        rules = {"get_image_location": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['locations'], 'value': []}]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          request, UUID1, changes)

    def test_update_set_image_location_unauthorized(self):
        def fake_delete_image_location_from_backend(self, *args, **kwargs):
            pass

        rules = {"set_image_location": False}
        self.policy.set_rules(rules)
        new_location = {'url': '%s/fake_location' % BASE_URI, 'metadata': {}}
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'add', 'path': ['locations', '-'],
                    'value': new_location}]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          request, UUID1, changes)

    def test_update_delete_image_location_unauthorized(self):
        rules = {"delete_image_location": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        changes = [{'op': 'replace', 'path': ['locations'], 'value': []}]
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
                          request, UUID1, changes)

    def test_delete_unauthorized(self):
        rules = {"delete_image": False}
        self.policy.set_rules(rules)
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
                          request, UUID1)


class TestImagesDeserializer(test_utils.BaseTestCase):

    def setUp(self):
        super(TestImagesDeserializer, self).setUp()
        self.deserializer = glance.api.v2.images.RequestDeserializer()

    def test_create_minimal(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({})
        output = self.deserializer.create(request)
        expected = {'image': {}, 'extra_properties': {}, 'tags': []}
        self.assertEqual(expected, output)

    def test_create_invalid_id(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({'id': 'gabe'})
        self.assertRaises(webob.exc.HTTPBadRequest, self.deserializer.create,
                          request)

    def test_create_id_to_image_id(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({'id': UUID4})
        output = self.deserializer.create(request)
        expected = {'image': {'image_id': UUID4},
                    'extra_properties': {},
                    'tags': []}
        self.assertEqual(expected, output)

    def test_create_no_body(self):
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPBadRequest, self.deserializer.create,
                          request)

    def test_create_full(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({
            'id': UUID3,
            'name': 'image-1',
            'visibility': 'public',
            'tags': ['one', 'two'],
            'container_format': 'ami',
            'disk_format': 'ami',
            'min_ram': 128,
            'min_disk': 10,
            'foo': 'bar',
            'protected': True,
        })
        output = self.deserializer.create(request)
        properties = {
            'image_id': UUID3,
            'name': 'image-1',
            'visibility': 'public',
            'container_format': 'ami',
            'disk_format': 'ami',
            'min_ram': 128,
            'min_disk': 10,
            'protected': True,
        }
        self.maxDiff = None
        expected = {'image': properties,
                    'extra_properties': {'foo': 'bar'},
                    'tags': ['one', 'two']}
        self.assertEqual(expected, output)

    def test_create_invalid_property_key(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({
            'id': UUID3,
            'name': 'image-1',
            'visibility': 'public',
            'tags': ['one', 'two'],
            'container_format': 'ami',
            'disk_format': 'ami',
            'min_ram': 128,
            'min_disk': 10,
            'f' * 256: 'bar',
            'protected': True,
        })
        self.assertRaises(webob.exc.HTTPBadRequest, self.deserializer.create,
                          request)

    def test_create_readonly_attributes_forbidden(self):
        bodies = [
            {'direct_url': 'http://example.com'},
            {'self': 'http://example.com'},
            {'file': 'http://example.com'},
            {'schema': 'http://example.com'},
            {'os_glance_foo': 'foo'},
        ]

        for body in bodies:
            request = unit_test_utils.get_fake_request()
            request.body = jsonutils.dump_as_bytes(body)
            self.assertRaises(webob.exc.HTTPForbidden,
                              self.deserializer.create, request)

    def _get_fake_patch_request(self, content_type_minor_version=1):
        request = unit_test_utils.get_fake_request()
        template = 'application/openstack-images-v2.%d-json-patch'
        request.content_type = template % content_type_minor_version
        return request

    def test_update_empty_body(self):
        request = self._get_fake_patch_request()
        request.body = jsonutils.dump_as_bytes([])
        output = self.deserializer.update(request)
        expected = {'changes': []}
        self.assertEqual(expected, output)

    def test_update_unsupported_content_type(self):
        request = unit_test_utils.get_fake_request()
        request.content_type = 'application/json-patch'
        request.body = jsonutils.dump_as_bytes([])
        try:
            self.deserializer.update(request)
        except webob.exc.HTTPUnsupportedMediaType as e:
            # desired result, but must have correct Accept-Patch header
            accept_patch = ['application/openstack-images-v2.1-json-patch',
                            'application/openstack-images-v2.0-json-patch']
            expected = ', '.join(sorted(accept_patch))
            self.assertEqual(expected, e.headers['Accept-Patch'])
        else:
            self.fail('Did not raise HTTPUnsupportedMediaType')

    def test_update_body_not_a_list(self):
        bodies = [
            {'op': 'add', 'path': '/someprop', 'value': 'somevalue'},
            'just some string',
            123,
            True,
            False,
            None,
        ]
        for body in bodies:
            request = self._get_fake_patch_request()
            request.body = jsonutils.dump_as_bytes(body)
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.deserializer.update, request)

    def test_update_invalid_changes(self):
        changes = [
            ['a', 'list', 'of', 'stuff'],
            'just some string',
            123,
            True,
            False,
            None,
            {'op': 'invalid', 'path': '/name', 'value': 'fedora'}
        ]
        for change in changes:
            request = self._get_fake_patch_request()
            request.body = jsonutils.dump_as_bytes([change])
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.deserializer.update, request)

    def test_update_invalid_validation_data(self):
        request = self._get_fake_patch_request()
        changes = [{
            'op': 'add',
            'path': '/locations/0',
            'value': {
                'url': 'http://localhost/fake',
                'metadata': {},
            }
        }]

        changes[0]['value']['validation_data'] = {
            'os_hash_algo': 'sha512',
            'os_hash_value': MULTIHASH1,
            'checksum': CHKSUM,
        }
        request.body = jsonutils.dump_as_bytes(changes)
        self.deserializer.update(request)

        changes[0]['value']['validation_data'] = {
            'os_hash_algo': 'sha512',
            'os_hash_value': MULTIHASH1,
            'checksum': CHKSUM,
            'bogus_key': 'bogus_value',
        }
        request.body = jsonutils.dump_as_bytes(changes)
        self.assertRaisesRegex(
            webob.exc.HTTPBadRequest,
            'Additional properties are not allowed',
            self.deserializer.update, request)

        changes[0]['value']['validation_data'] = {
            'checksum': CHKSUM,
        }
        request.body = jsonutils.dump_as_bytes(changes)
        self.assertRaisesRegex(
            webob.exc.HTTPBadRequest,
            'os_hash.* is a required property',
            self.deserializer.update, request)

    def test_update(self):
        request = self._get_fake_patch_request()
        body = [
            {'op': 'replace', 'path': '/name', 'value': 'fedora'},
            {'op': 'replace', 'path': '/tags', 'value': ['king', 'kong']},
            {'op': 'replace', 'path': '/foo', 'value': 'bar'},
            {'op': 'add', 'path': '/bebim', 'value': 'bap'},
            {'op': 'remove', 'path': '/sparks'},
            {'op': 'add', 'path': '/locations/-',
             'value': {'url': 'scheme3://path3', 'metadata': {}}},
            {'op': 'add', 'path': '/locations/10',
             'value': {'url': 'scheme4://path4', 'metadata': {}}},
            {'op': 'remove', 'path': '/locations/2'},
            {'op': 'replace', 'path': '/locations', 'value': []},
            {'op': 'replace', 'path': '/locations',
             'value': [{'url': 'scheme5://path5', 'metadata': {}},
                       {'url': 'scheme6://path6', 'metadata': {}}]},
        ]
        request.body = jsonutils.dump_as_bytes(body)
        output = self.deserializer.update(request)
        expected = {'changes': [
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['name'], 'value': 'fedora'},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['tags'], 'value': ['king', 'kong']},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['foo'], 'value': 'bar'},
            {'json_schema_version': 10, 'op': 'add',
             'path': ['bebim'], 'value': 'bap'},
            {'json_schema_version': 10, 'op': 'remove',
             'path': ['sparks']},
            {'json_schema_version': 10, 'op': 'add',
             'path': ['locations', '-'],
             'value': {'url': 'scheme3://path3', 'metadata': {}}},
            {'json_schema_version': 10, 'op': 'add',
             'path': ['locations', '10'],
             'value': {'url': 'scheme4://path4', 'metadata': {}}},
            {'json_schema_version': 10, 'op': 'remove',
             'path': ['locations', '2']},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['locations'], 'value': []},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['locations'],
             'value': [{'url': 'scheme5://path5', 'metadata': {}},
                       {'url': 'scheme6://path6', 'metadata': {}}]},
        ]}
        self.assertEqual(expected, output)

    def test_update_v2_0_compatibility(self):
        request = self._get_fake_patch_request(content_type_minor_version=0)
        body = [
            {'replace': '/name', 'value': 'fedora'},
            {'replace': '/tags', 'value': ['king', 'kong']},
            {'replace': '/foo', 'value': 'bar'},
            {'add': '/bebim', 'value': 'bap'},
            {'remove': '/sparks'},
            {'add': '/locations/-', 'value': {'url': 'scheme3://path3',
                                              'metadata': {}}},
            {'add': '/locations/10', 'value': {'url': 'scheme4://path4',
                                               'metadata': {}}},
            {'remove': '/locations/2'},
            {'replace': '/locations', 'value': []},
            {'replace': '/locations',
             'value': [{'url': 'scheme5://path5', 'metadata': {}},
                       {'url': 'scheme6://path6', 'metadata': {}}]},
        ]
        request.body = jsonutils.dump_as_bytes(body)
        output = self.deserializer.update(request)
        expected = {'changes': [
            {'json_schema_version': 4, 'op': 'replace',
             'path': ['name'], 'value': 'fedora'},
            {'json_schema_version': 4, 'op': 'replace',
             'path': ['tags'], 'value': ['king', 'kong']},
            {'json_schema_version': 4, 'op': 'replace',
             'path': ['foo'], 'value': 'bar'},
            {'json_schema_version': 4, 'op': 'add',
             'path': ['bebim'], 'value': 'bap'},
            {'json_schema_version': 4, 'op': 'remove', 'path': ['sparks']},
            {'json_schema_version': 4, 'op': 'add',
             'path': ['locations', '-'],
             'value': {'url': 'scheme3://path3', 'metadata': {}}},
            {'json_schema_version': 4, 'op': 'add',
             'path': ['locations', '10'],
             'value': {'url': 'scheme4://path4', 'metadata': {}}},
            {'json_schema_version': 4, 'op': 'remove',
             'path': ['locations', '2']},
            {'json_schema_version': 4, 'op': 'replace',
             'path': ['locations'], 'value': []},
            {'json_schema_version': 4, 'op': 'replace', 'path': ['locations'],
             'value': [{'url': 'scheme5://path5', 'metadata': {}},
                       {'url': 'scheme6://path6', 'metadata': {}}]},
        ]}
        self.assertEqual(expected, output)

    def test_update_base_attributes(self):
        request = self._get_fake_patch_request()
        body = [
            {'op': 'replace', 'path': '/name', 'value': 'fedora'},
            {'op': 'replace', 'path': '/visibility', 'value': 'public'},
            {'op': 'replace', 'path': '/tags', 'value': ['king', 'kong']},
            {'op': 'replace', 'path': '/protected', 'value': True},
            {'op': 'replace', 'path': '/container_format', 'value': 'bare'},
            {'op': 'replace', 'path': '/disk_format', 'value': 'raw'},
            {'op': 'replace', 'path': '/min_ram', 'value': 128},
            {'op': 'replace', 'path': '/min_disk', 'value': 10},
            {'op': 'replace', 'path': '/locations', 'value': []},
            {'op': 'replace', 'path': '/locations',
             'value': [{'url': 'scheme5://path5', 'metadata': {}},
                       {'url': 'scheme6://path6', 'metadata': {}}]}
        ]
        request.body = jsonutils.dump_as_bytes(body)
        output = self.deserializer.update(request)
        expected = {'changes': [
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['name'], 'value': 'fedora'},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['visibility'], 'value': 'public'},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['tags'], 'value': ['king', 'kong']},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['protected'], 'value': True},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['container_format'], 'value': 'bare'},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['disk_format'], 'value': 'raw'},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['min_ram'], 'value': 128},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['min_disk'], 'value': 10},
            {'json_schema_version': 10, 'op': 'replace',
             'path': ['locations'], 'value': []},
            {'json_schema_version': 10, 'op': 'replace', 'path': ['locations'],
             'value': [{'url': 'scheme5://path5', 'metadata': {}},
                       {'url': 'scheme6://path6', 'metadata': {}}]}
        ]}
        self.assertEqual(expected, output)

    def test_update_disallowed_attributes(self):
        samples = {
            'direct_url': '/a/b/c/d',
            'self': '/e/f/g/h',
            'file': '/e/f/g/h/file',
            'schema': '/i/j/k',
        }

        for key, value in samples.items():
            request = self._get_fake_patch_request()
            body = [{'op': 'replace', 'path': '/%s' % key, 'value': value}]
            request.body = jsonutils.dump_as_bytes(body)
            try:
                self.deserializer.update(request)
            except webob.exc.HTTPForbidden:
                pass  # desired behavior
            else:
                self.fail("Updating %s did not result in HTTPForbidden" % key)

    def test_update_readonly_attributes(self):
        samples = {
            'id': '00000000-0000-0000-0000-000000000000',
            'status': 'active',
            'checksum': 'abcdefghijklmnopqrstuvwxyz012345',
            'os_hash_algo': 'supersecure',
            'os_hash_value': 'a' * 32 + 'b' * 32 + 'c' * 32 + 'd' * 32,
            'size': 9001,
            'virtual_size': 9001,
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
        }

        for key, value in samples.items():
            request = self._get_fake_patch_request()
            body = [{'op': 'replace', 'path': '/%s' % key, 'value': value}]
            request.body = jsonutils.dump_as_bytes(body)
            try:
                self.deserializer.update(request)
            except webob.exc.HTTPForbidden:
                pass  # desired behavior
            else:
                self.fail("Updating %s did not result in HTTPForbidden" % key)

    def test_update_reserved_attributes(self):
        samples = {
            'deleted': False,
            'deleted_at': ISOTIME,
            'os_glance_import_task': 'foo',
            'os_glance_anything': 'bar',
            'os_glance_': 'baz',
            'os_glance': 'bat',
        }

        for key, value in samples.items():
            request = self._get_fake_patch_request()
            body = [{'op': 'replace', 'path': '/%s' % key, 'value': value}]
            request.body = jsonutils.dump_as_bytes(body)
            try:
                self.deserializer.update(request)
            except webob.exc.HTTPForbidden:
                pass  # desired behavior
            else:
                self.fail("Updating %s did not result in HTTPForbidden" % key)

    def test_update_invalid_attributes(self):
        keys = [
            'noslash',
            '///twoslash',
            '/two/   /slash',
            '/      /      ',
            '/trailingslash/',
            '/lone~tilde',
            '/trailingtilde~'
        ]

        for key in keys:
            request = self._get_fake_patch_request()
            body = [{'op': 'replace', 'path': '%s' % key, 'value': 'dummy'}]
            request.body = jsonutils.dump_as_bytes(body)
            try:
                self.deserializer.update(request)
            except webob.exc.HTTPBadRequest:
                pass  # desired behavior
            else:
                self.fail("Updating %s did not result in HTTPBadRequest" % key)

    def test_update_pointer_encoding(self):
        samples = {
            '/keywith~1slash': ['keywith/slash'],
            '/keywith~0tilde': ['keywith~tilde'],
            '/tricky~01': ['tricky~1'],
        }

        for encoded, decoded in samples.items():
            request = self._get_fake_patch_request()
            doc = [{'op': 'replace', 'path': '%s' % encoded, 'value': 'dummy'}]
            request.body = jsonutils.dump_as_bytes(doc)
            output = self.deserializer.update(request)
            self.assertEqual(decoded, output['changes'][0]['path'])

    def test_update_deep_limited_attributes(self):
        samples = {
            'locations/1/2': [],
        }

        for key, value in samples.items():
            request = self._get_fake_patch_request()
            body = [{'op': 'replace', 'path': '/%s' % key, 'value': value}]
            request.body = jsonutils.dump_as_bytes(body)
            try:
                self.deserializer.update(request)
            except webob.exc.HTTPBadRequest:
                pass  # desired behavior
            else:
                self.fail("Updating %s did not result in HTTPBadRequest" % key)

    def test_update_v2_1_missing_operations(self):
        request = self._get_fake_patch_request()
        body = [{'path': '/colburn', 'value': 'arcata'}]
        request.body = jsonutils.dump_as_bytes(body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update, request)

    def test_update_v2_1_missing_value(self):
        request = self._get_fake_patch_request()
        body = [{'op': 'replace', 'path': '/colburn'}]
        request.body = jsonutils.dump_as_bytes(body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update, request)

    def test_update_v2_1_missing_path(self):
        request = self._get_fake_patch_request()
        body = [{'op': 'replace', 'value': 'arcata'}]
        request.body = jsonutils.dump_as_bytes(body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update, request)

    def test_update_v2_0_multiple_operations(self):
        request = self._get_fake_patch_request(content_type_minor_version=0)
        body = [{'replace': '/foo', 'add': '/bar', 'value': 'snore'}]
        request.body = jsonutils.dump_as_bytes(body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update, request)

    def test_update_v2_0_missing_operations(self):
        request = self._get_fake_patch_request(content_type_minor_version=0)
        body = [{'value': 'arcata'}]
        request.body = jsonutils.dump_as_bytes(body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update, request)

    def test_update_v2_0_missing_value(self):
        request = self._get_fake_patch_request(content_type_minor_version=0)
        body = [{'replace': '/colburn'}]
        request.body = jsonutils.dump_as_bytes(body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update, request)

    def test_index(self):
        marker = str(uuid.uuid4())
        path = '/images?limit=1&marker=%s&member_status=pending' % marker
        request = unit_test_utils.get_fake_request(path)
        expected = {'limit': 1,
                    'marker': marker,
                    'sort_key': ['created_at'],
                    'sort_dir': ['desc'],
                    'member_status': 'pending',
                    'filters': {}}
        output = self.deserializer.index(request)
        self.assertEqual(expected, output)

    def test_index_with_filter(self):
        name = 'My Little Image'
        path = '/images?name=%s' % name
        request = unit_test_utils.get_fake_request(path)
        output = self.deserializer.index(request)
        self.assertEqual(name, output['filters']['name'])

    def test_index_strip_params_from_filters(self):
        name = 'My Little Image'
        path = '/images?name=%s' % name
        request = unit_test_utils.get_fake_request(path)
        output = self.deserializer.index(request)
        self.assertEqual(name, output['filters']['name'])
        self.assertEqual(1, len(output['filters']))

    def test_index_with_many_filter(self):
        name = 'My Little Image'
        instance_id = str(uuid.uuid4())
        path = ('/images?name=%(name)s&id=%(instance_id)s' %
                {'name': name, 'instance_id': instance_id})
        request = unit_test_utils.get_fake_request(path)
        output = self.deserializer.index(request)
        self.assertEqual(name, output['filters']['name'])
        self.assertEqual(instance_id, output['filters']['id'])

    def test_index_with_filter_and_limit(self):
        name = 'My Little Image'
        path = '/images?name=%s&limit=1' % name
        request = unit_test_utils.get_fake_request(path)
        output = self.deserializer.index(request)
        self.assertEqual(name, output['filters']['name'])
        self.assertEqual(1, output['limit'])

    def test_index_non_integer_limit(self):
        request = unit_test_utils.get_fake_request('/images?limit=blah')
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_zero_limit(self):
        request = unit_test_utils.get_fake_request('/images?limit=0')
        expected = {'limit': 0,
                    'sort_key': ['created_at'],
                    'member_status': 'accepted',
                    'sort_dir': ['desc'],
                    'filters': {}}
        output = self.deserializer.index(request)
        self.assertEqual(expected, output)

    def test_index_negative_limit(self):
        request = unit_test_utils.get_fake_request('/images?limit=-1')
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_fraction(self):
        request = unit_test_utils.get_fake_request('/images?limit=1.1')
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_invalid_status(self):
        path = '/images?member_status=blah'
        request = unit_test_utils.get_fake_request(path)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_marker(self):
        marker = str(uuid.uuid4())
        path = '/images?marker=%s' % marker
        request = unit_test_utils.get_fake_request(path)
        output = self.deserializer.index(request)
        self.assertEqual(marker, output.get('marker'))

    def test_index_marker_not_specified(self):
        request = unit_test_utils.get_fake_request('/images')
        output = self.deserializer.index(request)
        self.assertNotIn('marker', output)

    def test_index_limit_not_specified(self):
        request = unit_test_utils.get_fake_request('/images')
        output = self.deserializer.index(request)
        self.assertNotIn('limit', output)

    def test_index_sort_key_id(self):
        request = unit_test_utils.get_fake_request('/images?sort_key=id')
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['id'],
            'sort_dir': ['desc'],
            'member_status': 'accepted',
            'filters': {}
        }
        self.assertEqual(expected, output)

    def test_index_multiple_sort_keys(self):
        request = unit_test_utils.get_fake_request('/images?'
                                                   'sort_key=name&'
                                                   'sort_key=size')
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name', 'size'],
            'sort_dir': ['desc'],
            'member_status': 'accepted',
            'filters': {}
        }
        self.assertEqual(expected, output)

    def test_index_invalid_multiple_sort_keys(self):
        # blah is an invalid sort key
        request = unit_test_utils.get_fake_request('/images?'
                                                   'sort_key=name&'
                                                   'sort_key=blah')
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_sort_dir_asc(self):
        request = unit_test_utils.get_fake_request('/images?sort_dir=asc')
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['created_at'],
            'sort_dir': ['asc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

    def test_index_multiple_sort_dirs(self):
        req_string = ('/images?sort_key=name&sort_dir=asc&'
                      'sort_key=id&sort_dir=desc')
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name', 'id'],
            'sort_dir': ['asc', 'desc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

    def test_index_new_sorting_syntax_single_key_default_dir(self):
        req_string = '/images?sort=name'
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name'],
            'sort_dir': ['desc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

    def test_index_new_sorting_syntax_single_key_desc_dir(self):
        req_string = '/images?sort=name:desc'
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name'],
            'sort_dir': ['desc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

    def test_index_new_sorting_syntax_multiple_keys_default_dir(self):
        req_string = '/images?sort=name,size'
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name', 'size'],
            'sort_dir': ['desc', 'desc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

    def test_index_new_sorting_syntax_multiple_keys_asc_dir(self):
        req_string = '/images?sort=name:asc,size:asc'
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name', 'size'],
            'sort_dir': ['asc', 'asc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

    def test_index_new_sorting_syntax_multiple_keys_different_dirs(self):
        req_string = '/images?sort=name:desc,size:asc'
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name', 'size'],
            'sort_dir': ['desc', 'asc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

    def test_index_new_sorting_syntax_multiple_keys_optional_dir(self):
        req_string = '/images?sort=name:asc,size'
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name', 'size'],
            'sort_dir': ['asc', 'desc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

        req_string = '/images?sort=name,size:asc'
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name', 'size'],
            'sort_dir': ['desc', 'asc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

        req_string = '/images?sort=name,id:asc,size'
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name', 'id', 'size'],
            'sort_dir': ['desc', 'asc', 'desc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

        req_string = '/images?sort=name:asc,id,size:asc'
        request = unit_test_utils.get_fake_request(req_string)
        output = self.deserializer.index(request)
        expected = {
            'sort_key': ['name', 'id', 'size'],
            'sort_dir': ['asc', 'desc', 'asc'],
            'member_status': 'accepted',
            'filters': {}}
        self.assertEqual(expected, output)

    def test_index_sort_wrong_sort_dirs_number(self):
        req_string = '/images?sort_key=name&sort_dir=asc&sort_dir=desc'
        request = unit_test_utils.get_fake_request(req_string)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_sort_dirs_fewer_than_keys(self):
        req_string = ('/images?sort_key=name&sort_dir=asc&sort_key=id&'
                      'sort_dir=asc&sort_key=created_at')
        request = unit_test_utils.get_fake_request(req_string)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_sort_wrong_sort_dirs_number_without_key(self):
        req_string = '/images?sort_dir=asc&sort_dir=desc'
        request = unit_test_utils.get_fake_request(req_string)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_sort_private_key(self):
        request = unit_test_utils.get_fake_request('/images?sort_key=min_ram')
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_sort_key_invalid_value(self):
        # blah is an invalid sort key
        request = unit_test_utils.get_fake_request('/images?sort_key=blah')
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_sort_dir_invalid_value(self):
        # foo is an invalid sort dir
        request = unit_test_utils.get_fake_request('/images?sort_dir=foo')
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_new_sorting_syntax_invalid_request(self):
        # 'blah' is not a supported sorting key
        req_string = '/images?sort=blah'
        request = unit_test_utils.get_fake_request(req_string)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

        req_string = '/images?sort=name,blah'
        request = unit_test_utils.get_fake_request(req_string)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

        # 'foo' isn't a valid sort direction
        req_string = '/images?sort=name:foo'
        request = unit_test_utils.get_fake_request(req_string)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)
        # 'asc:desc' isn't a valid sort direction
        req_string = '/images?sort=name:asc:desc'
        request = unit_test_utils.get_fake_request(req_string)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_combined_sorting_syntax(self):
        req_string = '/images?sort_dir=name&sort=name'
        request = unit_test_utils.get_fake_request(req_string)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.index, request)

    def test_index_with_tag(self):
        path = '/images?tag=%s&tag=%s' % ('x86', '64bit')
        request = unit_test_utils.get_fake_request(path)
        output = self.deserializer.index(request)
        self.assertEqual(sorted(['x86', '64bit']),
                         sorted(output['filters']['tags']))

    def test_image_import(self):
        # Bug 1754634: make sure that what's considered valid
        # is determined by the config option
        self.config(enabled_import_methods=['party-time'])
        request = unit_test_utils.get_fake_request()
        import_body = {
            "method": {
                "name": "party-time"
            }
        }
        request.body = jsonutils.dump_as_bytes(import_body)
        output = self.deserializer.import_image(request)
        expected = {"body": import_body}
        self.assertEqual(expected, output)

    def test_import_image_invalid_body(self):
        request = unit_test_utils.get_fake_request()
        import_body = {
            "method1": {
                "name": "glance-direct"
            }
        }
        request.body = jsonutils.dump_as_bytes(import_body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.import_image,
                          request)

    def test_import_image_invalid_input(self):
        request = unit_test_utils.get_fake_request()
        import_body = {
            "method": {
                "abcd": "glance-direct"
            }
        }
        request.body = jsonutils.dump_as_bytes(import_body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.import_image,
                          request)

    def test_import_image_with_all_stores_not_boolean(self):
        request = unit_test_utils.get_fake_request()
        import_body = {
            'method': {
                'name': 'glance-direct'
            },
            'all_stores': "true"
        }
        request.body = jsonutils.dump_as_bytes(import_body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.import_image,
                          request)

    def test_import_image_with_allow_failure_not_boolean(self):
        request = unit_test_utils.get_fake_request()
        import_body = {
            'method': {
                'name': 'glance-direct'
            },
            'all_stores_must_succeed': "true"
        }
        request.body = jsonutils.dump_as_bytes(import_body)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.import_image,
                          request)

    def _get_request_for_method(self, method_name):
        request = unit_test_utils.get_fake_request()
        import_body = {
            "method": {
                "name": method_name
            }
        }
        request.body = jsonutils.dump_as_bytes(import_body)
        return request

    KNOWN_IMPORT_METHODS = ['glance-direct', 'web-download', 'glance-download']

    def test_import_image_invalid_import_method(self):
        # Bug 1754634: make sure that what's considered valid
        # is determined by the config option.  So put known bad
        # name in config, and known good name in request
        self.config(enabled_import_methods=['bad-method-name'])
        for m in self.KNOWN_IMPORT_METHODS:
            request = self._get_request_for_method(m)
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.deserializer.import_image,
                              request)


class TestImagesDeserializerWithExtendedSchema(test_utils.BaseTestCase):

    def setUp(self):
        super(TestImagesDeserializerWithExtendedSchema, self).setUp()
        self.config(allow_additional_image_properties=False)
        custom_image_properties = {
            'pants': {
                'type': 'string',
                'enum': ['on', 'off'],
            },
        }
        schema = glance.api.v2.images.get_schema(custom_image_properties)
        self.deserializer = glance.api.v2.images.RequestDeserializer(schema)

    def test_create(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({
            'name': 'image-1',
            'pants': 'on'
        })
        output = self.deserializer.create(request)
        expected = {
            'image': {'name': 'image-1'},
            'extra_properties': {'pants': 'on'},
            'tags': [],
        }
        self.assertEqual(expected, output)

    def test_create_bad_data(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({
            'name': 'image-1',
            'pants': 'borked'
        })
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.create, request)

    def test_update(self):
        request = unit_test_utils.get_fake_request()
        request.content_type = 'application/openstack-images-v2.1-json-patch'
        doc = [{'op': 'add', 'path': '/pants', 'value': 'off'}]
        request.body = jsonutils.dump_as_bytes(doc)
        output = self.deserializer.update(request)
        expected = {'changes': [
            {'json_schema_version': 10, 'op': 'add',
             'path': ['pants'], 'value': 'off'},
        ]}
        self.assertEqual(expected, output)

    def test_update_bad_data(self):
        request = unit_test_utils.get_fake_request()
        request.content_type = 'application/openstack-images-v2.1-json-patch'
        doc = [{'op': 'add', 'path': '/pants', 'value': 'cutoffs'}]
        request.body = jsonutils.dump_as_bytes(doc)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update,
                          request)


class TestImagesDeserializerWithAdditionalProperties(test_utils.BaseTestCase):

    def setUp(self):
        super(TestImagesDeserializerWithAdditionalProperties, self).setUp()
        self.config(allow_additional_image_properties=True)
        self.deserializer = glance.api.v2.images.RequestDeserializer()

    def test_create(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({'foo': 'bar'})
        output = self.deserializer.create(request)
        expected = {'image': {},
                    'extra_properties': {'foo': 'bar'},
                    'tags': []}
        self.assertEqual(expected, output)

    def test_create_with_numeric_property(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({'abc': 123})
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.create, request)

    def test_update_with_numeric_property(self):
        request = unit_test_utils.get_fake_request()
        request.content_type = 'application/openstack-images-v2.1-json-patch'
        doc = [{'op': 'add', 'path': '/foo', 'value': 123}]
        request.body = jsonutils.dump_as_bytes(doc)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update, request)

    def test_create_with_list_property(self):
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({'foo': ['bar']})
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.create, request)

    def test_update_with_list_property(self):
        request = unit_test_utils.get_fake_request()
        request.content_type = 'application/openstack-images-v2.1-json-patch'
        doc = [{'op': 'add', 'path': '/foo', 'value': ['bar', 'baz']}]
        request.body = jsonutils.dump_as_bytes(doc)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update, request)

    def test_update(self):
        request = unit_test_utils.get_fake_request()
        request.content_type = 'application/openstack-images-v2.1-json-patch'
        doc = [{'op': 'add', 'path': '/foo', 'value': 'bar'}]
        request.body = jsonutils.dump_as_bytes(doc)
        output = self.deserializer.update(request)
        change = {
            'json_schema_version': 10, 'op': 'add',
            'path': ['foo'], 'value': 'bar'
        }
        self.assertEqual({'changes': [change]}, output)


class TestImagesDeserializerNoAdditionalProperties(test_utils.BaseTestCase):

    def setUp(self):
        super(TestImagesDeserializerNoAdditionalProperties, self).setUp()
        self.config(allow_additional_image_properties=False)
        self.deserializer = glance.api.v2.images.RequestDeserializer()

    def test_create_with_additional_properties_disallowed(self):
        self.config(allow_additional_image_properties=False)
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({'foo': 'bar'})
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.create, request)

    def test_neg_create_with_stores(self):
        self.config(allow_additional_image_properties=True)
        request = unit_test_utils.get_fake_request()
        request.body = jsonutils.dump_as_bytes({'stores': 'test'})
        self.assertRaises(webob.exc.HTTPForbidden,
                          self.deserializer.create, request)

    def test_update(self):
        request = unit_test_utils.get_fake_request()
        request.content_type = 'application/openstack-images-v2.1-json-patch'
        doc = [{'op': 'add', 'path': '/foo', 'value': 'bar'}]
        request.body = jsonutils.dump_as_bytes(doc)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.deserializer.update, request)


class TestImagesSerializer(test_utils.BaseTestCase):

    def setUp(self):
        super(TestImagesSerializer, self).setUp()
        self.serializer = glance.api.v2.images.ResponseSerializer()
        self.fixtures = [
            # NOTE(bcwaldon): This first fixture has every property defined
            _domain_fixture(UUID1, name='image-1', size=1024,
                            virtual_size=3072, created_at=DATETIME,
                            updated_at=DATETIME, owner=TENANT1,
                            visibility='public', container_format='ami',
                            tags=['one', 'two'], disk_format='ami',
                            min_ram=128, min_disk=10,
                            checksum='ca425b88f047ce8ec45ee90e813ada91',
                            os_hash_algo=FAKEHASHALGO,
                            os_hash_value=MULTIHASH1),

            # NOTE(bcwaldon): This second fixture depends on default behavior
            # and sets most values to None
            _domain_fixture(UUID2, created_at=DATETIME, updated_at=DATETIME),
        ]

    def test_index(self):
        expected = {
            'images': [
                {
                    'id': UUID1,
                    'name': 'image-1',
                    'status': 'queued',
                    'visibility': 'public',
                    'protected': False,
                    'os_hidden': False,
                    'tags': set(['one', 'two']),
                    'size': 1024,
                    'virtual_size': 3072,
                    'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
                    'os_hash_algo': FAKEHASHALGO,
                    'os_hash_value': MULTIHASH1,
                    'container_format': 'ami',
                    'disk_format': 'ami',
                    'min_ram': 128,
                    'min_disk': 10,
                    'created_at': ISOTIME,
                    'updated_at': ISOTIME,
                    'self': '/v2/images/%s' % UUID1,
                    'file': '/v2/images/%s/file' % UUID1,
                    'schema': '/v2/schemas/image',
                    'owner': '6838eb7b-6ded-434a-882c-b344c77fe8df',
                },
                {
                    'id': UUID2,
                    'status': 'queued',
                    'visibility': 'private',
                    'protected': False,
                    'os_hidden': False,
                    'tags': set([]),
                    'created_at': ISOTIME,
                    'updated_at': ISOTIME,
                    'self': '/v2/images/%s' % UUID2,
                    'file': '/v2/images/%s/file' % UUID2,
                    'schema': '/v2/schemas/image',
                    'size': None,
                    'name': None,
                    'owner': None,
                    'min_ram': None,
                    'min_disk': None,
                    'checksum': None,
                    'os_hash_algo': None,
                    'os_hash_value': None,
                    'disk_format': None,
                    'virtual_size': None,
                    'container_format': None,

                },
            ],
            'first': '/v2/images',
            'schema': '/v2/schemas/images',
        }
        request = webob.Request.blank('/v2/images')
        response = webob.Response(request=request)
        result = {'images': self.fixtures}
        self.serializer.index(response, result)
        actual = jsonutils.loads(response.body)
        for image in actual['images']:
            image['tags'] = set(image['tags'])
        self.assertEqual(expected, actual)
        self.assertEqual('application/json', response.content_type)

    def test_index_next_marker(self):
        request = webob.Request.blank('/v2/images')
        response = webob.Response(request=request)
        result = {'images': self.fixtures, 'next_marker': UUID2}
        self.serializer.index(response, result)
        output = jsonutils.loads(response.body)
        self.assertEqual('/v2/images?marker=%s' % UUID2, output['next'])

    def test_index_carries_query_parameters(self):
        url = '/v2/images?limit=10&sort_key=id&sort_dir=asc'
        request = webob.Request.blank(url)
        response = webob.Response(request=request)
        result = {'images': self.fixtures, 'next_marker': UUID2}
        self.serializer.index(response, result)
        output = jsonutils.loads(response.body)

        expected_url = '/v2/images?limit=10&sort_dir=asc&sort_key=id'
        self.assertEqual(unit_test_utils.sort_url_by_qs_keys(expected_url),
                         unit_test_utils.sort_url_by_qs_keys(output['first']))
        expect_next = '/v2/images?limit=10&marker=%s&sort_dir=asc&sort_key=id'
        self.assertEqual(unit_test_utils.sort_url_by_qs_keys(
                         expect_next % UUID2),
                         unit_test_utils.sort_url_by_qs_keys(output['next']))

    def test_index_forbidden_get_image_location(self):
        """Make sure the serializer works fine.

        No matter if current user is authorized to get image location if the
        show_multiple_locations is False.

        """
        class ImageLocations(object):

            def __len__(self):
                raise exception.Forbidden()

        self.config(show_multiple_locations=False)
        self.config(show_image_direct_url=False)
        url = '/v2/images?limit=10&sort_key=id&sort_dir=asc'
        request = webob.Request.blank(url)
        response = webob.Response(request=request)
        result = {'images': self.fixtures}
        self.assertEqual(http.OK, response.status_int)

        # The image index should work though the user is forbidden
        result['images'][0].locations = ImageLocations()
        self.serializer.index(response, result)
        self.assertEqual(http.OK, response.status_int)

    def test_show_full_fixture(self):
        expected = {
            'id': UUID1,
            'name': 'image-1',
            'status': 'queued',
            'visibility': 'public',
            'protected': False,
            'os_hidden': False,
            'tags': set(['one', 'two']),
            'size': 1024,
            'virtual_size': 3072,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': FAKEHASHALGO,
            'os_hash_value': MULTIHASH1,
            'container_format': 'ami',
            'disk_format': 'ami',
            'min_ram': 128,
            'min_disk': 10,
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
            'self': '/v2/images/%s' % UUID1,
            'file': '/v2/images/%s/file' % UUID1,
            'schema': '/v2/schemas/image',
            'owner': '6838eb7b-6ded-434a-882c-b344c77fe8df',
        }
        response = webob.Response()
        self.serializer.show(response, self.fixtures[0])
        actual = jsonutils.loads(response.body)
        actual['tags'] = set(actual['tags'])
        self.assertEqual(expected, actual)
        self.assertEqual('application/json', response.content_type)

    def test_show_minimal_fixture(self):
        expected = {
            'id': UUID2,
            'status': 'queued',
            'visibility': 'private',
            'protected': False,
            'os_hidden': False,
            'tags': [],
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
            'self': '/v2/images/%s' % UUID2,
            'file': '/v2/images/%s/file' % UUID2,
            'schema': '/v2/schemas/image',
            'size': None,
            'name': None,
            'owner': None,
            'min_ram': None,
            'min_disk': None,
            'checksum': None,
            'os_hash_algo': None,
            'os_hash_value': None,
            'disk_format': None,
            'virtual_size': None,
            'container_format': None,
        }
        response = webob.Response()
        self.serializer.show(response, self.fixtures[1])
        self.assertEqual(expected, jsonutils.loads(response.body))

    def test_create(self):
        expected = {
            'id': UUID1,
            'name': 'image-1',
            'status': 'queued',
            'visibility': 'public',
            'protected': False,
            'os_hidden': False,
            'tags': ['one', 'two'],
            'size': 1024,
            'virtual_size': 3072,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': FAKEHASHALGO,
            'os_hash_value': MULTIHASH1,
            'container_format': 'ami',
            'disk_format': 'ami',
            'min_ram': 128,
            'min_disk': 10,
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
            'self': '/v2/images/%s' % UUID1,
            'file': '/v2/images/%s/file' % UUID1,
            'schema': '/v2/schemas/image',
            'owner': '6838eb7b-6ded-434a-882c-b344c77fe8df',
        }
        response = webob.Response()
        self.serializer.create(response, self.fixtures[0])
        self.assertEqual(http.CREATED, response.status_int)
        actual = jsonutils.loads(response.body)
        actual['tags'] = sorted(actual['tags'])
        self.assertEqual(expected, actual)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual('/v2/images/%s' % UUID1, response.location)

    def test_create_has_import_methods_header(self):
        # NOTE(rosmaita): enabled_import_methods is defined as type
        # oslo.config.cfg.ListOpt, so it is stored internally as a list
        # but is converted to a string for output in the HTTP header

        header_name = 'OpenStack-image-import-methods'

        # check multiple methods
        enabled_methods = ['one', 'two', 'three']
        self.config(enabled_import_methods=enabled_methods)
        response = webob.Response()
        self.serializer.create(response, self.fixtures[0])
        self.assertEqual(http.CREATED, response.status_int)
        header_value = response.headers.get(header_name)
        self.assertIsNotNone(header_value)
        self.assertCountEqual(enabled_methods, header_value.split(','))

        # check single method
        self.config(enabled_import_methods=['swift-party-time'])
        response = webob.Response()
        self.serializer.create(response, self.fixtures[0])
        self.assertEqual(http.CREATED, response.status_int)
        header_value = response.headers.get(header_name)
        self.assertIsNotNone(header_value)
        self.assertEqual('swift-party-time', header_value)

        # no header for empty config value
        self.config(enabled_import_methods=[])
        response = webob.Response()
        self.serializer.create(response, self.fixtures[0])
        self.assertEqual(http.CREATED, response.status_int)
        headers = response.headers.keys()
        self.assertNotIn(header_name, headers)

    def test_update(self):
        expected = {
            'id': UUID1,
            'name': 'image-1',
            'status': 'queued',
            'visibility': 'public',
            'protected': False,
            'os_hidden': False,
            'tags': set(['one', 'two']),
            'size': 1024,
            'virtual_size': 3072,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': FAKEHASHALGO,
            'os_hash_value': MULTIHASH1,
            'container_format': 'ami',
            'disk_format': 'ami',
            'min_ram': 128,
            'min_disk': 10,
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
            'self': '/v2/images/%s' % UUID1,
            'file': '/v2/images/%s/file' % UUID1,
            'schema': '/v2/schemas/image',
            'owner': '6838eb7b-6ded-434a-882c-b344c77fe8df',
        }
        response = webob.Response()
        self.serializer.update(response, self.fixtures[0])
        actual = jsonutils.loads(response.body)
        actual['tags'] = set(actual['tags'])
        self.assertEqual(expected, actual)
        self.assertEqual('application/json', response.content_type)

    def test_import_image(self):
        response = webob.Response()
        self.serializer.import_image(response, {})
        self.assertEqual(http.ACCEPTED, response.status_int)
        self.assertEqual('0', response.headers['Content-Length'])

    def test_image_stage_host_hidden(self):
        # Make sure that os_glance_stage_host is not exposed to clients
        response = webob.Response()
        self.serializer.show(response,
                             mock.MagicMock(extra_properties={
                                 'foo': 'bar',
                                 'os_glance_stage_host': 'http://foo'}))
        actual = jsonutils.loads(response.body)
        self.assertIn('foo', actual)
        self.assertNotIn('os_glance_stage_host', actual)


class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):

    def setUp(self):
        super(TestImagesSerializerWithUnicode, self).setUp()
        self.serializer = glance.api.v2.images.ResponseSerializer()
        self.fixtures = [
            # NOTE(bcwaldon): This first fixture has every property defined
            _domain_fixture(UUID1, **{
                'name': 'OpenStack\u2122-1',
                'size': 1024,
                'virtual_size': 3072,
                'tags': ['\u2160', '\u2161'],
                'created_at': DATETIME,
                'updated_at': DATETIME,
                'owner': TENANT1,
                'visibility': 'public',
                'container_format': 'ami',
                'disk_format': 'ami',
                'min_ram': 128,
                'min_disk': 10,
                'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
                'os_hash_algo': FAKEHASHALGO,
                'os_hash_value': MULTIHASH1,
                'extra_properties': {'lang': 'Fran\u00E7ais',
                                     'dispos\u00E9': 'f\u00E2ch\u00E9'},
            }),
        ]

    def test_index(self):
        expected = {
            'images': [
                {
                    'id': UUID1,
                    'name': 'OpenStack\u2122-1',
                    'status': 'queued',
                    'visibility': 'public',
                    'protected': False,
                    'os_hidden': False,
                    'tags': ['\u2160', '\u2161'],
                    'size': 1024,
                    'virtual_size': 3072,
                    'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
                    'os_hash_algo': str(FAKEHASHALGO),
                    'os_hash_value': str(MULTIHASH1),
                    'container_format': 'ami',
                    'disk_format': 'ami',
                    'min_ram': 128,
                    'min_disk': 10,
                    'created_at': str(ISOTIME),
                    'updated_at': str(ISOTIME),
                    'self': '/v2/images/%s' % UUID1,
                    'file': '/v2/images/%s/file' % UUID1,
                    'schema': '/v2/schemas/image',
                    'lang': 'Fran\u00E7ais',
                    'dispos\u00E9': 'f\u00E2ch\u00E9',
                    'owner': '6838eb7b-6ded-434a-882c-b344c77fe8df',
                },
            ],
            'first': '/v2/images',
            'schema': '/v2/schemas/images',
        }
        request = webob.Request.blank('/v2/images')
        response = webob.Response(request=request)
        result = {'images': self.fixtures}
        self.serializer.index(response, result)
        actual = jsonutils.loads(response.body)
        actual['images'][0]['tags'] = sorted(actual['images'][0]['tags'])
        self.assertEqual(expected, actual)
        self.assertEqual('application/json', response.content_type)

    def test_show_full_fixture(self):
        expected = {
            'id': UUID1,
            'name': 'OpenStack\u2122-1',
            'status': 'queued',
            'visibility': 'public',
            'protected': False,
            'os_hidden': False,
            'tags': set(['\u2160', '\u2161']),
            'size': 1024,
            'virtual_size': 3072,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': str(FAKEHASHALGO),
            'os_hash_value': str(MULTIHASH1),
            'container_format': 'ami',
            'disk_format': 'ami',
            'min_ram': 128,
            'min_disk': 10,
            'created_at': str(ISOTIME),
            'updated_at': str(ISOTIME),
            'self': '/v2/images/%s' % UUID1,
            'file': '/v2/images/%s/file' % UUID1,
            'schema': '/v2/schemas/image',
            'lang': 'Fran\u00E7ais',
            'dispos\u00E9': 'f\u00E2ch\u00E9',
            'owner': '6838eb7b-6ded-434a-882c-b344c77fe8df',
        }
        response = webob.Response()
        self.serializer.show(response, self.fixtures[0])
        actual = jsonutils.loads(response.body)
        actual['tags'] = set(actual['tags'])
        self.assertEqual(expected, actual)
        self.assertEqual('application/json', response.content_type)

    def test_create(self):
        expected = {
            'id': UUID1,
            'name': 'OpenStack\u2122-1',
            'status': 'queued',
            'visibility': 'public',
            'protected': False,
            'os_hidden': False,
            'tags': ['\u2160', '\u2161'],
            'size': 1024,
            'virtual_size': 3072,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': str(FAKEHASHALGO),
            'os_hash_value': str(MULTIHASH1),
            'container_format': 'ami',
            'disk_format': 'ami',
            'min_ram': 128,
            'min_disk': 10,
            'created_at': str(ISOTIME),
            'updated_at': str(ISOTIME),
            'self': '/v2/images/%s' % UUID1,
            'file': '/v2/images/%s/file' % UUID1,
            'schema': '/v2/schemas/image',
            'lang': 'Fran\u00E7ais',
            'dispos\u00E9': 'f\u00E2ch\u00E9',
            'owner': '6838eb7b-6ded-434a-882c-b344c77fe8df',
        }
        response = webob.Response()
        self.serializer.create(response, self.fixtures[0])
        self.assertEqual(http.CREATED, response.status_int)
        actual = jsonutils.loads(response.body)
        actual['tags'] = sorted(actual['tags'])
        self.assertEqual(expected, actual)
        self.assertEqual('application/json', response.content_type)
        self.assertEqual('/v2/images/%s' % UUID1, response.location)

    def test_update(self):
        expected = {
            'id': UUID1,
            'name': 'OpenStack\u2122-1',
            'status': 'queued',
            'visibility': 'public',
            'protected': False,
            'os_hidden': False,
            'tags': set(['\u2160', '\u2161']),
            'size': 1024,
            'virtual_size': 3072,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': str(FAKEHASHALGO),
            'os_hash_value': str(MULTIHASH1),
            'container_format': 'ami',
            'disk_format': 'ami',
            'min_ram': 128,
            'min_disk': 10,
            'created_at': str(ISOTIME),
            'updated_at': str(ISOTIME),
            'self': '/v2/images/%s' % UUID1,
            'file': '/v2/images/%s/file' % UUID1,
            'schema': '/v2/schemas/image',
            'lang': 'Fran\u00E7ais',
            'dispos\u00E9': 'f\u00E2ch\u00E9',
            'owner': '6838eb7b-6ded-434a-882c-b344c77fe8df',
        }
        response = webob.Response()
        self.serializer.update(response, self.fixtures[0])
        actual = jsonutils.loads(response.body)
        actual['tags'] = set(actual['tags'])
        self.assertEqual(expected, actual)
        self.assertEqual('application/json', response.content_type)


class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):

    def setUp(self):
        super(TestImagesSerializerWithExtendedSchema, self).setUp()
        self.config(allow_additional_image_properties=False)
        custom_image_properties = {
            'color': {
                'type': 'string',
                'enum': ['red', 'green'],
            },
        }
        schema = glance.api.v2.images.get_schema(custom_image_properties)
        self.serializer = glance.api.v2.images.ResponseSerializer(schema)

        props = dict(color='green', mood='grouchy')
        self.fixture = _domain_fixture(
            UUID2, name='image-2', owner=TENANT2,
            checksum='ca425b88f047ce8ec45ee90e813ada91',
            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
            created_at=DATETIME, updated_at=DATETIME, size=1024,
            virtual_size=3072, extra_properties=props)

    def test_show(self):
        expected = {
            'id': UUID2,
            'name': 'image-2',
            'status': 'queued',
            'visibility': 'private',
            'protected': False,
            'os_hidden': False,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': FAKEHASHALGO,
            'os_hash_value': MULTIHASH1,
            'tags': [],
            'size': 1024,
            'virtual_size': 3072,
            'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
            'color': 'green',
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
            'self': '/v2/images/%s' % UUID2,
            'file': '/v2/images/%s/file' % UUID2,
            'schema': '/v2/schemas/image',
            'min_ram': None,
            'min_disk': None,
            'disk_format': None,
            'container_format': None,
        }
        response = webob.Response()
        self.serializer.show(response, self.fixture)
        self.assertEqual(expected, jsonutils.loads(response.body))

    def test_show_reports_invalid_data(self):
        self.fixture.extra_properties['color'] = 'invalid'
        expected = {
            'id': UUID2,
            'name': 'image-2',
            'status': 'queued',
            'visibility': 'private',
            'protected': False,
            'os_hidden': False,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': FAKEHASHALGO,
            'os_hash_value': MULTIHASH1,
            'tags': [],
            'size': 1024,
            'virtual_size': 3072,
            'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
            'color': 'invalid',
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
            'self': '/v2/images/%s' % UUID2,
            'file': '/v2/images/%s/file' % UUID2,
            'schema': '/v2/schemas/image',
            'min_ram': None,
            'min_disk': None,
            'disk_format': None,
            'container_format': None,
        }
        response = webob.Response()
        self.serializer.show(response, self.fixture)
        self.assertEqual(expected, jsonutils.loads(response.body))


class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):

    def setUp(self):
        super(TestImagesSerializerWithAdditionalProperties, self).setUp()
        self.config(allow_additional_image_properties=True)
        self.fixture = _domain_fixture(
            UUID2, name='image-2', owner=TENANT2,
            checksum='ca425b88f047ce8ec45ee90e813ada91',
            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
            created_at=DATETIME, updated_at=DATETIME, size=1024,
            virtual_size=3072, extra_properties={'marx': 'groucho'})

    def test_show(self):
        serializer = glance.api.v2.images.ResponseSerializer()
        expected = {
            'id': UUID2,
            'name': 'image-2',
            'status': 'queued',
            'visibility': 'private',
            'protected': False,
            'os_hidden': False,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': FAKEHASHALGO,
            'os_hash_value': MULTIHASH1,
            'marx': 'groucho',
            'tags': [],
            'size': 1024,
            'virtual_size': 3072,
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
            'self': '/v2/images/%s' % UUID2,
            'file': '/v2/images/%s/file' % UUID2,
            'schema': '/v2/schemas/image',
            'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
            'min_ram': None,
            'min_disk': None,
            'disk_format': None,
            'container_format': None,
        }
        response = webob.Response()
        serializer.show(response, self.fixture)
        self.assertEqual(expected, jsonutils.loads(response.body))

    def test_show_invalid_additional_property(self):
        """Ensure that the serializer passes
        through invalid additional properties.

        It must not complains with i.e. non-string.
        """
        serializer = glance.api.v2.images.ResponseSerializer()
        self.fixture.extra_properties['marx'] = 123
        expected = {
            'id': UUID2,
            'name': 'image-2',
            'status': 'queued',
            'visibility': 'private',
            'protected': False,
            'os_hidden': False,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': FAKEHASHALGO,
            'os_hash_value': MULTIHASH1,
            'marx': 123,
            'tags': [],
            'size': 1024,
            'virtual_size': 3072,
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
            'self': '/v2/images/%s' % UUID2,
            'file': '/v2/images/%s/file' % UUID2,
            'schema': '/v2/schemas/image',
            'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
            'min_ram': None,
            'min_disk': None,
            'disk_format': None,
            'container_format': None,
        }
        response = webob.Response()
        serializer.show(response, self.fixture)
        self.assertEqual(expected, jsonutils.loads(response.body))

    def test_show_with_additional_properties_disabled(self):
        self.config(allow_additional_image_properties=False)
        serializer = glance.api.v2.images.ResponseSerializer()
        expected = {
            'id': UUID2,
            'name': 'image-2',
            'status': 'queued',
            'visibility': 'private',
            'protected': False,
            'os_hidden': False,
            'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
            'os_hash_algo': FAKEHASHALGO,
            'os_hash_value': MULTIHASH1,
            'tags': [],
            'size': 1024,
            'virtual_size': 3072,
            'owner': '2c014f32-55eb-467d-8fcb-4bd706012f81',
            'created_at': ISOTIME,
            'updated_at': ISOTIME,
            'self': '/v2/images/%s' % UUID2,
            'file': '/v2/images/%s/file' % UUID2,
            'schema': '/v2/schemas/image',
            'min_ram': None,
            'min_disk': None,
            'disk_format': None,
            'container_format': None,
        }
        response = webob.Response()
        serializer.show(response, self.fixture)
        self.assertEqual(expected, jsonutils.loads(response.body))


class TestImagesSerializerDirectUrl(test_utils.BaseTestCase):

    def setUp(self):
        super(TestImagesSerializerDirectUrl, self).setUp()
        self.serializer = glance.api.v2.images.ResponseSerializer()

        self.active_image = _domain_fixture(
            UUID1, name='image-1', visibility='public',
            status='active', size=1024, virtual_size=3072,
            created_at=DATETIME, updated_at=DATETIME,
            locations=[{'id': '1', 'url': 'http://some/fake/location',
                        'metadata': {}, 'status': 'active'}])

        self.queued_image = _domain_fixture(
            UUID2, name='image-2', status='active',
            created_at=DATETIME, updated_at=DATETIME,
            checksum='ca425b88f047ce8ec45ee90e813ada91')

        self.location_data_image_url = 'http://abc.com/somewhere'
        self.location_data_image_meta = {'key': 98231}
        self.location_data_image = _domain_fixture(
            UUID2, name='image-2', status='active',
            created_at=DATETIME, updated_at=DATETIME,
            locations=[{'id': '2',
                        'url': self.location_data_image_url,
                        'metadata': self.location_data_image_meta,
                        'status': 'active'}])

    def _do_index(self):
        request = webob.Request.blank('/v2/images')
        response = webob.Response(request=request)
        self.serializer.index(response,
                              {'images': [self.active_image,
                                          self.queued_image]})
        return jsonutils.loads(response.body)['images']

    def _do_show(self, image):
        request = webob.Request.blank('/v2/images')
        response = webob.Response(request=request)
        self.serializer.show(response, image)
        return jsonutils.loads(response.body)

    def test_index_store_location_enabled(self):
        self.config(show_image_direct_url=True)
        images = self._do_index()

        # NOTE(markwash): ordering sanity check
        self.assertEqual(UUID1, images[0]['id'])
        self.assertEqual(UUID2, images[1]['id'])

        self.assertEqual('http://some/fake/location', images[0]['direct_url'])
        self.assertNotIn('direct_url', images[1])

    def test_index_store_multiple_location_enabled(self):
        self.config(show_multiple_locations=True)
        request = webob.Request.blank('/v2/images')
        response = webob.Response(request=request)
        self.serializer.index(response,
                              {'images': [self.location_data_image]}),
        images = jsonutils.loads(response.body)['images']
        location = images[0]['locations'][0]
        self.assertEqual(location['url'], self.location_data_image_url)
        self.assertEqual(location['metadata'], self.location_data_image_meta)

    def test_index_store_location_explicitly_disabled(self):
        self.config(show_image_direct_url=False)
        images = self._do_index()
        self.assertNotIn('direct_url', images[0])
        self.assertNotIn('direct_url', images[1])

    def test_show_location_enabled(self):
        self.config(show_image_direct_url=True)
        image = self._do_show(self.active_image)
        self.assertEqual('http://some/fake/location', image['direct_url'])

    def test_show_location_enabled_but_not_set(self):
        self.config(show_image_direct_url=True)
        image = self._do_show(self.queued_image)
        self.assertNotIn('direct_url', image)

    def test_show_location_explicitly_disabled(self):
        self.config(show_image_direct_url=False)
        image = self._do_show(self.active_image)
        self.assertNotIn('direct_url', image)


class TestImageSchemaFormatConfiguration(test_utils.BaseTestCase):

    def test_default_disk_formats(self):
        schema = glance.api.v2.images.get_schema()
        expected = [None, 'ami', 'ari', 'aki', 'vhd', 'vhdx', 'vmdk',
                    'raw', 'qcow2', 'vdi', 'iso', 'ploop']
        actual = schema.properties['disk_format']['enum']
        self.assertEqual(expected, actual)

    def test_custom_disk_formats(self):
        self.config(disk_formats=['gabe'], group="image_format")
        schema = glance.api.v2.images.get_schema()
        expected = [None, 'gabe']
        actual = schema.properties['disk_format']['enum']
        self.assertEqual(expected, actual)

    def test_default_container_formats(self):
        schema = glance.api.v2.images.get_schema()
        expected = [None, 'ami', 'ari', 'aki', 'bare', 'ovf', 'ova', 'docker',
                    'compressed']
        actual = schema.properties['container_format']['enum']
        self.assertEqual(expected, actual)

    def test_custom_container_formats(self):
        self.config(container_formats=['mark'], group="image_format")
        schema = glance.api.v2.images.get_schema()
        expected = [None, 'mark']
        actual = schema.properties['container_format']['enum']
        self.assertEqual(expected, actual)


class TestImageSchemaDeterminePropertyBasis(test_utils.BaseTestCase):

    def test_custom_property_marked_as_non_base(self):
        self.config(allow_additional_image_properties=False)
        custom_image_properties = {
            'pants': {
                'type': 'string',
            },
        }
        schema = glance.api.v2.images.get_schema(custom_image_properties)
        self.assertFalse(schema.properties['pants'].get('is_base', True))

    def test_base_property_marked_as_base(self):
        schema = glance.api.v2.images.get_schema()
        self.assertTrue(schema.properties['disk_format'].get('is_base', True))


class TestMultiImagesController(base.MultiIsolatedUnitTest):

    def setUp(self):
        super(TestMultiImagesController, self).setUp()
        self.db = unit_test_utils.FakeDB(initialize=False)
        self.policy = unit_test_utils.FakePolicyEnforcer()
        self.notifier = unit_test_utils.FakeNotifier()
        self.store = store
        self._create_images()
        self._create_image_members()
        stores = {'cheap': 'file', 'fast': 'file', 'empty': 'file'}
        self.config(enabled_backends=stores)
        self.store.register_store_opts(CONF)
        self.controller = glance.api.v2.images.ImagesController(self.db,
                                                                self.policy,
                                                                self.notifier,
                                                                self.store)

    def _create_images(self):
        self.images = [
            _db_fixture(UUID1, owner=TENANT1, checksum=CHKSUM,
                        name='1', size=256, virtual_size=1024,
                        visibility='public',
                        locations=[{'url': '%s/%s' % (BASE_URI, UUID1),
                                    'metadata': {}, 'status': 'active'}],
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        created_at=DATETIME),
            _db_fixture(UUID2, owner=TENANT1, checksum=CHKSUM1,
                        name='2', size=512, virtual_size=2048,
                        visibility='public',
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        tags=['redhat', '64bit', 'power'],
                        properties={'hypervisor_type': 'kvm', 'foo': 'bar',
                                    'bar': 'foo'},
                        locations=[{'url': 'file://%s/%s' % (self.test_dir,
                                                             UUID2),
                                    'metadata': {}, 'status': 'active'}],
                        created_at=DATETIME + datetime.timedelta(seconds=1)),
            _db_fixture(UUID5, owner=TENANT3, checksum=CHKSUM1,
                        name='2', size=512, virtual_size=2048,
                        visibility='public',
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        tags=['redhat', '64bit', 'power'],
                        properties={'hypervisor_type': 'kvm', 'foo': 'bar',
                                    'bar': 'foo'},
                        locations=[{'url': 'file://%s/%s' % (self.test_dir,
                                                             UUID2),
                                    'metadata': {}, 'status': 'active'}],
                        created_at=DATETIME + datetime.timedelta(seconds=1)),
            _db_fixture(UUID3, owner=TENANT3, checksum=CHKSUM1,
                        name='3', size=512, virtual_size=2048,
                        visibility='public', tags=['windows', '64bit', 'x86'],
                        created_at=DATETIME + datetime.timedelta(seconds=2)),
            _db_fixture(UUID4, owner=TENANT4, name='4',
                        size=1024, virtual_size=3072,
                        created_at=DATETIME + datetime.timedelta(seconds=3)),
            _db_fixture(UUID6, owner=TENANT3, checksum=CHKSUM1,
                        name='3', size=512, virtual_size=2048,
                        visibility='public',
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        tags=['redhat', '64bit', 'power'],
                        properties={'hypervisor_type': 'kvm', 'foo': 'bar',
                                    'bar': 'foo'},
                        locations=[{'url': 'file://%s/%s' % (self.test_dir,
                                                             UUID6),
                                    'metadata': {'store': 'fast'},
                                    'status': 'active'},
                                   {'url': 'file://%s/%s' % (self.test_dir2,
                                                             UUID6),
                                    'metadata': {'store': 'cheap'},
                                    'status': 'active'}],
                        created_at=DATETIME + datetime.timedelta(seconds=1)),
            _db_fixture(UUID7, owner=TENANT3, checksum=CHKSUM1,
                        name='3', size=512, virtual_size=2048,
                        visibility='public',
                        disk_format='raw',
                        container_format='bare',
                        status='active',
                        tags=['redhat', '64bit', 'power'],
                        properties={'hypervisor_type': 'kvm', 'foo': 'bar',
                                    'bar': 'foo'},
                        locations=[{'url': 'file://%s/%s' % (self.test_dir,
                                                             UUID7),
                                    'metadata': {'store': 'fast'},
                                    'status': 'active'}],
                        created_at=DATETIME + datetime.timedelta(seconds=1)),
        ]
        [self.db.image_create(None, image) for image in self.images]

        self.db.image_tag_set_all(None, UUID1, ['ping', 'pong'])

    def _create_image_members(self):
        self.image_members = [
            _db_image_member_fixture(UUID4, TENANT2),
            _db_image_member_fixture(UUID4, TENANT3,
                                     status='accepted'),
        ]
        [self.db.image_member_create(None, image_member)
            for image_member in self.image_members]

    def test_image_import_image_not_exist(self):
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPNotFound,
                          self.controller.import_image,
                          request, 'invalid_image',
                          {'method': {'name': 'glance-direct'}})

    def test_image_import_with_active_image(self):
        request = unit_test_utils.get_fake_request()
        self.assertRaises(webob.exc.HTTPConflict,
                          self.controller.import_image,
                          request, UUID2,
                          {'method': {'name': 'glance-direct'}})

    def test_delete_from_store_as_non_owner(self):
        enforcer = unit_test_utils.enforcer_from_rules({
            "get_image": "",
            "delete_image_location": "'TENANT4':%(owner)s",
            "get_image_location": ""
        })
        request = unit_test_utils.get_fake_request()
        self.controller.policy = enforcer
        self.assertRaises(webob.exc.HTTPForbidden,
                          self.controller.delete_from_store,
                          request,
                          "fast",
                          UUID6)

    def test_delete_from_store_non_active(self):
        request = unit_test_utils.get_fake_request(tenant=TENANT3)
        self.assertRaises(webob.exc.HTTPConflict,
                          self.controller.delete_from_store,
                          request,
                          "fast",
                          UUID3)

    def test_delete_from_store_no_image(self):
        request = unit_test_utils.get_fake_request(tenant=TENANT3)
        self.assertRaises(webob.exc.HTTPNotFound,
                          self.controller.delete_from_store,
                          request,
                          "fast",
                          "nonexisting")

    def test_delete_from_store_invalid_store(self):
        request = unit_test_utils.get_fake_request(tenant=TENANT3)
        self.assertRaises(webob.exc.HTTPConflict,
                          self.controller.delete_from_store,
                          request,
                          "burn",
                          UUID6)

    def test_delete_from_store_not_in_store(self):
        request = unit_test_utils.get_fake_request(tenant=TENANT3)
        self.assertRaises(webob.exc.HTTPNotFound,
                          self.controller.delete_from_store,
                          request,
                          "empty",
                          UUID6)

    def test_delete_from_store_one_location(self):
        request = unit_test_utils.get_fake_request(tenant=TENANT3)
        self.assertRaises(webob.exc.HTTPForbidden,
                          self.controller.delete_from_store,
                          request,
                          "fast",
                          UUID7)

    def test_delete_from_store_as_non_admin(self):
        request = unit_test_utils.get_fake_request(tenant=TENANT3)
        self.controller.delete_from_store(request, "fast", UUID6)
        image = self.controller.show(request, UUID6)
        self.assertEqual(1, len(image.locations))
        self.assertEqual("cheap", image.locations[0]['metadata']['store'])

    def test_delete_from_store_as_admin(self):
        request = unit_test_utils.get_fake_request(is_admin=True)
        self.controller.delete_from_store(request, "fast", UUID6)
        image = self.controller.show(request, UUID6)
        self.assertEqual(1, len(image.locations))
        self.assertEqual("cheap", image.locations[0]['metadata']['store'])

    def test_image_lazy_loading_store(self):
        # assert existing image does not have store in metadata
        existing_image = self.images[1]
        self.assertNotIn('store', existing_image['locations'][0]['metadata'])

        # assert: store information will be added by lazy loading
        request = unit_test_utils.get_fake_request()
        with mock.patch.object(store_utils,
                               "_get_store_id_from_uri") as mock_uri:
            mock_uri.return_value = "fast"
            image = self.controller.show(request, UUID2)
            for loc in image.locations:
                self.assertIn('store', loc['metadata'])

    def test_image_lazy_loading_store_different_owner(self):
        # assert existing image does not have store in metadata
        existing_image = self.images[2]
        self.assertNotIn('store', existing_image['locations'][0]['metadata'])

        # assert: store information will be added by lazy loading even if owner
        # is different
        request = unit_test_utils.get_fake_request()
        request.headers.update({'X-Tenant_id': TENANT1})
        with mock.patch.object(store_utils,
                               "_get_store_id_from_uri") as mock_uri:
            mock_uri.return_value = "fast"
            image = self.controller.show(request, UUID5)
            for loc in image.locations:
                self.assertIn('store', loc['metadata'])

    def test_image_import_invalid_backend_in_request_header(self):
        request = unit_test_utils.get_fake_request()
        request.headers['x-image-meta-store'] = 'dummy'
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='uploading')
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image,
                              request, UUID4,
                              {'method': {'name': 'glance-direct'}})

    def test_image_import_raises_conflict_if_disk_format_is_none(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(disk_format=None)
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'glance-direct'}})

    def test_image_import_raises_conflict(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='queued')
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'glance-direct'}})

    def test_image_import_raises_conflict_for_web_download(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage()
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image, request, UUID4,
                              {'method': {'name': 'web-download'}})

    def test_copy_image_stores_specified_in_header_and_body(self):
        request = unit_test_utils.get_fake_request()
        request.headers['x-image-meta-store'] = 'fast'

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage()
            self.assertRaises(webob.exc.HTTPBadRequest,
                              self.controller.import_image, request, UUID7,
                              {'method': {'name': 'copy-image'},
                               'stores': ["fast"]})

    def test_copy_image_non_existing_image(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.side_effect = exception.NotFound
            self.assertRaises(webob.exc.HTTPNotFound,
                              self.controller.import_image, request, UUID1,
                              {'method': {'name': 'copy-image'},
                               'stores': ["fast"]})

    def test_copy_image_with_all_stores(self):
        request = unit_test_utils.get_fake_request()
        locations = {'url': 'file://%s/%s' % (self.test_dir,
                                              UUID7),
                     'metadata': {'store': 'fast'},
                     'status': 'active'},

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            with mock.patch.object(self.store,
                                   'get_store_from_store_identifier'):
                mock_get.return_value = FakeImage(id=UUID7, status='active',
                                                  locations=locations)
                self.assertIsNotNone(self.controller.import_image(
                    request, UUID7, {'method': {'name': 'copy-image'},
                                     'all_stores': True}))

    def test_copy_non_active_image(self):
        request = unit_test_utils.get_fake_request()

        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(status='uploading')
            self.assertRaises(webob.exc.HTTPConflict,
                              self.controller.import_image, request, UUID1,
                              {'method': {'name': 'copy-image'},
                               'stores': ["fast"]})

    def test_copy_image_in_existing_store(self):
        request = unit_test_utils.get_fake_request(tenant=TENANT3)
        self.assertRaises(webob.exc.HTTPBadRequest,
                          self.controller.import_image, request, UUID6,
                          {'method': {'name': 'copy-image'},
                           'stores': ["fast"]})

    def test_copy_image_to_other_stores(self):
        request = unit_test_utils.get_fake_request()
        locations = {'url': 'file://%s/%s' % (self.test_dir,
                                              UUID7),
                     'metadata': {'store': 'fast'},
                     'status': 'active'},
        with mock.patch.object(
                glance.notifier.ImageRepoProxy, 'get') as mock_get:
            mock_get.return_value = FakeImage(id=UUID7, status='active',
                                              locations=locations)

            output = self.controller.import_image(
                request, UUID7, {'method': {'name': 'copy-image'},
                                 'stores': ["cheap"]})

        self.assertEqual(UUID7, output)


class TestProxyHelpers(base.IsolatedUnitTest):
    def test_proxy_response_error(self):
        e = glance.api.v2.images.proxy_response_error(123, 'Foo')
        self.assertIsInstance(e, webob.exc.HTTPError)
        self.assertEqual(123, e.code)
        self.assertEqual('123 Foo', e.status)

    def test_is_proxyable(self):
        controller = glance.api.v2.images.ImagesController(None, None,
                                                           None, None)
        self.config(worker_self_reference_url='http://worker1')
        mock_image = mock.MagicMock(extra_properties={})

        self.assertFalse(controller.is_proxyable(mock_image))

        mock_image.extra_properties['os_glance_stage_host'] = 'http://worker1'
        self.assertFalse(controller.is_proxyable(mock_image))

        mock_image.extra_properties['os_glance_stage_host'] = 'http://worker2'
        self.assertTrue(controller.is_proxyable(mock_image))

    def test_self_url(self):
        controller = glance.api.v2.images.ImagesController(None, None,
                                                           None, None)
        self.assertIsNone(controller.self_url)

        self.config(public_endpoint='http://lb.example.com')
        self.assertEqual('http://lb.example.com', controller.self_url)

        self.config(worker_self_reference_url='http://worker1.example.com')
        self.assertEqual('http://worker1.example.com', controller.self_url)
