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
30 changes: 27 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,29 @@ jobs:
role-to-assume: ${{ secrets[format('AWS_ROLE_{0}', matrix.environment)] }}
aws-region: ${{ env.AWS_REGION }}

- name: Resolve backend config
id: backend
run: |
case "${{ matrix.environment }}" in
staging) echo "file=environments/staging/backend.hcl" >> $GITHUB_OUTPUT ;;
prod) echo "file=environments/production/backend.hcl" >> $GITHUB_OUTPUT ;;
*) echo "file=backend-config.hcl" >> $GITHUB_OUTPUT ;;
esac

- name: Terraform Init
working-directory: infrastructure/terraform
run: terraform init
run: terraform init -backend-config="${{ steps.backend.outputs.file }}"

- name: Terraform Validate
working-directory: infrastructure/terraform
run: terraform validate -var-file="environments/ci-validate.tfvars"

- name: Terraform Plan
working-directory: infrastructure/terraform
run: |
terraform plan \
-lock=true \
-lock-timeout=5m \
-var-file="environments/${{ matrix.environment }}.tfvars" \
-out=tfplan-${{ matrix.environment }}

Expand Down Expand Up @@ -118,6 +133,15 @@ jobs:
env:
PROMETHEUS_URL: ${{ secrets.PROMETHEUS_URL }}

- name: Resolve backend config
id: backend
run: |
case "${{ matrix.environment }}" in
staging) echo "file=environments/staging/backend.hcl" >> $GITHUB_OUTPUT ;;
prod) echo "file=environments/production/backend.hcl" >> $GITHUB_OUTPUT ;;
*) echo "file=backend-config.hcl" >> $GITHUB_OUTPUT ;;
esac

- name: Download plan
uses: actions/download-artifact@v4
with:
Expand All @@ -126,11 +150,11 @@ jobs:

- name: Terraform Init
working-directory: infrastructure/terraform
run: terraform init
run: terraform init -backend-config="${{ steps.backend.outputs.file }}"

- name: Terraform Apply
working-directory: infrastructure/terraform
run: terraform apply -auto-approve tfplan-${{ matrix.environment }}
run: terraform apply -lock=true -lock-timeout=5m -auto-approve tfplan-${{ matrix.environment }}

- name: Output infrastructure details
working-directory: infrastructure/terraform
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/components/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@ export const LandingPage: React.FC<LandingPageProps> = ({ className }) => {
}
};

const handleKeyDown = (e: React.KeyboardEvent) => {
// Allow form submission with Enter key
if (e.key === 'Enter' && e.currentTarget.tagName === 'FORM') {
handleSubmit(e as unknown as React.FormEvent);
const handleKeyDown = (e: React.KeyboardEvent<HTMLFormElement>) => {
if (e.key === 'Enter') {
e.currentTarget.requestSubmit();
}
};

Expand Down
41 changes: 41 additions & 0 deletions frontend/src/components/__tests__/LandingPage.keyboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LandingPage from '../LandingPage';

describe('LandingPage handleKeyDown', () => {
it('submits the form when Enter is pressed on the form with a valid email', async () => {
render(<LandingPage />);
const emailInput = screen.getByLabelText(/email address/i);
await userEvent.type(emailInput, 'test@example.com');

const form = emailInput.closest('form')!;
fireEvent.keyDown(form, { key: 'Enter', code: 'Enter' });

expect(
screen.getByRole('button', { name: /already subscribed/i }),
).toBeInTheDocument();
});

it('triggers validation when Enter is pressed on the form with empty email', () => {
render(<LandingPage />);
const form = screen.getByLabelText(/email address/i).closest('form')!;

fireEvent.keyDown(form, { key: 'Enter', code: 'Enter' });

expect(screen.getByRole('alert')).toHaveTextContent(/email is required/i);
});

it('does not trigger submission for non-Enter keys', async () => {
render(<LandingPage />);
const emailInput = screen.getByLabelText(/email address/i);
await userEvent.type(emailInput, 'test@example.com');

const form = emailInput.closest('form')!;
fireEvent.keyDown(form, { key: 'a', code: 'KeyA' });

expect(
screen.queryByRole('button', { name: /already subscribed/i }),
).not.toBeInTheDocument();
});
});
21 changes: 21 additions & 0 deletions infrastructure/terraform/environments/ci-validate.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# CI-only variable file used exclusively for `terraform validate`.
# Values are syntactically valid placeholders — never deployed.

environment = "dev"
aws_region = "us-east-1"
vpc_cidr_block = "10.0.0.0/16"
db_name = "predictiq"
db_username = "predictiqadmin"
db_password = "CiValidate!Test#Placeholder2024XYZ"
db_instance_class = "db.t3.micro"
allocated_storage = 20
backup_retention_days = 7
redis_node_type = "cache.t3.micro"
redis_num_nodes = 1
redis_engine_version = "7.0"
redis_auth_token = "CiValidate!Redis#Placeholder2024XYZ"
api_image_uri = "123456789012.dkr.ecr.us-east-1.amazonaws.com/predictiq:ci-validate"
api_container_port = 8080
api_desired_count = 1
api_cpu = 256
api_memory = 512
6 changes: 4 additions & 2 deletions infrastructure/terraform/modules/redis/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ resource "aws_elasticache_cluster" "main" {
}

output "endpoint" {
value = aws_elasticache_cluster.main.cache_nodes[0].address
value = aws_elasticache_cluster.main.cache_nodes[0].address
sensitive = true
}

output "redis_url" {
value = "redis://${aws_elasticache_cluster.main.cache_nodes[0].address}:6379"
value = "redis://${aws_elasticache_cluster.main.cache_nodes[0].address}:6379"
sensitive = true
}
1 change: 1 addition & 0 deletions infrastructure/terraform/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ output "rds_endpoint" {
output "redis_endpoint" {
description = "Redis endpoint"
value = module.redis.endpoint
sensitive = true
}

output "ecs_cluster_name" {
Expand Down
29 changes: 26 additions & 3 deletions infrastructure/terraform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,16 @@ variable "db_password" {
description = "Database master password"
type = string
sensitive = true

validation {
condition = length(var.db_password) >= 8
error_message = "Database password must be at least 8 characters long."
condition = (
length(var.db_password) >= 24 &&
can(regex("[A-Z]", var.db_password)) &&
can(regex("[a-z]", var.db_password)) &&
can(regex("[0-9]", var.db_password)) &&
can(regex("[^a-zA-Z0-9]", var.db_password))
)
error_message = "Database password must be at least 24 characters and contain uppercase letters, lowercase letters, numbers, and special characters."
}
}

Expand Down Expand Up @@ -128,6 +134,23 @@ variable "redis_engine_version" {
}
}

variable "redis_auth_token" {
description = "Auth token for Redis in-transit encryption"
type = string
sensitive = true

validation {
condition = (
length(var.redis_auth_token) >= 24 &&
can(regex("[A-Z]", var.redis_auth_token)) &&
can(regex("[a-z]", var.redis_auth_token)) &&
can(regex("[0-9]", var.redis_auth_token)) &&
can(regex("[^a-zA-Z0-9]", var.redis_auth_token))
)
error_message = "Redis auth token must be at least 24 characters and contain uppercase letters, lowercase letters, numbers, and special characters."
}
}

variable "api_image_uri" {
description = "ECR image URI for API"
type = string
Expand Down