|
| 1 | +"""Tests for the unified --exclude-paths flag (G2, Node alignment). |
| 2 | +
|
| 3 | +Covers the path matcher, config parsing + soft-deprecation of --reach-exclude-paths, |
| 4 | +and that --exclude-paths filters SCA manifest discovery via Core.find_files. |
| 5 | +""" |
| 6 | +import logging |
| 7 | +import types |
| 8 | +from unittest.mock import MagicMock |
| 9 | + |
| 10 | +import pytest |
| 11 | + |
| 12 | +from socketsecurity.config import CliConfig |
| 13 | +from socketsecurity.core import Core |
| 14 | +from socketsecurity.core.socket_config import SocketConfig |
| 15 | + |
| 16 | +# ---- matcher ------------------------------------------------------------- |
| 17 | + |
| 18 | +@pytest.mark.parametrize( |
| 19 | + "rel, patterns, expected", |
| 20 | + [ |
| 21 | + # directory prefix -> the directory's whole subtree |
| 22 | + ("packages/legacy/package.json", ["packages/legacy"], True), |
| 23 | + ("packages/keep/package.json", ["packages/legacy"], False), |
| 24 | + # root-anchored: a bare name matches at the root only, NOT nested |
| 25 | + ("tests/x.json", ["tests"], True), |
| 26 | + ("src/tests/x.json", ["tests"], False), |
| 27 | + # **/ matches at any depth |
| 28 | + ("src/tests/x.json", ["**/tests"], True), |
| 29 | + ("tests/unit/x.json", ["tests/**"], True), |
| 30 | + ("tests", ["tests/**"], False), # P/** is the subtree, not P itself |
| 31 | + # '*' does NOT cross '/': anchored basename glob is root-level only |
| 32 | + ("index.spec.ts", ["*.spec.ts"], True), |
| 33 | + ("src/app/index.spec.ts", ["*.spec.ts"], False), |
| 34 | + ("src/app/index.spec.ts", ["**/*.spec.ts"], True), |
| 35 | + ("src/app/index.ts", ["**/*.spec.ts"], False), |
| 36 | + # single-star matches exactly one path segment |
| 37 | + ("packages/a/node_modules/x.json", ["packages/*/node_modules"], True), |
| 38 | + ("packages/a/b/node_modules/x.json", ["packages/*/node_modules"], False), |
| 39 | + ], |
| 40 | +) |
| 41 | +def test_matches_exclude_paths(rel, patterns, expected): |
| 42 | + assert Core.matches_exclude_paths(rel, ".", patterns) is expected |
| 43 | + |
| 44 | + |
| 45 | +@pytest.mark.parametrize( |
| 46 | + "pattern, excluded, kept", |
| 47 | + [ |
| 48 | + # Node parity cases (src/commands/scan/exclude-paths.mts), anchored at scan root. |
| 49 | + ("tests", "tests/pkg/package.json", "src/tests/package.json"), |
| 50 | + ("package-lock.json", "package-lock.json", "packages/a/package-lock.json"), |
| 51 | + ("**/node_modules", "packages/a/node_modules/dep/package.json", "src/app/package.json"), |
| 52 | + ("packages/legacy", "packages/legacy/p.json", "packages/legacy-x/p.json"), |
| 53 | + ("src/*.json", "src/a.json", "src/sub/a.json"), |
| 54 | + ], |
| 55 | +) |
| 56 | +def test_matches_exclude_paths_node_parity(pattern, excluded, kept): |
| 57 | + assert Core.matches_exclude_paths(excluded, ".", [pattern]) is True |
| 58 | + assert Core.matches_exclude_paths(kept, ".", [pattern]) is False |
| 59 | + |
| 60 | + |
| 61 | +def test_matches_exclude_paths_empty_is_false(): |
| 62 | + assert Core.matches_exclude_paths("a/b.json", ".", []) is False |
| 63 | + assert Core.matches_exclude_paths("a/b.json", ".", [" "]) is False |
| 64 | + |
| 65 | + |
| 66 | +# ---- config parsing ------------------------------------------------------ |
| 67 | + |
| 68 | +BASE_ARGS = ["--api-token", "test-token", "--repo", "test-repo"] |
| 69 | + |
| 70 | + |
| 71 | +def test_exclude_paths_parses_to_list(): |
| 72 | + config = CliConfig.from_args(BASE_ARGS + ["--exclude-paths", "tests/**, packages/legacy , *.spec.ts"]) |
| 73 | + assert config.exclude_paths == ["tests/**", "packages/legacy", "*.spec.ts"] |
| 74 | + |
| 75 | + |
| 76 | +def test_exclude_paths_defaults_none(): |
| 77 | + config = CliConfig.from_args(BASE_ARGS) |
| 78 | + assert config.exclude_paths is None |
| 79 | + |
| 80 | + |
| 81 | +def test_reach_exclude_paths_still_works_and_warns(caplog): |
| 82 | + with caplog.at_level(logging.WARNING): |
| 83 | + config = CliConfig.from_args(BASE_ARGS + ["--reach", "--reach-exclude-paths", "a,b"]) |
| 84 | + assert config.reach_exclude_paths == ["a", "b"] |
| 85 | + assert any("deprecated" in r.message for r in caplog.records) |
| 86 | + |
| 87 | + |
| 88 | +@pytest.mark.parametrize( |
| 89 | + "bad", |
| 90 | + ["!foo", "/abs/path", "..", "../escape", "a/../b", ".", "**", "/**", "./**"], |
| 91 | +) |
| 92 | +def test_exclude_paths_validation_rejects(bad): |
| 93 | + with pytest.raises(SystemExit) as exc: |
| 94 | + CliConfig.from_args(BASE_ARGS + ["--exclude-paths", bad]) |
| 95 | + assert exc.value.code == 1 |
| 96 | + |
| 97 | + |
| 98 | +def test_exclude_paths_validation_rejects_within_csv(): |
| 99 | + with pytest.raises(SystemExit) as exc: |
| 100 | + CliConfig.from_args(BASE_ARGS + ["--exclude-paths", "src,..,tests"]) |
| 101 | + assert exc.value.code == 1 |
| 102 | + |
| 103 | + |
| 104 | +def test_exclude_paths_valid_globs_accepted(): |
| 105 | + config = CliConfig.from_args(BASE_ARGS + ["--exclude-paths", "tests/**,**/*.spec.ts,packages/legacy"]) |
| 106 | + assert config.exclude_paths == ["tests/**", "**/*.spec.ts", "packages/legacy"] |
| 107 | + |
| 108 | + |
| 109 | +# ---- find_files integration --------------------------------------------- |
| 110 | + |
| 111 | +def _make_core(exclude_paths): |
| 112 | + core = Core.__new__(Core) |
| 113 | + core.config = SocketConfig(api_key="test-key") |
| 114 | + core.cli_config = types.SimpleNamespace(exclude_paths=exclude_paths) |
| 115 | + core.sdk = MagicMock() |
| 116 | + return core |
| 117 | + |
| 118 | + |
| 119 | +def _seed_manifests(tmp_path): |
| 120 | + for rel in ("package.json", "sub/package.json", "legacy/package.json"): |
| 121 | + p = tmp_path / rel |
| 122 | + p.parent.mkdir(parents=True, exist_ok=True) |
| 123 | + p.write_text("{}", encoding="utf-8") |
| 124 | + |
| 125 | + |
| 126 | +def test_find_files_excludes_matching_paths(tmp_path, mocker): |
| 127 | + _seed_manifests(tmp_path) |
| 128 | + core = _make_core(["legacy"]) |
| 129 | + mocker.patch.object( |
| 130 | + core, "get_supported_patterns", |
| 131 | + return_value={"npm": {"package.json": {"pattern": "package.json"}}}, |
| 132 | + ) |
| 133 | + |
| 134 | + found = core.find_files(str(tmp_path)) |
| 135 | + assert any(f.endswith("/package.json") and "/legacy/" not in f for f in found) |
| 136 | + assert not any("/legacy/" in f for f in found) |
| 137 | + |
| 138 | + |
| 139 | +def test_find_files_no_exclude_paths_keeps_all(tmp_path, mocker): |
| 140 | + _seed_manifests(tmp_path) |
| 141 | + core = _make_core(None) |
| 142 | + mocker.patch.object( |
| 143 | + core, "get_supported_patterns", |
| 144 | + return_value={"npm": {"package.json": {"pattern": "package.json"}}}, |
| 145 | + ) |
| 146 | + |
| 147 | + found = core.find_files(str(tmp_path)) |
| 148 | + assert any("/legacy/" in f for f in found) |
| 149 | + assert len(found) == 3 |
0 commit comments