Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ jobs:

- name: Run Shell Script Test Suites
run: |
bash tests/scripts/run_shell_tests.sh
for test_file in tests/test_*.sh; do
echo "=== Running $(basename "${test_file}") ==="
bash "${test_file}"
done

tflint:
name: Terraform Lint
Expand All @@ -88,7 +91,7 @@ jobs:

- name: Test TFLint Runner
run: |
bash tests/scripts/test_tflint.sh
bash tests/test_tflint.sh

- name: Run TFLint
run: |
Expand Down Expand Up @@ -155,6 +158,7 @@ jobs:

- name: Run Python Unit Tests
run: |
uv run --locked python tools/check_boilerplate.py tests tools
uv run --locked pytest

cli-tests:
Expand Down
26 changes: 0 additions & 26 deletions tests/conftest.py

This file was deleted.

13 changes: 0 additions & 13 deletions tests/gem4gov_cli/__init__.py

This file was deleted.

33 changes: 0 additions & 33 deletions tests/scripts/run_shell_tests.sh

This file was deleted.

37 changes: 37 additions & 0 deletions tests/gem4gov_cli/test_auth.py → tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import subprocess
import unittest
from unittest.mock import MagicMock
from unittest.mock import call
from unittest.mock import patch

import auth
Expand Down Expand Up @@ -89,6 +90,42 @@ def test_get_credentials(self, mock_default, mock_run):
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)

@patch("subprocess.run")
@patch("auth.google.auth.default")
def test_get_credentials_triggers_gcloud_login_on_error(
self, mock_default, mock_run
):
mock_creds = MagicMock()
mock_default.return_value = (mock_creds, "dummy-project")
mock_run.side_effect = [
subprocess.CalledProcessError(1, "gcloud"),
MagicMock(),
MagicMock(),
]

creds = auth.get_credentials()

self.assertEqual(creds, mock_creds)
self.assertEqual(
mock_run.call_args_list[1:],
[
call(["gcloud", "auth", "login"]),
call(["gcloud", "auth", "application-default", "login"]),
],
)

@patch("subprocess.run")
def test_force_reauthentication_runs_both_login_commands(self, mock_run):
auth.force_reauthentication()

self.assertEqual(
mock_run.call_args_list,
[
call(["gcloud", "auth", "login"]),
call(["gcloud", "auth", "application-default", "login"]),
],
)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,17 @@
"""Tests for blueprint API enablement and module ID length preconditions."""

import pathlib
import re
import unittest

REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]


class TestBlueprintAndModuleValidations(unittest.TestCase):
"""Verify API enablement in blueprints and 30-char ID preconditions in
modules.
"""
"""Verify API enablement in blueprints and 30-char ID preconditions."""

def test_blueprints_enable_required_project_services(self):
"""Ensure app-engine and postgresql blueprints enable required APIs
(#104).
"""
"""Ensure app-engine and postgresql blueprints enable APIs (#104)."""
ae_tf = (
REPO_ROOT / "blueprints/fedramp-high/app-engine/main.tf"
).read_text(encoding="utf-8")
Expand All @@ -44,9 +41,7 @@ def test_blueprints_enable_required_project_services(self):
self.assertIn("google_project_service.sqladmin", pg_tf)

def test_modules_enforce_30_char_id_preconditions(self):
"""Ensure project and iam-service-account modules enforce 30-character
ID preconditions (#228).
"""
"""Ensure project and iam-service-account enforce 30-char IDs (#228)."""
proj_tf = (REPO_ROOT / "modules/project/main.tf").read_text(
encoding="utf-8"
)
Expand All @@ -70,9 +65,7 @@ def test_modules_enforce_30_char_id_preconditions(self):
)

def _iter_terraform_dirs(self):
"""Yield every directory under fast/, modules/, and blueprints/
containing .tf files.
"""
"""Yield every directory under fast/ and blueprints/ with .tf files."""
seen = set()
for root_name in ("fast", "blueprints"):
base = REPO_ROOT / root_name
Expand All @@ -86,8 +79,6 @@ def _iter_terraform_dirs(self):

@staticmethod
def _strip_comments_and_moved(text: str) -> str:
import re

lines = []
in_block = False
for line in text.splitlines():
Expand All @@ -113,11 +104,7 @@ def _strip_comments_and_moved(text: str) -> str:
def test_all_terraform_dirs_have_valid_variable_and_resource_references(
self,
):
"""Ensure every Terraform directory declares all referenced vars,
resources, data sources, and modules.
"""
import re

"""Ensure every Terraform directory declares referenced symbols."""
errors = []
for tf_dir in sorted(self._iter_terraform_dirs()):
rel_dir = tf_dir.relative_to(REPO_ROOT)
Expand All @@ -127,10 +114,9 @@ def test_all_terraform_dirs_have_valid_variable_and_resource_references(
)
cleaned = self._strip_comments_and_moved(raw_combined)

# Catch common HCL prefix typos like vvar. or lcoal.
for typo in re.findall(
r"\b(?:vvar|lcoal| moudle)\.[a-zA-Z0-9_-]+", cleaned
):
# Catch common HCL prefix typos like vvar., lcoal., or moudle.
typo_re = r"\b(?:vvar|lcoal|moudle)\.[a-zA-Z0-9_-]+"
for typo in re.findall(typo_re, cleaned):
errors.append(f"{rel_dir}: suspicious typo reference '{typo}'")

