From 527fc6e7398d6e70b7471f7647e02c2e74d77727 Mon Sep 17 00:00:00 2001 From: Roman Volykh Date: Fri, 26 Dec 2025 12:01:01 +0200 Subject: [PATCH] test: Add terraform tests --- .github/workflows/_tf_test.yml | 40 +++++ .github/workflows/validate.yml | 8 + infra/application.tf | 4 +- infra/cmd_poweron.tf | 4 +- infra/config.tf | 4 +- infra/main.tf | 9 +- infra/modules/api/data.tf | 4 +- infra/modules/api/variables.tf | 6 + infra/observability.tf | 6 +- infra/tests/01_unit.tftest.hcl | 212 ++++++++++++++++++++++++++ infra/tests/02_integration.tftest.hcl | 35 +++++ infra/tests/utils/http/main.tf | 45 ++++++ infra/tests/utils/inputs/main.tf | 41 +++++ infra/variables.tf | 12 ++ 14 files changed, 415 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/_tf_test.yml create mode 100644 infra/tests/01_unit.tftest.hcl create mode 100644 infra/tests/02_integration.tftest.hcl create mode 100644 infra/tests/utils/http/main.tf create mode 100644 infra/tests/utils/inputs/main.tf diff --git a/.github/workflows/_tf_test.yml b/.github/workflows/_tf_test.yml new file mode 100644 index 0000000..8b66987 --- /dev/null +++ b/.github/workflows/_tf_test.yml @@ -0,0 +1,40 @@ +name: "Terraform Test" + +on: + workflow_call: + inputs: + environment: + description: "Environment to test" + required: true + type: string + +permissions: + contents: read + id-token: write + +jobs: + terraform-test: + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + steps: + - name: "Checkout" + uses: actions/checkout@v6 + - name: "Install dependencies" + uses: ./.github/actions/dependencies + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@v5 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/bootstrap/github-telegram-bot + role-session-name: github-actions-test-${{ github.run_id }} + aws-region: ${{ secrets.AWS_REGION }} + - name: "Init" + working-directory: infra + env: + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} + AWS_REGION: ${{ secrets.AWS_REGION }} + run: | + terraform init -backend=false + - name: "🏄 Test" + working-directory: infra + run: | + terraform test diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3095166..a3d16e9 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -26,3 +26,11 @@ jobs: environment: "sandbox" enabled_cache_plan: false secrets: inherit + + terraform-test: + if: github.event_name == 'pull_request' + needs: [terraform-plan] + uses: ./.github/workflows/_tf_test.yml + with: + environment: "sandbox" + secrets: inherit diff --git a/infra/application.tf b/infra/application.tf index 9a2ded7..de4a24b 100644 --- a/infra/application.tf +++ b/infra/application.tf @@ -1,5 +1,5 @@ resource "aws_servicecatalogappregistry_application" "telegram_bot" { provider = aws.application - name = "telegram-bot" - description = "Telegram Bot" + name = "${var.prefix}telegram-bot" + description = "${var.prefix}Telegram Bot" } diff --git a/infra/cmd_poweron.tf b/infra/cmd_poweron.tf index 78e186e..f8845d2 100644 --- a/infra/cmd_poweron.tf +++ b/infra/cmd_poweron.tf @@ -1,7 +1,7 @@ module "telegram_bot_queue_cmd_poweron" { source = "./modules/queue" - queue_name = "telegram-bot-cmd-poweron" + queue_name = "${var.prefix}telegram-bot-cmd-poweron" enable_dead_letter_queue = true dead_letter_queue_arn = module.telegram_bot_queue_alerting.sqs_queue_arn } @@ -9,7 +9,7 @@ module "telegram_bot_queue_cmd_poweron" { module "telegram_bot_cmd_poweron" { source = "./modules/handler" - function_name = "telegram-bot-cmd-poweron" + function_name = "${var.prefix}telegram-bot-cmd-poweron" reserved_concurrent_executions = -1 source_path = "${path.root}/../apps/poweron" diff --git a/infra/config.tf b/infra/config.tf index 04c4eb4..ef4e413 100644 --- a/infra/config.tf +++ b/infra/config.tf @@ -1,13 +1,13 @@ module "telegram_bot_api_token" { source = "./modules/kv" - name = "/telegram/bot/api_token" + name = "/${var.prefix}telegram/bot/api_token" value = var.telegram_bot_api_token } module "telegram_bot_cache_poweron" { source = "./modules/kv" - name = "/telegram/bot/cache/poweron" + name = "/${var.prefix}telegram/bot/cache/poweron" value = "none" } diff --git a/infra/main.tf b/infra/main.tf index 944dc0c..75b683a 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -1,14 +1,14 @@ module "telegram_bot_queue_mux" { source = "./modules/queue" - queue_name = "telegram-bot-mux" + queue_name = "${var.prefix}telegram-bot-mux" enable_dead_letter_queue = true dead_letter_queue_arn = module.telegram_bot_queue_alerting.sqs_queue_arn } resource "aws_cloudwatch_metric_alarm" "mux_command_rate" { - alarm_name = "telegram-bot-mux-command-rate" + alarm_name = "${var.prefix}telegram-bot-mux-command-rate" comparison_operator = "GreaterThanThreshold" evaluation_periods = 3 # over 3 minutes period = 60 # 1 minute @@ -32,17 +32,18 @@ resource "aws_cloudwatch_metric_alarm" "mux_command_rate" { module "telegram_bot_api" { source = "./modules/api" - api_name = "telegram-bot" + api_name = "${var.prefix}telegram-bot" sqs_queue = { name = module.telegram_bot_queue_mux.sqs_queue_name arn = module.telegram_bot_queue_mux.sqs_queue_arn } + ip_allowlist = var.api_ip_allowlist } module "telegram_bot_handler_mux" { source = "./modules/handler" - function_name = "telegram-bot-mux" + function_name = "${var.prefix}telegram-bot-mux" reserved_concurrent_executions = -1 source_path = "${path.root}/../apps/mux" diff --git a/infra/modules/api/data.tf b/infra/modules/api/data.tf index 509722d..c8d0521 100644 --- a/infra/modules/api/data.tf +++ b/infra/modules/api/data.tf @@ -46,7 +46,7 @@ data "aws_iam_policy_document" "api_gateway_policy" { condition { test = "NotIpAddress" variable = "aws:SourceIp" - values = [ + values = concat([ "91.108.56.0/22", "91.108.4.0/22", "91.108.8.0/22", @@ -61,7 +61,7 @@ data "aws_iam_policy_document" "api_gateway_policy" { "2001:67c:4e8::/48", "2001:b28:f23c::/48", "2a0a:f280::/32", - ] + ], var.ip_allowlist) } } } diff --git a/infra/modules/api/variables.tf b/infra/modules/api/variables.tf index 3a68f00..bcf32c8 100644 --- a/infra/modules/api/variables.tf +++ b/infra/modules/api/variables.tf @@ -24,6 +24,12 @@ variable "sqs_queue" { } } +variable "ip_allowlist" { + description = "IP addresses to allow access to the API" + type = list(string) + default = [] +} + variable "tags" { description = "Tags to apply to the API Gateway and SQS resources" type = map(string) diff --git a/infra/observability.tf b/infra/observability.tf index 4e9c97c..6da929a 100644 --- a/infra/observability.tf +++ b/infra/observability.tf @@ -1,7 +1,7 @@ module "telegram_bot_queue_alerting" { source = "./modules/queue" - queue_name = "telegram-bot-alerting" + queue_name = "${var.prefix}telegram-bot-alerting" enable_dead_letter_queue = false dead_letter_queue_source_arns = [ @@ -11,7 +11,7 @@ module "telegram_bot_queue_alerting" { } resource "aws_cloudwatch_metric_alarm" "non_empty_dlq" { - alarm_name = "telegram-bot-non-empty-dlq" + alarm_name = "${var.prefix}telegram-bot-non-empty-dlq" comparison_operator = "GreaterThanOrEqualToThreshold" evaluation_periods = 1 period = 5 * 60 @@ -35,7 +35,7 @@ resource "aws_cloudwatch_metric_alarm" "non_empty_dlq" { module "telegram_bot_alerting" { source = "./modules/alerting" - name = "telegram-bot-alerting" + name = "${var.prefix}telegram-bot-alerting" reserved_concurrent_executions = -1 emails = var.alerting_emails telegram_chat_id = var.alerting_telegram_chat_id diff --git a/infra/tests/01_unit.tftest.hcl b/infra/tests/01_unit.tftest.hcl new file mode 100644 index 0000000..16c2437 --- /dev/null +++ b/infra/tests/01_unit.tftest.hcl @@ -0,0 +1,212 @@ +run "prepare" { + module { + source = "./tests/utils/inputs" + } +} + +run "verify_plan" { + command = plan + + variables { + telegram_bot_api_token = run.prepare.token + prefix = run.prepare.prefix + } + + # Configs + assert { + condition = module.telegram_bot_api_token.name == "/${run.prepare.prefix}telegram/bot/api_token" + error_message = "module telegram_bot_api_token should create the SSM parameter /${run.prepare.prefix}telegram/bot/api_token" + } + assert { + condition = module.telegram_bot_cache_poweron.name == "/${run.prepare.prefix}telegram/bot/cache/poweron" + error_message = "module telegram_bot_cache_poweron should create the SSM parameter /${run.prepare.prefix}telegram/bot/cache/poweron" + } + + # Queues + assert { + condition = module.telegram_bot_api.sqs_queue_name == "${run.prepare.prefix}telegram-bot-webhook.fifo" + error_message = "module telegram_bot_api should create the SQS queue ${run.prepare.prefix}telegram-bot-webhook.fifo" + } + assert { + condition = module.telegram_bot_queue_mux.sqs_queue_name == "${run.prepare.prefix}telegram-bot-mux.fifo" + error_message = "module telegram_bot_queue_mux should create the SQS queue ${run.prepare.prefix}telegram-bot-mux.fifo" + } + assert { + condition = module.telegram_bot_queue_alerting.sqs_queue_name == "${run.prepare.prefix}telegram-bot-alerting.fifo" + error_message = "module telegram_bot_queue_alerting should create the SQS queue ${run.prepare.prefix}telegram-bot-alerting.fifo" + } + assert { + condition = module.telegram_bot_queue_cmd_poweron.sqs_queue_name == "${run.prepare.prefix}telegram-bot-cmd-poweron.fifo" + error_message = "module telegram_bot_queue_cmd_poweron should create the SQS queue ${run.prepare.prefix}telegram-bot-cmd-poweron.fifo" + } + + # Functions + assert { + condition = module.telegram_bot_handler_mux.lambda_function_name == "${run.prepare.prefix}telegram-bot-mux" + error_message = "module telegram_bot_handler_mux should create the Lambda function ${run.prepare.prefix}telegram-bot-mux" + } + assert { + condition = module.telegram_bot_cmd_poweron.lambda_function_name == "${run.prepare.prefix}telegram-bot-cmd-poweron" + error_message = "module telegram_bot_cmd_poweron should create the Lambda function ${run.prepare.prefix}telegram-bot-cmd-poweron" + } +} + +run "verify_module_api_positive" { + command = plan + + module { + source = "./modules/api" + } + + variables { + api_name = "${run.prepare.prefix}api-positive" + sqs_queue = { + name = "${run.prepare.prefix}api.fifo" + arn = "arn:aws:sqs:000000000000:us-east-1:${run.prepare.prefix}api.fifo" + } + } + + assert { + condition = output.sqs_queue_name == "${run.prepare.prefix}api-positive-webhook.fifo" + error_message = "module api_positive should create the SQS queue ${run.prepare.prefix}api-positive-webhook.fifo" + } +} + +run "verify_module_api_negative" { + command = plan + + module { + source = "./modules/api" + } + + variables { + api_name = "${run.prepare.prefix}api-negative" + sqs_queue = { + name = "${run.prepare.prefix}my-queue.fifo" + arn = "arn:aws:sqs:000000000000:us-east-1:${run.prepare.prefix}unknown-queue.fifo" + } + } + + expect_failures = [var.sqs_queue] +} + +run "verify_module_queue_positive" { + command = plan + + module { + source = "./modules/queue" + } + + variables { + queue_name = "${run.prepare.prefix}queue-positive" + } + + assert { + condition = output.sqs_queue_name == "${run.prepare.prefix}queue-positive.fifo" + error_message = "module queue_positive should create the SQS queue ${run.prepare.prefix}queue-positive.fifo" + } +} + +run "verify_module_queue_negative" { + command = plan + + module { + source = "./modules/queue" + } + + variables { + queue_name = "${run.prepare.prefix}queue-negative" + enable_dead_letter_queue = true + } + + expect_failures = [var.dead_letter_queue_arn] +} + +run "verify_module_kv_positive" { + command = plan + + module { + source = "./modules/kv" + } + + variables { + name = "${run.prepare.prefix}kv-positive" + value = "test" + } + + assert { + condition = output.name == "${run.prepare.prefix}kv-positive" + error_message = "module kv_positive should create the SSM parameter ${run.prepare.prefix}kv-positive" + } +} + +run "verify_module_handler_positive" { + command = plan + + module { + source = "./modules/handler" + } + + variables { + function_name = "${run.prepare.prefix}handler-positive" + reserved_concurrent_executions = 1 + source_path = "../../apps/mux" + sqs_queue_arn = "arn:aws:sqs:000000000000:us-east-1:${run.prepare.prefix}queue.fifo" + sqs_batch_size = 10 + role_policies = [["{}"]] + } + + assert { + condition = output.lambda_function_name == "${run.prepare.prefix}handler-positive" + error_message = "module handler_positive should create the Lambda function ${run.prepare.prefix}handler-positive" + } +} + +run "verify_module_handler_negative" { + command = plan + + module { + source = "./modules/handler" + } + + variables { + function_name = "${run.prepare.prefix}handler-negative" + reserved_concurrent_executions = 1 + source_path = "../../apps/mux" + sqs_queue_arn = "arn:aws:sqs:000000000000:us-east-1:${run.prepare.prefix}queue.fifo" + sqs_batch_size = 10 + role_policies = [["1"], ["2"], ["3"], ["4"], ["5"], ["6"], ["7"], ["8"], ["9"]] + } + + expect_failures = [var.role_policies] +} + +run "verify_module_alerting_positive" { + command = plan + + module { + source = "./modules/alerting" + } + + variables { + name = "${run.prepare.prefix}alerting-positive" + reserved_concurrent_executions = 1 + role_policies = [["{}"]] + } +} + +run "verify_module_alerting_negative" { + command = plan + + module { + source = "./modules/alerting" + } + + variables { + name = "${run.prepare.prefix}alerting-negative" + reserved_concurrent_executions = 1 + role_policies = [["1"], ["2"], ["3"], ["4"], ["5"], ["6"], ["7"], ["8"], ["9"]] + } + + expect_failures = [var.role_policies] +} diff --git a/infra/tests/02_integration.tftest.hcl b/infra/tests/02_integration.tftest.hcl new file mode 100644 index 0000000..d7162dd --- /dev/null +++ b/infra/tests/02_integration.tftest.hcl @@ -0,0 +1,35 @@ +run "prepare" { + module { + source = "./tests/utils/inputs" + } +} + +variables { + telegram_bot_api_token = run.prepare.token + prefix = run.prepare.prefix + api_ip_allowlist = ["${run.prepare.my_ip}/32"] +} + +override_resource { + # We don't want to replace currently applied global account configuration for Lambda + target = module.telegram_bot_api.aws_api_gateway_account.this +} + +run "apply" { + command = apply +} + +run "verify" { + module { + source = "./tests/utils/http" + } + + variables { + url = run.apply.api_gateway_url + } + + assert { + condition = data.http.request.status_code == 200 + error_message = "HTTP request to Webhook URL should return 200" + } +} diff --git a/infra/tests/utils/http/main.tf b/infra/tests/utils/http/main.tf new file mode 100644 index 0000000..26485b6 --- /dev/null +++ b/infra/tests/utils/http/main.tf @@ -0,0 +1,45 @@ +terraform { + required_providers { + http = { + source = "hashicorp/http" + version = "~> 3.5" + } + } +} + +variable "url" { + type = string +} + +data "http" "request" { + url = var.url + method = "POST" + + request_body = jsonencode({ + update_id = 42, + message = { + message_id = 42 + date = 1767225600 + text = "/test" + chat = { + id = 42 + type = "private" + username = "test" + } + from = { + id = 42 + username = "test" + first_name = "Test" + last_name = "Test" + is_bot = true + } + entities = [ + { + type = "bot_command" + offset = 0 + length = 5 + } + ] + } + }) +} diff --git a/infra/tests/utils/inputs/main.tf b/infra/tests/utils/inputs/main.tf new file mode 100644 index 0000000..286df1b --- /dev/null +++ b/infra/tests/utils/inputs/main.tf @@ -0,0 +1,41 @@ +terraform { + required_providers { + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + http = { + source = "hashicorp/http" + version = "~> 3.5" + } + } +} + +# Resources + +resource "random_pet" "prefix" { + prefix = "test" + length = 1 +} + +resource "random_uuid" "token" {} + +# Data sources + +data "http" "my_ip" { + url = "https://ifconfig.me/ip" +} + +# Outputs + +output "prefix" { + value = "${random_pet.prefix.id}-" +} + +output "token" { + value = "test-token-${random_uuid.token.result}" +} + +output "my_ip" { + value = chomp(data.http.my_ip.response_body) +} diff --git a/infra/variables.tf b/infra/variables.tf index 0edff20..a25eb44 100644 --- a/infra/variables.tf +++ b/infra/variables.tf @@ -4,6 +4,18 @@ variable "telegram_bot_api_token" { sensitive = true } +variable "prefix" { + description = "Prefix to use for the resources" + type = string + default = "" +} + +variable "api_ip_allowlist" { + description = "IP addresses to allow access to the API" + type = list(string) + default = [] +} + variable "alerting_telegram_chat_id" { description = "Telegram chat ID to send alerts to" type = string