Beispiel #1
0
    def test_extract_symlink_mode(self):
        # lp:973260 - charms packed by different tools that record symlink
        # mode permissions differently (ie the charm store) don't extract
        # correctly.
        charm_path = self.copy_charm()
        sym_path = os.path.join(charm_path, 'foobar')
        os.symlink('metadata.yaml', sym_path)
        charm_dir = CharmDirectory(charm_path)
        normal_path = charm_dir.as_bundle().path
        zf_src = zipfile.ZipFile(normal_path, "r")
        foreign_path = os.path.join(self.makeDir(), "store.charm")
        zf_dst = zipfile.ZipFile(foreign_path, "w")
        for info in zf_src.infolist():
            if info.filename == "foobar":
                # This is what the charm store does:
                info.external_attr = (stat.S_IFLNK | 0777) << 16
            zf_dst.writestr(info, zf_src.read(info.filename))
        zf_src.close()
        zf_dst.close()

        bundle = CharmBundle(foreign_path)
        extract_dir = self.makeDir()
        bundle.extract_to(extract_dir)
        self.assertIn("foobar", os.listdir(extract_dir))
        self.assertTrue(os.path.islink(os.path.join(extract_dir, "foobar")))
        self.assertEqual(os.readlink(os.path.join(extract_dir, 'foobar')),
                        'metadata.yaml')
Beispiel #2
0
 def test_charm_base_inheritance(self):
     """
     get_sha256() should be implemented in the base class,
     and should use compute_sha256 to calculate the digest.
     """
     directory = CharmDirectory(self.sample_dir1)
     bundle = directory.as_bundle()
     digest = compute_file_hash(hashlib.sha256, bundle.path)
     self.assertEquals(digest, directory.get_sha256())
Beispiel #3
0
 def test_charm_base_inheritance(self):
     """
     get_sha256() should be implemented in the base class,
     and should use compute_sha256 to calculate the digest.
     """
     directory = CharmDirectory(self.sample_dir1)
     bundle = directory.as_bundle()
     digest = compute_file_hash(hashlib.sha256, bundle.path)
     self.assertEquals(digest, directory.get_sha256())
Beispiel #4
0
 def test_executable_extraction(self):
     sample_directory = os.path.join(
         repository_directory, "series", "varnish-alternative")
     charm_directory = CharmDirectory(sample_directory)
     source_hook_path = os.path.join(sample_directory, "hooks", "install")
     self.assertTrue(os.access(source_hook_path, os.X_OK))
     bundle = charm_directory.as_bundle()
     directory = bundle.as_directory()
     hook_path = os.path.join(directory.path, "hooks", "install")
     self.assertTrue(os.access(hook_path, os.X_OK))
Beispiel #5
0
 def test_executable_extraction(self):
     sample_directory = os.path.join(repository_directory, "series",
                                     "varnish-alternative")
     charm_directory = CharmDirectory(sample_directory)
     source_hook_path = os.path.join(sample_directory, "hooks", "install")
     self.assertTrue(os.access(source_hook_path, os.X_OK))
     bundle = charm_directory.as_bundle()
     directory = bundle.as_directory()
     hook_path = os.path.join(directory.path, "hooks", "install")
     self.assertTrue(os.access(hook_path, os.X_OK))
Beispiel #6
0
 def test_compute_sha256(self):
     """
     Computing the sha256 of a directory will use the bundled
     charm, since the hash of the file itself is needed.
     """
     directory = CharmDirectory(self.sample_dir1)
     sha256 = directory.compute_sha256()
     charm_bundle = directory.as_bundle()
     self.assertEquals(type(charm_bundle), CharmBundle)
     self.assertEquals(compute_file_hash(hashlib.sha256, charm_bundle.path),
                       sha256)
Beispiel #7
0
 def test_as_bundle_file_lifetime(self):
     """
     The temporary bundle file created should have a life time
     equivalent to that of the directory object itself.
     """
     directory = CharmDirectory(self.sample_dir1)
     charm_bundle = directory.as_bundle()
     gc.collect()
     self.assertTrue(os.path.isfile(charm_bundle.path))
     del directory
     gc.collect()
     self.assertFalse(os.path.isfile(charm_bundle.path))
