# 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 errno
import hashlib
import testtools
from unittest import mock

import ddt

from glanceclient.common import utils as common_utils
from glanceclient import exc
from glanceclient.tests.unit.v2 import base
from glanceclient.tests import utils
from glanceclient.v2 import images

_CHKSUM = '93264c3edf5972c9f1cb309543d38a5c'
_CHKSUM1 = '54264c3edf5972c9f1cb309453d38a46'

_HASHVAL = '54264c3edf93264c3edf5972c9f1cb309543d38a5c5972c9f1cb309453d38a46'
_HASHVAL1 = 'cb309543d38a5c5972c9f1cb309453d38a4654264c3edf93264c3edf5972c9f1'
_HASHBAD = '93264c3edf597254264c3edf5972c9f1cb309453d38a46c9f1cb309543d38a5c'

_TAG1 = 'power'
_TAG2 = '64bit'

_BOGUS_ID = '63e7f218-29de-4477-abdc-8db7c9533188'
_EVERYTHING_ID = '802cbbb7-0379-4c38-853f-37302b5e3d29'
_OWNED_IMAGE_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf'
_OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f'
_PRIVATE_ID = 'e33560a7-3964-4de5-8339-5a24559f99ab'
_PUBLIC_ID = '857806e7-05b6-48e0-9d40-cb0e6fb727b9'
_SHARED_ID = '331ac905-2a38-44c5-a83d-653db8f08313'
_COMMUNITY_ID = '609ec9fc-0ee4-44c4-854d-0480af576929'
_STATUS_REJECTED_ID = 'f3ea56ff-d7e4-4451-998c-1e3d33539c8e'