decl_vars = set(re.findall(r'variable\s+"([^"]+)"', raw_combined))
Expand All @@ -141,17 +127,17 @@ def test_all_terraform_dirs_have_valid_variable_and_resource_references(
f"{rel_dir}: undeclared var reference(s): {missing_vars}"
)

res_decl_re = r'resource\s+"([^"]+)"\s+"([^"]+)"'
decl_res = {
f"{m.group(1)}.{m.group(2)}"
for m in re.finditer(
r'resource\s+"([^"]+)"\s+"([^"]+)"', raw_combined
)
for m in re.finditer(res_decl_re, raw_combined)
}
ref_res = set()
for m in re.finditer(
r'(?<!\.)(?<!data\.)(?<!")\b(google_[a-zA-Z0-9_]+)\.([a-zA-Z0-9_-]+)\b',
cleaned,
):
res_ref_re = (
r'(?<!\.)(?<!data\.)(?<!")\b'
r"(google_[a-zA-Z0-9_]+)\.([a-zA-Z0-9_-]+)\b"
)
for m in re.finditer(res_ref_re, cleaned):
prefix = cleaned[max(0, m.start() - 45) : m.start()]
if re.search(
r"module\.[a-zA-Z0-9_-]+(?:\[[^\]]+\])?\.$", prefix
Expand Down Expand Up @@ -219,11 +205,7 @@ def test_all_terraform_dirs_have_valid_variable_and_resource_references(
)

def test_no_invalid_sensitive_type_constructors_in_variables(self):
"""Ensure variable type definitions do not use sensitive(...) as a type
constructor.
"""
import re

"""Ensure variable types do not use sensitive(...) constructor."""
offenders = []
for tf_dir in sorted(self._iter_terraform_dirs()):
for tf_file in sorted(tf_dir.glob("*.tf")):
Expand All @@ -235,20 +217,17 @@ def test_no_invalid_sensitive_type_constructors_in_variables(self):
self.assertEqual(
offenders,
[],
"Invalid optional(sensitive(...)) type constructor in: "
f"Invalid optional(sensitive(...)) type constructor in: "
f"{offenders}",
)

def test_all_blueprint_sample_tfvars_match_declared_variables(self):
"""Ensure every blueprint terraform.tfvars.sample only sets declared
variables and covers required ones.
"""
import re

"""Ensure every blueprint terraform.tfvars.sample matches variables."""
errors = []
for sample in sorted(
samples = sorted(
REPO_ROOT.glob("blueprints/**/terraform.tfvars.sample")
):
)
for sample in samples:
bp_dir = sample.parent
rel = bp_dir.relative_to(REPO_ROOT)
raw_vars = "\n".join(
Expand Down Expand Up @@ -306,29 +285,26 @@ def test_all_blueprint_sample_tfvars_match_declared_variables(self):
)
if missing_req:
errors.append(
f"{rel}: missing required variable(s) in sample tfvars: "
f"{rel}: missing required variable(s) in sample: "
f"{missing_req}"
)

self.assertEqual(
errors,
[],
"Invalid blueprint terraform.tfvars.sample files:\n"
+ "\n".join(errors),
"Invalid blueprint terraform.tfvars.sample:\n" + "\n".join(errors),
)

def test_custom_stellar_engine_modules_validations(self):
"""Verify hardening and logic fixes across custom Stellar Engine
modules.
"""
"""Verify hardening and logic fixes across Stellar Engine modules."""
alerts_main = (REPO_ROOT / "modules/cis-log-alerts/main.tf").read_text(
encoding="utf-8"
)
self.assertIn(
expected_concat = (
"concat([google_monitoring_notification_channel.email.name], "
"var.notification_channels)",
alerts_main,
"var.notification_channels)"
)
self.assertIn(expected_concat, alerts_main)
self.assertIn("display_name = each.key", alerts_main)
self.assertNotIn('display_name = "each.key"', alerts_main)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import re
import unittest

_REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
_REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
_BOOTSTRAP_DIR = _REPO_ROOT / "fast/stages-aw/0-bootstrap"


Expand Down Expand Up @@ -73,8 +73,8 @@ def test_org_policy_parameters_not_double_encoded(self):
self.assertNotIn(
"parameters = try(jsonencode(r.parameters), null)",
org_tf,
"org_policies should not unconditionally jsonencode string "
"parameters",
"org_policies should not unconditionally jsonencode "
"string parameters",
)
self.assertIn(
"try(r.parameters, null) != null",
Expand Down Expand Up @@ -103,7 +103,8 @@ def test_log_export_project_settings_provisioned_before_cmek(self):
log_export_tf,
)
self.assertIn(
"data.google_logging_project_settings.log_export[0].kms_service_account_id",
"data.google_logging_project_settings.log_export[0]"
".kms_service_account_id",
kms_tf,
)

Expand All @@ -123,8 +124,8 @@ def test_delegated_iam_conditions_guard_empty_grant_list(self):
):
content = (_REPO_ROOT / rel_path).read_text(encoding="utf-8")
self.assertIn(
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', "
"[]).size() > 0",
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole',"
" []).size() > 0",
content,
)

Expand Down
Loading
Loading