Beispiel #8
0
 def test_compute_sha256(self):
     """
     Computing the sha256 of a directory will use the bundled
     charm, since the hash of the file itself is needed.
     """
     directory = CharmDirectory(self.sample_dir1)
     sha256 = directory.compute_sha256()
     charm_bundle = directory.as_bundle()
     self.assertEquals(type(charm_bundle), CharmBundle)
     self.assertEquals(compute_file_hash(hashlib.sha256,
                                         charm_bundle.path),
                       sha256)
Beispiel #9
0
 def test_as_bundle_file_lifetime(self):
     """
     The temporary bundle file created should have a life time
     equivalent to that of the directory object itself.
     """
     directory = CharmDirectory(self.sample_dir1)
     charm_bundle = directory.as_bundle()
     gc.collect()
     self.assertTrue(os.path.isfile(charm_bundle.path))
     del directory
     gc.collect()
     self.assertFalse(os.path.isfile(charm_bundle.path))
Beispiel #10
0
    def test_as_bundle_with_relative_path(self):
        """
        Ensure that as_bundle works correctly with relative paths.
        """
        current_dir = os.getcwd()
        os.chdir(self.sample_dir2)
        self.addCleanup(os.chdir, current_dir)
        charm_dir = "../%s" % os.path.basename(self.sample_dir1)

        directory = CharmDirectory(charm_dir)
        charm_bundle = directory.as_bundle()
        self.assertEquals(type(charm_bundle), CharmBundle)
        self.assertEquals(charm_bundle.metadata.name, "sample")
Beispiel #11
0
    def test_as_bundle_with_relative_path(self):
        """
        Ensure that as_bundle works correctly with relative paths.
        """
        current_dir = os.getcwd()
        os.chdir(self.sample_dir2)
        self.addCleanup(os.chdir, current_dir)
        charm_dir = "../%s" % os.path.basename(self.sample_dir1)

        directory = CharmDirectory(charm_dir)
        charm_bundle = directory.as_bundle()
        self.assertEquals(type(charm_bundle), CharmBundle)
        self.assertEquals(charm_bundle.metadata.name, "sample")
Beispiel #12
0
    def test_extract_symlink(self):
        extract_dir = self.makeDir()
        charm_path = self.copy_charm()
        sym_path = os.path.join(charm_path, 'foobar')
        os.symlink('metadata.yaml', sym_path)

        charm_dir = CharmDirectory(charm_path)
        bundle = charm_dir.as_bundle()
        bundle.extract_to(extract_dir)
        self.assertIn("foobar", os.listdir(extract_dir))
        self.assertTrue(os.path.islink(os.path.join(extract_dir, "foobar")))
        self.assertEqual(os.readlink(os.path.join(extract_dir, 'foobar')),
                        'metadata.yaml')

        # Verify we can extract it over again
        os.remove(sym_path)
        os.symlink('./config.yaml', sym_path)
        charm_dir = CharmDirectory(charm_path)
        bundle = charm_dir.as_bundle()
        bundle.extract_to(extract_dir)
        self.assertEqual(os.readlink(os.path.join(extract_dir, 'foobar')),
                        './config.yaml')
Beispiel #13
0
    def test_as_bundle(self):
        directory = CharmDirectory(self.sample_dir1)
        charm_bundle = directory.as_bundle()
        self.assertEquals(type(charm_bundle), CharmBundle)
        self.assertEquals(charm_bundle.metadata.name, "sample")
        self.assertIn("sample-1.charm", charm_bundle.path)

        total_compressed = 0
        total_uncompressed = 0
        zip_file = zipfile.ZipFile(charm_bundle.path)
        for n in zip_file.namelist():
            info = zip_file.getinfo(n)
            total_compressed += info.compress_size
            total_uncompressed += info.file_size
        self.assertTrue(total_compressed < total_uncompressed)
Beispiel #14
0
    def test_as_bundle(self):
        directory = CharmDirectory(self.sample_dir1)
        charm_bundle = directory.as_bundle()
        self.assertEquals(type(charm_bundle), CharmBundle)
        self.assertEquals(charm_bundle.metadata.name, "sample")
        self.assertIn("sample-1.charm", charm_bundle.path)

        total_compressed = 0
        total_uncompressed = 0
        zip_file = zipfile.ZipFile(charm_bundle.path)
        for n in zip_file.namelist():
            info = zip_file.getinfo(n)
            total_compressed += info.compress_size
            total_uncompressed += info.file_size
        self.assertTrue(total_compressed < total_uncompressed)