data_fixtures = {
    '/v2/schemas/image': {
        'GET': (
            {},
            {
                'name': 'image',
                'properties': {
                    'id': {},
                    'name': {},
                    'locations': {
                        'type': 'array',
                        'items': {
                            'type': 'object',
                            'properties': {
                                'metadata': {'type': 'object'},
                                'url': {'type': 'string'},
                            },
                            'required': ['url', 'metadata'],
                        },
                    },
                    'color': {'type': 'string', 'is_base': False},
                },
                'additionalProperties': {'type': 'string'},
            },
        ),
    },
    '/v2/images?limit=%d' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                },
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
            ]},
        ),
    },
    '/v2/images?limit=2': {
        'GET': (
            {},
            {
                'images': [
                    {
                        'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
                        'name': 'image-1',
                    },
                    {
                        'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                        'name': 'image-2',
                    },
                ],
                'next': ('/v2/images?limit=2&'
                         'marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'),
            },
        ),
    },
    '/v2/images?limit=1': {
        'GET': (
            {},
            {
                'images': [
                    {
                        'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
                        'name': 'image-1',
                    },
                ],
                'next': ('/v2/images?limit=1&'
                         'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'),
            },
        ),
    },
    ('/v2/images?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): {
        'GET': (
            {},
            {'images': [
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
            ]},
        ),
    },
    ('/v2/images?limit=1&marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'): {
        'GET': (
            {},
            {'images': [
                {
                    'id': '3f99bf80-2ee6-47cf-acfe-1f1fabb7e811',
                    'name': 'image-3',
                },
            ]},
        ),
    },
    '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1': {
        'GET': (
            {},
            {
                'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
                'name': 'image-1',
            },
        ),
        'PATCH': (
            {},
            '',
        ),
    },
    '/v2/images/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': {
        'GET': (
            {},
            {
                'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3',
                'name': 'image-3',
                'barney': 'rubble',
                'george': 'jetson',
                'color': 'red',
            },
        ),
        'PATCH': (
            {},
            '',
        ),
    },
    '/v2/images': {
        'POST': (
            {},
            {
                'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
                'name': 'image-1',
            },
        ),
    },
    '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a': {
        'DELETE': (
            {},
            {
                'id': '87b634c1-f893-33c9-28a9-e5673c99239a',
            },
        ),
    },
    '/v2/images/606b0e88-7c5a-4d54-b5bb-046105d4de6f/file': {
        'PUT': (
            {},
            '',
        ),
    },
    '/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205/file': {
        'GET': (
            {},
            'A',
        ),
    },
    '/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205': {
        'GET': (
            {},
            {},
        ),
    },
    '/v2/images/headeronly-db27-11e1-a1eb-080027cbe205/file': {
        'GET': (
            {
                'content-md5': 'wrong'
            },
            'BB',
        ),
    },
    '/v2/images/headeronly-db27-11e1-a1eb-080027cbe205': {
        'GET': (
            {},
            {},
        ),
    },
    '/v2/images/chkonly-db27-11e1-a1eb-080027cbe205/file': {
        'GET': (
            {
                'content-md5': 'wrong'
            },
            'BB',
        ),
    },
    '/v2/images/chkonly-db27-11e1-a1eb-080027cbe205': {
        'GET': (
            {},
            {
                'checksum': 'wrong',
            },
        ),
    },
    '/v2/images/multihash-db27-11e1-a1eb-080027cbe205/file': {
        'GET': (
            {
                'content-md5': 'wrong'
            },
            'BB',
        ),
    },
    '/v2/images/multihash-db27-11e1-a1eb-080027cbe205': {
        'GET': (
            {},
            {
                'checksum': 'wrong',
                'os_hash_algo': 'md5',
                'os_hash_value': 'junk'
            },
        ),
    },
    '/v2/images/badalgo-db27-11e1-a1eb-080027cbe205/file': {
        'GET': (
            {
                'content-md5': hashlib.md5(b'BB').hexdigest()
            },
            'BB',
        ),
    },
    '/v2/images/badalgo-db27-11e1-a1eb-080027cbe205': {
        'GET': (
            {},
            {
                'checksum': hashlib.md5(b'BB').hexdigest(),
                'os_hash_algo': 'not_an_algo',
                'os_hash_value': 'whatever'
            },
        ),
    },
    '/v2/images/bad-multihash-value-good-checksum/file': {
        'GET': (
            {
                'content-md5': hashlib.md5(b'GOODCHECKSUM').hexdigest()
            },
            'GOODCHECKSUM',
        ),
    },
    '/v2/images/bad-multihash-value-good-checksum': {
        'GET': (
            {},
            {
                'checksum': hashlib.md5(b'GOODCHECKSUM').hexdigest(),
                'os_hash_algo': 'sha512',
                'os_hash_value': 'badmultihashvalue'
            },
        ),
    },
    '/v2/images/headeronly-dd57-11e1-af0f-02163e68b1d8/file': {
        'GET': (
            {
                'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae'
            },
            'CCC',
        ),
    },
    '/v2/images/headeronly-dd57-11e1-af0f-02163e68b1d8': {
        'GET': (
            {},
            {},
        ),
    },
    '/v2/images/chkonly-dd57-11e1-af0f-02163e68b1d8/file': {
        'GET': (
            {
                'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae'
            },
            'CCC',
        ),
    },
    '/v2/images/chkonly-dd57-11e1-af0f-02163e68b1d8': {
        'GET': (
            {},
            {
                'checksum': 'defb99e69a9f1f6e06f15006b1f166ae',
            },
        ),
    },
    '/v2/images/multihash-dd57-11e1-af0f-02163e68b1d8/file': {
        'GET': (
            {
                'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae'
            },
            'CCC',
        ),
    },
    '/v2/images/multihash-dd57-11e1-af0f-02163e68b1d8': {
        'GET': (
            {},
            {
                'checksum': 'defb99e69a9f1f6e06f15006b1f166ae',
                'os_hash_algo': 'sha384',
                'os_hash_value': hashlib.sha384(b'CCC').hexdigest()
            },
        ),
    },
    '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/reactivate': {
        'POST': ({}, None)
    },
    '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/deactivate': {
        'POST': ({}, None)
    },
    '/v2/images/606b0e88-7c5a-4d54-b5bb-046105d4de6f/import': {
        'POST': ({}, None)
    },
    '/v2/images?limit=%d&visibility=public' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': _PUBLIC_ID,
                    'harvey': 'lipshitz',
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&visibility=private' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': _PRIVATE_ID,
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&visibility=shared' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': _SHARED_ID,
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&visibility=community' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': _COMMUNITY_ID,
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&member_status=rejected' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': _STATUS_REJECTED_ID,
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&member_status=pending' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': []},
        ),
    },
    '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _OWNER_ID): {
        'GET': (
            {},
            {'images': [
                {
                    'id': _OWNED_IMAGE_ID,
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): {
        'GET': (
            {},
            {'images': []},
        ),
    },
    '/v2/images?limit=%d&member_status=pending&owner=%s&visibility=shared'
    % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): {
        'GET': (
            {},
            {'images': [
                {
                    'id': _EVERYTHING_ID,
                },
            ]},
        ),
    },
    '/v2/images?checksum=%s&limit=%d' % (_CHKSUM, images.DEFAULT_PAGE_SIZE): {
        'GET': (
            {},
            {'images': [
                {
                    'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                }
            ]},
        ),
    },
    '/v2/images?checksum=%s&limit=%d' % (_CHKSUM1, images.DEFAULT_PAGE_SIZE): {
        'GET': (
            {},
            {'images': [
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                },
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
            ]},
        ),
    },
    '/v2/images?checksum=wrong&limit=%d' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': []},
        ),
    },
    '/v2/images?limit=%d&os_hash_value=%s' % (images.DEFAULT_PAGE_SIZE,
                                              _HASHVAL): {
        'GET': (
            {},
            {'images': [
                {
                    'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                }
            ]},
        ),
    },
    '/v2/images?limit=%d&os_hash_value=%s' % (images.DEFAULT_PAGE_SIZE,
                                              _HASHVAL1): {
        'GET': (
            {},
            {'images': [
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                },
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&os_hash_value=%s' % (images.DEFAULT_PAGE_SIZE,
                                              _HASHBAD): {
        'GET': (
            {},
            {'images': []},
        ),
    },
    '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG1): {
        'GET': (
            {},
            {'images': [
                {
                    'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                }
            ]},
        ),
    },
    '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG2): {
        'GET': (
            {},
            {'images': [
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                },
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&tag=%s&tag=%s' % (images.DEFAULT_PAGE_SIZE,
                                           _TAG1, _TAG2):
    {
        'GET': (
            {},
            {'images': [
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                }
            ]},
        ),
    },
    '/v2/images?limit=%d&tag=fake' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': []},
        ),
    },
    '/v2/images/a2b83adc-888e-11e3-8872-78acc0b951d8': {
        'GET': (
            {},
            {
                'id': 'a2b83adc-888e-11e3-8872-78acc0b951d8',
                'name': 'image-location-tests',
                'locations': [{u'url': u'http://foo.com/',
                               u'metadata': {u'foo': u'foometa'}},
                              {u'url': u'http://bar.com/',
                               u'metadata': {u'bar': u'barmeta'}}],
            },
        ),
        'PATCH': (
            {},
            '',
        )
    },
    '/v2/images?limit=%d&os_distro=NixOS' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': '8b052954-c76c-4e02-8e90-be89a70183a8',
                    'name': 'image-5',
                    'os_distro': 'NixOS',
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&my_little_property=cant_be_this_cute' %
    images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': []},
        ),
    },
    '/v2/images?limit=%d&sort_key=name' % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                },
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&sort_key=name&sort_key=id'
    % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image',
                },
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image',
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&sort_dir=desc&sort_key=id'
    % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&sort_dir=desc&sort_key=name&sort_key=id'
    % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&sort_dir=desc&sort_dir=asc&sort_key=name&sort_key=id'
    % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                },
            ]},
        ),
    },
    '/v2/images?limit=%d&sort=name%%3Adesc%%2Csize%%3Aasc'
    % images.DEFAULT_PAGE_SIZE: {
        'GET': (
            {},
            {'images': [
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'name': 'image-2',
                },
                {
                    'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
                    'name': 'image-1',
                },
            ]},
        ),
    },
    '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1/tasks': {
        'GET': (
            {},
            {'tasks': [
                {
                    'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
                    'status': 'succeed',
                    'message': 'Copied 44 MiB',
                    'updated_at': '2021-03-01T18:28:26.000000'
                }
            ]},
        ),
    },
}

