Skip to content
Snippets Groups Projects
Verified Commit 0ca74e24 authored by Levente Polyak's avatar Levente Polyak :rocket:
Browse files

feature(keyringctl): adding basic infrastructure for running tests

parent 8ba7dc1d
No related branches found
No related tags found
No related merge requests found
......@@ -2,49 +2,39 @@
image: archlinux:latest
stages:
- lint
- integration
- test
variables:
PACMAN_CACHE: "${CI_PROJECT_DIR}/.pacman/pkg"
cache:
paths:
- .pacman/pkg
key: ${CI_JOB_NAME}
check-new-key:
stage: lint
lint:
stage: test
needs: []
before_script:
- pacman -Syu --needed --noconfirm make flake8 mypy python-black python-isort
script:
- install -d "${PACMAN_CACHE}"
- pacman -Syu --needed --noconfirm --cachedir "${PACMAN_CACHE}" git grep hopenpgp-tools sequoia-keyring-linter
- ./.gitlab/check-keyids-change
- make lint
only:
refs:
- merge_requests
changes:
- master-keyids
- packager-keyids
- keyringctl
- libkeyringctl/*
- test/*
lint:
stage: lint
test:
stage: test
needs: []
before_script:
- install -d "${PACMAN_CACHE}"
- pacman -Syu --needed --noconfirm --cachedir "${PACMAN_CACHE}" make flake8 mypy python-black python-isort
- pacman -Syu --needed --noconfirm make python sequoia-sq python-pytest
script:
- make lint
- make test
only:
refs:
- merge_requests
changes:
- keyringctl
- libkeyringctl/*
- test/*
build_install:
stage: integration
stage: test
needs: []
before_script:
- install -d "${PACMAN_CACHE}"
- pacman -Syu --needed --noconfirm --cachedir "${PACMAN_CACHE}" make python sequoia-sq
- pacman -Syu --needed --noconfirm make python sequoia-sq
script:
- ./keyringctl import --main master master-revoked
- ./keyringctl import packager packager-revoked
......
......@@ -31,6 +31,7 @@ and develop this project:
* python-black
* python-isort
* python-pytest
* flake8
* mypy
......@@ -41,3 +42,15 @@ The `keyringctl` script is written in typed python, which makes use of
The script is type checked, linted and formatted using standard tooling.
When providing a merge request make sure to run `make lint`.
## Testing
Test cases are developed per module in the [test](test) directory and should
consist of atomic single expectation tests. A Huge test case asserting various
different expectations are discouraged and should be split into finer grained
test cases.
To execute all tests using pytest
```bash
make test
```
......@@ -14,6 +14,9 @@ fmt:
black .
isort .
test:
py.test
build:
./keyringctl -v build
......@@ -24,4 +27,4 @@ uninstall:
rm -f $(KEYRING_TARGET_DIR)/archlinux{.gpg,-trusted,-revoked}
rmdir -p --ignore-fail-on-non-empty $(KEYRING_TARGET_DIR)
.PHONY: build install lint uninstall
.PHONY: all lint fmt test build install uninstall
......@@ -118,7 +118,7 @@ def convert_certificate( # noqa: ignore=C901
Returns
-------
The path of the user_dir (which is located below working_dir)
The path of the key directory (which is located below working_dir below the user_dir)
"""
# root packets
......@@ -279,7 +279,7 @@ def convert_certificate( # noqa: ignore=C901
key_dir=key_dir,
)
return user_dir
return key_dir
def persist_public_key(
......@@ -579,8 +579,9 @@ def convert(
)
for path in directories:
(target_dir / path.name).mkdir(parents=True, exist_ok=True)
copytree(src=path, dst=(target_dir / path.name), dirs_exist_ok=True)
user_dir = path.parent
(target_dir / user_dir.name).mkdir(parents=True, exist_ok=True)
copytree(src=user_dir, dst=(target_dir / user_dir.name), dirs_exist_ok=True)
return target_dir
......
......@@ -11,6 +11,7 @@ from typing import List
from typing import Optional
from .types import Fingerprint
from .types import Uid
from .types import Username
from .util import cwd
from .util import natural_sort_path
......@@ -222,3 +223,63 @@ def latest_certification(certifications: Iterable[Path]) -> Path:
lambda a, b: a if packet_signature_creation_time(a) > packet_signature_creation_time(b) else b,
certifications,
)
def key_generate(uids: List[Uid], outfile: Path) -> str:
"""Generate a PGP key with specific uids
Parameters
----------
uids: List of uids that the key should have
outfile: Path to the file to which the key should be written to
Returns
-------
The result of the key generate call
"""
cmd = ["sq", "key", "generate"]
for uid in uids:
cmd.extend(["--userid", str(uid)])
cmd.extend(["--export", str(outfile)])
return system(cmd)
def key_extract_certificate(key: Path, output: Optional[Path]) -> str:
"""Extracts the non secret part from a key into a certificate
Parameters
----------
key: Path to a file that contain secret key material
output: Path to the file to which the key should be written to, stdout if None
Returns
-------
The result of the extract in case output is None
"""
cmd = ["sq", "key", "extract-cert", str(key)]
if output:
cmd.extend(["--output", str(output)])
return system(cmd)
def certify(key: Path, certificate: Path, uid: Uid, output: Optional[Path]) -> str:
"""Inspect PGP packet data and return the result
Parameters
----------
key: Path to a file that contain secret key material
certificate: Path to a certificate file whose uid should be certified
uid: Uid contain in the certificate that should be certified
output: Path to the file to which the key should be written to, stdout if None
Returns
-------
The result of the certification in case output is None
"""
cmd = ["sq", "certify", str(key), str(certificate), uid]
if output:
cmd.extend(["--output", str(output)])
return system(cmd)
from collections import defaultdict
from functools import wraps
from pathlib import Path
from shutil import copytree
from tempfile import TemporaryDirectory
from typing import Dict
from typing import List
from typing import Set
from pytest import fixture
from libkeyringctl.keyring import convert_certificate
from libkeyringctl.keyring import simplify_user_id
from libkeyringctl.sequoia import certify
from libkeyringctl.sequoia import key_extract_certificate
from libkeyringctl.sequoia import key_generate
from libkeyringctl.types import Fingerprint
from libkeyringctl.types import Uid
from libkeyringctl.types import Username
from libkeyringctl.util import cwd
test_keys: Dict[Username, List[Path]] = defaultdict(list)
test_certificates: Dict[Username, List[Path]] = defaultdict(list)
test_keyring_certificates: Dict[Username, List[Path]] = defaultdict(list)
test_main_fingerprints: Set[Fingerprint] = set()
@fixture(autouse=True)
def reset_storage():
test_keys.clear()
test_certificates.clear()
test_keyring_certificates.clear()
test_main_fingerprints.clear()
def create_certificate(username: Username, uids: List[Uid], keyring_type: str = "packager", func=None):
def decorator(decorated_func):
@wraps(decorated_func)
def wrapper(working_dir: Path, *args, **kwargs):
print(username)
key_directory = working_dir / "secret" / f"{id}"
key_directory.mkdir(parents=True, exist_ok=True)
key_file: Path = key_directory / f"{username}.asc"
key_generate(uids=uids, outfile=key_file)
test_keys[username].append(key_file)
certificate_directory = working_dir / "certificate" / f"{id}"
certificate_directory.mkdir(parents=True, exist_ok=True)
keyring_root: Path = working_dir / "keyring"
keyring_root.mkdir(parents=True, exist_ok=True)
certificate_file: Path = certificate_directory / f"{username}.asc"
key_extract_certificate(key=key_file, output=certificate_file)
test_certificates[username].append(certificate_file)
target_dir = keyring_root / keyring_type
decomposed_path: Path = convert_certificate(
working_dir=working_dir,
certificate=certificate_file,
keyring_dir=keyring_root / keyring_type,
)
user_dir = decomposed_path.parent
(target_dir / user_dir.name).mkdir(parents=True, exist_ok=True)
copytree(src=user_dir, dst=(target_dir / user_dir.name), dirs_exist_ok=True)
test_keyring_certificates[username].append(target_dir / user_dir.name / decomposed_path.name)
if "main" == keyring_type:
test_main_fingerprints.add(Fingerprint(decomposed_path.name))
decorated_func(working_dir=working_dir, *args, **kwargs)
return wrapper
if not func:
return decorator
return decorator(func)
def create_uid_certification(issuer: Username, certified: Username, uid: Uid, func=None):
def decorator(decorated_func):
@wraps(decorated_func)
def wrapper(working_dir: Path, *args, **kwargs):
key: Path = test_keys[issuer][0]
certificate: Path = test_certificates[certified][0]
fingerprint: Fingerprint = Fingerprint(test_keyring_certificates[certified][0].name)
issuer_fingerprint: Fingerprint = Fingerprint(test_keyring_certificates[issuer][0].name)
simplified_uid = simplify_user_id(uid)
output: Path = (
working_dir
/ "keyring"
/ "packager"
/ certified
/ fingerprint
/ "uid"
/ simplified_uid
/ "certification"
/ f"{issuer_fingerprint}.asc"
)
output.parent.mkdir(parents=True, exist_ok=True)
certify(key, certificate, uid, output)
decorated_func(working_dir=working_dir, *args, **kwargs)
return wrapper
if not func:
return decorator
return decorator(func)
@fixture(scope="function")
def working_dir():
with TemporaryDirectory(prefix="arch-keyringctl-test-") as tempdir:
with cwd(tempdir):
yield Path(tempdir)
@fixture(scope="function")
def keyring_dir(working_dir: Path):
yield working_dir / "keyring"
from pathlib import Path
from libkeyringctl.trust import certificate_trust
from libkeyringctl.types import Trust
from libkeyringctl.types import Uid
from libkeyringctl.types import Username
from .conftest import create_certificate
from .conftest import create_uid_certification
from .conftest import test_keyring_certificates
from .conftest import test_main_fingerprints
@create_certificate(username=Username("foobar"), uids=[Uid("foobar <foo@bar.xyz>")], keyring_type="main")
def test_certificate_trust_main_key_has_full_trust(working_dir: Path, keyring_dir: Path):
trust = certificate_trust(
test_keyring_certificates[Username("foobar")][0],
test_main_fingerprints,
)
assert Trust.full == trust
@create_certificate(username=Username("main"), uids=[Uid("main <foo@bar.xyz>")])
@create_certificate(username=Username("foobar"), uids=[Uid("foobar <foo@bar.xyz>")])
def test_certificate_trust_no_signature_is_unknown(working_dir: Path, keyring_dir: Path):
trust = certificate_trust(
test_keyring_certificates[Username("foobar")][0],
test_main_fingerprints,
)
assert Trust.unknown == trust
@create_certificate(username=Username("main"), uids=[Uid("main <foo@bar.xyz>")], keyring_type="main")
@create_certificate(username=Username("foobar"), uids=[Uid("foobar <foo@bar.xyz>")])
@create_uid_certification(issuer=Username("main"), certified=Username("foobar"), uid=Uid("foobar <foo@bar.xyz>"))
def test_certificate_trust_one_signature_is_marginal(working_dir: Path, keyring_dir: Path):
trust = certificate_trust(
test_keyring_certificates[Username("foobar")][0],
test_main_fingerprints,
)
assert Trust.marginal == trust
@create_certificate(username=Username("main"), uids=[Uid("main <foo@bar.xyz>")], keyring_type="main")
@create_certificate(username=Username("not_main"), uids=[Uid("main <foo@bar.xyz>")])
@create_certificate(username=Username("foobar"), uids=[Uid("foobar <foo@bar.xyz>")])
@create_uid_certification(issuer=Username("not_main"), certified=Username("foobar"), uid=Uid("foobar <foo@bar.xyz>"))
def test_certificate_trust_one_none_main_signature_gives_no_trust(working_dir: Path, keyring_dir: Path):
trust = certificate_trust(
test_keyring_certificates[Username("foobar")][0],
test_main_fingerprints,
)
assert Trust.unknown == trust
@create_certificate(username=Username("main1"), uids=[Uid("main1 <foo@bar.xyz>")], keyring_type="main")
@create_certificate(username=Username("main2"), uids=[Uid("main2 <foo@bar.xyz>")], keyring_type="main")
@create_certificate(username=Username("main3"), uids=[Uid("main3 <foo@bar.xyz>")], keyring_type="main")
@create_certificate(username=Username("foobar"), uids=[Uid("foobar <foo@bar.xyz>")])
@create_uid_certification(issuer=Username("main1"), certified=Username("foobar"), uid=Uid("foobar <foo@bar.xyz>"))
@create_uid_certification(issuer=Username("main2"), certified=Username("foobar"), uid=Uid("foobar <foo@bar.xyz>"))
@create_uid_certification(issuer=Username("main3"), certified=Username("foobar"), uid=Uid("foobar <foo@bar.xyz>"))
def test_certificate_trust_three_main_signature_gives_full_trust(working_dir: Path, keyring_dir: Path):
trust = certificate_trust(
test_keyring_certificates[Username("foobar")][0],
test_main_fingerprints,
)
assert Trust.full == trust
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment