diff --git a/muxplex/cli.py b/muxplex/cli.py index 7cd605d..ec2f66c 100644 --- a/muxplex/cli.py +++ b/muxplex/cli.py @@ -149,6 +149,18 @@ def reset_secret() -> None: print("Warning: all active sessions are now invalid.") +def reset_device_id_command() -> None: + """Regenerate the device identity UUID and warn about orphaned session keys.""" + from muxplex.identity import IDENTITY_PATH, load_device_id, reset_device_id # noqa: PLC0415 + + old_id = load_device_id() + new_id = reset_device_id() + print(f"New device_id: {new_id}") + print(f"Identity file: {IDENTITY_PATH}") + print(f"Previous device_id: {old_id}") + print("Warning: existing session keys are now orphaned.") + + def show_password() -> None: """Print the current muxplex password or indicate PAM mode.""" auth_mode = os.environ.get("MUXPLEX_AUTH", "").lower() @@ -953,6 +965,11 @@ def main() -> None: "reset-secret", help="Regenerate signing secret (invalidates sessions)" ) + sub.add_parser( + "reset-device-id", + help="Regenerate device identity UUID (orphans existing session keys)", + ) + sub.add_parser( "generate-federation-key", help="Generate a random federation key and write it to disk", @@ -1005,6 +1022,8 @@ def main() -> None: show_password() elif args.command == "reset-secret": reset_secret() + elif args.command == "reset-device-id": + reset_device_id_command() elif args.command == "generate-federation-key": generate_federation_key() elif args.command == "doctor": diff --git a/muxplex/tests/test_cli.py b/muxplex/tests/test_cli.py index 8d0bdc2..6560307 100644 --- a/muxplex/tests/test_cli.py +++ b/muxplex/tests/test_cli.py @@ -2154,6 +2154,72 @@ def test_upgrade_pypi_install_uses_package_name(monkeypatch, capsys): assert not any("git+" in str(arg) for arg in install_cmd) +# --------------------------------------------------------------------------- +# task-6-reset-device-id: --reset-device-id CLI command tests +# --------------------------------------------------------------------------- + + +def test_reset_device_id_writes_new_id(tmp_path, monkeypatch, capsys): + """reset_device_id_command() writes a new device_id different from the previous one. + + Monkeypatches the identity path, creates an initial identity, calls the + command, and verifies that: + - The file now has a different device_id + - Output mentions 'device_id' or 'identity' + - Output includes a warning or mentions 'orphan' + """ + import json + + import muxplex.identity as identity_mod + from muxplex.cli import reset_device_id_command + + # Set up fake identity path + identity_path = tmp_path / "identity.json" + monkeypatch.setattr(identity_mod, "IDENTITY_PATH", identity_path) + + # Create an initial identity + original_id = "11111111-1111-4111-a111-111111111111" + identity_path.write_text(json.dumps({"device_id": original_id})) + + # Run the command + reset_device_id_command() + + # Verify new ID was written and is different + data = json.loads(identity_path.read_text()) + new_id = data["device_id"] + assert new_id != original_id, ( + f"reset_device_id_command() must write a new different device_id, " + f"got: {new_id!r} (same as original)" + ) + + # Verify output mentions device_id/identity + captured = capsys.readouterr() + output_lower = captured.out.lower() + assert "device_id" in output_lower or "identity" in output_lower, ( + f"Output must mention 'device_id' or 'identity', got: {captured.out!r}" + ) + + # Verify output includes warning about orphaned session keys + assert "orphan" in output_lower or "warning" in output_lower, ( + f"Output must include warning about orphaned session keys, got: {captured.out!r}" + ) + + +def test_main_dispatches_to_reset_device_id(monkeypatch): + """main() with 'reset-device-id' subcommand must invoke reset_device_id_command().""" + import muxplex.cli as cli_mod + + calls = [] + monkeypatch.setattr(cli_mod, "reset_device_id_command", lambda: calls.append(True)) + + with patch("sys.argv", ["muxplex", "reset-device-id"]): + cli_mod.main() + + assert len(calls) == 1, ( + "reset_device_id_command() must be called once for 'reset-device-id' subcommand" + ) + + def test_upgrade_git_install_uses_git_url(monkeypatch, capsys): """upgrade() for git installs must still use git+https URL.""" import subprocess