schema_fixtures = {
    'image': {
        'GET': (
            {},
            {
                'name': 'image',
                'properties': {
                    'id': {},
                    'name': {},
                    'locations': {
                        'type': 'array',
                        'items': {
                            'type': 'object',
                            'properties': {
                                'metadata': {'type': 'object'},
                                'url': {'type': 'string'},
                            },
                            'required': ['url', 'metadata'],
                        }
                    },
                    'color': {'type': 'string', 'is_base': False},
                    'tags': {'type': 'array'},
                },
                'additionalProperties': {'type': 'string'},
            }
        )
    }
}


@ddt.ddt
class TestController(testtools.TestCase):
    def setUp(self):
        super(TestController, self).setUp()
        self.api = utils.FakeAPI(data_fixtures)
        self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
        self.controller = base.BaseController(self.api, self.schema_api,
                                              images.Controller)

    def test_image_tasks_supported(self):
        with mock.patch.object(common_utils,
                               'has_version') as mock_has_version:
            mock_has_version.return_value = True
            image_tasks = self.controller.get_associated_image_tasks(
                '3a4560a1-e585-443e-9b39-553b46ec92d1')
            self.assertEqual(1, len(image_tasks['tasks']))

    def test_image_tasks_not_supported(self):
        with mock.patch.object(common_utils,
                               'has_version') as mock_has_version:
            mock_has_version.return_value = False
            self.assertRaises(exc.HTTPNotImplemented,
                              self.controller.get_associated_image_tasks,
                              '3a4560a1-e585-443e-9b39-553b46ec92d1')

    def test_list_images(self):
        images = self.controller.list()
        self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id)
        self.assertEqual('image-1', images[0].name)
        self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id)
        self.assertEqual('image-2', images[1].name)

    def test_list_images_paginated(self):
        images = self.controller.list(page_size=1)
        self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id)
        self.assertEqual('image-1', images[0].name)
        self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id)
        self.assertEqual('image-2', images[1].name)

    def test_list_images_paginated_with_limit(self):
        images = self.controller.list(limit=3, page_size=2)
        self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id)
        self.assertEqual('image-1', images[0].name)
        self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id)
        self.assertEqual('image-2', images[1].name)
        self.assertEqual('3f99bf80-2ee6-47cf-acfe-1f1fabb7e811', images[2].id)
        self.assertEqual('image-3', images[2].name)
        self.assertEqual(3, len(images))

    def test_list_images_with_marker(self):
        images = self.controller.list(
            limit=1, marker='3a4560a1-e585-443e-9b39-553b46ec92d1')
        self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[0].id)
        self.assertEqual('image-2', images[0].name)

    def test_list_images_visibility_public(self):
        filters = {'filters': {'visibility': 'public'}}
        images = self.controller.list(**filters)
        self.assertEqual(_PUBLIC_ID, images[0].id)

    def test_list_images_visibility_private(self):
        filters = {'filters': {'visibility': 'private'}}
        images = self.controller.list(**filters)
        self.assertEqual(_PRIVATE_ID, images[0].id)

    def test_list_images_visibility_shared(self):
        filters = {'filters': {'visibility': 'shared'}}
        images = self.controller.list(**filters)
        self.assertEqual(_SHARED_ID, images[0].id)

    def test_list_images_visibility_community(self):
        filters = {'filters': {'visibility': 'community'}}
        images = list(self.controller.list(**filters))
        self.assertEqual(_COMMUNITY_ID, images[0].id)

    def test_list_images_member_status_rejected(self):
        filters = {'filters': {'member_status': 'rejected'}}
        images = self.controller.list(**filters)
        self.assertEqual(_STATUS_REJECTED_ID, images[0].id)

    def test_list_images_for_owner(self):
        filters = {'filters': {'owner': _OWNER_ID}}
        images = self.controller.list(**filters)
        self.assertEqual(_OWNED_IMAGE_ID, images[0].id)

    def test_list_images_for_checksum_single_image(self):
        fake_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
        filters = {'filters': {'checksum': _CHKSUM}}
        images = self.controller.list(**filters)
        self.assertEqual(1, len(images))
        self.assertEqual('%s' % fake_id, images[0].id)

    def test_list_images_for_checksum_multiple_images(self):
        fake_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        fake_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'
        filters = {'filters': {'checksum': _CHKSUM1}}
        images = self.controller.list(**filters)
        self.assertEqual(2, len(images))
        self.assertEqual('%s' % fake_id1, images[0].id)
        self.assertEqual('%s' % fake_id2, images[1].id)

    def test_list_images_for_wrong_checksum(self):
        filters = {'filters': {'checksum': 'wrong'}}
        images = self.controller.list(**filters)
        self.assertEqual(0, len(images))

    def test_list_images_for_hash_single_image(self):
        fake_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
        filters = {'filters': {'os_hash_value': _HASHVAL}}
        images = self.controller.list(**filters)
        self.assertEqual(1, len(images))
        self.assertEqual('%s' % fake_id, images[0].id)

    def test_list_images_for_hash_multiple_images(self):
        fake_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        fake_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'
        filters = {'filters': {'os_hash_value': _HASHVAL1}}
        images = self.controller.list(**filters)
        self.assertEqual(2, len(images))
        self.assertEqual('%s' % fake_id1, images[0].id)
        self.assertEqual('%s' % fake_id2, images[1].id)

    def test_list_images_for_wrong_hash(self):
        filters = {'filters': {'os_hash_value': _HASHBAD}}
        images = self.controller.list(**filters)
        self.assertEqual(0, len(images))

    def test_list_images_for_bogus_owner(self):
        filters = {'filters': {'owner': _BOGUS_ID}}
        images = self.controller.list(**filters)
        self.assertEqual([], images)

    def test_list_images_for_bunch_of_filters(self):
        filters = {'filters': {'owner': _BOGUS_ID,
                               'visibility': 'shared',
                               'member_status': 'pending'}}
        images = self.controller.list(**filters)
        self.assertEqual(_EVERYTHING_ID, images[0].id)

    def test_list_images_filters_encoding(self):
        filters = {"owner": u"ni\xf1o"}
        try:
            self.controller.list(filters=filters)
        except KeyError:
            # NOTE(flaper87): It raises KeyError because there's
            # no fixture supporting this query:
            #   /v2/images?owner=ni%C3%B1o&limit=20
            # We just want to make sure filters are correctly encoded.
            pass
        self.assertEqual(b"ni\xc3\xb1o", filters["owner"])

    def test_list_images_for_tag_single_image(self):
        img_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
        filters = {'filters': {'tag': [_TAG1]}}
        images = self.controller.list(**filters)
        self.assertEqual(1, len(images))
        self.assertEqual('%s' % img_id, images[0].id)

    def test_list_images_for_tag_multiple_images(self):
        img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        img_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'
        filters = {'filters': {'tag': [_TAG2]}}
        images = self.controller.list(**filters)
        self.assertEqual(2, len(images))
        self.assertEqual('%s' % img_id1, images[0].id)
        self.assertEqual('%s' % img_id2, images[1].id)

    def test_list_images_for_multi_tags(self):
        img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        filters = {'filters': {'tag': [_TAG1, _TAG2]}}
        images = self.controller.list(**filters)
        self.assertEqual(1, len(images))
        self.assertEqual('%s' % img_id1, images[0].id)

    def test_list_images_for_non_existent_tag(self):
        filters = {'filters': {'tag': ['fake']}}
        images = self.controller.list(**filters)
        self.assertEqual(0, len(images))

    def test_list_images_for_invalid_tag(self):
        filters = {'filters': {'tag': [[]]}}

        self.assertRaises(exc.HTTPBadRequest,
                          self.controller.list, **filters)

    def test_list_images_with_single_sort_key(self):
        img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        sort_key = 'name'
        images = self.controller.list(sort_key=sort_key)
        self.assertEqual(2, len(images))
        self.assertEqual('%s' % img_id1, images[0].id)

    def test_list_with_multiple_sort_keys(self):
        img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        sort_key = ['name', 'id']
        images = self.controller.list(sort_key=sort_key)
        self.assertEqual(2, len(images))
        self.assertEqual('%s' % img_id1, images[0].id)

    def test_list_images_with_desc_sort_dir(self):
        img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        sort_key = 'id'
        sort_dir = 'desc'
        images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir)
        self.assertEqual(2, len(images))
        self.assertEqual('%s' % img_id1, images[1].id)

    def test_list_images_with_multiple_sort_keys_and_one_sort_dir(self):
        img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        sort_key = ['name', 'id']
        sort_dir = 'desc'
        images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir)
        self.assertEqual(2, len(images))
        self.assertEqual('%s' % img_id1, images[1].id)

    def test_list_images_with_multiple_sort_dirs(self):
        img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        sort_key = ['name', 'id']
        sort_dir = ['desc', 'asc']
        images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir)
        self.assertEqual(2, len(images))
        self.assertEqual('%s' % img_id1, images[1].id)

    def test_list_images_with_new_sorting_syntax(self):
        img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
        sort = 'name:desc,size:asc'
        images = self.controller.list(sort=sort)
        self.assertEqual(2, len(images))
        self.assertEqual('%s' % img_id1, images[1].id)

    def test_list_images_sort_dirs_fewer_than_keys(self):
        sort_key = ['name', 'id', 'created_at']
        sort_dir = ['desc', 'asc']
        self.assertRaises(exc.HTTPBadRequest, self.controller.list,
                          sort_key=sort_key, sort_dir=sort_dir)

    def test_list_images_combined_syntax(self):
        sort_key = ['name', 'id']
        sort_dir = ['desc', 'asc']
        sort = 'name:asc'
        self.assertRaises(exc.HTTPBadRequest,
                          self.controller.list, sort=sort, sort_key=sort_key,
                          sort_dir=sort_dir)

    def test_list_images_new_sorting_syntax_invalid_key(self):
        sort = 'INVALID:asc'
        self.assertRaises(exc.HTTPBadRequest, self.controller.list,
                          sort=sort)

    def test_list_images_new_sorting_syntax_invalid_direction(self):
        sort = 'name:INVALID'
        self.assertRaises(exc.HTTPBadRequest, self.controller.list,
                          sort=sort)

    def test_list_images_for_property(self):
        filters = {'filters': dict([('os_distro', 'NixOS')])}
        images = self.controller.list(**filters)
        self.assertEqual(1, len(images))

    def test_list_images_for_non_existent_property(self):
        filters = {'filters': dict([('my_little_property',
                                     'cant_be_this_cute')])}
        images = self.controller.list(**filters)
        self.assertEqual(0, len(images))

    def test_get_image(self):
        image = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1')
        self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id)
        self.assertEqual('image-1', image.name)

    def test_create_image(self):
        properties = {
            'name': 'image-1'
        }
        image = self.controller.create(**properties)
        self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id)
        self.assertEqual('image-1', image.name)

    def test_create_bad_additionalProperty_type(self):
        properties = {
            'name': 'image-1',
            'bad_prop': True,
        }
        with testtools.ExpectedException(TypeError):
            self.controller.create(**properties)

    def test_delete_image(self):
        self.controller.delete('87b634c1-f893-33c9-28a9-e5673c99239a')
        expect = [
            ('DELETE',
                '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a',
                {},
                None)]
        self.assertEqual(expect, self.api.calls)

    def test_deactivate_image(self):
        id_image = '87b634c1-f893-33c9-28a9-e5673c99239a'
        self.controller.deactivate(id_image)
        expect = [('POST',
                   '/v2/images/%s/actions/deactivate' % id_image,
                   {},
                   None)]
        self.assertEqual(expect, self.api.calls)

    def test_reactivate_image(self):
        id_image = '87b634c1-f893-33c9-28a9-e5673c99239a'
        self.controller.reactivate(id_image)
        expect = [('POST',
                   '/v2/images/%s/actions/reactivate' % id_image,
                   {},
                   None)]
        self.assertEqual(expect, self.api.calls)

    def test_data_upload(self):
        image_data = 'CCC'
        image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f'
        self.controller.upload(image_id, image_data)
        expect = [('PUT', '/v2/images/%s/file' % image_id,
                  {'Content-Type': 'application/octet-stream'},
                  image_data)]
        self.assertEqual(expect, self.api.calls)

    def test_data_upload_w_size(self):
        image_data = 'CCC'
        image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f'
        self.controller.upload(image_id, image_data, image_size=3)
        expect = [('PUT', '/v2/images/%s/file' % image_id,
                  {'Content-Type': 'application/octet-stream'},
                  image_data)]
        self.assertEqual(expect, self.api.calls)

    def test_data_without_checksum(self):
        body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205',
                                    do_checksum=False)
        body = ''.join([b for b in body])
        self.assertEqual('A', body)

        body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205')
        body = ''.join([b for b in body])
        self.assertEqual('A', body)

    def test_data_with_wrong_checksum(self):
        body = self.controller.data('headeronly-db27-11e1-a1eb-080027cbe205',
                                    do_checksum=False)
        body = ''.join([b for b in body])
        self.assertEqual('BB', body)
        body = self.controller.data('headeronly-db27-11e1-a1eb-080027cbe205')
        try:
            body = ''.join([b for b in body])
            self.fail('data did not raise an error.')
        except IOError as e:
            self.assertEqual(errno.EPIPE, e.errno)
            msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong'
            self.assertIn(msg, str(e))

        body = self.controller.data('chkonly-db27-11e1-a1eb-080027cbe205',
                                    do_checksum=False)
        body = ''.join([b for b in body])
        self.assertEqual('BB', body)
        body = self.controller.data('chkonly-db27-11e1-a1eb-080027cbe205')
        try:
            body = ''.join([b for b in body])
            self.fail('data did not raise an error.')
        except IOError as e:
            self.assertEqual(errno.EPIPE, e.errno)
            msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong'
            self.assertIn(msg, str(e))

        body = self.controller.data('multihash-db27-11e1-a1eb-080027cbe205',
                                    do_checksum=False)
        body = ''.join([b for b in body])
        self.assertEqual('BB', body)
        body = self.controller.data('multihash-db27-11e1-a1eb-080027cbe205')
        try:
            body = ''.join([b for b in body])
            self.fail('data did not raise an error.')
        except IOError as e:
            self.assertEqual(errno.EPIPE, e.errno)
            msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected junk'
            self.assertIn(msg, str(e))

        body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205',
                                    do_checksum=False)
        body = ''.join([b for b in body])
        self.assertEqual('BB', body)
        try:
            body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205')
            self.fail('bad os_hash_algo did not raise an error.')
        except ValueError as e:
            msg = 'unsupported hash type not_an_algo'
            self.assertIn(msg, str(e))

    def test_data_with_checksum(self):
        for prefix in ['headeronly', 'chkonly', 'multihash']:
            body = self.controller.data(prefix +
                                        '-dd57-11e1-af0f-02163e68b1d8',
                                        do_checksum=False)
            body = ''.join([b for b in body])
            self.assertEqual('CCC', body)

            body = self.controller.data(prefix +
                                        '-dd57-11e1-af0f-02163e68b1d8')
            body = ''.join([b for b in body])
            self.assertEqual('CCC', body)

    @ddt.data('headeronly', 'chkonly', 'multihash')
    def test_data_with_checksum_but_no_md5_algo(self, prefix):
        with mock.patch('hashlib.new', mock.MagicMock(
                side_effect=ValueError('unsupported hash type'))):
            body = self.controller.data(prefix +
                                        '-dd57-11e1-af0f-02163e68b1d8',
                                        allow_md5_fallback=True)
            try:
                body = ''.join([b for b in body])
                self.fail('missing md5 algo did not raise an error')
            except IOError as e:
                self.assertEqual(errno.EPIPE, e.errno)
                msg = 'md5 algorithm is not available on the client'
                self.assertIn(msg, str(e))

    def test_data_with_checksum_and_fallback(self):
        # make sure the allow_md5_fallback option does not cause any
        # incorrect behavior when fallback is not needed
        for prefix in ['headeronly', 'chkonly', 'multihash']:
            body = self.controller.data(prefix +
                                        '-dd57-11e1-af0f-02163e68b1d8',
                                        do_checksum=False,
                                        allow_md5_fallback=True)
            body = ''.join([b for b in body])
            self.assertEqual('CCC', body)

            body = self.controller.data(prefix +
                                        '-dd57-11e1-af0f-02163e68b1d8',
                                        allow_md5_fallback=True)
            body = ''.join([b for b in body])
            self.assertEqual('CCC', body)

    def test_data_with_bad_hash_algo_and_fallback(self):
        # shouldn't matter when do_checksum is False
        body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205',
                                    do_checksum=False,
                                    allow_md5_fallback=True)
        body = ''.join([b for b in body])
        self.assertEqual('BB', body)

        # default value for do_checksum is True
        body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205',
                                    allow_md5_fallback=True)
        body = ''.join([b for b in body])
        self.assertEqual('BB', body)

    def test_neg_data_with_bad_hash_value_and_fallback_enabled(self):
        # make sure download fails when good hash_algo but bad hash_value
        # even when correct checksum is present regardless of
        # allow_md5_fallback setting
        body = self.controller.data('bad-multihash-value-good-checksum',
                                    allow_md5_fallback=False)
        try:
            body = ''.join([b for b in body])
            self.fail('bad os_hash_value did not raise an error.')
        except IOError as e:
            self.assertEqual(errno.EPIPE, e.errno)
            msg = 'expected badmultihashvalue'
            self.assertIn(msg, str(e))

        body = self.controller.data('bad-multihash-value-good-checksum',
                                    allow_md5_fallback=True)
        try:
            body = ''.join([b for b in body])
            self.fail('bad os_hash_value did not raise an error.')
        except IOError as e:
            self.assertEqual(errno.EPIPE, e.errno)
            msg = 'expected badmultihashvalue'
            self.assertIn(msg, str(e))

        # download should succeed when do_checksum is off, though
        body = self.controller.data('bad-multihash-value-good-checksum',
                                    do_checksum=False)
        body = ''.join([b for b in body])
        self.assertEqual('GOODCHECKSUM', body)

    def test_image_import(self):
        uri = 'http://example.com/image.qcow'
        data = [('method', {'name': 'web-download',
                 'uri': uri})]
        image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f'
        self.controller.image_import(image_id, 'web-download', uri)
        expect = [('POST', '/v2/images/%s/import' % image_id, {},
                  data)]
        self.assertEqual(expect, self.api.calls)

    def test_download_no_data(self):
        resp = utils.FakeResponse(headers={}, status_code=204)
        self.controller.controller.http_client.get = mock.Mock(
            return_value=(resp, {}))
        self.controller.data('image_id')

    def test_download_forbidden(self):
        self.controller.controller.http_client.get = mock.Mock(
            side_effect=exc.HTTPForbidden())
        try:
            self.controller.data('image_id')
            self.fail('No forbidden exception raised.')
        except exc.HTTPForbidden:
            pass

    def test_update_replace_prop(self):
        image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
        params = {'name': 'pong'}
        image = self.controller.update(image_id, **params)
        expect_hdrs = {
            'Content-Type': 'application/openstack-images-v2.1-json-patch',
        }
        expect_body = [[('op', 'replace'), ('path', '/name'),
                        ('value', 'pong')]]
        expect = [
            ('GET', '/v2/images/%s' % image_id, {}, None),
            ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body),
            ('GET', '/v2/images/%s' % image_id,
             {'x-openstack-request-id': 'req-1234'}, None),
        ]
        self.assertEqual(expect, self.api.calls)
        self.assertEqual(image_id, image.id)
        # NOTE(bcwaldon):due to limitations of our fake api framework, the name
        # will not actually change - yet in real life it will...
        self.assertEqual('image-1', image.name)

    def test_update_add_prop(self):
        image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
        params = {'finn': 'human'}
        image = self.controller.update(image_id, **params)
        expect_hdrs = {
            'Content-Type': 'application/openstack-images-v2.1-json-patch',
        }
        expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]]
        expect = [
            ('GET', '/v2/images/%s' % image_id, {}, None),
            ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body),
            ('GET', '/v2/images/%s' % image_id,
             {'x-openstack-request-id': 'req-1234'}, None),
        ]
        self.assertEqual(expect, self.api.calls)
        self.assertEqual(image_id, image.id)
        # NOTE(bcwaldon):due to limitations of our fake api framework, the name
        # will not actually change - yet in real life it will...
        self.assertEqual('image-1', image.name)

    def test_update_remove_prop(self):
        image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
        remove_props = ['barney']
        image = self.controller.update(image_id, remove_props)
        expect_hdrs = {
            'Content-Type': 'application/openstack-images-v2.1-json-patch',
        }
        expect_body = [[('op', 'remove'), ('path', '/barney')]]
        expect = [
            ('GET', '/v2/images/%s' % image_id, {}, None),
            ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body),
            ('GET', '/v2/images/%s' % image_id,
             {'x-openstack-request-id': 'req-1234'}, None),
        ]
        self.assertEqual(expect, self.api.calls)
        self.assertEqual(image_id, image.id)
        # NOTE(bcwaldon):due to limitations of our fake api framework, the name
        # will not actually change - yet in real life it will...
        self.assertEqual('image-3', image.name)

    def test_update_replace_remove_same_prop(self):
        image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
        # Updating a property takes precedence over removing a property
        params = {'barney': 'miller'}
        remove_props = ['barney']
        image = self.controller.update(image_id, remove_props, **params)
        expect_hdrs = {
            'Content-Type': 'application/openstack-images-v2.1-json-patch',
        }
        expect_body = ([[('op', 'replace'), ('path', '/barney'),
                         ('value', 'miller')]])
        expect = [
            ('GET', '/v2/images/%s' % image_id, {}, None),
            ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body),
            ('GET', '/v2/images/%s' % image_id,
             {'x-openstack-request-id': 'req-1234'}, None),
        ]
        self.assertEqual(expect, self.api.calls)
        self.assertEqual(image_id, image.id)
        # NOTE(bcwaldon):due to limitations of our fake api framework, the name
        # will not actually change - yet in real life it will...
        self.assertEqual('image-3', image.name)

    def test_update_add_remove_same_prop(self):
        image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
        # Adding a property takes precedence over removing a property
        params = {'finn': 'human'}
        remove_props = ['finn']
        image = self.controller.update(image_id, remove_props, **params)
        expect_hdrs = {
            'Content-Type': 'application/openstack-images-v2.1-json-patch',
        }
        expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]]
        expect = [
            ('GET', '/v2/images/%s' % image_id, {}, None),
            ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body),
            ('GET', '/v2/images/%s' % image_id,
             {'x-openstack-request-id': 'req-1234'}, None),
        ]
        self.assertEqual(expect, self.api.calls)
        self.assertEqual(image_id, image.id)
        # NOTE(bcwaldon):due to limitations of our fake api framework, the name
        # will not actually change - yet in real life it will...
        self.assertEqual('image-3', image.name)

    def test_update_bad_additionalProperty_type(self):
        image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
        params = {'name': 'pong', 'bad_prop': False}
        with testtools.ExpectedException(TypeError):
            self.controller.update(image_id, **params)

    def test_update_add_custom_property(self):
        image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
        params = {'color': 'red'}
        image = self.controller.update(image_id, **params)
        expect_hdrs = {
            'Content-Type': 'application/openstack-images-v2.1-json-patch',
        }
        expect_body = [[('op', 'add'), ('path', '/color'), ('value', 'red')]]
        expect = [
            ('GET', '/v2/images/%s' % image_id, {}, None),
            ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body),
            ('GET', '/v2/images/%s' % image_id,
             {'x-openstack-request-id': 'req-1234'}, None),
        ]
        self.assertEqual(expect, self.api.calls)
        self.assertEqual(image_id, image.id)

    def test_update_replace_custom_property(self):
        image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
        params = {'color': 'blue'}
        image = self.controller.update(image_id, **params)
        expect_hdrs = {
            'Content-Type': 'application/openstack-images-v2.1-json-patch',
        }
        expect_body = [[('op', 'replace'), ('path', '/color'),
                        ('value', 'blue')]]
        expect = [
            ('GET', '/v2/images/%s' % image_id, {}, None),
            ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body),
            ('GET', '/v2/images/%s' % image_id,
             {'x-openstack-request-id': 'req-1234'}, None),
        ]
        self.assertEqual(expect, self.api.calls)
        self.assertEqual(image_id, image.id)

    def test_location_ops_when_server_disabled_location_ops(self):
        # Location operations should not be allowed if server has not
        # enabled location related operations. There is no need to check it
        # when do location add, because the check would be done in server side.
        image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
        estr = 'The administrator has disabled API access to image locations'
        url = 'http://bar.com/'
        meta = {'bar': 'barmeta'}

        e = self.assertRaises(exc.HTTPBadRequest,
                              self.controller.delete_locations,
                              image_id, set([url]))
        self.assertIn(estr, str(e))

        e = self.assertRaises(exc.HTTPBadRequest,
                              self.controller.update_location,
                              image_id, url, meta)
        self.assertIn(estr, str(e))

    def _empty_get(self, image_id, headers=None):
        return ('GET', '/v2/images/%s' % image_id,
                headers or {}, None)

    def _patch_req(self, image_id, patch_body):
        c_type = 'application/openstack-images-v2.1-json-patch'
        data = [sorted(d.items()) for d in patch_body]
        return ('PATCH',
                '/v2/images/%s' % image_id,
                {'Content-Type': c_type},
                data)

    def test_add_location(self):
        image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
        new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}}
        add_patch = {'path': '/locations/-', 'value': new_loc, 'op': 'add'}
        headers = {'x-openstack-request-id': 'req-1234'}
        self.controller.add_location(image_id, **new_loc)
        self.assertEqual([self._patch_req(image_id, [add_patch]),
                          self._empty_get(image_id, headers=headers)],
                         self.api.calls)

    @mock.patch.object(images.Controller, '_send_image_update_request',
                       side_effect=exc.HTTPBadRequest)
    def test_add_duplicate_location(self, mock_request):
        image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
        new_loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'newfoo'}}

        self.assertRaises(exc.HTTPBadRequest,
                          self.controller.add_location,
                          image_id, **new_loc)

    def test_remove_location(self):
        image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
        url_set = set(['http://foo.com/', 'http://bar.com/'])
        del_patches = [{'path': '/locations/1', 'op': 'remove'},
                       {'path': '/locations/0', 'op': 'remove'}]
        self.controller.delete_locations(image_id, url_set)
        self.assertEqual([self._empty_get(image_id),
                          self._patch_req(image_id, del_patches)],
                         self.api.calls)

    def test_remove_missing_location(self):
        image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
        url_set = set(['http://spam.ham/'])
        err_str = 'Unknown URL(s): %s' % list(url_set)

        err = self.assertRaises(exc.HTTPNotFound,
                                self.controller.delete_locations,
                                image_id, url_set)
        self.assertIn(err_str, str(err))

    def test_update_location(self):
        image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
        new_loc = {'url': 'http://foo.com/', 'metadata': {'spam': 'ham'}}
        headers = {'x-openstack-request-id': 'req-1234'}
        fixture_idx = '/v2/images/%s' % (image_id)
        orig_locations = data_fixtures[fixture_idx]['GET'][1]['locations']
        loc_map = dict([(l['url'], l) for l in orig_locations])
        loc_map[new_loc['url']] = new_loc
        mod_patch = [{'path': '/locations', 'op': 'replace',
                      'value': list(loc_map.values())}]
        self.controller.update_location(image_id, **new_loc)