#!/usr/bin/env python3 """Tests for check_app_terraform_isolation.""" from __future__ import annotations import os import re import subprocess import sys import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] ACTION = ROOT / ".github" / "actions" / "app-terraform-isolation" WORKFLOW = ROOT / ".github" / "workflows" / "ci-terraform.yaml" sys.path.insert(0, str(ACTION)) from check_app_terraform_isolation import ( # noqa: E402 first_isolation_violation, isolation_violation, ) APP_PATHS = "src/\npackage.json\npackage-lock.json\n" class IsolationTests(unittest.TestCase): def test_terraform_only(self) -> None: self.assertIsNone( isolation_violation( ["terraform/lambda.tf", "terraform/README.md"], APP_PATHS, ) ) def test_app_only(self) -> None: self.assertIsNone( isolation_violation( ["src/processPaymentCsv.js", "package.json", "package-lock.json"], APP_PATHS, ) ) def test_docs_and_workflows_with_terraform(self) -> None: self.assertIsNone( isolation_violation( [ "terraform/lambda.tf", ".github/workflows/deploy.yaml", "SETUP.md", "scripts/check_app_terraform_isolation.py", ], APP_PATHS, ) ) def test_mixed_app_and_terraform_fails(self) -> None: violation = isolation_violation( ["terraform/lambda.tf", "src/processPaymentCsv.js", "package.json"], APP_PATHS, ) self.assertIsNotNone(violation) terraform_files, app_files = violation or ([], []) self.assertEqual(terraform_files, ["terraform/lambda.tf"]) self.assertEqual(app_files, ["package.json", "src/processPaymentCsv.js"]) def test_empty_app_paths_skips(self) -> None: self.assertIsNone( isolation_violation( ["terraform/lambda.tf", "src/processPaymentCsv.js"], "", ) ) def test_separate_commits_pass_when_classified_alone(self) -> None: self.assertIsNone( first_isolation_violation( [ ["terraform/lambda.tf"], ["src/processPaymentCsv.js"], ], APP_PATHS, ) ) def test_union_of_separate_commits_fails(self) -> None: violation = isolation_violation( ["terraform/lambda.tf", "src/processPaymentCsv.js"], APP_PATHS, ) self.assertIsNotNone(violation) def test_non_default_terraform_dir_mixed_fails(self) -> None: violation = isolation_violation( ["infra/main.tf", "src/app.js"], "src/\n", terraform_dir="infra", ) self.assertIsNotNone(violation) terraform_files, app_files = violation or ([], []) self.assertEqual(terraform_files, ["infra/main.tf"]) self.assertEqual(app_files, ["src/app.js"]) def test_default_dir_leaves_other_prefixes_neutral(self) -> None: self.assertIsNone( isolation_violation(["infra/main.tf", "src/app.js"], "src/\n") ) def test_terraform_prefix_does_not_match_a_longer_directory(self) -> None: self.assertIsNone( isolation_violation( ["infrastructure/main.tf", "src/app.js"], "src/\n", terraform_dir="infra", ) ) def test_terraform_dir_trailing_slash(self) -> None: violation = isolation_violation( ["infra/main.tf", "src/app.js"], "src/\n", terraform_dir="infra/", ) self.assertIsNotNone(violation) def test_blank_terraform_dir_defaults_to_terraform(self) -> None: violation = isolation_violation( ["terraform/lambda.tf", "src/app.js"], "src/\n", terraform_dir=" ", ) self.assertIsNotNone(violation) class WorkflowDiffTests(unittest.TestCase): def test_classified_diffs_include_deletions(self) -> None: self.assertEqual(_workflow_diff_filters(), ["ACMRD", "ACMRD"]) def test_workflow_passes_working_directory(self) -> None: self.assertIn( "terraform-dir: ${{ inputs.working-directory }}", WORKFLOW.read_text(), ) def test_checker_is_this_repos_action_at_the_workflow_commit(self) -> None: text = WORKFLOW.read_text() self.assertIn("uses: $/.github/actions/app-terraform-isolation", text) self.assertNotIn("github.workflow_sha", text) self.assertNotIn("repository: Sea-Haven-Industries/.github", text) def test_deleted_app_file_is_classified(self) -> None: diff_filter = _workflow_diff_filters()[0] with tempfile.TemporaryDirectory() as tmp: repo = Path(tmp) base = _commit_base(repo) (repo / "terraform" / "lambda.tf").write_text("changed\n") (repo / "src" / "processPaymentCsv.js").unlink() _git(repo, "add", "-A") _git(repo, "commit", "-m", "decommission handler") omitted = _changed_paths(repo, base, "HEAD", "ACMR") included = _changed_paths(repo, base, "HEAD", diff_filter) self.assertNotIn("src/processPaymentCsv.js", omitted) self.assertIn("src/processPaymentCsv.js", included) self.assertIn("terraform/lambda.tf", included) self.assertIsNotNone(isolation_violation(included, APP_PATHS)) def test_deleted_terraform_file_is_classified(self) -> None: diff_filter = _workflow_diff_filters()[0] with tempfile.TemporaryDirectory() as tmp: repo = Path(tmp) base = _commit_base(repo) (repo / "terraform" / "lambda.tf").unlink() (repo / "src" / "processPaymentCsv.js").write_text("changed\n") _git(repo, "add", "-A") _git(repo, "commit", "-m", "remove lambda") omitted = _changed_paths(repo, base, "HEAD", "ACMR") included = _changed_paths(repo, base, "HEAD", diff_filter) self.assertNotIn("terraform/lambda.tf", omitted) violation = isolation_violation(included, APP_PATHS) self.assertIsNotNone(violation) terraform_files, app_files = violation or ([], []) self.assertEqual(terraform_files, ["terraform/lambda.tf"]) self.assertEqual(app_files, ["src/processPaymentCsv.js"]) def _workflow_diff_filters() -> list[str]: action = (ACTION / "action.yml").read_text() return re.findall(r"--diff-filter=([A-Z]+)", action) def _git(repo: Path, *args: str) -> str: env = os.environ.copy() env["GIT_CONFIG_GLOBAL"] = os.devnull env["GIT_CONFIG_NOSYSTEM"] = "1" completed = subprocess.run( [ "git", "-c", "commit.gpgsign=false", "-c", "user.name=test", "-c", "user.email=test@example.com", *args, ], cwd=repo, check=True, capture_output=True, text=True, env=env, ) return completed.stdout def _commit_base(repo: Path) -> str: _git(repo, "init", "-b", "main") (repo / "terraform").mkdir() (repo / "src").mkdir() (repo / "terraform" / "lambda.tf").write_text("resource\n") (repo / "src" / "processPaymentCsv.js").write_text("export {}\n") _git(repo, "add", ".") _git(repo, "commit", "-m", "base") return _git(repo, "rev-parse", "HEAD").strip() def _changed_paths(repo: Path, base: str, head: str, diff_filter: str) -> list[str]: output = _git( repo, "diff", "--name-only", f"--diff-filter={diff_filter}", base, head, ) return [line for line in output.splitlines() if line] if __name__ == "__main__": unittest.main()