#!/usr/bin/env python3 """Tests for check_app_terraform_isolation.""" from __future__ import annotations import os import re import subprocess import tempfile import unittest from pathlib import Path from check_app_terraform_isolation import first_isolation_violation, isolation_violation WORKFLOW = ( Path(__file__).resolve().parents[1] / ".github" / "workflows" / "ci-terraform.yaml" ) 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_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]: return re.findall(r"--diff-filter=([A-Z]+)", WORKFLOW.read_text()) 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()