diff --git a/eks_automation/__pycache__/__init__.cpython-39.pyc b/eks_automation/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..b52cf49 Binary files /dev/null and b/eks_automation/__pycache__/__init__.cpython-39.pyc differ diff --git a/eks_automation/__pycache__/app.cpython-39.pyc b/eks_automation/__pycache__/app.cpython-39.pyc new file mode 100644 index 0000000..7d9dfa2 Binary files /dev/null and b/eks_automation/__pycache__/app.cpython-39.pyc differ diff --git a/eks_automation/app.py b/eks_automation/app.py index 57eb741..88734d4 100644 --- a/eks_automation/app.py +++ b/eks_automation/app.py @@ -1,8 +1,7 @@ #################################################################################### -# This Lambda function takes JSON input, processes it using a Jinja2 template, -# and writes the output to a file in a cloned GitHub repository. -# The changes are then committed and pushed to the GitHub API, -# creating a new repository for the EKS CI/CD pipeline. +# This Lambda function takes JSON input and writes it directly to a config.json file +# in a cloned GitHub repository. The changes are then committed and pushed to the +# GitHub API, creating a new repository for the EKS CI/CD pipeline. # This implementation uses only pure Python with requests library (no Git CLI dependency). #################################################################################### @@ -14,33 +13,16 @@ import time import requests import json -from jinja2 import Environment, FileSystemLoader from urllib.parse import urlparse from datetime import datetime import boto3 from botocore.exceptions import ClientError -import os - - -# Get configuration from environment variables with defaults -GITHUB_API = os.environ.get("GITHUB_API") # No default - must be configured -ORG_NAME = os.environ.get("GITHUB_ORG_NAME") # No default - must be configured -SECRET_NAME = os.environ.get("GITHUB_TOKEN_SECRET_NAME", "/eks-cluster-deployment/github_token") -COMMIT_AUTHOR_EMAIL = os.environ.get("GITHUB_COMMIT_AUTHOR_EMAIL", "eks-automation@noreply.github.com") -COMMIT_AUTHOR_NAME = os.environ.get("GITHUB_COMMIT_AUTHOR_NAME", "EKS Automation Lambda") -SOURCE_VERSION = os.environ.get("TEMPLATE_SOURCE_VERSION") # Optional - if not set, uses default branch - -ORIG_REPO_NAME = os.environ.get("TEMPLATE_REPO_NAME", "template-eks-cluster") - -TEMPLATE_FILE_NAME = os.environ.get("TEMPLATE_FILE_NAME", "eks.hcl.j2") -HCL_FILE_NAME = os.environ.get("HCL_FILE_NAME", "eks.hcl") # Initialize the logger logger = logging.getLogger() logger.setLevel("INFO") # Set to "ERROR" to reduce logging messages. - class GitHubClient: """A class to interact with GitHub API without relying on external Git binaries. @@ -48,17 +30,27 @@ class GitHubClient: branches, files, commits and other Git operations using only the requests library. """ - def __init__(self, api_base_url, token, org_name): + def __init__(self, api_base_url, token, org_name, commit_author_name, commit_author_email, source_version=None, template_repo_name=None, config_file_name="config.json"): """Initialize the GitHub client Args: api_base_url (str): Base URL for the GitHub API token (str): GitHub access token org_name (str): GitHub organization name + commit_author_name (str): Name of the commit author + commit_author_email (str): Email of the commit author + source_version (str, optional): Version to use from template repo + template_repo_name (str, optional): Name of the template repository + config_file_name (str, optional): Name of the config file to write """ self.api_base_url = api_base_url self.token = token self.org_name = org_name + self.commit_author_name = commit_author_name + self.commit_author_email = commit_author_email + self.source_version = source_version + self.template_repo_name = template_repo_name + self.config_file_name = config_file_name self.headers = self._create_headers() def _create_headers(self): @@ -306,20 +298,21 @@ def create_commit(self, repo_name, message, tree_sha, parent_shas): """ api_url = f"{self.api_base_url}/repos/{self.org_name}/{repo_name}/git/commits" - data = { - "message": message, - "tree": tree_sha, - "parents": parent_shas - } - # Add committer/author information current_time = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") - data["author"] = { - "name": COMMIT_AUTHOR_NAME, - "email": COMMIT_AUTHOR_EMAIL, + author_info = { + "name": self.commit_author_name, + "email": self.commit_author_email, "date": current_time } - data["committer"] = data["author"] + + data = { + "message": message, + "tree": tree_sha, + "parents": parent_shas, + "author": author_info, + "committer": author_info + } response = requests.post(api_url, headers=self.headers, json=data, verify=False) @@ -391,14 +384,14 @@ def clone_repository_contents(self, source_repo, target_dir): logger.info(f"Getting file tree from {source_repo}") ref = None - if SOURCE_VERSION: + if self.source_version: try: # Try to get the tag/release reference first - ref = f"tags/{SOURCE_VERSION}" + ref = f"tags/{self.source_version}" tree_sha = self.get_reference_sha(source_repo, ref) - logger.info(f"Using source version: {SOURCE_VERSION}") + logger.info(f"Using source version: {self.source_version}") except Exception as e: - logger.warning(f"Failed to get version {SOURCE_VERSION}, falling back to default branch: {str(e)}") + logger.warning(f"Failed to get version {self.source_version}, falling back to default branch: {str(e)}") ref = f"heads/{default_branch}" tree_sha = self.get_reference_sha(source_repo, ref) else: @@ -521,12 +514,6 @@ def lambda_handler(event, context): Returns: dict: Dict containing status message. """ - - # For test, load input data from a local file. - # input_data = "" - # with open("data.json", "r") as file: - # input_data = json.load(file) - input_data = json.loads(event["body"]) project_name = input_data["project_name"] @@ -538,7 +525,7 @@ def lambda_handler(event, context): } try: - rendered = operate_github(project_name, eks_settings, HCL_FILE_NAME) + operate_github(project_name, eks_settings) except Exception as e: # pylint: disable=broad-exception-caught logger.error(f"Error in operate_github: {str(e)}") return {"statusCode": 400, "body": json.dumps({"error": str(e)})} @@ -546,12 +533,12 @@ def lambda_handler(event, context): return { "statusCode": 200, "headers": {"Access-Control-Allow-Origin": "*"}, - "body": json.dumps({"result": rendered}), + "body": json.dumps({"result": "Success"}), } -def operate_github(new_repo_name, eks_settings, output_hcl): - """Process template and create/update repository using GitHub API +def operate_github(new_repo_name, eks_settings): + """Write EKS settings to config.json and create/update repository using GitHub API This implementation uses only the requests library and does not rely on git CLI or any external binaries. @@ -559,14 +546,19 @@ def operate_github(new_repo_name, eks_settings, output_hcl): Args: new_repo_name (str): Name of the new GitHub repo. eks_settings (json): Input JSON data with all the EKS parameter values. - output_hcl (str): Name of the EKS parameter file in HCL format. Returns: - str: The rendered EKS parameter string. + None """ - - # Get GitHub access token + # Get GitHub access token and environment variables token = github_token() + github_api = os.environ.get("GITHUB_API") # No default - must be configured + org_name = os.environ.get("GITHUB_ORG_NAME") # No default - must be configured + commit_author_email = os.environ.get("GITHUB_COMMIT_AUTHOR_EMAIL", "eks-automation@example.com") + commit_author_name = os.environ.get("GITHUB_COMMIT_AUTHOR_NAME", "EKS Automation Lambda") + source_version = os.environ.get("TEMPLATE_SOURCE_VERSION") # Optional + template_repo_name = os.environ.get("TEMPLATE_REPO_NAME", "template-eks-cluster") + config_file_name = "config.json" # Create work directory if it doesn't exist work_dir = f"/tmp/{new_repo_name}" @@ -574,53 +566,40 @@ def operate_github(new_repo_name, eks_settings, output_hcl): shutil.rmtree(work_dir, ignore_errors=False, onerror=remove_readonly) os.makedirs(work_dir, exist_ok=True) - # Initialize GitHub client - github = GitHubClient(GITHUB_API, token, ORG_NAME) + # Initialize GitHub client with all required parameters + github = GitHubClient( + github_api, + token, + org_name, + commit_author_name, + commit_author_email, + source_version, + template_repo_name, + config_file_name + ) # Get info about original repo - logger.info(f"Fetching original repository information: {ORIG_REPO_NAME}") - orig_repo = github.get_repository(ORIG_REPO_NAME) + logger.info(f"Fetching original repository information: {template_repo_name}") + orig_repo = github.get_repository(template_repo_name) # Get or create the new repository logger.info(f"Getting or creating repository: {new_repo_name}") new_repo = github.get_repository(new_repo_name, create=True) # Clone the original repository contents - github.clone_repository_contents(ORIG_REPO_NAME, work_dir) + github.clone_repository_contents(template_repo_name, work_dir) - # Render the template and write to file - rendered = render_j2_template(eks_settings, TEMPLATE_FILE_NAME) - output_file_path = os.path.join(work_dir, output_hcl) - - logger.info(f"Writing rendered template to {output_file_path}") + # Write EKS settings directly to config.json + output_file_path = os.path.join(work_dir, config_file_name) + logger.info(f"Writing EKS settings to {output_file_path}") with open(output_file_path, "w") as file: - file.write(rendered) + json.dump(eks_settings, file, indent=2) # Commit all files to the new repository - commit_message = "Add the EKS parameter file by the Lambda function" + commit_message = "Add the EKS configuration file by the Lambda function" github.commit_repository_contents(new_repo_name, work_dir, commit_message) logger.info(f"Successfully updated {new_repo_name} repository") - return rendered - - -def render_j2_template(eks_settings, j2_template, j2_template_dir="templates/"): - """Render the j2 template with the input JSON data - - Args: - eks_settings (json): input data in JSON format. - j2_template (j2): Name of the template file to generate the output. - j2_template_dir (str, optional): The directory where the templates are stored. Defaults to "templates/". - - Returns: - str: Rendered template string. - """ - - # Render template - jinja_env = Environment(loader=FileSystemLoader(j2_template_dir), trim_blocks=True) - template = jinja_env.get_template(j2_template) - - return template.render(data=eks_settings) def github_token(): diff --git a/eks_automation/pytest.ini b/eks_automation/pytest.ini new file mode 100644 index 0000000..a2bd95b --- /dev/null +++ b/eks_automation/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +markers = + integration: marks tests as integration tests (deselect with '-m "not integration"') +testpaths = + tests +python_files = + test_*.py + *_test.py +addopts = + -v + --strict-markers \ No newline at end of file diff --git a/eks_automation/requirements.txt b/eks_automation/requirements.txt index b7f4404..e06cdc0 100644 --- a/eks_automation/requirements.txt +++ b/eks_automation/requirements.txt @@ -2,8 +2,11 @@ # black # pre-commit -jinja2 boto3 requests -pygithub -gitpython + +# Testing dependencies +pytest>=7.0.0 +pytest-mock>=3.10.0 +requests-mock>=1.11.0 +coverage>=7.2.0 diff --git a/eks_automation/templates/eks.hcl.j2 b/eks_automation/templates/eks.hcl.j2 deleted file mode 100644 index 40c4feb..0000000 --- a/eks_automation/templates/eks.hcl.j2 +++ /dev/null @@ -1,10 +0,0 @@ -locals { - {% for key, value in data['attrs'] | items -%} - {{ key }} = "{{ value }}" - {% endfor -%} - tags = { - {% for key, value in data['tags'] | items -%} - {{ key }} = "{{ value }}" - {% endfor -%} - } -} diff --git a/eks_automation/test_payload.json b/eks_automation/test_payload.json new file mode 100644 index 0000000..01b2e4f --- /dev/null +++ b/eks_automation/test_payload.json @@ -0,0 +1,26 @@ +{ + "project_name": "eks-automation-lambda-test1", + "eks_settings": { + "attrs": { + "account_name": "dev-account", + "aws_region": "us-east-1", + "cluster_mailing_list": "someone@example.com", + "cluster_name": "example-cluster-dev", + "eks_instance_disk_size": 100, + "eks_ng_desired_size": 2, + "eks_ng_max_size": 10, + "eks_ng_min_size": 2, + "environment": "development", + "environment_abbr": "dev", + "organization": "example:dept:team", + "finops_project_name": "example_project", + "finops_project_number": "fp00000001", + "finops_project_role": "example_project_app", + "vpc_domain_name": "dev.example.com", + "vpc_name": "vpc-dev" + }, + "tags": { + "slim:schedule": "8:00-17:00" + } + } +} \ No newline at end of file diff --git a/eks_automation/tests/__init__.py b/eks_automation/tests/__init__.py new file mode 100644 index 0000000..739954c --- /dev/null +++ b/eks_automation/tests/__init__.py @@ -0,0 +1 @@ +# Tests package \ No newline at end of file diff --git a/eks_automation/tests/__pycache__/__init__.cpython-39.pyc b/eks_automation/tests/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..5326f36 Binary files /dev/null and b/eks_automation/tests/__pycache__/__init__.cpython-39.pyc differ diff --git a/eks_automation/tests/__pycache__/conftest.cpython-39-pytest-8.1.1.pyc b/eks_automation/tests/__pycache__/conftest.cpython-39-pytest-8.1.1.pyc new file mode 100644 index 0000000..063d2ad Binary files /dev/null and b/eks_automation/tests/__pycache__/conftest.cpython-39-pytest-8.1.1.pyc differ diff --git a/eks_automation/tests/__pycache__/test_github_client.cpython-39-pytest-8.1.1.pyc b/eks_automation/tests/__pycache__/test_github_client.cpython-39-pytest-8.1.1.pyc new file mode 100644 index 0000000..0105335 Binary files /dev/null and b/eks_automation/tests/__pycache__/test_github_client.cpython-39-pytest-8.1.1.pyc differ diff --git a/eks_automation/tests/conftest.py b/eks_automation/tests/conftest.py new file mode 100644 index 0000000..769a94b --- /dev/null +++ b/eks_automation/tests/conftest.py @@ -0,0 +1,74 @@ +import pytest +import os +import json + +@pytest.fixture +def github_client_params(): + """Fixture providing standard GitHubClient parameters""" + return { + "api_base_url": "https://api.github.example.com", + "token": "test-token", + "org_name": "test-org", + "commit_author_name": "Test Author", + "commit_author_email": "test@example.com", + "source_version": "v1.0.0", + "template_repo_name": "template-repo", + "config_file_name": "config.json" + } + +@pytest.fixture +def mock_repository_response(): + """Fixture providing a standard repository API response""" + return { + "id": 1234, + "name": "test-repo", + "default_branch": "main", + "private": True, + "description": "Test repository" + } + +@pytest.fixture +def mock_tree_response(): + """Fixture providing a standard tree API response""" + return { + "sha": "test-tree-sha", + "tree": [ + { + "path": "test.txt", + "mode": "100644", + "type": "blob", + "sha": "test-blob-sha", + "size": 100 + } + ] + } + +@pytest.fixture +def mock_blob_response(): + """Fixture providing a standard blob API response""" + return { + "sha": "test-blob-sha", + "content": "SGVsbG8gV29ybGQh", # Base64 encoded "Hello World!" + "encoding": "base64" + } + +@pytest.fixture +def mock_commit_response(): + """Fixture providing a standard commit API response""" + return { + "sha": "test-commit-sha", + "tree": { + "sha": "test-tree-sha" + } + } + +@pytest.fixture +def mock_reference_response(): + """Fixture providing a standard reference API response""" + return { + "ref": "refs/heads/main", + "object": { + "sha": "test-commit-sha", + "type": "commit" + } + } \ No newline at end of file diff --git a/eks_automation/tests/test_github_client.py b/eks_automation/tests/test_github_client.py new file mode 100644 index 0000000..89bcd65 --- /dev/null +++ b/eks_automation/tests/test_github_client.py @@ -0,0 +1,245 @@ +import os +import pytest +import base64 +import tempfile +import shutil +from datetime import datetime +from urllib.parse import urljoin + +import requests +import requests_mock + +from ..app import GitHubClient + +class TestGitHubClient: + """Test suite for GitHubClient class""" + + def test_init(self, github_client_params): + """Test GitHubClient initialization""" + client = GitHubClient(**github_client_params) + assert client.api_base_url == github_client_params["api_base_url"] + assert client.token == github_client_params["token"] + assert client.org_name == github_client_params["org_name"] + assert client.commit_author_name == github_client_params["commit_author_name"] + assert client.commit_author_email == github_client_params["commit_author_email"] + assert "Authorization" in client.headers + assert client.headers["Authorization"] == f"token {github_client_params['token']}" + + def test_get_repository_existing(self, requests_mock, github_client_params, mock_repository_response): + """Test getting an existing repository""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + + # Mock the API response + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}", + json=mock_repository_response + ) + + repo = client.get_repository(repo_name) + assert repo["name"] == mock_repository_response["name"] + assert repo["default_branch"] == mock_repository_response["default_branch"] + + def test_get_repository_create_new(self, requests_mock, github_client_params, mock_repository_response): + """Test creating a new repository""" + client = GitHubClient(**github_client_params) + repo_name = "new-test-repo" + + # Mock 404 for get request and success for create + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}", + status_code=404 + ) + requests_mock.post( + f"{github_client_params['api_base_url']}/orgs/{github_client_params['org_name']}/repos", + json=mock_repository_response + ) + + repo = client.get_repository(repo_name, create=True) + assert repo["name"] == mock_repository_response["name"] + + def test_get_default_branch(self, requests_mock, github_client_params, mock_repository_response): + """Test getting repository default branch""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}", + json=mock_repository_response + ) + + branch = client.get_default_branch(repo_name) + assert branch == mock_repository_response["default_branch"] + + def test_create_blob(self, requests_mock, github_client_params, mock_blob_response): + """Test creating a blob""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + content = b"Hello World!" + + requests_mock.post( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/blobs", + json=mock_blob_response + ) + + blob_sha = client.create_blob(repo_name, content) + assert blob_sha == mock_blob_response["sha"] + + def test_create_tree(self, requests_mock, github_client_params, mock_tree_response): + """Test creating a tree""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + tree_items = [{ + "path": "test.txt", + "mode": "100644", + "type": "blob", + "sha": "test-blob-sha" + }] + + requests_mock.post( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/trees", + json=mock_tree_response + ) + + tree_sha = client.create_tree(repo_name, tree_items) + assert tree_sha == mock_tree_response["sha"] + + def test_create_commit(self, requests_mock, github_client_params, mock_commit_response): + """Test creating a commit""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + message = "Test commit" + tree_sha = "test-tree-sha" + parent_shas = ["parent-sha"] + + requests_mock.post( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/commits", + json=mock_commit_response + ) + + commit_sha = client.create_commit(repo_name, message, tree_sha, parent_shas) + assert commit_sha == mock_commit_response["sha"] + + def test_update_reference(self, requests_mock, github_client_params): + """Test updating a reference""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + ref = "heads/main" + sha = "test-commit-sha" + + requests_mock.patch( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/refs/{ref}", + status_code=200 + ) + + # Should not raise an exception + client.update_reference(repo_name, ref, sha) + + def test_create_reference(self, requests_mock, github_client_params): + """Test creating a reference""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + ref = "refs/heads/main" + sha = "test-commit-sha" + + requests_mock.post( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/refs", + status_code=201 + ) + + # Should not raise an exception + client.create_reference(repo_name, ref, sha) + + def test_clone_repository_contents(self, requests_mock, github_client_params, mock_repository_response, + mock_reference_response, mock_tree_response, mock_blob_response, tmp_path): + """Test cloning repository contents""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + target_dir = str(tmp_path) + + # Mock all required API calls + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}", + json=mock_repository_response + ) + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/refs/heads/main", + json=mock_reference_response + ) + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/trees/{mock_reference_response['object']['sha']}?recursive=1", + json=mock_tree_response + ) + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/blobs/{mock_tree_response['tree'][0]['sha']}", + json=mock_blob_response + ) + + default_branch = client.clone_repository_contents(repo_name, target_dir) + assert default_branch == mock_repository_response["default_branch"] + assert os.path.exists(os.path.join(target_dir, mock_tree_response["tree"][0]["path"])) + + def test_commit_repository_contents(self, requests_mock, github_client_params, mock_repository_response, + mock_reference_response, mock_tree_response, mock_commit_response, tmp_path): + """Test committing repository contents""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + work_dir = str(tmp_path) + + # Create a test file + test_file = os.path.join(work_dir, "test.txt") + with open(test_file, "w") as f: + f.write("test content") + + # Mock all required API calls + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}", + json=mock_repository_response + ) + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/refs/heads/main", + json=mock_reference_response + ) + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/commits/{mock_reference_response['object']['sha']}", + json=mock_commit_response + ) + requests_mock.post( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/blobs", + json={"sha": "new-blob-sha"} + ) + requests_mock.post( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/trees", + json={"sha": "new-tree-sha"} + ) + requests_mock.post( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/commits", + json={"sha": "new-commit-sha"} + ) + requests_mock.patch( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}/git/refs/heads/main", + status_code=200 + ) + + default_branch = client.commit_repository_contents(repo_name, work_dir, "Test commit") + assert default_branch == mock_repository_response["default_branch"] + + def test_error_handling(self, requests_mock, github_client_params): + """Test error handling in GitHubClient methods""" + client = GitHubClient(**github_client_params) + repo_name = "test-repo" + + # Test error on repository creation + requests_mock.get( + f"{github_client_params['api_base_url']}/repos/{github_client_params['org_name']}/{repo_name}", + status_code=404 + ) + requests_mock.post( + f"{github_client_params['api_base_url']}/orgs/{github_client_params['org_name']}/repos", + status_code=500, + text="Internal Server Error" + ) + + with pytest.raises(Exception) as exc_info: + client.get_repository(repo_name, create=True) + assert "Failed to create repository" in str(exc_info.value) \ No newline at end of file diff --git a/eks_automation/tests/test_github_client_integration.py b/eks_automation/tests/test_github_client_integration.py new file mode 100644 index 0000000..7332b83 --- /dev/null +++ b/eks_automation/tests/test_github_client_integration.py @@ -0,0 +1,182 @@ +import os +import json +import pytest +import requests +import tempfile +import shutil +import uuid +from datetime import datetime + +from ..app import GitHubClient + +# Skip all tests if no GitHub token is available +pytestmark = [ + pytest.mark.skipif( + "GITHUB_TOKEN" not in os.environ, + reason="GITHUB_TOKEN environment variable not set" + ), + pytest.mark.integration +] + +@pytest.fixture +def integration_client(): + """Create a GitHubClient instance for integration testing""" + token = os.environ["GITHUB_TOKEN"] + api_url = os.environ.get("GITHUB_API", "https://api.github.com") + org_name = os.environ.get("GITHUB_ORG", "test-org") + + client = GitHubClient( + api_base_url=api_url, + token=token, + org_name=org_name, + commit_author_name="Integration Test", + commit_author_email="test@example.com", + source_version=None, + template_repo_name="template-lambda-deployment", + config_file_name="config.json" + ) + return client + +@pytest.fixture +def temp_repo_name(): + """Generate a unique temporary repository name""" + return f"temp-test-repo-{uuid.uuid4().hex[:8]}" + +@pytest.fixture +def cleanup_repo(integration_client): + """Fixture to clean up test repository after tests""" + repo_names = [] + + def _register_repo(repo_name): + repo_names.append(repo_name) + return repo_name + + yield _register_repo + + # Clean up all registered repos + for repo_name in repo_names: + try: + # Note: Real deletion would require additional API calls + # For safety in testing, we just archive the repo + requests.patch( + f"{integration_client.api_base_url}/repos/{integration_client.org_name}/{repo_name}", + headers=integration_client.headers, + json={"archived": True}, + verify=False + ) + except Exception as e: + print(f"Failed to archive repository {repo_name}: {e}") + +class TestGitHubClientIntegration: + """Integration tests for GitHubClient using real GitHub API""" + + def test_repository_creation(self, integration_client, temp_repo_name, cleanup_repo): + """Test creating a new repository via the API""" + repo_name = cleanup_repo(temp_repo_name) + + # Create new repository + repo = integration_client.get_repository(repo_name, create=True) + + assert repo is not None + assert repo["name"] == repo_name + assert not repo["archived"] + + # Verify we can get the repository + repo = integration_client.get_repository(repo_name) + assert repo["name"] == repo_name + + def test_file_operations(self, integration_client, temp_repo_name, cleanup_repo, tmp_path): + """Test file operations with real repository""" + repo_name = cleanup_repo(temp_repo_name) + + # Create new repository + repo = integration_client.get_repository(repo_name, create=True) + + # Create a test file + test_content = { + "test": True, + "timestamp": datetime.utcnow().isoformat() + } + + # Write test content to work directory + work_dir = str(tmp_path) + os.makedirs(work_dir, exist_ok=True) + test_file = os.path.join(work_dir, "test-config.json") + + with open(test_file, "w") as f: + json.dump(test_content, f, indent=2) + + # Commit the file + integration_client.commit_repository_contents( + repo_name, + work_dir, + "Test commit from integration tests" + ) + + # Verify the file exists in the repository + # Clone to a new directory and verify contents + clone_dir = os.path.join(str(tmp_path), "clone") + os.makedirs(clone_dir, exist_ok=True) + + integration_client.clone_repository_contents(repo_name, clone_dir) + + cloned_file = os.path.join(clone_dir, "test-config.json") + assert os.path.exists(cloned_file) + + with open(cloned_file, "r") as f: + cloned_content = json.load(f) + + assert cloned_content["test"] == test_content["test"] + assert cloned_content["timestamp"] == test_content["timestamp"] + + def test_branch_operations(self, integration_client, temp_repo_name, cleanup_repo, tmp_path): + """Test branch creation and updates""" + repo_name = cleanup_repo(temp_repo_name) + + # Create new repository + repo = integration_client.get_repository(repo_name, create=True) + + # Create a test file and commit to main + work_dir = str(tmp_path) + os.makedirs(work_dir, exist_ok=True) + + with open(os.path.join(work_dir, "test.txt"), "w") as f: + f.write("main branch content") + + # Commit to main + integration_client.commit_repository_contents( + repo_name, + work_dir, + "Initial commit" + ) + + # Create a new branch + main_sha = integration_client.get_reference_sha(repo_name, "heads/main") + integration_client.create_reference( + repo_name, + "refs/heads/test-branch", + main_sha + ) + + # Update file in new branch + with open(os.path.join(work_dir, "test.txt"), "w") as f: + f.write("test branch content") + + # Commit to test branch + integration_client.commit_repository_contents( + repo_name, + work_dir, + "Update in test branch" + ) + + # Verify the changes + clone_dir = os.path.join(str(tmp_path), "clone") + os.makedirs(clone_dir, exist_ok=True) + + # Clone and verify main branch + main_dir = os.path.join(clone_dir, "main") + integration_client.clone_repository_contents(repo_name, main_dir) + + with open(os.path.join(main_dir, "test.txt"), "r") as f: + main_content = f.read() + assert main_content == "main branch content" \ No newline at end of file