Beispiel #15
0
class RemoteRepositoryTest(RepositoryTestBase):

    def setUp(self):
        super(RemoteRepositoryTest, self).setUp()
        self.cache_path = os.path.join(
            self.makeDir(), "notexistyet")
        self.download_path = os.path.join(self.cache_path, "downloads")

        def delete():
            if os.path.exists(self.cache_path):
                shutil.rmtree(self.cache_path)
        self.addCleanup(delete)

        self.charm = CharmDirectory(
            os.path.join(self.unbundled_repo_path, "series", "dummy"))
        with open(self.charm.as_bundle().path, "rb") as f:
            self.bundle_data = f.read()
        self.sha256 = self.charm.as_bundle().get_sha256()
        self.getPage = self.mocker.replace("twisted.web.client.getPage")
        self.downloadPage = self.mocker.replace(
            "twisted.web.client.downloadPage")

    def repo(self, url_base):
        return RemoteCharmRepository(url_base, self.cache_path)

    def cache_location(self, url_str, revision):
        charm_url = CharmURL.parse(url_str)
        cache_key = under.quote(
            "%s.charm" % (charm_url.with_revision(revision)))
        return os.path.join(self.cache_path, cache_key)

    def charm_info(self, url_str, revision, warnings=None, errors=None):
        info = {"revision": revision, "sha256": self.sha256}
        if errors:
            info["errors"] = errors
        if warnings:
            info["warnings"] = warnings
        return json.dumps({url_str: info})

    def mock_charm_info(self, url, result):
        self.getPage(url)
        self.mocker.result(result)

    def mock_download(self, url, error=None):
        self.downloadPage(url, ANY)
        if error:
            return self.mocker.result(fail(error))

        def download(_, path):
            self.assertTrue(path.startswith(self.download_path))
            with open(path, "wb") as f:
                f.write(self.bundle_data)
            return succeed(None)
        self.mocker.call(download)

    @inlineCallbacks
    def assert_find_uncached(self, dns_name, url_str, info_url, find_url):
        self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1)))
        self.mock_download(find_url)
        self.mocker.replay()

        repo = self.repo(dns_name)
        charm = yield repo.find(CharmURL.parse(url_str))
        self.assertEquals(charm.get_sha256(), self.sha256)
        self.assertEquals(charm.path, self.cache_location(url_str, 1))
        self.assertEquals(os.listdir(self.download_path), [])

    @inlineCallbacks
    def assert_find_cached(self, dns_name, url_str, info_url):
        os.makedirs(self.cache_path)
        cache_location = self.cache_location(url_str, 1)
        shutil.copy(self.charm.as_bundle().path, cache_location)

        self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1)))
        self.mocker.replay()

        repo = self.repo(dns_name)
        charm = yield repo.find(CharmURL.parse(url_str))
        self.assertEquals(charm.get_sha256(), self.sha256)
        self.assertEquals(charm.path, cache_location)

    def assert_find_error(self, dns_name, url_str, err_type, message):
        self.mocker.replay()
        repo = self.repo(dns_name)
        d = self.assertFailure(repo.find(CharmURL.parse(url_str)), err_type)

        def verify(error):
            self.assertEquals(str(error), message)
        d.addCallback(verify)
        return d

    @inlineCallbacks
    def assert_latest(self, dns_name, url_str, revision):
        self.mocker.replay()
        repo = self.repo(dns_name)
        result = yield repo.latest(CharmURL.parse(url_str))
        self.assertEquals(result, revision)

    def assert_latest_error(self, dns_name, url_str, err_type, message):
        self.mocker.replay()
        repo = self.repo(dns_name)
        d = self.assertFailure(repo.latest(CharmURL.parse(url_str)), err_type)

        def verify(error):
            self.assertEquals(str(error), message)
        d.addCallback(verify)
        return d

    def test_find_plain_uncached(self):
        return self.assert_find_uncached(
            "https://somewhe.re", "cs:series/name",
            "https://somewhe.re/charm-info?charms=cs%3Aseries/name",
            "https://somewhe.re/charm/series/name-1")

    def test_find_revision_uncached(self):
        return self.assert_find_uncached(
            "https://somewhe.re", "cs:series/name-1",
            "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1",
            "https://somewhe.re/charm/series/name-1")

    def test_find_user_uncached(self):
        return self.assert_find_uncached(
            "https://somewhereel.se", "cs:~user/srs/name",
            "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name",
            "https://somewhereel.se/charm/%7Euser/srs/name-1")

    def test_find_plain_cached(self):
        return self.assert_find_cached(
            "https://somewhe.re", "cs:series/name",
            "https://somewhe.re/charm-info?charms=cs%3Aseries/name")

    def test_find_revision_cached(self):
        return self.assert_find_cached(
            "https://somewhe.re", "cs:series/name-1",
            "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1")

    def test_find_user_cached(self):
        return self.assert_find_cached(
            "https://somewhereel.se", "cs:~user/srs/name",
            "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name")

    def test_find_info_http_error(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            fail(Error("500")))
        return self.assert_find_error(
            "https://anoth.er", "cs:series/name", CharmNotFound,
            "Charm 'cs:series/name' not found in repository https://anoth.er")

    @inlineCallbacks
    def test_find_info_store_warning(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name-1",
            succeed(self.charm_info(
                "cs:series/name-1", 1, warnings=["omg", "halp"])))
        self.mock_download("https://anoth.er/charm/series/name-1")
        self.mocker.replay()

        repo = self.repo("https://anoth.er")
        log = self.capture_logging("juju.charm")
        charm = yield repo.find(CharmURL.parse("cs:series/name-1"))
        self.assertIn("omg", log.getvalue())
        self.assertIn("halp", log.getvalue())
        self.assertEquals(charm.get_sha256(), self.sha256)

    def test_find_info_store_error(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name-101",
            succeed(self.charm_info(
                "cs:series/name-101", 101, errors=["oh", "noes"])))
        return self.assert_find_error(
            "https://anoth.er", "cs:series/name-101", CharmError,
            "Error processing 'cs:series/name-101': oh; noes")

    def test_find_info_bad_revision(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name-99",
            succeed(self.charm_info("cs:series/name-99", 1)))
        return self.assert_find_error(
            "https://anoth.er", "cs:series/name-99", AssertionError,
            "bad url revision")

    def test_find_download_error(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(json.dumps({"cs:series/name": {"revision": 123}})))
        self.mock_download(
            "https://anoth.er/charm/series/name-123", Error("999"))
        return self.assert_find_error(
            "https://anoth.er", "cs:series/name", CharmNotFound,
            "Charm 'cs:series/name-123' not found in repository "
            "https://anoth.er")

    def test_find_charm_revision_mismatch(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(json.dumps({"cs:series/name": {"revision": 99}})))
        self.mock_download("https://anoth.er/charm/series/name-99")
        return self.assert_find_error(
            "https://anoth.er", "cs:series/name", AssertionError,
            "bad charm revision")

    @inlineCallbacks
    def test_find_downloaded_hash_mismatch(self):
        cache_location = self.cache_location("cs:series/name-1", 1)
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(json.dumps(
                {"cs:series/name": {"revision": 1, "sha256": "NO YUO"}})))
        self.mock_download("https://anoth.er/charm/series/name-1")
        yield self.assert_find_error(
            "https://anoth.er", "cs:series/name", CharmError,
            "Error processing 'cs:series/name-1 (downloaded)': SHA256 "
            "mismatch")
        self.assertFalse(os.path.exists(cache_location))

    @inlineCallbacks
    def test_find_cached_hash_mismatch(self):
        os.makedirs(self.cache_path)
        cache_location = self.cache_location("cs:series/name-1", 1)
        shutil.copy(self.charm.as_bundle().path, cache_location)

        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(json.dumps(
                {"cs:series/name": {"revision": 1, "sha256": "NO YUO"}})))
        yield self.assert_find_error(
            "https://anoth.er", "cs:series/name", CharmError,
            "Error processing 'cs:series/name-1 (cached)': SHA256 mismatch")
        self.assertFalse(os.path.exists(cache_location))

    def test_latest_plain(self):
        self.mock_charm_info(
            "https://somewhe.re/charm-info?charms=cs%3Afoo/bar",
            succeed(self.charm_info("cs:foo/bar", 99)))
        return self.assert_latest("https://somewhe.re", "cs:foo/bar-1", 99)

    def test_latest_user(self):
        self.mock_charm_info(
            "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar",
            succeed(self.charm_info("cs:~fee/foo/bar", 123)))
        return self.assert_latest(
            "https://somewhereel.se", "cs:~fee/foo/bar", 123)

    def test_latest_revision(self):
        self.mock_charm_info(
            "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar",
            succeed(self.charm_info("cs:~fee/foo/bar", 123)))
        return self.assert_latest(
            "https://somewhereel.se", "cs:~fee/foo/bar-99", 123)

    def test_latest_http_error(self):
        self.mock_charm_info(
            "https://andanoth.er/charm-info?charms=cs%3A%7Eblib/blab/blob",
            fail(Error("404")))
        return self.assert_latest_error(
            "https://andanoth.er", "cs:~blib/blab/blob", CharmNotFound,
            "Charm 'cs:~blib/blab/blob' not found in repository "
            "https://andanoth.er")

    @inlineCallbacks
    def test_latest_store_warning(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(self.charm_info(
                "cs:series/name", 1, warnings=["eww", "yuck"])))
        self.mocker.replay()

        repo = self.repo("https://anoth.er")
        log = self.capture_logging("juju.charm")
        revision = yield repo.latest(CharmURL.parse("cs:series/name-1"))
        self.assertIn("eww", log.getvalue())
        self.assertIn("yuck", log.getvalue())
        self.assertEquals(revision, 1)

    def test_latest_store_error(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(self.charm_info(
                "cs:series/name", 1, errors=["blam", "dink"])))
        return self.assert_latest_error(
            "https://anoth.er", "cs:series/name-1", CharmError,
            "Error processing 'cs:series/name': blam; dink")

    def test_repo_type(self):
        self.mocker.replay()
        self.assertEqual(self.repo("http://fbaro.com").type, "store")
Beispiel #16
0
class RemoteRepositoryTest(RepositoryTestBase):
    def setUp(self):
        super(RemoteRepositoryTest, self).setUp()
        self.cache_path = os.path.join(tempfile.mkdtemp(), "notexistyet")
        self.download_path = os.path.join(self.cache_path, "downloads")

        def delete():
            if os.path.exists(self.cache_path):
                shutil.rmtree(self.cache_path)

        self.addCleanup(delete)

        self.charm = CharmDirectory(
            os.path.join(self.unbundled_repo_path, "series", "dummy"))
        with open(self.charm.as_bundle().path, "rb") as f:
            self.bundle_data = f.read()
        self.sha256 = self.charm.as_bundle().get_sha256()
        self.getPage = self.mocker.replace("twisted.web.client.getPage")
        self.downloadPage = self.mocker.replace(
            "twisted.web.client.downloadPage")

    def repo(self, url_base):
        return RemoteCharmRepository(url_base, self.cache_path)

    def cache_location(self, url_str, revision):
        charm_url = CharmURL.parse(url_str)
        cache_key = under.quote("%s.charm" %
                                (charm_url.with_revision(revision)))
        return os.path.join(self.cache_path, cache_key)

    def charm_info(self, url_str, revision, warnings=None, errors=None):
        info = {"revision": revision, "sha256": self.sha256}
        if errors:
            info["errors"] = errors
        if warnings:
            info["warnings"] = warnings
        return json.dumps({url_str: info})

    def mock_charm_info(self, url, result):
        self.getPage(url)
        self.mocker.result(result)

    def mock_download(self, url, error=None):
        self.downloadPage(url, ANY)
        if error:
            return self.mocker.result(fail(error))

        def download(_, path):
            self.assertTrue(path.startswith(self.download_path))
            with open(path, "wb") as f:
                f.write(self.bundle_data)
            return succeed(None)

        self.mocker.call(download)

    @inlineCallbacks
    def assert_find_uncached(self, dns_name, url_str, info_url, find_url):
        self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1)))
        self.mock_download(find_url)
        self.mocker.replay()

        repo = self.repo(dns_name)
        charm = yield repo.find(CharmURL.parse(url_str))
        self.assertEquals(charm.get_sha256(), self.sha256)
        self.assertEquals(charm.path, self.cache_location(url_str, 1))
        self.assertEquals(os.listdir(self.download_path), [])

    @inlineCallbacks
    def assert_find_cached(self, dns_name, url_str, info_url):
        os.makedirs(self.cache_path)
        cache_location = self.cache_location(url_str, 1)
        shutil.copy(self.charm.as_bundle().path, cache_location)

        self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1)))
        self.mocker.replay()

        repo = self.repo(dns_name)
        charm = yield repo.find(CharmURL.parse(url_str))
        self.assertEquals(charm.get_sha256(), self.sha256)
        self.assertEquals(charm.path, cache_location)

    def assert_find_error(self, dns_name, url_str, err_type, message):
        self.mocker.replay()
        repo = self.repo(dns_name)
        d = self.assertFailure(repo.find(CharmURL.parse(url_str)), err_type)

        def verify(error):
            self.assertEquals(str(error), message)

        d.addCallback(verify)
        return d

    @inlineCallbacks
    def assert_latest(self, dns_name, url_str, revision):
        self.mocker.replay()
        repo = self.repo(dns_name)
        result = yield repo.latest(CharmURL.parse(url_str))
        self.assertEquals(result, revision)

    def assert_latest_error(self, dns_name, url_str, err_type, message):
        self.mocker.replay()
        repo = self.repo(dns_name)
        d = self.assertFailure(repo.latest(CharmURL.parse(url_str)), err_type)

        def verify(error):
            self.assertEquals(str(error), message)

        d.addCallback(verify)
        return d

    def test_find_plain_uncached(self):
        return self.assert_find_uncached(
            "https://somewhe.re", "cs:series/name",
            "https://somewhe.re/charm-info?charms=cs%3Aseries/name",
            "https://somewhe.re/charm/series/name-1")

    def test_find_revision_uncached(self):
        return self.assert_find_uncached(
            "https://somewhe.re", "cs:series/name-1",
            "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1",
            "https://somewhe.re/charm/series/name-1")

    def test_find_user_uncached(self):
        return self.assert_find_uncached(
            "https://somewhereel.se", "cs:~user/srs/name",
            "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name",
            "https://somewhereel.se/charm/%7Euser/srs/name-1")

    def test_find_plain_cached(self):
        return self.assert_find_cached(
            "https://somewhe.re", "cs:series/name",
            "https://somewhe.re/charm-info?charms=cs%3Aseries/name")

    def test_find_revision_cached(self):
        return self.assert_find_cached(
            "https://somewhe.re", "cs:series/name-1",
            "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1")

    def test_find_user_cached(self):
        return self.assert_find_cached(
            "https://somewhereel.se", "cs:~user/srs/name",
            "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name")

    def test_find_info_http_error(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            fail(Error("500")))
        return self.assert_find_error(
            "https://anoth.er", "cs:series/name", CharmNotFound,
            "Charm 'cs:series/name' not found in repository https://anoth.er")

    @inlineCallbacks
    def test_find_info_store_warning(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name-1",
            succeed(
                self.charm_info("cs:series/name-1",
                                1,
                                warnings=["omg", "halp"])))
        self.mock_download("https://anoth.er/charm/series/name-1")
        self.mocker.replay()

        repo = self.repo("https://anoth.er")
        log = self.capture_logging("juju.charm")
        charm = yield repo.find(CharmURL.parse("cs:series/name-1"))
        self.assertIn("omg", log.getvalue())
        self.assertIn("halp", log.getvalue())
        self.assertEquals(charm.get_sha256(), self.sha256)

    def test_find_info_store_error(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name-101",
            succeed(
                self.charm_info("cs:series/name-101",
                                101,
                                errors=["oh", "noes"])))
        return self.assert_find_error(
            "https://anoth.er", "cs:series/name-101", CharmError,
            "Error processing 'cs:series/name-101': oh; noes")

    def test_find_info_bad_revision(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name-99",
            succeed(self.charm_info("cs:series/name-99", 1)))
        return self.assert_find_error("https://anoth.er", "cs:series/name-99",
                                      AssertionError, "bad url revision")

    def test_find_download_error(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(json.dumps({"cs:series/name": {
                "revision": 123
            }})))
        self.mock_download("https://anoth.er/charm/series/name-123",
                           Error("999"))
        return self.assert_find_error(
            "https://anoth.er", "cs:series/name", CharmNotFound,
            "Charm 'cs:series/name-123' not found in repository "
            "https://anoth.er")

    def test_find_charm_revision_mismatch(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(json.dumps({"cs:series/name": {
                "revision": 99
            }})))
        self.mock_download("https://anoth.er/charm/series/name-99")
        return self.assert_find_error("https://anoth.er", "cs:series/name",
                                      AssertionError, "bad charm revision")

    @inlineCallbacks
    def test_find_downloaded_hash_mismatch(self):
        cache_location = self.cache_location("cs:series/name-1", 1)
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(
                json.dumps(
                    {"cs:series/name": {
                        "revision": 1,
                        "sha256": "NO YUO"
                    }})))
        self.mock_download("https://anoth.er/charm/series/name-1")
        yield self.assert_find_error(
            "https://anoth.er", "cs:series/name", CharmError,
            "Error processing 'cs:series/name-1 (downloaded)': SHA256 "
            "mismatch")
        self.assertFalse(os.path.exists(cache_location))

    @inlineCallbacks
    def test_find_cached_hash_mismatch(self):
        os.makedirs(self.cache_path)
        cache_location = self.cache_location("cs:series/name-1", 1)
        shutil.copy(self.charm.as_bundle().path, cache_location)

        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(
                json.dumps(
                    {"cs:series/name": {
                        "revision": 1,
                        "sha256": "NO YUO"
                    }})))
        yield self.assert_find_error(
            "https://anoth.er", "cs:series/name", CharmError,
            "Error processing 'cs:series/name-1 (cached)': SHA256 mismatch")
        self.assertFalse(os.path.exists(cache_location))

    def test_latest_plain(self):
        self.mock_charm_info(
            "https://somewhe.re/charm-info?charms=cs%3Afoo/bar",
            succeed(self.charm_info("cs:foo/bar", 99)))
        return self.assert_latest("https://somewhe.re", "cs:foo/bar-1", 99)

    def test_latest_user(self):
        self.mock_charm_info(
            "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar",
            succeed(self.charm_info("cs:~fee/foo/bar", 123)))
        return self.assert_latest("https://somewhereel.se", "cs:~fee/foo/bar",
                                  123)

    def test_latest_revision(self):
        self.mock_charm_info(
            "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar",
            succeed(self.charm_info("cs:~fee/foo/bar", 123)))
        return self.assert_latest("https://somewhereel.se",
                                  "cs:~fee/foo/bar-99", 123)

    def test_latest_http_error(self):
        self.mock_charm_info(
            "https://andanoth.er/charm-info?charms=cs%3A%7Eblib/blab/blob",
            fail(Error("404")))
        return self.assert_latest_error(
            "https://andanoth.er", "cs:~blib/blab/blob", CharmNotFound,
            "Charm 'cs:~blib/blab/blob' not found in repository "
            "https://andanoth.er")

    @inlineCallbacks
    def test_latest_store_warning(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(
                self.charm_info("cs:series/name", 1, warnings=["eww",
                                                               "yuck"])))
        self.mocker.replay()

        repo = self.repo("https://anoth.er")
        log = self.capture_logging("juju.charm")
        revision = yield repo.latest(CharmURL.parse("cs:series/name-1"))
        self.assertIn("eww", log.getvalue())
        self.assertIn("yuck", log.getvalue())
        self.assertEquals(revision, 1)

    def test_latest_store_error(self):
        self.mock_charm_info(
            "https://anoth.er/charm-info?charms=cs%3Aseries/name",
            succeed(
                self.charm_info("cs:series/name", 1, errors=["blam", "dink"])))
        return self.assert_latest_error(
            "https://anoth.er", "cs:series/name-1", CharmError,
            "Error processing 'cs:series/name': blam; dink")