import json
from pathlib import Path
import pytest
from cockpit.jsonutil import JsonValue
from cockpit.packages import Packages, PackagesLoader, parse_accept_language
@pytest.mark.parametrize(("test_input", "expected"), [
('', ()),
(' ', ()),
(' , ', ()),
(' , ,xx', ('xx',)),
('en', ()),
(' , en', ()),
('aa;q===,bb;q=abc,cc;q=.,zz', ('zz',)),
('aa-bb-cc-dd,ee-ff-gg-hh', ('aa-bb-cc-dd', 'aa-bb-cc', 'aa-bb', 'aa', 'ee-ff-gg-hh', 'ee-ff-gg', 'ee-ff', 'ee')),
('fr-ch;q=0.8,es-mx;q=1.0,en-ca;q=0.9', ('es-mx', 'es', 'en-ca')),
('de-at, zh-CN, en,', ('de-at', 'de', 'zh-cn', 'zh')),
('es-es, nl;q=0.8, fr;q=0.9', ('es-es', 'es', 'fr', 'nl')),
('fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5', ('fr-ch', 'fr'))
])
def test_parse_accept_language(test_input: str, expected: 'tuple[str]') -> None:
assert parse_accept_language(test_input) == expected
@pytest.fixture
def pkgdir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
monkeypatch.setenv('XDG_DATA_DIRS', str(tmp_path))
monkeypatch.setenv('XDG_DATA_HOME', '/nonexisting')
self = tmp_path / 'cockpit'
self.mkdir()
make_package(self, 'basic', description="standard package", requires={"cockpit": "42"})
return self
@pytest.fixture
def confdir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
monkeypatch.setenv('XDG_CONFIG_DIRS', str(tmp_path))
return tmp_path / 'cockpit'
def make_package(pkgdir: Path, dirname: str, **kwargs: JsonValue) -> None:
(pkgdir / dirname).mkdir()
with (pkgdir / dirname / 'manifest.json').open('w') as file:
json.dump(kwargs, file, indent=2)
def test_basic(pkgdir: Path) -> None:
packages = Packages()
assert len(packages.packages) == 1
assert packages.packages['basic'].name == 'basic'
assert packages.packages['basic'].manifest['description'] == 'standard package'
assert packages.packages['basic'].manifest['requires'] == {'cockpit': "42"}
assert packages.packages['basic'].priority == 1
assert packages.manifests == '{"basic": {"description": "standard package", "requires": {"cockpit": "42"}}}'
def test_override_etc(pkgdir: Path, confdir: Path) -> None:
(confdir / 'basic.override.json').write_text('{"description": null, "priority": 5, "does-not-exist": null}')
packages = Packages()
assert len(packages.packages) == 1
assert packages.packages['basic'].name == 'basic'
assert packages.packages['basic'].manifest['requires'] == {'cockpit': '42'}
assert 'description' not in packages.packages['basic'].manifest
assert packages.packages['basic'].priority == 5
assert json.loads(packages.manifests) == {
'basic': {
'requires': {'cockpit': '42'},
'priority': 5,
}
}
def test_override_invalid_json(pkgdir: Path, caplog: pytest.LogCaptureFixture) -> None:
"""An override file with broken JSON must be ignored, not crash the bridge."""
(pkgdir / 'basic' / 'override.json').write_text('{ "priority": 5,, }')
packages = Packages()
assert len(packages.packages) == 1
assert packages.packages['basic'].manifest['description'] == 'standard package'
assert packages.packages['basic'].priority == 1
assert 'override.json' in caplog.text
def test_override_invalid_json_does_not_hide_later_ones(
pkgdir: Path, confdir: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A broken override file must not stop the remaining ones from being applied."""
(pkgdir / 'basic' / 'override.json').write_text('not json at all')
(confdir / 'basic.override.json').write_text('{"priority": 5}')
packages = Packages()
assert len(packages.packages) == 1
assert packages.packages['basic'].priority == 5
assert 'override.json' in caplog.text
def test_priority(pkgdir: Path) -> None:
make_package(pkgdir, 'vip', name='basic', description='VIP', priority=100)
make_package(pkgdir, 'guest', description='Guest')
packages = Packages()
assert len(packages.packages) == 2
assert packages.packages['basic'].name == 'basic'
assert packages.packages['basic'].priority == 100
assert packages.packages['basic'].manifest['description'] == 'VIP'
assert packages.packages['guest'].name == 'guest'
assert packages.packages['guest'].priority == 1
parsed = json.loads(packages.manifests)
assert parsed['basic'] == {'name': 'basic', 'description': 'VIP', 'priority': 100}
assert parsed['guest'] == {'description': 'Guest'}
def test_conditions(pkgdir: Path) -> None:
make_package(pkgdir, 'empty', conditions=[])
make_package(pkgdir, 'exists-1-yes', conditions=[{'path-exists': '/usr'}])
make_package(pkgdir, 'exists-1-no', conditions=[{'path-exists': '/nonexisting'}])
make_package(pkgdir, 'exists-2-yes', conditions=[{"path-exists": "/usr"},
{"path-exists": "/bin/sh"}])
make_package(pkgdir, 'exists-2-no', conditions=[{"path-exists": "/usr"},
{"path-exists": "/nonexisting"}])
make_package(pkgdir, 'notexists-1-yes', conditions=[{"path-not-exists": "/nonexisting"}])
make_package(pkgdir, 'notexists-1-no', conditions=[{"path-not-exists": "/usr"}])
make_package(pkgdir, 'notexists-2-yes', conditions=[{"path-not-exists": "/nonexisting"},
{"path-not-exists": "/obscure"}])
make_package(pkgdir, 'notexists-2-no', conditions=[{"path-not-exists": "/nonexisting"},
{"path-not-exists": "/usr"}])
make_package(pkgdir, 'mixed-yes', conditions=[{"path-exists": "/usr"},
{"path-not-exists": "/nonexisting"}])
make_package(pkgdir, 'mixed-no', conditions=[{"path-exists": "/nonexisting"},
{"path-not-exists": "/obscure"}])
packages = Packages()
assert set(packages.packages.keys()) == {
'basic', 'empty', 'exists-1-yes', 'exists-2-yes', 'notexists-1-yes', 'notexists-2-yes', 'mixed-yes'
}
assert set(packages.packages['empty'].manifest.get_condition_files()) == set()
assert set(packages.packages['exists-1-yes'].manifest.get_condition_files()) == {'/usr'}
assert set(packages.packages['exists-2-yes'].manifest.get_condition_files()) == {'/usr', '/bin/sh'}
assert set(packages.packages['mixed-yes'].manifest.get_condition_files()) == {'/usr', '/nonexisting'}
assert set(PackagesLoader.get_condition_files()) == {'/usr', '/bin/sh', '/nonexisting', '/obscure'}
def test_conditions_errors(pkgdir: Path) -> None:
make_package(pkgdir, 'broken-syntax-1', conditions=[1])
make_package(pkgdir, 'broken-syntax-2', conditions=[["path-exists"]])
make_package(pkgdir, 'broken-syntax-3', conditions=[{"path-exists": "/foo", "path-not-exists": "/bar"}])
make_package(pkgdir, 'unknown-predicate-good', conditions=[{"path-exists": "/usr"},
{"frobnicated": True}])
make_package(pkgdir, 'unknown-predicate-bad', conditions=[{"path-exists": "/nonexisting"},
{"frobnicated": True}])
packages = Packages()
assert set(packages.packages.keys()) == {'basic', 'unknown-predicate-good'}
assert set(packages.packages['unknown-predicate-good'].manifest.get_condition_files()) == {'/usr'}
assert set(PackagesLoader.get_condition_files()) == {'/usr', '/nonexisting'}
def test_condition_hides_priority(pkgdir: Path) -> None:
make_package(pkgdir, 'vip', name="basic", description="VIP", priority=100,
conditions=[{"path-exists": "/nonexisting"}])
packages = Packages()
assert packages.packages['basic'].name == 'basic'
assert packages.packages['basic'].manifest['description'] == 'standard package'
assert packages.packages['basic'].manifest['requires'] == {'cockpit': "42"}
assert packages.packages['basic'].priority == 1
def test_conditions_any(pkgdir: Path) -> None:
make_package(pkgdir, 'any-empty-fail', conditions=[{"any": []}])
make_package(pkgdir, 'any-pass-1', conditions=[
{"any": [{"path-exists": "/usr"}, {"path-exists": "/nonexisting"}]}
])
make_package(pkgdir, 'any-pass-2', conditions=[
{"any": [{"path-exists": "/nonexisting"}, {"path-exists": "/usr"}]}
])
make_package(pkgdir, 'any-fail', conditions=[
{"any": [{"path-exists": "/nonexisting"}, {"path-exists": "/alsonotexisting"}]}
])
make_package(pkgdir, 'any-mixed-pass', conditions=[
{"any": [{"path-not-exists": "/nonexisting"}, {"path-exists": "/alsonotexisting"}]}
])
make_package(pkgdir, 'any-mixed-fail', conditions=[
{"any": [{"path-not-exists": "/usr"}, {"path-exists": "/nonexisting"}]}
])
make_package(pkgdir, 'mixed-any-pass', conditions=[
{"any": [{"path-exists": "/usr"}, {"path-exists": "/nonexisting"}]},
{"path-not-exists": "/nonexisting"}
])
make_package(pkgdir, 'mixed-any-fail-1', conditions=[
{"any": [{"path-exists": "/nonexisting"}, {"path-exists": "/alsonotexisting"}]},
{"path-not-exists": "/nonexisting"}
])
make_package(pkgdir, 'mixed-any-fail-2', conditions=[
{"any": [{"path-exists": "/usr"}, {"path-exists": "/nonexisting"}]},
{"path-not-exists": "/usr"}
])
make_package(pkgdir, 'nested-any-pass', conditions=[
{"any": [
{"any": [{"path-exists": "/usr"}, {"path-exists": "/nonexisting2"}]},
{"path-not-exists": "/nonexisting3"}
]}
])
make_package(pkgdir, 'nested-any-fail', conditions=[
{"any": [
{"any": [{"path-exists": "/nonexisting"}, {"path-exists": "/alsonotexisting"}]},
{"path-not-exists": "/usr"}
]}
])
packages = Packages()
assert set(packages.packages.keys()) == {
'basic', 'any-pass-1', 'any-pass-2', 'any-mixed-pass', 'mixed-any-pass', 'nested-any-pass',
}
assert set(packages.packages['any-pass-1'].manifest.get_condition_files()) == {'/usr', '/nonexisting'}
assert set(packages.packages['mixed-any-pass'].manifest.get_condition_files()) == {'/usr', '/nonexisting'}
assert set(packages.packages['nested-any-pass'].manifest.get_condition_files()) == {
'/usr', '/nonexisting2', '/nonexisting3'
}
assert set(PackagesLoader.get_condition_files()) == {'/usr', '/nonexisting', '/nonexisting2',
'/nonexisting3', '/alsonotexisting'}
def test_conditions_any_errors(pkgdir: Path) -> None:
make_package(pkgdir, 'any-invalid-1', conditions=[{"any": "not-a-list"}])
make_package(pkgdir, 'any-invalid-2', conditions=[{"any": [1, 2, 3]}])
make_package(pkgdir, 'any-invalid-3', conditions=[{"any": [{"invalid": "dict", "multiple": "keys"}]}])
make_package(pkgdir, 'any-mixed-unknown', conditions=[
{"any": [{"path-exists": "/usr"}, {"frobnicated": True}]}
])
packages = Packages()
assert set(packages.packages.keys()) == {'basic', 'any-mixed-unknown'}
def test_english_translation(pkgdir: Path) -> None:
make_package(pkgdir, 'one')
(pkgdir / 'one' / 'po.de.js').write_text('eins')
packages = Packages()
document = packages.load_path('/one/po.js', {'Accept-Language': 'de'})
assert '/javascript' in document.content_type
assert document.data.read() == b'eins'
document = packages.load_path('/one/po.js', {'Accept-Language': 'en;q=0.9, de-ch'})
assert '/javascript' in document.content_type
assert document.data.read() == b'eins'
document = packages.load_path('/one/po.js', {'Accept-Language': 'en, de'})
assert '/javascript' in document.content_type
assert document.data.read() == b''
document = packages.load_path('/one/po.js', {'Accept-Language': 'de;q=0.9, fr;q=0.7, en'})
assert '/javascript' in document.content_type
assert document.data.read() == b''
document = packages.load_path('/one/po.js', {'Accept-Language': 'de;q=0.9, fr, en-ca'})
assert '/javascript' in document.content_type
assert document.data.read() == b''
document = packages.load_path('/one/po.js', {'Accept-Language': ''})
assert '/javascript' in document.content_type
assert document.data.read() == b''
document = packages.load_path('/one/po.js', {})
assert '/javascript' in document.content_type
assert document.data.read() == b''
def test_translation(pkgdir: Path) -> None:
make_package(pkgdir, 'one')
(pkgdir / 'one' / 'po.de.js').write_text('eins')
make_package(pkgdir, 'two')
(pkgdir / 'two' / 'po.de.js').write_text('zwei')
(pkgdir / 'two' / 'po.manifest.de.js').write_text('zwo')
packages = Packages()
document = packages.load_path('/one/po.js', {'Accept-Language': 'es, de'})
assert '/javascript' in document.content_type
assert document.data.read() == b'eins'
document = packages.load_path('/one/po.js', {'Accept-Language': 'es'})
assert '/javascript' in document.content_type
assert document.data.read() == b''
document = packages.load_path('/manifests-i18n.js', {'Accept-Language': 'de'})
contents = document.data.read()
assert b'eins\n' in contents
assert b'zwo\n' in contents
assert b'zwei\n' not in contents
def test_filename_mangling(pkgdir: Path) -> None:
make_package(pkgdir, 'one')
(pkgdir / 'one' / 'one.js').write_text('this is one.js')
(pkgdir / 'one' / 'two.js.gz').write_text('this is two.js')
(pkgdir / 'one' / 'three.min.js.gz').write_text('this is three.js')
(pkgdir / 'one' / 'four.min.js').write_text('this is four.js')
packages = Packages()
encodings = set()
for name in ['one', 'two', 'three', 'four']:
document = packages.load_path(f'/one/{name}.js', {})
assert document.data.read().decode() == f'this is {name}.js'
assert '/javascript' in document.content_type
encodings.add(document.content_encoding)
assert encodings == {None, 'gzip'}
def test_filename_gz_suffix_only(pkgdir: Path) -> None:
"""Only a literal '.gz' suffix may be stripped, not any character + 'gz'."""
make_package(pkgdir, 'one')
(pkgdir / 'one' / 'data.tgz').write_bytes(b'a tarball')
(pkgdir / 'one' / 'logo.svgz').write_bytes(b'a compressed svg')
packages = Packages()
assert packages.load_path('/one/data.tgz', {}).data.read() == b'a tarball'
assert packages.load_path('/one/logo.svgz', {}).data.read() == b'a compressed svg'
with pytest.raises(KeyError):
packages.load_path('/one/data.', {})
with pytest.raises(KeyError):
packages.load_path('/one/logo.s', {})
def test_overlapping_minified(pkgdir: Path) -> None:
make_package(pkgdir, 'one')
(pkgdir / 'one' / 'one.min.js').write_text('min')
(pkgdir / 'one' / 'one.js').write_text('max')
(pkgdir / 'one' / 'two.js').write_text('max')
(pkgdir / 'one' / 'two.min.js').write_text('min')
packages = Packages()
document = packages.load_path('/one/one.js', {})
assert document.data.read().decode() == 'max'
document = packages.load_path('/one/two.js', {})
assert document.data.read().decode() == 'max'
document = packages.load_path('/one/one.min.js', {})
assert document.data.read().decode() == 'min'
document = packages.load_path('/one/two.min.js', {})
assert document.data.read().decode() == 'min'