diff --git a/muxplex/cli.py b/muxplex/cli.py index fa37ada..d2c0d01 100644 --- a/muxplex/cli.py +++ b/muxplex/cli.py @@ -125,6 +125,19 @@ def _check_for_update(info: dict) -> tuple[bool, str]: return True, "unknown install source — upgrading to be safe" +def generate_federation_key() -> None: + """Generate a random federation key and write it to FEDERATION_KEY_PATH.""" + import muxplex.settings as settings_mod + + path = settings_mod.FEDERATION_KEY_PATH + path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + key = _secrets.token_urlsafe(32) + path.write_text(key + "\n") + path.chmod(0o600) + print(f"Federation key written to {path}") + print(f"Key: {key}") + + def reset_secret() -> None: """Regenerate the signing secret and warn that all sessions are now invalid.""" path = get_secret_path() @@ -675,6 +688,11 @@ def main() -> None: "reset-secret", help="Regenerate signing secret (invalidates sessions)" ) + sub.add_parser( + "generate-federation-key", + help="Generate a random federation key and write it to disk", + ) + sub.add_parser("doctor", help="Check dependencies and system status") upgrade_parser = sub.add_parser( @@ -707,6 +725,8 @@ def main() -> None: show_password() elif args.command == "reset-secret": reset_secret() + elif args.command == "generate-federation-key": + generate_federation_key() elif args.command == "doctor": doctor() elif args.command in ("upgrade", "update"): diff --git a/muxplex/tests/test_cli.py b/muxplex/tests/test_cli.py index a0f2caa..db5e6cb 100644 --- a/muxplex/tests/test_cli.py +++ b/muxplex/tests/test_cli.py @@ -1021,6 +1021,42 @@ def test_config_subcommand_registered(): assert "reset" in result.stdout +# --------------------------------------------------------------------------- +# task-3: generate-federation-key subcommand tests +# --------------------------------------------------------------------------- + + +def test_generate_federation_key_creates_file(tmp_path, monkeypatch, capsys): + """generate_federation_key() creates key file with mode 0600 and prints key info.""" + import muxplex.settings as settings_mod + + key_file = tmp_path / ".config" / "muxplex" / "federation_key" + monkeypatch.setattr(settings_mod, "FEDERATION_KEY_PATH", key_file) + + from muxplex.cli import generate_federation_key + + generate_federation_key() + + # File must exist + assert key_file.exists(), "Federation key file must be created" + + # Content must be longer than 20 chars (stripping the trailing newline) + content = key_file.read_text().strip() + assert len(content) > 20, f"Key must be > 20 chars, got {len(content)}" + + # File mode must be 0600 + file_mode = stat.S_IMODE(key_file.stat().st_mode) + assert file_mode == 0o600, f"Expected 0o600, got {oct(file_mode)}" + + # Output must include key info + captured = capsys.readouterr() + assert "federation" in captured.out.lower() or "key" in captured.out.lower(), ( + f"Output must mention key info, got: {captured.out!r}" + ) + # The actual key value must appear in output + assert content in captured.out, "Key value must appear in output" + + def test_upgrade_uses_service_module_install(monkeypatch, capsys): """upgrade() must call muxplex.service.service_install.""" import subprocess