blob: be6eb259cc3897060198c1b527576d2c200fba5b [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
"""Tests for HTTP operations against a file-backend transfer."""
import json
import os
import unittest
import urllib.error
from .test_base import (
IMAGE_SIZE,
ImageServerTestCase,
http_get,
http_options,
http_patch,
http_post,
http_put,
make_file_transfer,
randbytes,
shutdown_image_server,
)
class FileBackendTestCase(ImageServerTestCase):
"""Base that creates a file-backend transfer per test."""
def setUp(self):
self._tid, self._url, self._path, self._cleanup = make_file_transfer()
def tearDown(self):
self._cleanup()
class TestOptions(FileBackendTestCase):
def test_options_returns_features(self):
resp = http_options(self._url)
self.assertEqual(resp.status, 200)
body = json.loads(resp.read())
self.assertIn("flush", body["features"])
self.assertGreaterEqual(body["max_readers"], 1)
self.assertGreaterEqual(body["max_writers"], 1)
def test_options_allowed_methods(self):
resp = http_options(self._url)
methods = resp.getheader("Access-Control-Allow-Methods")
for m in ("GET", "PUT", "POST", "OPTIONS"):
self.assertIn(m, methods)
class TestGetFull(FileBackendTestCase):
def test_get_full_returns_file_content(self):
with open(self._path, "rb") as f:
expected = f.read()
resp = http_get(self._url)
self.assertEqual(resp.status, 200)
data = resp.read()
self.assertEqual(len(data), len(expected))
self.assertEqual(data, expected)
def test_get_full_content_type(self):
resp = http_get(self._url)
resp.read()
self.assertIn("application/octet-stream", resp.getheader("Content-Type"))
def test_get_full_content_length(self):
resp = http_get(self._url)
resp.read()
self.assertEqual(int(resp.getheader("Content-Length")), os.path.getsize(self._path))
class TestGetRange(FileBackendTestCase):
def test_get_range_partial(self):
with open(self._path, "rb") as f:
f.seek(100)
expected = f.read(200)
resp = http_get(self._url, headers={"Range": "bytes=100-299"})
self.assertEqual(resp.status, 206)
self.assertEqual(resp.read(), expected)
def test_get_range_content_range_header(self):
size = os.path.getsize(self._path)
resp = http_get(self._url, headers={"Range": "bytes=0-99"})
self.assertEqual(resp.status, 206)
resp.read()
self.assertEqual(resp.getheader("Content-Range"), f"bytes 0-99/{size}")
def test_get_range_suffix(self):
with open(self._path, "rb") as f:
expected = f.read()[-100:]
resp = http_get(self._url, headers={"Range": "bytes=-100"})
self.assertEqual(resp.status, 206)
self.assertEqual(resp.read(), expected)
def test_get_range_open_ended(self):
with open(self._path, "rb") as f:
f.seek(IMAGE_SIZE - 50)
expected = f.read()
resp = http_get(self._url, headers={"Range": f"bytes={IMAGE_SIZE - 50}-"})
self.assertEqual(resp.status, 206)
self.assertEqual(resp.read(), expected)
def test_get_range_unsatisfiable(self):
with self.assertRaises(urllib.error.HTTPError) as ctx:
http_get(self._url, headers={"Range": f"bytes={IMAGE_SIZE + 100}-{IMAGE_SIZE + 200}"})
self.assertEqual(ctx.exception.code, 416)
class TestPut(FileBackendTestCase):
def test_put_full_upload(self):
new_data = randbytes(99, IMAGE_SIZE)
resp = http_put(self._url, new_data)
body = json.loads(resp.read())
self.assertEqual(resp.status, 200)
self.assertTrue(body["ok"])
self.assertEqual(body["bytes_written"], IMAGE_SIZE)
with open(self._path, "rb") as f:
self.assertEqual(f.read(), new_data)
def test_put_with_flush(self):
new_data = randbytes(100, IMAGE_SIZE)
resp = http_put(f"{self._url}?flush=y", new_data)
body = json.loads(resp.read())
self.assertTrue(body["ok"])
self.assertTrue(body["flushed"])
def test_put_verify_by_get(self):
new_data = randbytes(101, IMAGE_SIZE)
http_put(self._url, new_data)
resp = http_get(self._url)
self.assertEqual(resp.read(), new_data)
def test_put_with_content_range_rejected(self):
data = b"x" * 100
with self.assertRaises(urllib.error.HTTPError) as ctx:
http_put(self._url, data, headers={"Content-Range": "bytes 0-99/*"})
self.assertEqual(ctx.exception.code, 400)
def test_put_with_range_header_rejected(self):
data = b"x" * 100
with self.assertRaises(urllib.error.HTTPError) as ctx:
http_put(self._url, data, headers={"Range": "bytes=0-99"})
self.assertEqual(ctx.exception.code, 400)
class TestFlush(FileBackendTestCase):
def test_post_flush(self):
resp = http_post(f"{self._url}/flush")
body = json.loads(resp.read())
self.assertEqual(resp.status, 200)
self.assertTrue(body["ok"])
class TestPatchRejected(FileBackendTestCase):
def test_patch_rejected_for_file(self):
data = json.dumps({"op": "zero", "size": 100}).encode()
with self.assertRaises(urllib.error.HTTPError) as ctx:
http_patch(self._url, data, headers={
"Content-Type": "application/json",
"Content-Length": str(len(data)),
})
self.assertEqual(ctx.exception.code, 400)
class TestExtentsRejected(FileBackendTestCase):
def test_extents_rejected_for_file(self):
with self.assertRaises(urllib.error.HTTPError) as ctx:
http_get(f"{self._url}/extents")
self.assertEqual(ctx.exception.code, 400)
class TestUnknownImage(ImageServerTestCase):
def test_get_unknown_image(self):
with self.assertRaises(urllib.error.HTTPError) as ctx:
http_get(f"{self.base_url}/images/nonexistent-id")
self.assertEqual(ctx.exception.code, 404)
def test_put_unknown_image(self):
with self.assertRaises(urllib.error.HTTPError) as ctx:
http_put(f"{self.base_url}/images/nonexistent-id", b"data")
self.assertEqual(ctx.exception.code, 404)
def test_options_unknown_image(self):
with self.assertRaises(urllib.error.HTTPError) as ctx:
http_options(f"{self.base_url}/images/nonexistent-id")
self.assertEqual(ctx.exception.code, 404)
class TestRoundTrip(FileBackendTestCase):
def test_put_then_get_roundtrip(self):
payload = randbytes(200, IMAGE_SIZE)
http_put(self._url, payload)
resp = http_get(self._url)
self.assertEqual(resp.read(), payload)
def test_put_then_ranged_get_roundtrip(self):
payload = randbytes(201, IMAGE_SIZE)
http_put(self._url, payload)
resp = http_get(self._url, headers={"Range": "bytes=512-1023"})
self.assertEqual(resp.read(), payload[512:1024])
def test_multiple_puts_last_wins(self):
first = randbytes(300, IMAGE_SIZE)
second = randbytes(301, IMAGE_SIZE)
http_put(self._url, first)
http_put(self._url, second)
resp = http_get(self._url)
self.assertEqual(resp.read(), second)
if __name__ == "__main__":
try:
unittest.main()
finally:
shutdown_image_server()