diff --git a/test/modules/brotli/__init__.py b/test/modules/brotli/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/modules/brotli/conftest.py b/test/modules/brotli/conftest.py new file mode 100644 index 00000000000..7caeef9549c --- /dev/null +++ b/test/modules/brotli/conftest.py @@ -0,0 +1,32 @@ +import logging +import os +import sys + +import pytest + +from .env import BrotliTestEnv + +sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + + +def pytest_report_header(config, start_path): + env = BrotliTestEnv() + return f"brotli [apache: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]" + + +@pytest.fixture(scope="package") +def env(pytestconfig) -> BrotliTestEnv: + logging.getLogger('').setLevel(level=logging.INFO) + env = BrotliTestEnv(pytestconfig=pytestconfig) + if not env.has_shared_module("brotli"): + pytest.skip("mod_brotli not available") + env.setup_httpd() + env.apache_access_log_clear() + env.httpd_error_log.clear_log() + return env + + +@pytest.fixture(autouse=True, scope="package") +def _stop_package_scope(env): + yield + assert env.apache_stop() == 0 diff --git a/test/modules/brotli/env.py b/test/modules/brotli/env.py new file mode 100644 index 00000000000..c87e513323a --- /dev/null +++ b/test/modules/brotli/env.py @@ -0,0 +1,25 @@ +import inspect +import logging +import os + +from pyhttpd.env import HttpdTestEnv, HttpdTestSetup + +log = logging.getLogger(__name__) + + +class BrotliTestSetup(HttpdTestSetup): + + def __init__(self, env: 'HttpdTestEnv'): + super().__init__(env=env) + self.add_source_dir(os.path.dirname(inspect.getfile(BrotliTestSetup))) + self.add_modules(["brotli"]) + + +class BrotliTestEnv(HttpdTestEnv): + + def __init__(self, pytestconfig=None): + super().__init__(pytestconfig=pytestconfig) + self.add_httpd_log_modules(["brotli", "core"]) + + def setup_httpd(self, setup: HttpdTestSetup = None): + super().setup_httpd(setup=BrotliTestSetup(env=self)) diff --git a/test/modules/brotli/htdocs/test1/greeting.txt b/test/modules/brotli/htdocs/test1/greeting.txt new file mode 100644 index 00000000000..512c014af03 --- /dev/null +++ b/test/modules/brotli/htdocs/test1/greeting.txt @@ -0,0 +1 @@ +Good morning, fellow! \ No newline at end of file diff --git a/test/modules/brotli/htdocs/test1/lorem.txt b/test/modules/brotli/htdocs/test1/lorem.txt new file mode 100644 index 00000000000..41266f0adb0 --- /dev/null +++ b/test/modules/brotli/htdocs/test1/lorem.txt @@ -0,0 +1,19 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea +commodo consequat. Duis aute irure dolor in reprehenderit in voluptate +velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat +cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id +est laborum. + +Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, +turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis +sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac +tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam +tempor. Ut ullamcorper, ligula ut dictum pharetra, nisi nunc fringilla +magna, in commodo elit erat nec turpis. Ut pharetra augue nec augue. +Nam elit agna, endrerit sit amet, tincidunt ac, viverra sed, nulla. +Donec porta diam eu massa. Quisque diam lorem, interdum vitae, dapibus +ac, scelerisque vitae, pede. Donec eget tellus non erat lacinia fermentum. +Donec in velit vel ipsum auctor pulvinar. Class aptent taciti sociosqu ad +litora torquent per conubia nostra, per inceptos hymenaeos. diff --git a/test/modules/brotli/test_001_compression.py b/test/modules/brotli/test_001_compression.py new file mode 100644 index 00000000000..89d3066bebc --- /dev/null +++ b/test/modules/brotli/test_001_compression.py @@ -0,0 +1,89 @@ +import os + +import pytest + +from pyhttpd.conf import HttpdConf + + +class TestBrotliCompression: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + conf = HttpdConf(env, extras={ + f"test1.{env.http_tld}": """ + + AddOutputFilterByType BROTLI_COMPRESS text/plain text/html text/css + + """, + }) + conf.add_vhost_test1() + conf.install() + assert env.apache_restart() == 0 + + # Request a small text file with Accept-Encoding: br. + # Verify Content-Encoding: br header is present. + def test_brotli_001_01(self, env): + url = env.mkurl("http", "test1", "/greeting.txt") + r = env.curl_get(url, options=[ + "-H", "Accept-Encoding: br", + ]) + assert r.response, "no response" + assert r.response["status"] == 200 + assert r.response["header"].get("content-encoding") == "br", \ + f"expected Content-Encoding: br, got: {r.response['header']}" + raw_body = r.response["body"] + assert len(raw_body) > 0, "compressed body should not be empty" + original = b"Good morning, fellow!" + assert len(raw_body) != len(original) or raw_body != original, \ + "body should be compressed, not plain text" + + # Request a larger text file; verify brotli compression is applied. + def test_brotli_001_02(self, env): + url = env.mkurl("http", "test1", "/lorem.txt") + r = env.curl_get(url, options=[ + "-H", "Accept-Encoding: br", + ]) + assert r.response, "no response" + assert r.response["status"] == 200 + assert r.response["header"].get("content-encoding") == "br", \ + f"expected Content-Encoding: br, got: {r.response['header']}" + lorem_path = os.path.join(env.server_dir, "htdocs", "test1", + "lorem.txt") + orig_size = os.path.getsize(lorem_path) + assert len(r.response["body"]) < orig_size, \ + "compressed response should be smaller than the original" + + # When the client does not advertise Accept-Encoding: br the response + # must be served uncompressed -- no Content-Encoding header. + def test_brotli_001_03(self, env): + url = env.mkurl("http", "test1", "/greeting.txt") + r = env.curl_get(url) + assert r.response, "no response" + assert r.response["status"] == 200 + assert "content-encoding" not in r.response["header"], \ + f"unexpected Content-Encoding present: " \ + f"{r.response['header'].get('content-encoding')}" + body = r.response["body"].decode() + assert body.strip() == "Good morning, fellow!" + + # Binary / non-matching content types must not be compressed. + def test_brotli_001_04(self, env): + bin_dir = os.path.join(env.server_dir, "htdocs", "test1") + bin_path = os.path.join(bin_dir, "pixel.png") + with open(bin_path, "wb") as f: + f.write( + b'\x89PNG\r\n\x1a\n' + b'\x00\x00\x00\rIHDR\x00\x00\x00\x01' + b'\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde' + b'\x00\x00\x00\x0cIDATx' + b'\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05' + b'\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82' + ) + url = env.mkurl("http", "test1", "/pixel.png") + r = env.curl_get(url, options=[ + "-H", "Accept-Encoding: br", + ]) + assert r.response, "no response" + assert r.response["status"] == 200 + assert r.response["header"].get("content-encoding") != "br", \ + "binary content should not be brotli-compressed"