clan-cli: tests/age_keys.py add notes, move function to check sops recipients

This supports the new integration test for `clan machines delete`.
This commit is contained in:
Louis Opter
2025-03-10 22:10:09 +00:00
committed by Mic92
parent c8db27340e
commit cac4b1200c
2 changed files with 52 additions and 38 deletions

View File

@@ -1,7 +1,9 @@
import json
import os import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from clan_cli.secrets.folders import sops_secrets_folder
from helpers import cli from helpers import cli
@@ -12,13 +14,26 @@ class KeyPair:
class SopsSetup: class SopsSetup:
"""Hold a list of three key pairs and create an "admin" user in the clan.
The first key in the list is used as the admin key and
the private part of the key is exposed in the
`SOPS_AGE_KEY` environment variable, the two others can
be used to add machines or other users.
"""
def __init__(self, keys: list[KeyPair]) -> None: def __init__(self, keys: list[KeyPair]) -> None:
self.keys = keys self.keys = keys
self.user = os.environ.get("USER", "admin")
# louis@(2025-03-10): It is odd to have to call an init function on a
# fixture: the fixture should already be initialized when it is received in
# the test function. Maybe we can arrange for the `flake` fixtures, to take
# the `sops_setup` fixture as input and call its `init` function on the
# correct path.
def init(self, flake_path: Path | None = None) -> None: def init(self, flake_path: Path | None = None) -> None:
if flake_path is None: if flake_path is None:
flake_path = Path.cwd() flake_path = Path.cwd()
self.user = os.environ.get("USER", "user")
cli.run( cli.run(
[ [
"vars", "vars",
@@ -49,9 +64,6 @@ KEYS = [
@pytest.fixture @pytest.fixture
def age_keys() -> list[KeyPair]: def age_keys() -> list[KeyPair]:
"""
Root directory of the tests
"""
return KEYS return KEYS
@@ -59,8 +71,33 @@ def age_keys() -> list[KeyPair]:
def sops_setup( def sops_setup(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> SopsSetup: ) -> SopsSetup:
"""
Root directory of the tests
"""
monkeypatch.setenv("SOPS_AGE_KEY", KEYS[0].privkey) monkeypatch.setenv("SOPS_AGE_KEY", KEYS[0].privkey)
return SopsSetup(KEYS) return SopsSetup(KEYS)
# louis@(2025-03-10): right now this is specific to the `sops/secrets` folder,
# but we could make it generic to any sops file if the need arises.
def assert_secrets_file_recipients(
flake_path: Path,
secret_name: str,
expected_age_recipients_keypairs: list["KeyPair"],
err_msg: str | None = None,
) -> None:
"""Checks that the recipients of a secrets file matches expectations.
This looks up the `secret` file for `secret_name` in the `sops` directory
under `flake_path`.
:param err_msg: in case of failure, if you gave an error message then it
will be displayed, otherwise pytest will display the two different sets
of recipients.
"""
sops_file = sops_secrets_folder(flake_path) / secret_name / "secret"
with sops_file.open("rb") as fp:
sops_data = json.load(fp)
age_recipients = {each["recipient"] for each in sops_data["sops"]["age"]}
expected_age_recipients = {pair.pubkey for pair in expected_age_recipients_keypairs}
if not err_msg:
assert age_recipients == expected_age_recipients
return
assert age_recipients == expected_age_recipients, err_msg

View File

@@ -10,8 +10,8 @@ from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import pytest import pytest
from age_keys import assert_secrets_file_recipients
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.secrets.folders import sops_secrets_folder
from fixtures_flakes import FlakeForTest from fixtures_flakes import FlakeForTest
from helpers import cli from helpers import cli
from stdout import CaptureOutput from stdout import CaptureOutput
@@ -91,7 +91,7 @@ def _test_identities(
test_secret_name, test_secret_name,
] ]
) )
assert_sops_file_recipients( assert_secrets_file_recipients(
test_flake.path, test_flake.path,
test_secret_name, test_secret_name,
expected_age_recipients_keypairs=[age_keys[0], admin_age_key], expected_age_recipients_keypairs=[age_keys[0], admin_age_key],
@@ -111,7 +111,7 @@ def _test_identities(
age_keys[1].privkey, age_keys[1].privkey,
] ]
) )
assert_sops_file_recipients( assert_secrets_file_recipients(
test_flake.path, test_flake.path,
test_secret_name, test_secret_name,
expected_age_recipients_keypairs=[age_keys[1], admin_age_key], expected_age_recipients_keypairs=[age_keys[1], admin_age_key],
@@ -302,7 +302,7 @@ def test_groups(
] ]
) )
assert_sops_file_recipients( assert_secrets_file_recipients(
test_flake.path, test_flake.path,
secret_name, secret_name,
expected_age_recipients_keypairs=[ expected_age_recipients_keypairs=[
@@ -327,7 +327,7 @@ def test_groups(
"user1", "user1",
] ]
) )
assert_sops_file_recipients( assert_secrets_file_recipients(
test_flake.path, test_flake.path,
secret_name, secret_name,
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key], expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
@@ -349,7 +349,7 @@ def test_groups(
"user1", "user1",
] ]
) )
assert_sops_file_recipients( assert_secrets_file_recipients(
test_flake.path, test_flake.path,
secret_name, secret_name,
expected_age_recipients_keypairs=[ expected_age_recipients_keypairs=[
@@ -370,7 +370,7 @@ def test_groups(
"user1", "user1",
] ]
) )
assert_sops_file_recipients( assert_secrets_file_recipients(
test_flake.path, test_flake.path,
secret_name, secret_name,
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key], expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
@@ -391,7 +391,7 @@ def test_groups(
"machine1", "machine1",
] ]
) )
assert_sops_file_recipients( assert_secrets_file_recipients(
test_flake.path, test_flake.path,
secret_name, secret_name,
expected_age_recipients_keypairs=[admin_age_key], expected_age_recipients_keypairs=[admin_age_key],
@@ -413,29 +413,6 @@ def test_groups(
assert not group_symlink.exists(follow_symlinks=False), err_msg assert not group_symlink.exists(follow_symlinks=False), err_msg
def assert_sops_file_recipients(
flake_path: Path,
secret_name: str,
expected_age_recipients_keypairs: list["KeyPair"],
err_msg: str | None = None,
) -> None:
"""Checks that the recipients of a SOPS file matches expectations.
:param err_msg: in case of failure, if you gave an error message then it
will be displayed, otherwise pytest will display the two different sets
of recipients.
"""
sops_file = sops_secrets_folder(flake_path) / secret_name / "secret"
with sops_file.open("rb") as fp:
sops_data = json.load(fp)
age_recipients = {each["recipient"] for each in sops_data["sops"]["age"]}
expected_age_recipients = {pair.pubkey for pair in expected_age_recipients_keypairs}
if not err_msg:
assert age_recipients == expected_age_recipients
return
assert age_recipients == expected_age_recipients, err_msg
@contextmanager @contextmanager
def use_age_key(key: str, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: def use_age_key(key: str, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
old_key = os.environ["SOPS_AGE_KEY_FILE"] old_key = os.environ["SOPS_AGE_KEY_FILE"]