diff --git a/tests/Makefile.am.inc b/tests/Makefile.am.inc
index b378d7a0..adcdc9d8 100644
--- a/tests/Makefile.am.inc
+++ b/tests/Makefile.am.inc
@@ -100,6 +100,8 @@ dist_test_scripts = \
tests/test-bundle.sh \
tests/test-bundle-system.sh \
tests/test-oci.sh \
+ tests/test-oci-registry.sh \
+ tests/test-oci-registry-system.sh \
tests/test-unsigned-summaries.sh \
tests/test-update-remote-configuration.sh \
$(NULL)
diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py
new file mode 100644
index 00000000..033b54d3
--- /dev/null
+++ b/tests/oci-registry-client.py
@@ -0,0 +1,38 @@
+#!/usr/bin/python2
+
+import httplib
+import urllib
+import sys
+
+if sys.argv[2] == 'add':
+ detach_icons = '--detach-icons' in sys.argv
+ if detach_icons:
+ sys.argv.remove('--detach-icons')
+ params = {'d': sys.argv[5]}
+ if detach_icons:
+ params['detach-icons'] = 1
+ query = urllib.urlencode(params)
+ conn = httplib.HTTPConnection(sys.argv[1])
+ path = "/testing/{repo}/{tag}?{query}".format(repo=sys.argv[3],
+ tag=sys.argv[4],
+ query=query)
+ conn.request("POST", path)
+ response = conn.getresponse()
+ if response.status != 200:
+ print >>sys.stderr, response.read()
+ print >>sys.stderr, "Failed: status={}".format(response.status)
+ sys.exit(1)
+elif sys.argv[2] == 'delete':
+ conn = httplib.HTTPConnection(sys.argv[1])
+ path = "/testing/{repo}/{ref}".format(repo=sys.argv[3],
+ ref=sys.argv[4])
+ conn.request("DELETE", path)
+ response = conn.getresponse()
+ if response.status != 200:
+ print >>sys.stderr, response.read()
+ print >>sys.stderr, "Failed: status={}".format(response.status)
+ sys.exit(1)
+else:
+ print >>sys.stderr, "Usage: oci-registry-client.py [add|remove] ARGS"
+ sys.exit(1)
+
diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py
new file mode 100644
index 00000000..f31eecdd
--- /dev/null
+++ b/tests/oci-registry-server.py
@@ -0,0 +1,233 @@
+#!/usr/bin/python2
+
+import BaseHTTPServer
+import base64
+import hashlib
+import json
+import os
+import sys
+from urlparse import parse_qs
+import time
+
+repositories = {}
+icons = {}
+
+def get_index():
+ results = []
+ for repo_name in sorted(repositories.keys()):
+ repo = repositories[repo_name]
+ results.append({
+ 'Name': repo_name,
+ 'Images': repo['images'],
+ 'Lists': [],
+ })
+
+ return json.dumps({
+ 'Registry': '/',
+ 'Results': results
+ }, indent=4)
+
+def cache_icon(data_uri):
+ prefix = 'data:image/png;base64,'
+ assert data_uri.startswith(prefix)
+ data = base64.b64decode(data_uri[len(prefix):])
+ h = hashlib.sha256()
+ h.update(data)
+ digest = h.hexdigest()
+ filename = digest + '.png'
+ icons[filename] = data
+
+ return '/icons/' + filename
+
+serial = 0
+server_start_time = int(time.time())
+
+def get_etag():
+ return str(server_start_time) + '-' + str(serial)
+
+def modified():
+ global serial
+ serial += 1
+
+def parse_http_date(date):
+ parsed = parsedate(date)
+ if parsed is not None:
+ return timegm(parsed)
+ else:
+ return None
+
+class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ def check_route(self, route):
+ parts = self.path.split('?', 1)
+ path = parts[0].split('/')
+
+ result = []
+
+ route_path = route.split('/')
+ print(route_path, path)
+ if len(route_path) != len(path):
+ return False
+
+ matches = {}
+ for i in range(1, len(route_path)):
+ if route_path[i][0] == '@':
+ matches[route_path[i][1:]] = path[i]
+ elif route_path[i] != path[i]:
+ return False
+
+ self.matches = matches
+ if len(parts) == 1:
+ self.query = {}
+ else:
+ self.query = parse_qs(parts[1], keep_blank_values=True)
+
+ return True
+
+ def do_GET(self):
+ response = 200
+ response_string = None
+ response_content_type = "application/octet-stream"
+ response_file = None
+
+ add_headers = {}
+
+ if self.check_route('/v2/@repo_name/blobs/@digest'):
+ repo_name = self.matches['repo_name']
+ digest = self.matches['digest']
+ response_file = repositories[repo_name]['blobs'][digest]
+ elif self.check_route('/v2/@repo_name/manifests/@ref'):
+ repo_name = self.matches['repo_name']
+ ref = self.matches['ref']
+ response_file = repositories[repo_name]['manifests'][ref]
+ elif self.check_route('/index/static') or self.check_route('/index/dynamic'):
+ etag = get_etag()
+ if self.headers.get("If-None-Match") == etag:
+ response = 304
+ else:
+ response_string = get_index()
+ add_headers['Etag'] = etag
+ elif self.check_route('/icons/@filename') :
+ response_string = icons[self.matches['filename']]
+ response_content_type = 'image/png'
+ else:
+ response = 404
+
+ self.send_response(response)
+ for k, v in add_headers.items():
+ self.send_header(k, v)
+
+ if response == 200:
+ self.send_header("Content-Type", response_content_type)
+
+ if response == 200 or response == 304:
+ self.send_header('Cache-Control', 'no-cache')
+
+ self.end_headers()
+
+ if response == 200:
+ if response_file:
+ with open(response_file) as f:
+ response_string = f.read()
+ self.wfile.write(response_string)
+
+ def do_POST(self):
+ if self.check_route('/testing/@repo_name/@tag'):
+ repo_name = self.matches['repo_name']
+ tag = self.matches['tag']
+ d = self.query['d'][0]
+ detach_icons = 'detach-icons' in self.query
+
+ repo = repositories.setdefault(repo_name, {})
+ blobs = repo.setdefault('blobs', {})
+ manifests = repo.setdefault('manifests', {})
+ images = repo.setdefault('images', [])
+
+ with open(os.path.join(d, 'index.json')) as f:
+ index = json.load(f)
+
+ manifest_digest = index['manifests'][0]['digest']
+ manifest_path = os.path.join(d, 'blobs', *manifest_digest.split(':'))
+ manifests[manifest_digest] = manifest_path
+ manifests[tag] = manifest_path
+
+ with open(manifest_path) as f:
+ manifest = json.load(f)
+
+ config_digest = manifest['config']['digest']
+ config_path = os.path.join(d, 'blobs', *config_digest.split(':'))
+
+ with open(config_path) as f:
+ config = json.load(f)
+
+ for dig in os.listdir(os.path.join(d, 'blobs', 'sha256')):
+ digest = 'sha256:' + dig
+ path = os.path.join(d, 'blobs', 'sha256', dig)
+ if digest != manifest_digest:
+ blobs[digest] = path
+
+ if detach_icons:
+ for size in (64, 128):
+ annotation = 'org.freedesktop.appstream.icon-{}'.format(size)
+ icon = manifest['annotations'].get(annotation)
+ if icon:
+ path = cache_icon(icon)
+ manifest['annotations'][annotation] = path
+
+ image = {
+ "Tags": [tag],
+ "Digest": manifest_digest,
+ "MediaType": "application/vnd.oci.image.manifest.v1+json",
+ "OS": config['os'],
+ "Architecture": config['architecture'],
+ "Annotations": manifest['annotations'],
+ "Labels": {},
+ }
+
+ images.append(image)
+
+ modified()
+ self.send_response(200)
+ self.end_headers()
+ return
+ else:
+ self.send_response(404)
+ self.end_headers()
+ return
+
+ def do_DELETE(self):
+ if self.check_route('/testing/@repo_name/@ref'):
+ repo_name = self.matches['repo_name']
+ ref = self.matches['ref']
+
+ repo = repositories.setdefault(repo_name, {})
+ blobs = repo.setdefault('blobs', {})
+ manifests = repo.setdefault('manifests', {})
+ images = repo.setdefault('images', [])
+
+ image = None
+ for i in images:
+ if i['Digest'] == ref or ref in i['Tags']:
+ image = i
+ break
+
+ assert image
+
+ images.remove(image)
+ del manifests[image['Digest']]
+ for t in image['Tags']:
+ del manifests[t]
+
+ modified()
+ self.send_response(200)
+ self.end_headers()
+ return
+ else:
+ self.send_response(404)
+ self.end_headers()
+ return
+
+def test():
+ BaseHTTPServer.test(RequestHandler)
+
+if __name__ == '__main__':
+ test()
diff --git a/tests/test-oci-registry-system.sh b/tests/test-oci-registry-system.sh
new file mode 100755
index 00000000..c0847760
--- /dev/null
+++ b/tests/test-oci-registry-system.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+#
+# Copyright (C) 2018 Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+export USE_SYSTEMDIR=yes
+
+. $(dirname $0)/test-oci-registry.sh
diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh
new file mode 100755
index 00000000..49fb4b22
--- /dev/null
+++ b/tests/test-oci-registry.sh
@@ -0,0 +1,134 @@
+#!/bin/bash
+#
+# Copyright (C) 2018 Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+set -euo pipefail
+
+. $(dirname $0)/libtest.sh
+
+export FLATPAK_ENABLE_EXPERIMENTAL_OCI=1
+
+skip_without_bwrap
+
+echo "1..7"
+
+# Start the fake registry server
+
+$(dirname $0)/test-webserver.sh "" "python2 $test_srcdir/oci-registry-server.py 0"
+FLATPAK_HTTP_PID=$(cat httpd-pid)
+mv httpd-port httpd-port-main
+port=$(cat httpd-port-main)
+client="python2 $test_srcdir/oci-registry-client.py 127.0.0.1:$port"
+
+setup_repo_no_add oci
+
+# Add OCI bundles to it
+
+${FLATPAK} build-bundle --runtime --oci $FL_GPGARGS repos/oci oci/platform-image org.test.Platform
+$client add platform latest $(pwd)/oci/platform-image
+
+${FLATPAK} build-bundle --oci $FL_GPGARGS repos/oci oci/app-image org.test.Hello
+$client add hello latest $(pwd)/oci/app-image
+
+# Add an OCI remote
+
+flatpak remote-add ${U} --oci oci-registry "http://127.0.0.1:${port}"
+
+# Check that the images we expect are listed
+
+images=$(flatpak remote-ls ${U} oci-registry | sort | tr '\n' ' ' | sed 's/ $//')
+assert_streq "$images" "org.test.Hello org.test.Platform"
+echo "ok list remote"
+
+# Pull appstream data
+
+flatpak update ${U} --appstream oci-registry
+
+# Check that the appstream and icons exist
+
+if [ x${USE_SYSTEMDIR-} == xyes ] ; then
+ appstream=$SYSTEMDIR/appstream/oci-registry/$ARCH/appstream.xml.gz
+ icondir=$SYSTEMDIR/appstream/oci-registry/$ARCH/icons
+else
+ appstream=$USERDIR/appstream/oci-registry/$ARCH/appstream.xml.gz
+ icondir=$USERDIR/appstream/oci-registry/$ARCH/icons
+fi
+
+gunzip -c $appstream > appstream-uncompressed
+assert_file_has_content appstream-uncompressed 'org.test.Hello.desktop'
+assert_has_file $icondir/64x64/org.test.Hello.png
+
+echo "ok appstream"
+
+# Test that 'flatpak search' works
+flatpak search org.test.Hello > search-results
+assert_file_has_content search-results "Print a greeting"
+
+echo "ok search"
+
+# Replace with the app image with detached icons, check that the icons work
+
+old_icon_hash=(md5sum $icondir/64x64/org.test.Hello.png)
+rm $icondir/64x64/org.test.Hello.png
+$client delete hello latest
+$client add --detach-icons hello latest $(pwd)/oci/app-image
+flatpak update ${U} --appstream oci-registry
+assert_has_file $icondir/64x64/org.test.Hello.png
+new_icon_hash=(md5sum $icondir/64x64/org.test.Hello.png)
+assert_streq $old_icon_hash $new_icon_hash
+
+echo "ok detached icons"
+
+# Try installing from the remote
+
+${FLATPAK} ${U} install -y oci-registry org.test.Platform
+echo "ok install"
+
+# Remove the app from the registry, check that things were removed properly
+
+$client delete hello latest
+
+images=$(flatpak remote-ls ${U} oci-registry | sort | tr '\n' ' ' | sed 's/ $//')
+assert_streq "$images" "org.test.Platform"
+
+flatpak update ${U} --appstream oci-registry
+
+assert_not_file_has_content $appstream 'org.test.Hello.desktop'
+assert_not_has_file $icondir/64x64/org.test.Hello.png
+assert_not_has_file $icondir/64x64
+
+echo "ok appstream change"
+
+# Delete the remote, check that everything was removed
+
+if [ x${USE_SYSTEMDIR-} == xyes ] ; then
+ base=$SYSTEMDIR
+else
+ base=$USERDIR
+fi
+
+assert_has_file $base/oci/oci-registry.index.gz
+assert_has_file $base/oci/oci-registry.summary
+assert_has_dir $base/appstream/oci-registry
+flatpak ${U} -y uninstall org.test.Platform
+flatpak ${U} remote-delete oci-registry
+assert_not_has_file $base/oci/oci-registry.index.gz
+assert_not_has_file $base/oci/oci-registry.summary
+assert_not_has_dir $base/appstream/oci-registry
+
+echo "ok delete remote"
diff --git a/tests/test-oci.sh b/tests/test-oci.sh
index f4cd3540..44e4577a 100755
--- a/tests/test-oci.sh
+++ b/tests/test-oci.sh
@@ -27,12 +27,10 @@ skip_without_bwrap
echo "1..2"
-setup_repo
-
-${FLATPAK} ${U} install test-repo org.test.Platform master
+setup_repo_no_add oci
mkdir -p oci
-${FLATPAK} build-bundle --oci $FL_GPGARGS repos/test oci/image org.test.Hello
+${FLATPAK} build-bundle --oci $FL_GPGARGS repos/oci oci/image org.test.Hello
assert_has_file oci/image/oci-layout
assert_has_dir oci/image/blobs/sha256