import json import os from pathlib import Path import subprocess import sys import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit")) from govoplan_devkit.common import atomic_json, read_json, redact, safe_output from govoplan_devkit.context import build_context from govoplan_devkit.workspace import ( load_project, selected_repositories, git_bytes, inspect_repository, Repository, ) def test_portable_project_rejects_escape_alias_collisions_and_duplicate_json(tmp_path): config = tmp_path / "project.json" for repositories in ( [{"name": "repo", "path": "../outside"}], [ {"name": "repo", "path": "repo", "aliases": ["other"]}, {"name": "other", "path": "other"}, ], ): config.write_text( json.dumps({"schema_version": 1, "repositories": repositories}) ) with pytest.raises(ValueError): load_project(tmp_path, config) config.write_text('{"schema_version":1,"schema_version":2}') with pytest.raises(ValueError, match="Duplicate"): read_json(config) def test_missing_repository_is_error_not_clean(tmp_path): config = tmp_path / "project.json" config.write_text( json.dumps( { "schema_version": 1, "name": "Test", "repositories": [{"name": "missing", "path": "missing"}], } ) ) result = build_context(tmp_path, config, [], True) assert result["_exit_code"] == 1 assert result["repositories"][0]["errors"] assert not result["remote_checked"] def test_unknown_repo_filter_is_not_silently_ignored(tmp_path): config = tmp_path / "project.json" config.write_text( json.dumps( {"schema_version": 1, "repositories": [{"name": "repo", "path": "repo"}]} ) ) with pytest.raises(ValueError, match="Unknown"): selected_repositories(load_project(tmp_path, config), ["typo"]) def test_atomic_output_does_not_chmod_existing_parent(tmp_path): folder = tmp_path / "public" folder.mkdir(mode=0o755) atomic_json(folder / "private.json", {"safe": True}) assert folder.stat().st_mode & 0o777 == 0o755 assert (folder / "private.json").stat().st_mode & 0o777 == 0o600 def test_read_rejects_fifo_and_symlink(tmp_path): fifo = tmp_path / "fifo" os.mkfifo(fifo) with pytest.raises(ValueError, match="regular"): read_json(fifo) target = tmp_path / "target.json" target.write_text("{}") alias = tmp_path / "alias.json" alias.symlink_to(target) with pytest.raises(ValueError, match="symlink"): read_json(alias) def test_generic_secret_display_hygiene(): assert "topsecret" not in redact( "Authorization: Bearer topsecret\nhttps://user:topsecret@example.invalid/" ) def test_separate_secret_arguments_are_redacted_in_nested_json(): value = { "stages": [{"argv": ["check", "--token", "hidden-value"]}], "password": "hidden-password", } output = json.dumps(safe_output(value)) assert "hidden-value" not in output and "hidden-password" not in output @pytest.mark.parametrize( "name", [ "GIT_DIR", "GIT_NAMESPACE", "GIT_COMMON_DIR", "GIT_CONFIG_PARAMETERS", "GIT_AUTHOR_NAME", ], ) def test_inherited_git_redirects_are_rejected(tmp_path, monkeypatch, name): monkeypatch.setenv(name, "fixture") assert inspect_repository(Repository("example", tmp_path))["errors"] with pytest.raises(ValueError, match="overrides"): git_bytes(tmp_path, "status") def test_deeply_nested_json_fails_as_controlled_input_error(tmp_path): path = tmp_path / "deep.json" path.write_text("[" * 10000 + "]" * 10000) with pytest.raises(ValueError, match="nesting"): read_json(path) def test_clean_commit_without_upstream_is_still_changed(tmp_path): subprocess.run(["git", "init", "-q", str(tmp_path)], check=True) subprocess.run( [ "git", "-C", str(tmp_path), "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "commit", "--allow-empty", "-qm", "fixture", ], check=True, ) from govoplan_devkit.workspace import Project repo = Repository("example", tmp_path) assert selected_repositories(Project("Example", (repo,), {}), [], changed=True) == [ repo ]