diff --git a/.github/workflows/lambda_crawler-govuk-reference-content_deploy.yml b/.github/workflows/lambda_crawler-govuk-reference-content_deploy.yml index 5fa66e0..6c6090f 100644 --- a/.github/workflows/lambda_crawler-govuk-reference-content_deploy.yml +++ b/.github/workflows/lambda_crawler-govuk-reference-content_deploy.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main" ] paths: - - lambda/crawler-govuk-reference-content/** + - lambda_/crawler-govuk-reference-content/** - .github/workflows/lambda_crawler-govuk-reference-content_deploy.yml workflow_dispatch: env: @@ -48,7 +48,7 @@ jobs: echo "github.ref: ${{ github.ref }}" ls -lah bash build.sh - working-directory: lambda/crawler-govuk-reference-content/ + working-directory: lambda_/crawler-govuk-reference-content/ - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v2 @@ -61,17 +61,17 @@ jobs: run: | aws sts get-caller-identity ls -lah - working-directory: lambda/crawler-govuk-reference-content/ + working-directory: lambda_/crawler-govuk-reference-content/ # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. - name: Terraform Init run: terraform init - working-directory: lambda/crawler-govuk-reference-content/ + working-directory: lambda_/crawler-govuk-reference-content/ # Checks that all Terraform configuration files adhere to a canonical format - name: Terraform Format run: terraform fmt -check - working-directory: lambda/crawler-govuk-reference-content/ + working-directory: lambda_/crawler-govuk-reference-content/ # Generates an execution plan for Terraform - name: Terraform Apply @@ -83,4 +83,4 @@ jobs: -var="production_iam_role=${{ secrets.PRODUCTION_IAM_ROLE }}" env: TERRAFORM_WORKSPACE: ${{ matrix.environment }} - working-directory: lambda/crawler-govuk-reference-content/ + working-directory: lambda_/crawler-govuk-reference-content/ diff --git a/.github/workflows/lambda_email-forwarder_deploy.yml b/.github/workflows/lambda_email-forwarder_deploy.yml index d121150..d0e47ef 100644 --- a/.github/workflows/lambda_email-forwarder_deploy.yml +++ b/.github/workflows/lambda_email-forwarder_deploy.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] paths: - - lambda/email-forwarder/** + - lambda_/email-forwarder/** workflow_dispatch: permissions: @@ -52,12 +52,12 @@ jobs: # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. - name: Terraform Init run: terraform init - working-directory: lambda/email-forwarder/ + working-directory: lambda_/email-forwarder/ # Checks that all Terraform configuration files adhere to a canonical format - name: Terraform Format run: terraform fmt -check - working-directory: lambda/email-forwarder/ + working-directory: lambda_/email-forwarder/ # Generates an execution plan for Terraform - name: Terraform Apply @@ -68,4 +68,4 @@ jobs: -var="production_iam_role=${{ secrets.PRODUCTION_IAM_ROLE }}" env: TF_WORKSPACE: ${{ matrix.environment }} - working-directory: lambda/email-forwarder/ + working-directory: lambda_/email-forwarder/ diff --git a/.github/workflows/lambda_hackerone-zendesk-integration_deploy.yml b/.github/workflows/lambda_hackerone-zendesk-integration_deploy.yml index fe03168..48b8e9c 100644 --- a/.github/workflows/lambda_hackerone-zendesk-integration_deploy.yml +++ b/.github/workflows/lambda_hackerone-zendesk-integration_deploy.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] paths: - - lambda/hackerone-zendesk-integration/** + - lambda_/hackerone-zendesk-integration/** workflow_dispatch: permissions: @@ -42,7 +42,7 @@ jobs: run: | ls -lah bash build.sh - working-directory: lambda/hackerone-zendesk-integration/ + working-directory: lambda_/hackerone-zendesk-integration/ - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v2 @@ -58,12 +58,12 @@ jobs: # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. - name: Terraform Init run: terraform init - working-directory: lambda/hackerone-zendesk-integration/ + working-directory: lambda_/hackerone-zendesk-integration/ # Checks that all Terraform configuration files adhere to a canonical format - name: Terraform Format run: terraform fmt -check - working-directory: lambda/hackerone-zendesk-integration/ + working-directory: lambda_/hackerone-zendesk-integration/ # Generates an execution plan for Terraform - name: Terraform Apply @@ -74,4 +74,4 @@ jobs: -var="production_iam_role=${{ secrets.PRODUCTION_IAM_ROLE }}" env: TF_WORKSPACE: ${{ matrix.environment }} - working-directory: lambda/hackerone-zendesk-integration/ + working-directory: lambda_/hackerone-zendesk-integration/ diff --git a/.github/workflows/lambda_zendesk-backup_deploy.yml b/.github/workflows/lambda_zendesk-backup_deploy.yml index 98bd5c8..2e2dff6 100644 --- a/.github/workflows/lambda_zendesk-backup_deploy.yml +++ b/.github/workflows/lambda_zendesk-backup_deploy.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] paths: - - lambda/zendesk-backup/** + - lambda_/zendesk_backup/** workflow_dispatch: permissions: @@ -42,7 +42,7 @@ jobs: run: | ls -lah bash build.sh - working-directory: lambda/zendesk-backup/ + working-directory: lambda_/zendesk_backup/ - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v2 @@ -58,12 +58,12 @@ jobs: # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. - name: Terraform Init run: terraform init - working-directory: lambda/zendesk-backup/ + working-directory: lambda_/zendesk_backup/ # Checks that all Terraform configuration files adhere to a canonical format - name: Terraform Format run: terraform fmt -check - working-directory: lambda/zendesk-backup/ + working-directory: lambda_/zendesk_backup/ # Generates an execution plan for Terraform - name: Terraform Apply @@ -74,4 +74,4 @@ jobs: -var="production_iam_role=${{ secrets.PRODUCTION_IAM_ROLE }}" env: TF_WORKSPACE: ${{ matrix.environment }} - working-directory: lambda/zendesk-backup/ + working-directory: lambda_/zendesk_backup/ diff --git a/lambda/zendesk-backup/main.py b/lambda/zendesk-backup/main.py deleted file mode 100644 index 72161a9..0000000 --- a/lambda/zendesk-backup/main.py +++ /dev/null @@ -1,159 +0,0 @@ -import os -import json -import boto3 -import time -import re - -from zenpy import Zenpy - -s3_bucket = os.environ["S3_BUCKET"] -s3_client = boto3.client("s3") - -s3_helpcentre_prefix = "helpcentre/" -s3_support_prefix = "support/" - -zendesk_creds = { - "email": os.environ["ZENDESK_API_EMAIL"], - "token": os.environ["ZENDESK_API_KEY"], - "subdomain": os.environ["ZENDESK_SUBDOMAIN"], -} - -zenpy_client = Zenpy(**zendesk_creds) - - -def jprint(obj): - new_obj = {} - if type(obj) != dict: - obj = {"message": str(obj)} - if "_time" not in obj: - new_obj["_time"] = time.time() - for k in sorted(obj): - new_obj[k] = obj[k] - print(json.dumps(new_obj, default=str)) - - -def get_key(obj: dict) -> str: - res = None - if obj and "html_url" in obj and "/" in obj["html_url"]: - html_url_split = obj["html_url"].rsplit("/", 2) - if len(html_url_split) == 3: - res = f"{html_url_split[1]}/{html_url_split[2]}" - return res - - -def add_athena_datetimes(d: dict = {}) -> dict: - res = {} - for key in d: - if d[key] and type(d[key]) == str: - if re.match("(?i)2[\d\-]+t\d\d:", d[key]): - res[f"{key}_athena"] = ( - d[key].lower().replace("t", " ").replace("z", "").split(".")[0] - ) - - res.update(d) - return res - - -def save_support(ticket_ids: list = []): - tickets = [] - - if ticket_ids: - for ticket_id in ticket_ids: - tickets.append(zenpy_client.tickets(id=str(ticket_id))) - else: - tickets = zenpy_client.search_export(type="ticket") - - for ticket in tickets: - # subject = re.sub(r"\s+", " ", re.sub(r"[^a-zA-Z0-9 ]", "", ticket.raw_subject)) - filename = f"gc3-{ticket.id}.json" - key = f"{s3_support_prefix}tickets/{filename}" - jprint(f"Saving 's3://{s3_bucket}/{key}'") - - dobj = add_athena_datetimes(ticket.to_dict()) - - s3_client.put_object( - Body=json.dumps(dobj, default=str).encode("utf-8"), - Bucket=s3_bucket, - Key=key, - ) - - -def save_helpcentre(article_ids: list = []): - files = {} - - categories = zenpy_client.help_center.categories() - for category in categories: - category_key = get_key(category.to_dict()) - if category_key: - if article_ids == []: - files[category_key] = category.to_dict() - - sections = zenpy_client.help_center.sections(category_id=category.id) - for section in sections: - if section.category_id == category.id: - section_ref = get_key(section.to_dict()) - if section_ref: - section_key = f"{category_key}/{section_ref}" - if article_ids == []: - files[section_key] = section.to_dict() - - articles = zenpy_client.help_center.articles( - section_id=section.id - ) - for article in articles: - if article.section_id == section.id: - article_ref = get_key(article.to_dict()) - if article_ref: - article_key = f"{section_key}/{article_ref}" - if article_ids == [] or article.id in article_ids: - files[article_key] = article.to_dict() - - for file in files: - filename = f"{file}.json" - file_obj = files[file] - - html = None - html_filename = None - if "body" in file_obj: - html = file_obj["body"] - html_filename = f"{file}.html" - - wdt = add_athena_datetimes(file_obj) - - jprint(f"Saving 's3://{s3_bucket}/{s3_helpcentre_prefix}{filename}'") - s3_client.put_object( - Body=json.dumps(wdt, default=str).encode("utf-8"), - Bucket=s3_bucket, - Key=f"{s3_helpcentre_prefix}{filename}", - ) - if html and html_filename: - jprint(f"Saving 's3://{s3_bucket}/{s3_helpcentre_prefix}{html_filename}'") - s3_client.put_object( - Body=html.encode("utf-8"), - Bucket=s3_bucket, - Key=f"{s3_helpcentre_prefix}{html_filename}", - ) - - -def lambda_handler(event, context): - try: - jprint({"event": event, "context": context}) - - do_save_support_ticket_ids = [] - do_save_helpcentre_article_ids = [] - - if "ticket_id" in event: - do_save_support_ticket_ids = [event["ticket_id"]] - save_support(ticket_ids=do_save_support_ticket_ids) - elif "only_support" in event and event["only_support"]: - save_support() - elif "article_id" in event: - do_save_helpcentre_article_ids = [event["article_id"]] - save_helpcentre(article_ids=do_save_helpcentre_article_ids) - elif "only_helpcentre" in event and event["only_helpcentre"]: - save_helpcentre() - else: - save_helpcentre() - save_support() - except Exception as e: - jprint(e) diff --git a/lambda/crawler-govuk-reference-content/build.sh b/lambda_/crawler-govuk-reference-content/build.sh similarity index 100% rename from lambda/crawler-govuk-reference-content/build.sh rename to lambda_/crawler-govuk-reference-content/build.sh diff --git a/lambda/crawler-govuk-reference-content/dev-requirements.txt b/lambda_/crawler-govuk-reference-content/dev-requirements.txt similarity index 100% rename from lambda/crawler-govuk-reference-content/dev-requirements.txt rename to lambda_/crawler-govuk-reference-content/dev-requirements.txt diff --git a/lambda/crawler-govuk-reference-content/lambda.tf b/lambda_/crawler-govuk-reference-content/lambda.tf similarity index 100% rename from lambda/crawler-govuk-reference-content/lambda.tf rename to lambda_/crawler-govuk-reference-content/lambda.tf diff --git a/lambda/crawler-govuk-reference-content/main.py b/lambda_/crawler-govuk-reference-content/main.py similarity index 100% rename from lambda/crawler-govuk-reference-content/main.py rename to lambda_/crawler-govuk-reference-content/main.py diff --git a/lambda/email-forwarder/aws.tf b/lambda_/email-forwarder/aws.tf similarity index 100% rename from lambda/email-forwarder/aws.tf rename to lambda_/email-forwarder/aws.tf diff --git a/lambda/email-forwarder/lambda.tf b/lambda_/email-forwarder/lambda.tf similarity index 100% rename from lambda/email-forwarder/lambda.tf rename to lambda_/email-forwarder/lambda.tf diff --git a/lambda/email-forwarder/main.py b/lambda_/email-forwarder/main.py similarity index 100% rename from lambda/email-forwarder/main.py rename to lambda_/email-forwarder/main.py diff --git a/lambda/email-forwarder/s3.tf b/lambda_/email-forwarder/s3.tf similarity index 100% rename from lambda/email-forwarder/s3.tf rename to lambda_/email-forwarder/s3.tf diff --git a/lambda/email-forwarder/variables.tf b/lambda_/email-forwarder/variables.tf similarity index 100% rename from lambda/email-forwarder/variables.tf rename to lambda_/email-forwarder/variables.tf diff --git a/lambda/hackerone-zendesk-integration/build.sh b/lambda_/hackerone-zendesk-integration/build.sh similarity index 100% rename from lambda/hackerone-zendesk-integration/build.sh rename to lambda_/hackerone-zendesk-integration/build.sh diff --git a/lambda/hackerone-zendesk-integration/hackerone.py b/lambda_/hackerone-zendesk-integration/hackerone.py similarity index 100% rename from lambda/hackerone-zendesk-integration/hackerone.py rename to lambda_/hackerone-zendesk-integration/hackerone.py diff --git a/lambda/hackerone-zendesk-integration/lambda.tf b/lambda_/hackerone-zendesk-integration/lambda.tf similarity index 100% rename from lambda/hackerone-zendesk-integration/lambda.tf rename to lambda_/hackerone-zendesk-integration/lambda.tf diff --git a/lambda/hackerone-zendesk-integration/main.py b/lambda_/hackerone-zendesk-integration/main.py similarity index 100% rename from lambda/hackerone-zendesk-integration/main.py rename to lambda_/hackerone-zendesk-integration/main.py diff --git a/lambda/hackerone-zendesk-integration/requirements.txt b/lambda_/hackerone-zendesk-integration/requirements.txt similarity index 100% rename from lambda/hackerone-zendesk-integration/requirements.txt rename to lambda_/hackerone-zendesk-integration/requirements.txt diff --git a/lambda/hackerone-zendesk-integration/zendesk.py b/lambda_/hackerone-zendesk-integration/zendesk.py similarity index 100% rename from lambda/hackerone-zendesk-integration/zendesk.py rename to lambda_/hackerone-zendesk-integration/zendesk.py diff --git a/lambda/maintenance-load-athena-partitions/main.py b/lambda_/maintenance-load-athena-partitions/main.py similarity index 100% rename from lambda/maintenance-load-athena-partitions/main.py rename to lambda_/maintenance-load-athena-partitions/main.py diff --git a/lambda_/zendesk_backup/__init__.py b/lambda_/zendesk_backup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lambda/zendesk-backup/build.sh b/lambda_/zendesk_backup/build.sh similarity index 100% rename from lambda/zendesk-backup/build.sh rename to lambda_/zendesk_backup/build.sh diff --git a/lambda/zendesk-backup/eventbridge.tf b/lambda_/zendesk_backup/eventbridge.tf similarity index 100% rename from lambda/zendesk-backup/eventbridge.tf rename to lambda_/zendesk_backup/eventbridge.tf diff --git a/lambda/zendesk-backup/lambda.tf b/lambda_/zendesk_backup/lambda.tf similarity index 100% rename from lambda/zendesk-backup/lambda.tf rename to lambda_/zendesk_backup/lambda.tf diff --git a/lambda_/zendesk_backup/main.py b/lambda_/zendesk_backup/main.py new file mode 100644 index 0000000..e826b17 --- /dev/null +++ b/lambda_/zendesk_backup/main.py @@ -0,0 +1,254 @@ +import dataclasses +import os +import json +from typing import Optional, Union, Literal, Any +import boto3 +import time +import re +import functools +from zenpy import Zenpy + + +@functools.cache +def get_s3_bucket() -> str: + return os.environ["S3_BUCKET"] + + +@functools.cache +def s3_client(): + return boto3.client("s3") + + +s3_helpcentre_prefix = "helpcentre/" +s3_support_prefix = "support/" + + +@functools.cache +def zenpy_client() -> Zenpy: + zendesk_creds = { + "email": os.environ["ZENDESK_API_EMAIL"], + "token": os.environ["ZENDESK_API_KEY"], + "subdomain": os.environ["ZENDESK_SUBDOMAIN"], + } + + return Zenpy(**zendesk_creds) + + +def jprint(obj): + new_obj = {} + if type(obj) != dict: + obj = {"message": str(obj)} + if "_time" not in obj: + new_obj["_time"] = time.time() + for k in sorted(obj): + new_obj[k] = obj[k] + print(json.dumps(new_obj, default=str)) + + +def get_key(obj: Optional[dict[str, Any]]) -> Optional[str]: + """ + Get a URL from a dictionary and return the string consisting of the last two slash-separated elements, ie: + >>> get_key({"html_url": "example.com/a/long/path"}) + long/path + + :param obj: + :return: + """ + res = None + if obj and "html_url" in obj and "/" in obj["html_url"]: + html_url_split = obj["html_url"].rsplit("/", 2) + if len(html_url_split) == 3: + res = f"{html_url_split[1]}/{html_url_split[2]}" + return res + + +def add_athena_datetimes(json_dict: dict[str, Union[int, str, dict, list]]) -> dict: + """ + Take a JSON formatted dictionary. Find a string that contains this pattern: a 2, followed by any number of digits, + followed by a T/t, followed by two digits, followed by a colon. Having found that, replace the 't' with a space, + remove the z (we are assuming there's a 'z' in this), and then split the string on the dots. Only take the first + section of this newly split string, and add it back to the dictionary under the key f"{key}_athena" + + :param json_dict: + :return: + """ + res = {} + for key, value in json_dict.items(): + if type(value) is str and re.match(r"(?i)2[\d\-]+t\d\d:", value): + res[f"{key}_athena"] = (value.lower().replace("t", " ").replace("z", "").split(".")[0]) + + res.update(json_dict) + return res + + +def save_support(ticket_ids: Optional[list] = None): + """ + Save support tickets from Zendesk. Additionally, add a datetime to them + + :param ticket_ids: + :return: + """ + s3_bucket = get_s3_bucket + if ticket_ids: + tickets = [zenpy_client().tickets(id=str(ticket_id)) for ticket_id in ticket_ids] + else: + tickets = zenpy_client().search_export(type="ticket") + + for ticket in tickets: + # subject = re.sub(r"\s+", " ", re.sub(r"[^a-zA-Z0-9 ]", "", ticket.raw_subject)) + filename = f"gc3-{ticket.id}.json" + key = f"{s3_support_prefix}tickets/{filename}" + jprint(f"Saving 's3://{s3_bucket}/{key}'") + + dobj = add_athena_datetimes(ticket.to_dict()) + + s3_client().put_object( + Body=json.dumps(dobj, default=str).encode("utf-8"), + Bucket=s3_bucket, + Key=key, + ) + + +@dataclasses.dataclass +class ZendeskObject: + html_url: str + id: str + + to_dict = dataclasses.asdict + + +ObjectTypes = Literal["article", "section"] + + +def get_relations(subject: ObjectTypes) -> dict[str, ObjectTypes]: + relations = { + "article": { + "parent": "section" + }, + "section": { + "parent": "category" + } + } + return relations[subject] + + +def extract_substructure(object_type: ObjectTypes, zendesk_object: ZendeskObject, parent_id: str, parent_key: str, + article_ids: list) -> tuple[dict, str]: + relations = get_relations(object_type) + parent_type = relations["parent"] + substructure = {} + parent_type_id = f"{parent_type}_id" + if zendesk_object.__getattribute__(parent_type_id) == parent_id: + object_ref = get_key(zendesk_object.to_dict()) + if object_ref: + object_key = f"{parent_key}/{object_ref}" + if article_ids == [] or zendesk_object.id in article_ids: + substructure = {object_key: zendesk_object.to_dict()} + return substructure, object_key + + +def extract_helpcenter(article_ids: list) -> dict[str, dict]: + """ + This takes a complex, nested set of dictionaries and flattens them into a more simple structure. With more time, + we could make this recursive and very simple. However, it's good enough as it is + + :param article_ids: + :return: + """ + files = {} + + categories = zenpy_client().help_center.categories() + for category in categories: + category_key = get_key(category.to_dict()) + if category_key: + if not article_ids: + files[category_key] = category.to_dict() + + sections = zenpy_client().help_center.sections(category_id=category.id) + for section in sections: + section_file, section_key = extract_substructure( + object_type="section", + zendesk_object=section, + parent_id=category.id, + parent_key=category_key, + article_ids=article_ids + ) + files.update(section_file) + + articles = zenpy_client().help_center.articles(section_id=section.id) + for article in articles: + article_file, _ = extract_substructure( + object_type="article", + zendesk_object=article, + parent_id=section.id, + parent_key=section_key, + article_ids=article_ids, + ) + files.update(article_file) + return files + + +def save_helpcentre(article_ids=None): + if article_ids is None: + article_ids = [] + + s3_bucket = get_s3_bucket() + files = extract_helpcenter(article_ids) + + for file in files: + filename = f"{file}.json" + file_obj = files[file] + + html = None + html_filename = None + if "body" in file_obj: + html = file_obj["body"] + html_filename = f"{file}.html" + + wdt = add_athena_datetimes(file_obj) + + jprint(f"Saving 's3://{s3_bucket}/{s3_helpcentre_prefix}{filename}'") + s3_client().put_object( + Body=json.dumps(wdt, default=str).encode("utf-8"), + Bucket=s3_bucket, + Key=f"{s3_helpcentre_prefix}{filename}", + ) + if html and html_filename: + jprint(f"Saving 's3://{s3_bucket}/{s3_helpcentre_prefix}{html_filename}'") + s3_client().put_object( + Body=html.encode("utf-8"), + Bucket=s3_bucket, + Key=f"{s3_helpcentre_prefix}{html_filename}", + ) + + +def lambda_handler(event, context): + """ + This is the lambda handler for the code above. At the moment, the only path that's covered is the final 'else'. In + future, we can send slightly different events through the EventBridge cron job. + + :param event: + :param context: + :return: + """ + try: + jprint({"event": event, "context": context}) + + do_save_support_ticket_ids = [] + do_save_helpcentre_article_ids = [] + + if "ticket_id" in event: + do_save_support_ticket_ids = [event["ticket_id"]] + save_support(ticket_ids=do_save_support_ticket_ids) + elif "only_support" in event and event["only_support"]: + save_support() + elif "article_id" in event: + do_save_helpcentre_article_ids = [event["article_id"]] + save_helpcentre(article_ids=do_save_helpcentre_article_ids) + elif "only_helpcentre" in event and event["only_helpcentre"]: + save_helpcentre() + else: + save_helpcentre() + save_support() + except Exception as e: + jprint(e) diff --git a/lambda/zendesk-backup/requirements.txt b/lambda_/zendesk_backup/requirements.txt similarity index 100% rename from lambda/zendesk-backup/requirements.txt rename to lambda_/zendesk_backup/requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..89e2f35 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +-r lambda/zendesk-backup/requirements.txt +pytest \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_zendesk_backup.py b/tests/test_zendesk_backup.py new file mode 100644 index 0000000..3cc60b7 --- /dev/null +++ b/tests/test_zendesk_backup.py @@ -0,0 +1,107 @@ +import dataclasses +import os +from unittest import mock +from unittest.mock import Mock, call + +import pytest +from lambda_.zendesk_backup import main as zendesk_backup +from lambda_.zendesk_backup.main import ZendeskObject + + +@pytest.fixture(autouse=True) +def mock_env_vars(): + with mock.patch.dict(os.environ, values={"S3_BUCKET": "test"}): + yield + + +@pytest.fixture +def zendesk_backup_event(): + return { + "context": "LambdaContext([aws_request_id=1f6df4b9-0a8e-434d-87e5-00f59f07c2f2," + "log_group_name=/aws/lambda/zendesk-backup,log_stream_name=2024/04/30/[" + "$LATEST]20ac904278394d5ba8163542dfa8e884,function_name=zendesk-backup,memory_limit_in_mb=512," + "function_version=$LATEST," + "invoked_function_arn=arn:aws:lambda:eu-west-2:468623140221:function:zendesk-backup," + "client_context=None,identity=CognitoIdentity([cognito_identity_id=None," + "cognito_identity_pool_id=None])])", + "event": { + "version": "0", + "id": "1697296e-2030-dda6-7f4a-ac16427e291a", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "468623140221", + "time": "2024-04-30T03:00:00Z", + "region": "eu-west-2", + "resources": [ + "arn:aws:events:eu-west-2:468623140221:rule/lambda-zendesk-backup-event-rule" + ], + "detail": {} + } + } + + +@pytest.fixture +def json_ticket() -> dict[str, str]: + return { + "created_at": "2024-03-15T15:50:18Z" + } + + +def test_athena_datetime(json_ticket): + ticket_under_test = zendesk_backup.add_athena_datetimes(json_ticket) + json_ticket.update({"created_at_athena": "2024-03-15 15:50:18"}) + assert ticket_under_test == json_ticket + + +def test_lambda_handler(zendesk_backup_event): + path = "lambda_.zendesk_backup.main" + with mock.patch(f"{path}.save_helpcentre") as mock_save_helpcentre, mock.patch(f"{path}.save_support") as mock_save_support: + zendesk_backup.lambda_handler(**zendesk_backup_event) + mock_save_helpcentre.assert_called_once_with() + mock_save_support.assert_called_once_with() + + +def test_get_key(): + dictionary = {"html_url": "example.com/a/long/path"} + assert zendesk_backup.get_key(dictionary) == "long/path" + + +@mock.patch("lambda_.zendesk_backup.main.zenpy_client") +@mock.patch("lambda_.zendesk_backup.main.s3_client") +def test_save_helpcenter(s3_client: Mock, zenpy_client: Mock): + category = ZendeskCategory("example.com/example/path", id="category_id") + section = ZendeskSection(category_id=category.id, html_url="example.com/section/path", id="section_id") + article = ZendeskArticle(html_url="example.com/article/path", section_id=section.id, id="article_id") + zenpy_client.return_value.help_center.categories.return_value = [category] + zenpy_client.return_value.help_center.sections.return_value = [section] + zenpy_client.return_value.help_center.articles.return_value = [article] + zendesk_backup.save_helpcentre() + s3_put: Mock = s3_client.return_value.put_object + s3_put.assert_has_calls( + [ + call(Body=b'{"html_url": "example.com/example/path", "id": "category_id"}', Bucket='test', + Key='helpcentre/example/path.json'), + call( + Body=b'{"html_url": "example.com/section/path", "id": "section_id", "category_id": "category_id"}', + Bucket='test', Key='helpcentre/example/path/section/path.json'), + call( + Body=b'{"html_url": "example.com/article/path", "id": "article_id", "section_id": "section_id"}', + Bucket='test', Key='helpcentre/example/path/section/path/article/path.json') + ], + any_order=True + ) + + +@dataclasses.dataclass +class ZendeskCategory(ZendeskObject): + id: str + + +@dataclasses.dataclass +class ZendeskSection(ZendeskObject): + category_id: str + + +@dataclasses.dataclass +class ZendeskArticle(ZendeskObject): + section_id: str