second first commit
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env bats
|
||||
# ACM integration tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
CERT_ARN=""
|
||||
IMPORTED_CERT_ARN=""
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ -n "$CERT_ARN" ]; then
|
||||
aws_cmd acm delete-certificate --certificate-arn "$CERT_ARN" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$IMPORTED_CERT_ARN" ]; then
|
||||
aws_cmd acm delete-certificate --certificate-arn "$IMPORTED_CERT_ARN" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# US1: Certificate Lifecycle
|
||||
# ============================================
|
||||
|
||||
@test "ACM: request certificate" {
|
||||
run aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS
|
||||
assert_success
|
||||
CERT_ARN=$(json_get "$output" '.CertificateArn')
|
||||
[ -n "$CERT_ARN" ]
|
||||
[[ "$CERT_ARN" =~ ^arn:aws:acm: ]]
|
||||
}
|
||||
|
||||
@test "ACM: describe certificate" {
|
||||
out=$(aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS)
|
||||
CERT_ARN=$(json_get "$out" '.CertificateArn')
|
||||
|
||||
run aws_cmd acm describe-certificate --certificate-arn "$CERT_ARN"
|
||||
assert_success
|
||||
domain=$(json_get "$output" '.Certificate.DomainName')
|
||||
status=$(json_get "$output" '.Certificate.Status')
|
||||
[ "$domain" = "cli-test.example.com" ]
|
||||
[ "$status" = "ISSUED" ]
|
||||
}
|
||||
|
||||
@test "ACM: get certificate" {
|
||||
out=$(aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS)
|
||||
CERT_ARN=$(json_get "$out" '.CertificateArn')
|
||||
|
||||
run aws_cmd acm get-certificate --certificate-arn "$CERT_ARN"
|
||||
assert_success
|
||||
cert=$(json_get "$output" '.Certificate')
|
||||
[ -n "$cert" ]
|
||||
[[ "$cert" =~ "BEGIN CERTIFICATE" ]]
|
||||
}
|
||||
|
||||
@test "ACM: list certificates" {
|
||||
out=$(aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS)
|
||||
CERT_ARN=$(json_get "$out" '.CertificateArn')
|
||||
|
||||
run aws_cmd acm list-certificates
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg arn "$CERT_ARN" '.CertificateSummaryList | any(.CertificateArn == $arn)')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "ACM: delete certificate" {
|
||||
out=$(aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS)
|
||||
CERT_ARN=$(json_get "$out" '.CertificateArn')
|
||||
|
||||
run aws_cmd acm delete-certificate --certificate-arn "$CERT_ARN"
|
||||
assert_success
|
||||
CERT_ARN=""
|
||||
|
||||
# Verify it's gone
|
||||
run aws_cmd acm describe-certificate --certificate-arn "$(json_get "$out" '.CertificateArn')"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# US2: Import and Export
|
||||
# ============================================
|
||||
|
||||
@test "ACM: import certificate" {
|
||||
# Generate self-signed certificate with openssl
|
||||
local key_file cert_file
|
||||
key_file=$(mktemp)
|
||||
cert_file=$(mktemp)
|
||||
openssl req -x509 -newkey rsa:2048 -keyout "$key_file" -out "$cert_file" \
|
||||
-days 365 -nodes -subj "/CN=cli-import.example.com" 2>/dev/null
|
||||
|
||||
run aws_cmd acm import-certificate \
|
||||
--certificate "fileb://$cert_file" \
|
||||
--private-key "fileb://$key_file"
|
||||
rm -f "$key_file" "$cert_file"
|
||||
assert_success
|
||||
IMPORTED_CERT_ARN=$(json_get "$output" '.CertificateArn')
|
||||
[ -n "$IMPORTED_CERT_ARN" ]
|
||||
}
|
||||
|
||||
@test "ACM: get imported certificate" {
|
||||
local key_file cert_file
|
||||
key_file=$(mktemp)
|
||||
cert_file=$(mktemp)
|
||||
openssl req -x509 -newkey rsa:2048 -keyout "$key_file" -out "$cert_file" \
|
||||
-days 365 -nodes -subj "/CN=cli-import.example.com" 2>/dev/null
|
||||
|
||||
out=$(aws_cmd acm import-certificate \
|
||||
--certificate "fileb://$cert_file" \
|
||||
--private-key "fileb://$key_file")
|
||||
rm -f "$key_file" "$cert_file"
|
||||
IMPORTED_CERT_ARN=$(json_get "$out" '.CertificateArn')
|
||||
|
||||
run aws_cmd acm get-certificate --certificate-arn "$IMPORTED_CERT_ARN"
|
||||
assert_success
|
||||
cert=$(json_get "$output" '.Certificate')
|
||||
[[ "$cert" =~ "BEGIN CERTIFICATE" ]]
|
||||
}
|
||||
|
||||
@test "ACM: export certificate" {
|
||||
local key_file cert_file
|
||||
key_file=$(mktemp)
|
||||
cert_file=$(mktemp)
|
||||
openssl req -x509 -newkey rsa:2048 -keyout "$key_file" -out "$cert_file" \
|
||||
-days 365 -nodes -subj "/CN=cli-export.example.com" 2>/dev/null
|
||||
|
||||
out=$(aws_cmd acm import-certificate \
|
||||
--certificate "fileb://$cert_file" \
|
||||
--private-key "fileb://$key_file")
|
||||
rm -f "$key_file" "$cert_file"
|
||||
IMPORTED_CERT_ARN=$(json_get "$out" '.CertificateArn')
|
||||
|
||||
run aws_cmd acm export-certificate \
|
||||
--certificate-arn "$IMPORTED_CERT_ARN" \
|
||||
--passphrase "dGVzdC1wYXNzcGhyYXNl"
|
||||
assert_success
|
||||
cert=$(json_get "$output" '.Certificate')
|
||||
[ -n "$cert" ]
|
||||
pk=$(json_get "$output" '.PrivateKey')
|
||||
[ -n "$pk" ]
|
||||
}
|
||||
|
||||
@test "ACM: export requested certificate fails" {
|
||||
out=$(aws_cmd acm request-certificate --domain-name "cli-noexport.example.com" --validation-method DNS)
|
||||
CERT_ARN=$(json_get "$out" '.CertificateArn')
|
||||
|
||||
run aws_cmd acm export-certificate \
|
||||
--certificate-arn "$CERT_ARN" \
|
||||
--passphrase "dGVzdC1wYXNzcGhyYXNl"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# US3: Tagging
|
||||
# ============================================
|
||||
|
||||
@test "ACM: add and list tags" {
|
||||
out=$(aws_cmd acm request-certificate --domain-name "cli-tag.example.com" --validation-method DNS)
|
||||
CERT_ARN=$(json_get "$out" '.CertificateArn')
|
||||
|
||||
run aws_cmd acm add-tags-to-certificate \
|
||||
--certificate-arn "$CERT_ARN" \
|
||||
--tags Key=Env,Value=test Key=Project,Value=floci
|
||||
assert_success
|
||||
|
||||
run aws_cmd acm list-tags-for-certificate --certificate-arn "$CERT_ARN"
|
||||
assert_success
|
||||
env_tag=$(echo "$output" | jq '.Tags[] | select(.Key == "Env") | .Value' -r)
|
||||
[ "$env_tag" = "test" ]
|
||||
project_tag=$(echo "$output" | jq '.Tags[] | select(.Key == "Project") | .Value' -r)
|
||||
[ "$project_tag" = "floci" ]
|
||||
}
|
||||
|
||||
@test "ACM: remove tags" {
|
||||
out=$(aws_cmd acm request-certificate --domain-name "cli-untag.example.com" --validation-method DNS)
|
||||
CERT_ARN=$(json_get "$out" '.CertificateArn')
|
||||
|
||||
aws_cmd acm add-tags-to-certificate \
|
||||
--certificate-arn "$CERT_ARN" \
|
||||
--tags Key=Env,Value=test Key=Project,Value=floci
|
||||
|
||||
run aws_cmd acm remove-tags-from-certificate \
|
||||
--certificate-arn "$CERT_ARN" \
|
||||
--tags Key=Env
|
||||
assert_success
|
||||
|
||||
run aws_cmd acm list-tags-for-certificate --certificate-arn "$CERT_ARN"
|
||||
assert_success
|
||||
env_gone=$(echo "$output" | jq '.Tags[] | select(.Key == "Env")' 2>/dev/null)
|
||||
[ -z "$env_gone" ]
|
||||
project_tag=$(echo "$output" | jq '.Tags[] | select(.Key == "Project") | .Value' -r)
|
||||
[ "$project_tag" = "floci" ]
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# US4: Account Configuration
|
||||
# ============================================
|
||||
|
||||
@test "ACM: put and get account configuration" {
|
||||
run aws_cmd acm put-account-configuration \
|
||||
--expiry-events DaysBeforeExpiry=45 \
|
||||
--idempotency-token "cli-test-$(date +%s)"
|
||||
assert_success
|
||||
|
||||
run aws_cmd acm get-account-configuration
|
||||
assert_success
|
||||
days=$(json_get "$output" '.ExpiryEvents.DaysBeforeExpiry')
|
||||
[ "$days" = "45" ]
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# US5: Error Handling
|
||||
# ============================================
|
||||
|
||||
@test "ACM: describe non-existent certificate" {
|
||||
run aws_cmd acm describe-certificate \
|
||||
--certificate-arn "arn:aws:acm:us-east-1:000000000000:certificate/00000000-0000-0000-0000-000000000000"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "ACM: request certificate with SANs" {
|
||||
run aws_cmd acm request-certificate \
|
||||
--domain-name "cli-san.example.com" \
|
||||
--validation-method DNS \
|
||||
--subject-alternative-names "alt1.example.com" "alt2.example.com"
|
||||
assert_success
|
||||
CERT_ARN=$(json_get "$output" '.CertificateArn')
|
||||
|
||||
run aws_cmd acm describe-certificate --certificate-arn "$CERT_ARN"
|
||||
assert_success
|
||||
sans=$(json_get "$output" '.Certificate.SubjectAlternativeNames')
|
||||
[[ "$sans" =~ "alt1.example.com" ]]
|
||||
[[ "$sans" =~ "alt2.example.com" ]]
|
||||
}
|
||||
|
||||
@test "ACM: import invalid PEM" {
|
||||
run aws_cmd acm import-certificate \
|
||||
--certificate "not-valid-pem-data" \
|
||||
--private-key "also-not-valid-pem"
|
||||
assert_failure
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env bats
|
||||
# CloudFormation tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
STACK_NAME="bats-cfn-stack-$(unique_name)"
|
||||
TEMPLATE_FILE=$(mktemp /tmp/cfn-bats-XXXXXX.yaml)
|
||||
}
|
||||
|
||||
teardown() {
|
||||
aws_cmd cloudformation delete-stack --stack-name "$STACK_NAME" >/dev/null 2>&1 || true
|
||||
[ -n "$TEMPLATE_FILE" ] && rm -f "$TEMPLATE_FILE"
|
||||
}
|
||||
|
||||
# ── CreateStack / DescribeStacks ──────────────────────────────────────────────
|
||||
|
||||
@test "CloudFormation: create stack reaches CREATE_COMPLETE" {
|
||||
cat > "$TEMPLATE_FILE" << 'EOF'
|
||||
AWSTemplateFormatVersion: '2010-09-09'
|
||||
Resources:
|
||||
MyQueue:
|
||||
Type: AWS::SQS::Queue
|
||||
Properties:
|
||||
QueueName: bats-cfn-basic-queue
|
||||
EOF
|
||||
run aws_cmd cloudformation create-stack \
|
||||
--stack-name "$STACK_NAME" \
|
||||
--template-body "file://$TEMPLATE_FILE"
|
||||
assert_success
|
||||
|
||||
run aws_cmd cloudformation describe-stacks --stack-name "$STACK_NAME"
|
||||
assert_success
|
||||
local stack_status
|
||||
stack_status=$(json_get "$output" '.Stacks[0].StackStatus')
|
||||
[ "$stack_status" = "CREATE_COMPLETE" ]
|
||||
}
|
||||
|
||||
@test "CloudFormation: describe-stack-resources lists provisioned resources" {
|
||||
cat > "$TEMPLATE_FILE" << 'EOF'
|
||||
AWSTemplateFormatVersion: '2010-09-09'
|
||||
Resources:
|
||||
MyQueue:
|
||||
Type: AWS::SQS::Queue
|
||||
Properties:
|
||||
QueueName: bats-cfn-resources-queue
|
||||
EOF
|
||||
aws_cmd cloudformation create-stack \
|
||||
--stack-name "$STACK_NAME" \
|
||||
--template-body "file://$TEMPLATE_FILE" >/dev/null
|
||||
|
||||
run aws_cmd cloudformation describe-stack-resources --stack-name "$STACK_NAME"
|
||||
assert_success
|
||||
local count
|
||||
count=$(json_get "$output" '.StackResources | length')
|
||||
[ "$count" -gt 0 ]
|
||||
}
|
||||
|
||||
@test "CloudFormation: delete stack removes resources" {
|
||||
cat > "$TEMPLATE_FILE" << 'EOF'
|
||||
AWSTemplateFormatVersion: '2010-09-09'
|
||||
Resources:
|
||||
MyQueue:
|
||||
Type: AWS::SQS::Queue
|
||||
Properties:
|
||||
QueueName: bats-cfn-delete-queue
|
||||
EOF
|
||||
aws_cmd cloudformation create-stack \
|
||||
--stack-name "$STACK_NAME" \
|
||||
--template-body "file://$TEMPLATE_FILE" >/dev/null
|
||||
|
||||
run aws_cmd cloudformation delete-stack --stack-name "$STACK_NAME"
|
||||
assert_success
|
||||
|
||||
run aws_cmd cloudformation describe-stacks --stack-name "$STACK_NAME"
|
||||
# Stack should no longer exist
|
||||
[[ "$output" == *"does not exist"* ]]
|
||||
STACK_NAME="" # prevent teardown from trying again
|
||||
}
|
||||
|
||||
# ── aws cloudformation deploy (CreateChangeSet + ExecuteChangeSet by ARN) ─────
|
||||
#
|
||||
# Regression: DescribeChangeSet / ExecuteChangeSet failed when called with the
|
||||
# changeset ARN (the AWS CLI always passes the ARN, not the short name).
|
||||
# See: https://github.com/floci-io/floci/issues/606
|
||||
|
||||
@test "CloudFormation: deploy creates stack via changeset" {
|
||||
cat > "$TEMPLATE_FILE" << 'EOF'
|
||||
AWSTemplateFormatVersion: '2010-09-09'
|
||||
Resources:
|
||||
MyQueue:
|
||||
Type: AWS::SQS::Queue
|
||||
Properties:
|
||||
QueueName: bats-cfn-deploy-queue
|
||||
EOF
|
||||
run aws_cmd cloudformation deploy \
|
||||
--stack-name "$STACK_NAME" \
|
||||
--template-file "$TEMPLATE_FILE"
|
||||
assert_success
|
||||
|
||||
run aws_cmd cloudformation describe-stacks --stack-name "$STACK_NAME"
|
||||
assert_success
|
||||
local stack_status
|
||||
stack_status=$(json_get "$output" '.Stacks[0].StackStatus')
|
||||
[ "$stack_status" = "CREATE_COMPLETE" ]
|
||||
}
|
||||
|
||||
@test "CloudFormation: deploy provisions resources correctly" {
|
||||
cat > "$TEMPLATE_FILE" << 'EOF'
|
||||
AWSTemplateFormatVersion: '2010-09-09'
|
||||
Resources:
|
||||
MyQueue:
|
||||
Type: AWS::SQS::Queue
|
||||
Properties:
|
||||
QueueName: bats-cfn-deploy-res-queue
|
||||
EOF
|
||||
aws_cmd cloudformation deploy \
|
||||
--stack-name "$STACK_NAME" \
|
||||
--template-file "$TEMPLATE_FILE" >/dev/null
|
||||
|
||||
run aws_cmd cloudformation describe-stack-resources --stack-name "$STACK_NAME"
|
||||
assert_success
|
||||
local resource_status
|
||||
resource_status=$(json_get "$output" '.StackResources[0].ResourceStatus')
|
||||
[ "$resource_status" = "CREATE_COMPLETE" ]
|
||||
}
|
||||
|
||||
# ── EC2 VPC/Subnet provisioning (regression: issue #1297) ─────────────────────
|
||||
#
|
||||
# CloudFormation must create AWS::EC2::VPC / AWS::EC2::Subnet resources for real
|
||||
# (in EC2), not stub them. Before the fix, describe-stacks reported CREATE_COMPLETE
|
||||
# and exported the subnet ids, but ec2 describe-subnets did not return them and
|
||||
# elbv2 create-load-balancer failed with "The subnet ID '...' does not exist".
|
||||
# https://github.com/floci-io/floci/issues/1297
|
||||
|
||||
@test "CloudFormation: VPC/subnet stack exports subnet ids that EC2 and ELBv2 can use" {
|
||||
cat > "$TEMPLATE_FILE" << 'EOF'
|
||||
{
|
||||
"Resources": {
|
||||
"Vpc": {
|
||||
"Type": "AWS::EC2::VPC",
|
||||
"Properties": {"CidrBlock": "10.20.0.0/16"}
|
||||
},
|
||||
"Subnet1": {
|
||||
"Type": "AWS::EC2::Subnet",
|
||||
"Properties": {
|
||||
"VpcId": {"Ref": "Vpc"},
|
||||
"CidrBlock": "10.20.1.0/24",
|
||||
"AvailabilityZone": "us-east-1a"
|
||||
}
|
||||
},
|
||||
"Subnet2": {
|
||||
"Type": "AWS::EC2::Subnet",
|
||||
"Properties": {
|
||||
"VpcId": {"Ref": "Vpc"},
|
||||
"CidrBlock": "10.20.2.0/24",
|
||||
"AvailabilityZone": "us-east-1b"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Outputs": {
|
||||
"Subnet1Id": {"Value": {"Ref": "Subnet1"}},
|
||||
"Subnet2Id": {"Value": {"Ref": "Subnet2"}}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
run aws_cmd cloudformation create-stack \
|
||||
--stack-name "$STACK_NAME" \
|
||||
--template-body "file://$TEMPLATE_FILE"
|
||||
assert_success
|
||||
|
||||
run aws_cmd cloudformation describe-stacks --stack-name "$STACK_NAME"
|
||||
assert_success
|
||||
local stack_status
|
||||
stack_status=$(json_get "$output" '.Stacks[0].StackStatus')
|
||||
[ "$stack_status" = "CREATE_COMPLETE" ]
|
||||
|
||||
# The exported subnet ids must be real subnet-xxxx ids (not the old stub shape).
|
||||
local subnet1 subnet2
|
||||
subnet1=$(json_get "$output" '.Stacks[0].Outputs[] | select(.OutputKey=="Subnet1Id") | .OutputValue')
|
||||
subnet2=$(json_get "$output" '.Stacks[0].Outputs[] | select(.OutputKey=="Subnet2Id") | .OutputValue')
|
||||
[[ "$subnet1" == subnet-* ]]
|
||||
[[ "$subnet2" == subnet-* ]]
|
||||
|
||||
# describe-subnets must return the exact subnet ids CloudFormation exported.
|
||||
run aws_cmd ec2 describe-subnets --subnet-ids "$subnet1" "$subnet2"
|
||||
assert_success
|
||||
local count
|
||||
count=$(json_get "$output" '.Subnets | length')
|
||||
[ "$count" -eq 2 ]
|
||||
|
||||
# ELBv2 create-load-balancer must accept the exported subnets (no SubnetNotFound).
|
||||
run aws_cmd elbv2 create-load-balancer \
|
||||
--name "bats-vpc-alb-${RANDOM}" \
|
||||
--type application \
|
||||
--scheme internal \
|
||||
--subnets "$subnet1" "$subnet2"
|
||||
assert_success
|
||||
local lb_arn
|
||||
lb_arn=$(json_get "$output" '.LoadBalancers[0].LoadBalancerArn')
|
||||
[ -n "$lb_arn" ]
|
||||
[[ "$lb_arn" == *":loadbalancer/"* ]]
|
||||
}
|
||||
|
||||
# ── DeleteStack with managed S3 buckets (regression: issue #1539) ──────────────
|
||||
#
|
||||
# Real AWS CloudFormation does not report a stack as DELETE_COMPLETE while a
|
||||
# managed resource still exists. Deleting a stack that owns a NON-EMPTY
|
||||
# AWS::S3::Bucket must put the stack into DELETE_FAILED (S3 DeleteBucket refuses
|
||||
# a non-empty bucket) and leave the bucket in place — it must NOT silently report
|
||||
# success while the bucket and its objects remain.
|
||||
# An EMPTY managed bucket must be deleted as part of the stack delete.
|
||||
# https://github.com/floci-io/floci/issues/1539
|
||||
|
||||
# Poll describe-stacks until the stack reaches the expected status, or report the
|
||||
# special value DOES_NOT_EXIST once the stack is gone.
|
||||
cfn_wait_status() {
|
||||
local stack="$1" expected="$2" tries="${3:-40}"
|
||||
local i out status
|
||||
for ((i = 0; i < tries; i++)); do
|
||||
# `|| true`: describe-stacks exits non-zero once the stack is gone, which
|
||||
# would otherwise abort this command substitution under bats' errexit.
|
||||
out=$(aws_cmd cloudformation describe-stacks --stack-name "$stack" 2>&1 || true)
|
||||
if [[ "$out" == *"does not exist"* ]]; then
|
||||
status="DOES_NOT_EXIST"
|
||||
else
|
||||
status=$(echo "$out" | jq -r '.Stacks[0].StackStatus' 2>/dev/null)
|
||||
fi
|
||||
if [ "$status" = "$expected" ]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
echo "timeout waiting for stack '$stack' to reach '$expected' (last status: '$status')" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
@test "CloudFormation: delete-stack with non-empty S3 bucket fails and keeps the bucket (issue #1539)" {
|
||||
local bucket="bats-cfn-nonempty-$(unique_name bkt)"
|
||||
cat > "$TEMPLATE_FILE" << EOF
|
||||
{
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Resources": {
|
||||
"Bucket": {
|
||||
"Type": "AWS::S3::Bucket",
|
||||
"Properties": { "BucketName": "$bucket" }
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
run aws_cmd cloudformation create-stack \
|
||||
--stack-name "$STACK_NAME" \
|
||||
--template-body "file://$TEMPLATE_FILE"
|
||||
assert_success
|
||||
cfn_wait_status "$STACK_NAME" "CREATE_COMPLETE"
|
||||
|
||||
# Put an object so the managed bucket is non-empty.
|
||||
local body_file
|
||||
body_file=$(mktemp)
|
||||
echo -n "hello floci 1539" > "$body_file"
|
||||
run aws_cmd s3api put-object --bucket "$bucket" --key "object.txt" --body "$body_file"
|
||||
assert_success
|
||||
rm -f "$body_file"
|
||||
|
||||
run aws_cmd cloudformation delete-stack --stack-name "$STACK_NAME"
|
||||
assert_success
|
||||
|
||||
# AWS leaves the stack in DELETE_FAILED — it must NOT report DELETE_COMPLETE
|
||||
# (i.e. the stack must NOT silently disappear).
|
||||
cfn_wait_status "$STACK_NAME" "DELETE_FAILED"
|
||||
|
||||
# The managed bucket and its object must still exist after the failed delete.
|
||||
run aws_cmd s3api head-bucket --bucket "$bucket"
|
||||
assert_success
|
||||
|
||||
# Cleanup: empty + remove the leftover bucket, then drop the stack.
|
||||
aws_cmd s3 rm "s3://$bucket" --recursive >/dev/null 2>&1 || true
|
||||
aws_cmd s3api delete-bucket --bucket "$bucket" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "CloudFormation: delete-stack with empty S3 bucket removes the bucket" {
|
||||
local bucket="bats-cfn-empty-$(unique_name bkt)"
|
||||
cat > "$TEMPLATE_FILE" << EOF
|
||||
{
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Resources": {
|
||||
"Bucket": {
|
||||
"Type": "AWS::S3::Bucket",
|
||||
"Properties": { "BucketName": "$bucket" }
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
run aws_cmd cloudformation create-stack \
|
||||
--stack-name "$STACK_NAME" \
|
||||
--template-body "file://$TEMPLATE_FILE"
|
||||
assert_success
|
||||
cfn_wait_status "$STACK_NAME" "CREATE_COMPLETE"
|
||||
|
||||
run aws_cmd s3api head-bucket --bucket "$bucket"
|
||||
assert_success
|
||||
|
||||
run aws_cmd cloudformation delete-stack --stack-name "$STACK_NAME"
|
||||
assert_success
|
||||
cfn_wait_status "$STACK_NAME" "DOES_NOT_EXIST"
|
||||
STACK_NAME="" # prevent teardown from trying again
|
||||
|
||||
# The empty managed bucket must be gone.
|
||||
run aws_cmd s3api head-bucket --bucket "$bucket"
|
||||
assert_failure
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bats
|
||||
# CloudFront AWS CLI compatibility tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
}
|
||||
|
||||
@test "CloudFront: list response headers policies decodes the modeled payload" {
|
||||
run aws_cmd cloudfront list-response-headers-policies \
|
||||
--type managed \
|
||||
--max-items 1 \
|
||||
--no-paginate
|
||||
assert_success
|
||||
|
||||
[ "$(json_get "$output" '.ResponseHeadersPolicyList.Quantity')" = "1" ]
|
||||
[ "$(json_get "$output" '.ResponseHeadersPolicyList.MaxItems')" = "1" ]
|
||||
[ "$(json_get "$output" '.ResponseHeadersPolicyList.Items | length')" = "1" ]
|
||||
[ "$(json_get "$output" '.ResponseHeadersPolicyList.Items[0].Type')" = "managed" ]
|
||||
[ -n "$(json_get "$output" \
|
||||
'.ResponseHeadersPolicyList.Items[0].ResponseHeadersPolicy.Id // empty')" ]
|
||||
[ -n "$(json_get "$output" \
|
||||
'.ResponseHeadersPolicyList.NextMarker // empty')" ]
|
||||
|
||||
first_id="$(json_get "$output" \
|
||||
'.ResponseHeadersPolicyList.Items[0].ResponseHeadersPolicy.Id')"
|
||||
next_marker="$(json_get "$output" \
|
||||
'.ResponseHeadersPolicyList.NextMarker')"
|
||||
|
||||
run aws_cmd cloudfront list-response-headers-policies \
|
||||
--type managed \
|
||||
--marker "$next_marker" \
|
||||
--max-items 1 \
|
||||
--no-paginate
|
||||
assert_success
|
||||
|
||||
[ "$(json_get "$output" '.ResponseHeadersPolicyList.Quantity')" = "1" ]
|
||||
[ "$(json_get "$output" \
|
||||
'.ResponseHeadersPolicyList.Items[0].ResponseHeadersPolicy.Id')" != "$first_id" ]
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env bats
|
||||
# Cognito tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
POOL_ID=""
|
||||
CLIENT_ID=""
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ -n "$POOL_ID" ]; then
|
||||
aws_cmd cognito-idp delete-user-pool --user-pool-id "$POOL_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
@test "Cognito: create user pool" {
|
||||
run aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)"
|
||||
assert_success
|
||||
POOL_ID=$(json_get "$output" '.UserPool.Id')
|
||||
[ -n "$POOL_ID" ]
|
||||
}
|
||||
|
||||
@test "Cognito: create user pool client" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
run aws_cmd cognito-idp create-user-pool-client \
|
||||
--user-pool-id "$POOL_ID" \
|
||||
--client-name "bats-test-client"
|
||||
assert_success
|
||||
CLIENT_ID=$(json_get "$output" '.UserPoolClient.ClientId')
|
||||
[ -n "$CLIENT_ID" ]
|
||||
}
|
||||
|
||||
@test "Cognito: list user pool clients returns only description fields" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
aws_cmd cognito-idp create-user-pool-client \
|
||||
--user-pool-id "$POOL_ID" \
|
||||
--client-name "bats-list-client" \
|
||||
--generate-secret >/dev/null
|
||||
|
||||
run aws_cmd cognito-idp list-user-pool-clients --user-pool-id "$POOL_ID"
|
||||
assert_success
|
||||
|
||||
# Should have the required fields
|
||||
client_id=$(echo "$output" | jq -r '.UserPoolClients[0].ClientId')
|
||||
[ -n "$client_id" ]
|
||||
client_name=$(echo "$output" | jq -r '.UserPoolClients[0].ClientName')
|
||||
[ "$client_name" = "bats-list-client" ]
|
||||
|
||||
# Must NOT have fields that belong to the full UserPoolClient type
|
||||
has_secret=$(echo "$output" | jq 'any(.UserPoolClients[]; has("ClientSecret"))')
|
||||
[ "$has_secret" = "false" ]
|
||||
has_generate=$(echo "$output" | jq 'any(.UserPoolClients[]; has("GenerateSecret"))')
|
||||
[ "$has_generate" = "false" ]
|
||||
has_flows=$(echo "$output" | jq 'any(.UserPoolClients[]; has("AllowedOAuthFlows"))')
|
||||
[ "$has_flows" = "false" ]
|
||||
}
|
||||
|
||||
@test "Cognito: admin create user" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
run aws_cmd cognito-idp admin-create-user \
|
||||
--user-pool-id "$POOL_ID" \
|
||||
--username "testuser" \
|
||||
--temporary-password "Temp123!" \
|
||||
--user-attributes Name=email,Value=test@example.com
|
||||
assert_success
|
||||
username=$(json_get "$output" '.User.Username')
|
||||
[ "$username" = "testuser" ]
|
||||
}
|
||||
|
||||
@test "Cognito: admin set user password" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
aws_cmd cognito-idp admin-create-user \
|
||||
--user-pool-id "$POOL_ID" \
|
||||
--username "testuser" \
|
||||
--temporary-password "Temp123!" \
|
||||
--user-attributes Name=email,Value=test@example.com >/dev/null
|
||||
|
||||
run aws_cmd cognito-idp admin-set-user-password \
|
||||
--user-pool-id "$POOL_ID" \
|
||||
--username "testuser" \
|
||||
--password "Perm456!" \
|
||||
--permanent
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "Cognito: admin get user" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
aws_cmd cognito-idp admin-create-user \
|
||||
--user-pool-id "$POOL_ID" \
|
||||
--username "testuser" \
|
||||
--temporary-password "Temp123!" \
|
||||
--user-attributes Name=email,Value=test@example.com >/dev/null
|
||||
|
||||
aws_cmd cognito-idp admin-set-user-password \
|
||||
--user-pool-id "$POOL_ID" \
|
||||
--username "testuser" \
|
||||
--password "Perm456!" \
|
||||
--permanent >/dev/null
|
||||
|
||||
run aws_cmd cognito-idp admin-get-user \
|
||||
--user-pool-id "$POOL_ID" \
|
||||
--username "testuser"
|
||||
assert_success
|
||||
status=$(json_get "$output" '.UserStatus')
|
||||
[ "$status" = "CONFIRMED" ]
|
||||
}
|
||||
|
||||
@test "Cognito: list users" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
aws_cmd cognito-idp admin-create-user \
|
||||
--user-pool-id "$POOL_ID" \
|
||||
--username "testuser" \
|
||||
--temporary-password "Temp123!" >/dev/null
|
||||
|
||||
run aws_cmd cognito-idp list-users --user-pool-id "$POOL_ID"
|
||||
assert_success
|
||||
found=$(echo "$output" | jq '.Users | any(.Username == "testuser")')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "Cognito: JWKS endpoint" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
run curl -sS "$FLOCI_ENDPOINT/$POOL_ID/.well-known/jwks.json"
|
||||
assert_success
|
||||
|
||||
keys_count=$(echo "$output" | jq '.keys | length')
|
||||
[ "$keys_count" -gt 0 ]
|
||||
|
||||
key_type=$(echo "$output" | jq -r '.keys[0].kty')
|
||||
[ "$key_type" = "RSA" ]
|
||||
|
||||
alg=$(echo "$output" | jq -r '.keys[0].alg')
|
||||
[ "$alg" = "RS256" ]
|
||||
}
|
||||
|
||||
@test "Cognito: delete user pool" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
run aws_cmd cognito-idp delete-user-pool --user-pool-id "$POOL_ID"
|
||||
assert_success
|
||||
POOL_ID=""
|
||||
}
|
||||
|
||||
@test "Cognito: create user pool with reserved override tag uses pinned ID and strips reserved tag" {
|
||||
run aws_cmd cognito-idp create-user-pool \
|
||||
--pool-name "bats-test-pool-$(unique_name)" \
|
||||
--user-pool-tags floci:override-id=us-east-1_batspool1,env=test
|
||||
assert_success
|
||||
POOL_ID=$(json_get "$output" '.UserPool.Id')
|
||||
[ "$POOL_ID" = "us-east-1_batspool1" ]
|
||||
|
||||
has_reserved=$(echo "$output" | jq '.UserPool.UserPoolTags | has("floci:override-id")')
|
||||
[ "$has_reserved" = "false" ]
|
||||
|
||||
run aws_cmd cognito-idp describe-user-pool --user-pool-id "$POOL_ID"
|
||||
assert_success
|
||||
has_reserved=$(echo "$output" | jq '.UserPool.UserPoolTags | has("floci:override-id")')
|
||||
[ "$has_reserved" = "false" ]
|
||||
env_value=$(json_get "$output" '.UserPool.UserPoolTags.env')
|
||||
[ "$env_value" = "test" ]
|
||||
}
|
||||
|
||||
@test "Cognito: duplicate reserved override tag fails with ResourceConflictException" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool \
|
||||
--pool-name "bats-test-pool-$(unique_name)" \
|
||||
--user-pool-tags floci:override-id=us-east-1_batsdup01)
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
run aws_cmd cognito-idp create-user-pool \
|
||||
--pool-name "bats-test-pool-$(unique_name)" \
|
||||
--user-pool-tags floci:override-id=us-east-1_batsdup01
|
||||
assert_failure
|
||||
[[ "$output" == *"ResourceConflictException"* ]]
|
||||
}
|
||||
|
||||
@test "Cognito: tag-resource list-tags-for-resource and untag-resource manage user pool tags" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
RESOURCE_ARN=$(json_get "$out" '.UserPool.Arn')
|
||||
|
||||
run aws_cmd cognito-idp tag-resource \
|
||||
--resource-arn "$RESOURCE_ARN" \
|
||||
--tags env=test,team=platform
|
||||
assert_success
|
||||
|
||||
run aws_cmd cognito-idp list-tags-for-resource --resource-arn "$RESOURCE_ARN"
|
||||
assert_success
|
||||
env_value=$(json_get "$output" '.Tags.env')
|
||||
[ "$env_value" = "test" ]
|
||||
team_value=$(json_get "$output" '.Tags.team')
|
||||
[ "$team_value" = "platform" ]
|
||||
|
||||
run aws_cmd cognito-idp untag-resource \
|
||||
--resource-arn "$RESOURCE_ARN" \
|
||||
--tag-keys team
|
||||
assert_success
|
||||
|
||||
run aws_cmd cognito-idp list-tags-for-resource --resource-arn "$RESOURCE_ARN"
|
||||
assert_success
|
||||
env_value=$(json_get "$output" '.Tags.env')
|
||||
[ "$env_value" = "test" ]
|
||||
has_team=$(echo "$output" | jq '.Tags | has("team")')
|
||||
[ "$has_team" = "false" ]
|
||||
}
|
||||
|
||||
@test "Cognito: standalone tag-resource rejects reserved floci tags" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
RESOURCE_ARN=$(json_get "$out" '.UserPool.Arn')
|
||||
|
||||
run aws_cmd cognito-idp tag-resource \
|
||||
--resource-arn "$RESOURCE_ARN" \
|
||||
--tags floci:override-id=late-id
|
||||
assert_failure
|
||||
[[ "$output" == *"ValidationException"* ]]
|
||||
}
|
||||
|
||||
@test "Cognito: describe-user-pool returns all 20 standard SchemaAttributes" {
|
||||
out=$(aws_cmd cognito-idp create-user-pool --pool-name "bats-test-pool-$(unique_name)")
|
||||
POOL_ID=$(json_get "$out" '.UserPool.Id')
|
||||
|
||||
run aws_cmd cognito-idp describe-user-pool --user-pool-id "$POOL_ID"
|
||||
assert_success
|
||||
|
||||
count=$(echo "$output" | jq '.UserPool.SchemaAttributes | length')
|
||||
[ "$count" -eq 20 ]
|
||||
|
||||
for attr in sub name given_name family_name middle_name nickname \
|
||||
preferred_username profile picture website email email_verified \
|
||||
gender birthdate zoneinfo locale phone_number phone_number_verified \
|
||||
address updated_at; do
|
||||
found=$(echo "$output" | jq --arg n "$attr" '[.UserPool.SchemaAttributes[] | select(.Name == $n)] | length')
|
||||
[ "$found" -eq 1 ] || { echo "missing standard attribute: $attr"; return 1; }
|
||||
done
|
||||
|
||||
sub_required=$(echo "$output" | jq '[.UserPool.SchemaAttributes[] | select(.Name == "sub")] | .[0].Required')
|
||||
[ "$sub_required" = "true" ]
|
||||
|
||||
sub_mutable=$(echo "$output" | jq '[.UserPool.SchemaAttributes[] | select(.Name == "sub")] | .[0].Mutable')
|
||||
[ "$sub_mutable" = "false" ]
|
||||
}
|
||||
@@ -0,0 +1,854 @@
|
||||
#!/usr/bin/env bats
|
||||
# DynamoDB tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
TABLE_NAME="bats-test-table-$(unique_name)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
aws_cmd dynamodb delete-table --table-name "$TABLE_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "DynamoDB: create table" {
|
||||
run aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
|
||||
--billing-mode PAY_PER_REQUEST
|
||||
assert_success
|
||||
status=$(json_get "$output" '.TableDescription.TableStatus')
|
||||
[ "$status" = "ACTIVE" ] || [ "$status" = "CREATING" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: describe table" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.Table.TableName')
|
||||
[ "$name" = "$TABLE_NAME" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: describe table by ARN" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
table_arn="arn:aws:dynamodb:${AWS_REGION:-us-east-1}:000000000000:table/$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb describe-table --table-name "$table_arn"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.Table.TableName')
|
||||
[ "$name" = "$TABLE_NAME" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: list tables" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb list-tables
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$TABLE_NAME" '.TableNames | any(. == $name)')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: put and get item" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb put-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"user#1"},"name":{"S":"Alice"}}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd dynamodb get-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"user#1"}}'
|
||||
assert_success
|
||||
name=$(json_get "$output" '.Item.name.S')
|
||||
[ "$name" = "Alice" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: update item" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"user#1"},"name":{"S":"Alice"}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb update-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"user#1"}}' \
|
||||
--update-expression 'SET #n = :v' \
|
||||
--expression-attribute-names '{"#n":"name"}' \
|
||||
--expression-attribute-values '{":v":{"S":"Bob"}}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd dynamodb get-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"user#1"}}'
|
||||
assert_success
|
||||
name=$(json_get "$output" '.Item.name.S')
|
||||
[ "$name" = "Bob" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: ADD resolves nested placeholder paths" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-1"},"functionInvocationSummaries":{"M":{"ISA_10136":{"M":{"eventsAvailable":{"N":"0"}}}}}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb update-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"item-1"}}' \
|
||||
--update-expression 'ADD #fis.#isa.#ea :one' \
|
||||
--expression-attribute-names '{"#fis":"functionInvocationSummaries","#isa":"ISA_10136","#ea":"eventsAvailable"}' \
|
||||
--expression-attribute-values '{":one":{"N":"1"}}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd dynamodb get-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"item-1"}}'
|
||||
assert_success
|
||||
nested_counter=$(json_get "$output" '.Item.functionInvocationSummaries.M.ISA_10136.M.eventsAvailable.N')
|
||||
[ "$nested_counter" = "1" ]
|
||||
phantom_attribute=$(echo "$output" | jq '.Item | has("#fis.#isa.#ea")')
|
||||
[ "$phantom_attribute" = "false" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: DELETE resolves nested placeholder paths" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-1"},"settings":{"M":{"labels":{"SS":["keep","drop"]}}}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb update-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"item-1"}}' \
|
||||
--update-expression 'DELETE #settings.#labels :labels' \
|
||||
--expression-attribute-names '{"#settings":"settings","#labels":"labels"}' \
|
||||
--expression-attribute-values '{":labels":{"SS":["drop"]}}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd dynamodb get-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"item-1"}}'
|
||||
assert_success
|
||||
labels=$(echo "$output" | jq -c '.Item.settings.M.labels.SS')
|
||||
[ "$labels" = '["keep"]' ]
|
||||
phantom_attribute=$(echo "$output" | jq '.Item | has("#settings.#labels")')
|
||||
[ "$phantom_attribute" = "false" ]
|
||||
|
||||
run aws_cmd dynamodb update-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"item-1"}}' \
|
||||
--update-expression 'DELETE #settings.#labels :labels' \
|
||||
--expression-attribute-names '{"#settings":"settings","#labels":"labels"}' \
|
||||
--expression-attribute-values '{":labels":{"SS":["keep"]}}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd dynamodb get-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"item-1"}}'
|
||||
assert_success
|
||||
nested_attribute=$(echo "$output" | jq '.Item.settings.M | has("labels")')
|
||||
[ "$nested_attribute" = "false" ]
|
||||
|
||||
run aws_cmd dynamodb update-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"item-1"}}' \
|
||||
--update-expression 'DELETE #settings.#labels :labels' \
|
||||
--expression-attribute-names '{"#settings":"settings","#labels":"labels"}' \
|
||||
--expression-attribute-values '{":labels":{"SS":["missing"]}}'
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "DynamoDB: scan table" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" --item '{"pk":{"S":"user#1"},"name":{"S":"Alice"}}' >/dev/null
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" --item '{"pk":{"S":"user#2"},"name":{"S":"Bob"}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb scan --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" -ge 2 ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: query table" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" --item '{"pk":{"S":"user#1"},"sk":{"S":"profile"},"name":{"S":"Alice"}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb query \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key-condition-expression 'pk = :pk' \
|
||||
--expression-attribute-values '{":pk":{"S":"user#1"}}'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" -ge 1 ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: delete item" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" --item '{"pk":{"S":"user#1"}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb delete-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"user#1"}}'
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "DynamoDB: delete table" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb delete-table --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "DynamoDB: update and describe continuous backups" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb describe-continuous-backups --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
status=$(json_get "$output" '.ContinuousBackupsDescription.ContinuousBackupsStatus')
|
||||
[ "$status" = "ENABLED" ]
|
||||
pitr_status=$(json_get "$output" '.ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus')
|
||||
[ "$pitr_status" = "DISABLED" ]
|
||||
missing_period=$(echo "$output" | jq '.ContinuousBackupsDescription.PointInTimeRecoveryDescription | has("RecoveryPeriodInDays")')
|
||||
[ "$missing_period" = "false" ]
|
||||
|
||||
run aws_cmd dynamodb update-continuous-backups \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
|
||||
assert_success
|
||||
updated_status=$(json_get "$output" '.ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus')
|
||||
[ "$updated_status" = "ENABLED" ]
|
||||
recovery_period=$(json_get "$output" '.ContinuousBackupsDescription.PointInTimeRecoveryDescription.RecoveryPeriodInDays')
|
||||
[ "$recovery_period" = "35" ]
|
||||
}
|
||||
|
||||
# --- DynamoDB GSI/LSI Tests ---
|
||||
|
||||
@test "DynamoDB: create table with GSI and LSI" {
|
||||
run aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions \
|
||||
AttributeName=pk,AttributeType=S \
|
||||
AttributeName=sk,AttributeType=S \
|
||||
AttributeName=gsiPk,AttributeType=S \
|
||||
AttributeName=lsiSk,AttributeType=S \
|
||||
--key-schema \
|
||||
AttributeName=pk,KeyType=HASH \
|
||||
AttributeName=sk,KeyType=RANGE \
|
||||
--global-secondary-indexes \
|
||||
'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
|
||||
--local-secondary-indexes \
|
||||
'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \
|
||||
--billing-mode PAY_PER_REQUEST
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "DynamoDB: GSI count is 1" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions \
|
||||
AttributeName=pk,AttributeType=S \
|
||||
AttributeName=sk,AttributeType=S \
|
||||
AttributeName=gsiPk,AttributeType=S \
|
||||
AttributeName=lsiSk,AttributeType=S \
|
||||
--key-schema \
|
||||
AttributeName=pk,KeyType=HASH \
|
||||
AttributeName=sk,KeyType=RANGE \
|
||||
--global-secondary-indexes \
|
||||
'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
|
||||
--local-secondary-indexes \
|
||||
'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
count=$(echo "$output" | jq '.Table.GlobalSecondaryIndexes | length')
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: GSI name is gsi-1" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions \
|
||||
AttributeName=pk,AttributeType=S \
|
||||
AttributeName=sk,AttributeType=S \
|
||||
AttributeName=gsiPk,AttributeType=S \
|
||||
AttributeName=lsiSk,AttributeType=S \
|
||||
--key-schema \
|
||||
AttributeName=pk,KeyType=HASH \
|
||||
AttributeName=sk,KeyType=RANGE \
|
||||
--global-secondary-indexes \
|
||||
'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
|
||||
--local-secondary-indexes \
|
||||
'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
name=$(echo "$output" | jq -r '.Table.GlobalSecondaryIndexes[0].IndexName')
|
||||
[ "$name" = "gsi-1" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: GSI projection is ALL" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions \
|
||||
AttributeName=pk,AttributeType=S \
|
||||
AttributeName=sk,AttributeType=S \
|
||||
AttributeName=gsiPk,AttributeType=S \
|
||||
AttributeName=lsiSk,AttributeType=S \
|
||||
--key-schema \
|
||||
AttributeName=pk,KeyType=HASH \
|
||||
AttributeName=sk,KeyType=RANGE \
|
||||
--global-secondary-indexes \
|
||||
'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
|
||||
--local-secondary-indexes \
|
||||
'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
proj=$(echo "$output" | jq -r '.Table.GlobalSecondaryIndexes[0].Projection.ProjectionType')
|
||||
[ "$proj" = "ALL" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: LSI count is 1" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions \
|
||||
AttributeName=pk,AttributeType=S \
|
||||
AttributeName=sk,AttributeType=S \
|
||||
AttributeName=gsiPk,AttributeType=S \
|
||||
AttributeName=lsiSk,AttributeType=S \
|
||||
--key-schema \
|
||||
AttributeName=pk,KeyType=HASH \
|
||||
AttributeName=sk,KeyType=RANGE \
|
||||
--global-secondary-indexes \
|
||||
'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
|
||||
--local-secondary-indexes \
|
||||
'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
count=$(echo "$output" | jq '.Table.LocalSecondaryIndexes | length')
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: LSI name is lsi-1" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions \
|
||||
AttributeName=pk,AttributeType=S \
|
||||
AttributeName=sk,AttributeType=S \
|
||||
AttributeName=gsiPk,AttributeType=S \
|
||||
AttributeName=lsiSk,AttributeType=S \
|
||||
--key-schema \
|
||||
AttributeName=pk,KeyType=HASH \
|
||||
AttributeName=sk,KeyType=RANGE \
|
||||
--global-secondary-indexes \
|
||||
'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
|
||||
--local-secondary-indexes \
|
||||
'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
name=$(echo "$output" | jq -r '.Table.LocalSecondaryIndexes[0].IndexName')
|
||||
[ "$name" = "lsi-1" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: LSI projection is KEYS_ONLY" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions \
|
||||
AttributeName=pk,AttributeType=S \
|
||||
AttributeName=sk,AttributeType=S \
|
||||
AttributeName=gsiPk,AttributeType=S \
|
||||
AttributeName=lsiSk,AttributeType=S \
|
||||
--key-schema \
|
||||
AttributeName=pk,KeyType=HASH \
|
||||
AttributeName=sk,KeyType=RANGE \
|
||||
--global-secondary-indexes \
|
||||
'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
|
||||
--local-secondary-indexes \
|
||||
'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
proj=$(echo "$output" | jq -r '.Table.LocalSecondaryIndexes[0].Projection.ProjectionType')
|
||||
[ "$proj" = "KEYS_ONLY" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: query GSI sparse index" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions \
|
||||
AttributeName=pk,AttributeType=S \
|
||||
AttributeName=sk,AttributeType=S \
|
||||
AttributeName=gsiPk,AttributeType=S \
|
||||
--key-schema \
|
||||
AttributeName=pk,KeyType=HASH \
|
||||
AttributeName=sk,KeyType=RANGE \
|
||||
--global-secondary-indexes \
|
||||
'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
# Put 2 items with gsiPk
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-1"},"sk":{"S":"rev-1"},"gsiPk":{"S":"group-A"}}' >/dev/null
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-2"},"sk":{"S":"rev-1"},"gsiPk":{"S":"group-A"}}' >/dev/null
|
||||
# Put 1 item without gsiPk (sparse)
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-3"},"sk":{"S":"rev-1"},"data":{"S":"no-gsi"}}' >/dev/null
|
||||
|
||||
# Query GSI - should return only the 2 items with gsiPk
|
||||
run aws_cmd dynamodb query \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--index-name gsi-1 \
|
||||
--key-condition-expression 'gsiPk = :gpk' \
|
||||
--expression-attribute-values '{":gpk":{"S":"group-A"}}'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" = "2" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: query LSI" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions \
|
||||
AttributeName=pk,AttributeType=S \
|
||||
AttributeName=sk,AttributeType=S \
|
||||
AttributeName=lsiSk,AttributeType=S \
|
||||
--key-schema \
|
||||
AttributeName=pk,KeyType=HASH \
|
||||
AttributeName=sk,KeyType=RANGE \
|
||||
--local-secondary-indexes \
|
||||
'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-1"},"sk":{"S":"rev-1"},"lsiSk":{"S":"2024-01-01"}}' >/dev/null
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-1"},"sk":{"S":"rev-2"},"lsiSk":{"S":"2024-01-02"}}' >/dev/null
|
||||
|
||||
# Query LSI with range condition
|
||||
run aws_cmd dynamodb query \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--index-name lsi-1 \
|
||||
--key-condition-expression 'pk = :pk AND lsiSk > :d' \
|
||||
--expression-attribute-values '{":pk":{"S":"item-1"},":d":{"S":"2024-01-00"}}'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" = "2" ]
|
||||
}
|
||||
|
||||
# --- DynamoDB FilterExpression Tests ---
|
||||
|
||||
@test "DynamoDB: query with FilterExpression" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
# Put items with different status values
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"user#1"},"sk":{"S":"order#1"},"status":{"S":"pending"}}' >/dev/null
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"user#1"},"sk":{"S":"order#2"},"status":{"S":"complete"}}' >/dev/null
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"user#1"},"sk":{"S":"order#3"},"status":{"S":"pending"}}' >/dev/null
|
||||
|
||||
# Query with FilterExpression to get only pending
|
||||
run aws_cmd dynamodb query \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key-condition-expression 'pk = :pk' \
|
||||
--filter-expression '#s = :status' \
|
||||
--expression-attribute-names '{"#s":"status"}' \
|
||||
--expression-attribute-values '{":pk":{"S":"user#1"},":status":{"S":"pending"}}'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" = "2" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: query FilterExpression with limit" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
# Put multiple items
|
||||
for i in 1 2 3 4 5; do
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item "{\"pk\":{\"S\":\"user#1\"},\"sk\":{\"S\":\"order#$i\"},\"status\":{\"S\":\"pending\"}}" >/dev/null
|
||||
done
|
||||
|
||||
# Query with limit
|
||||
run aws_cmd dynamodb query \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key-condition-expression 'pk = :pk' \
|
||||
--expression-attribute-values '{":pk":{"S":"user#1"}}' \
|
||||
--limit 2
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" = "2" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: query FilterExpression pagination" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
# Put multiple items
|
||||
for i in 1 2 3 4 5; do
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item "{\"pk\":{\"S\":\"user#1\"},\"sk\":{\"S\":\"order#$i\"},\"status\":{\"S\":\"pending\"}}" >/dev/null
|
||||
done
|
||||
|
||||
# First page
|
||||
run aws_cmd dynamodb query \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key-condition-expression 'pk = :pk' \
|
||||
--expression-attribute-values '{":pk":{"S":"user#1"}}' \
|
||||
--limit 2
|
||||
assert_success
|
||||
|
||||
# Check that LastEvaluatedKey exists for pagination
|
||||
lek=$(echo "$output" | jq '.LastEvaluatedKey')
|
||||
[ "$lek" != "null" ]
|
||||
|
||||
# Get next page
|
||||
run aws_cmd dynamodb query \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key-condition-expression 'pk = :pk' \
|
||||
--expression-attribute-values '{":pk":{"S":"user#1"}}' \
|
||||
--limit 2 \
|
||||
--exclusive-start-key "$lek"
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" = "2" ]
|
||||
}
|
||||
|
||||
# --- DynamoDB Advanced Filter Tests ---
|
||||
|
||||
@test "DynamoDB: scan with contains on List" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-1"},"tags":{"L":[{"S":"foo"},{"S":"bar"}]}}' >/dev/null
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-2"},"tags":{"L":[{"S":"baz"},{"S":"qux"}]}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb scan \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--filter-expression 'contains(tags, :tag)' \
|
||||
--expression-attribute-values '{":tag":{"S":"foo"}}'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: scan with BOOL filter" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-1"},"active":{"BOOL":true}}' >/dev/null
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-2"},"active":{"BOOL":false}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb scan \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--filter-expression 'active = :val' \
|
||||
--expression-attribute-values '{":val":{"BOOL":true}}'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
@test "DynamoDB: scan with attribute_exists on nested Map" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-1"},"meta":{"M":{"created":{"S":"2024-01-01"}}}}' >/dev/null
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-2"},"other":{"S":"data"}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb scan \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--filter-expression 'attribute_exists(meta.created)'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
# --- DynamoDB Kinesis Streaming Destination Tests ---
|
||||
|
||||
@test "DynamoDB: enable kinesis streaming destination" {
|
||||
STREAM_NAME="bats-kinesis-$(unique_name)"
|
||||
|
||||
aws_cmd kinesis create-stream --stream-name "$STREAM_NAME" --shard-count 1 >/dev/null
|
||||
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST \
|
||||
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
STREAM_ARN=$(aws_cmd kinesis describe-stream-summary --stream-name "$STREAM_NAME" | jq -r '.StreamDescriptionSummary.StreamARN')
|
||||
|
||||
run aws_cmd dynamodb enable-kinesis-streaming-destination \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--stream-arn "$STREAM_ARN"
|
||||
assert_success
|
||||
status=$(json_get "$output" '.DestinationStatus')
|
||||
[ "$status" = "ACTIVE" ]
|
||||
|
||||
aws_cmd kinesis delete-stream --stream-name "$STREAM_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "DynamoDB: describe kinesis streaming destination" {
|
||||
STREAM_NAME="bats-kinesis-$(unique_name)"
|
||||
|
||||
aws_cmd kinesis create-stream --stream-name "$STREAM_NAME" --shard-count 1 >/dev/null
|
||||
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST \
|
||||
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
STREAM_ARN=$(aws_cmd kinesis describe-stream-summary --stream-name "$STREAM_NAME" | jq -r '.StreamDescriptionSummary.StreamARN')
|
||||
|
||||
aws_cmd dynamodb enable-kinesis-streaming-destination \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--stream-arn "$STREAM_ARN" >/dev/null
|
||||
|
||||
run aws_cmd dynamodb describe-kinesis-streaming-destination \
|
||||
--table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
count=$(echo "$output" | jq '.KinesisDataStreamDestinations | length')
|
||||
[ "$count" = "1" ]
|
||||
dest_status=$(echo "$output" | jq -r '.KinesisDataStreamDestinations[0].DestinationStatus')
|
||||
[ "$dest_status" = "ACTIVE" ]
|
||||
|
||||
aws_cmd kinesis delete-stream --stream-name "$STREAM_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "DynamoDB: disable kinesis streaming destination" {
|
||||
STREAM_NAME="bats-kinesis-$(unique_name)"
|
||||
|
||||
aws_cmd kinesis create-stream --stream-name "$STREAM_NAME" --shard-count 1 >/dev/null
|
||||
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST \
|
||||
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
STREAM_ARN=$(aws_cmd kinesis describe-stream-summary --stream-name "$STREAM_NAME" | jq -r '.StreamDescriptionSummary.StreamARN')
|
||||
|
||||
aws_cmd dynamodb enable-kinesis-streaming-destination \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--stream-arn "$STREAM_ARN" >/dev/null
|
||||
|
||||
run aws_cmd dynamodb disable-kinesis-streaming-destination \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--stream-arn "$STREAM_ARN"
|
||||
assert_success
|
||||
status=$(json_get "$output" '.DestinationStatus')
|
||||
[ "$status" = "DISABLED" ]
|
||||
|
||||
aws_cmd kinesis delete-stream --stream-name "$STREAM_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "DynamoDB: scan with contains on String Set" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-1"},"roles":{"SS":["admin","user"]}}' >/dev/null
|
||||
aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \
|
||||
--item '{"pk":{"S":"item-2"},"roles":{"SS":["guest"]}}' >/dev/null
|
||||
|
||||
run aws_cmd dynamodb scan \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--filter-expression 'contains(#r, :role)' \
|
||||
--expression-attribute-names '{"#r":"roles"}' \
|
||||
--expression-attribute-values '{":role":{"S":"admin"}}'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Count')
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
# --- DynamoDB REMOVE nested map key tests (GH #402) ---
|
||||
|
||||
@test "DynamoDB: REMOVE key from nested map" {
|
||||
aws_cmd dynamodb create-table \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
|
||||
--key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
|
||||
--billing-mode PAY_PER_REQUEST >/dev/null
|
||||
|
||||
ddb_wait_table "$TABLE_NAME"
|
||||
|
||||
# Set a map attribute with a key
|
||||
aws_cmd dynamodb update-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"user1"},"sk":{"S":"sort1"}}' \
|
||||
--update-expression 'SET ratings = :ratings' \
|
||||
--expression-attribute-values '{":ratings":{"M":{"foo":{"S":"5"},"bar":{"S":"3"}}}}' >/dev/null
|
||||
|
||||
# REMOVE ratings.foo
|
||||
aws_cmd dynamodb update-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"user1"},"sk":{"S":"sort1"}}' \
|
||||
--update-expression 'REMOVE ratings.foo' >/dev/null
|
||||
|
||||
# Verify foo is removed but bar remains
|
||||
run aws_cmd dynamodb get-item \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--key '{"pk":{"S":"user1"},"sk":{"S":"sort1"}}'
|
||||
assert_success
|
||||
foo=$(echo "$output" | jq -r '.Item.ratings.M.foo // "null"')
|
||||
bar=$(echo "$output" | jq -r '.Item.ratings.M.bar.S')
|
||||
[ "$foo" = "null" ]
|
||||
[ "$bar" = "3" ]
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
#!/usr/bin/env bats
|
||||
# EC2 tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
PREFIX_LIST_NAME="bats-prefix-list-$(unique_name)"
|
||||
PREFIX_LIST_ID=""
|
||||
SG_VPC_ID=""
|
||||
SG_SOURCE_ID=""
|
||||
SG_TARGET_ID=""
|
||||
TRANSIT_GATEWAY_ID=""
|
||||
TGW_ATTACHMENT_ID=""
|
||||
TGW_VPC_ID=""
|
||||
TGW_ROUTE_TABLE_ID=""
|
||||
MONITOR_INSTANCE_ID=""
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ -n "$MONITOR_INSTANCE_ID" ]; then
|
||||
aws_cmd ec2 terminate-instances --instance-ids "$MONITOR_INSTANCE_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$PREFIX_LIST_ID" ]; then
|
||||
aws_cmd ec2 delete-managed-prefix-list --prefix-list-id "$PREFIX_LIST_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$TGW_ROUTE_TABLE_ID" ]; then
|
||||
aws_cmd ec2 delete-transit-gateway-route-table \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$TGW_ATTACHMENT_ID" ]; then
|
||||
aws_cmd ec2 delete-transit-gateway-vpc-attachment \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$TRANSIT_GATEWAY_ID" ]; then
|
||||
aws_cmd ec2 delete-transit-gateway --transit-gateway-id "$TRANSIT_GATEWAY_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$TGW_VPC_ID" ]; then
|
||||
aws_cmd ec2 delete-vpc --vpc-id "$TGW_VPC_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
for sg in "$SG_TARGET_ID" "$SG_SOURCE_ID"; do
|
||||
if [ -n "$sg" ]; then
|
||||
aws_cmd ec2 delete-security-group --group-id "$sg" >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
if [ -n "$SG_VPC_ID" ]; then
|
||||
aws_cmd ec2 delete-vpc --vpc-id "$SG_VPC_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Creates a prefix list holding 10.0.0.0/8 and sets PREFIX_LIST_ID.
|
||||
create_prefix_list() {
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-managed-prefix-list \
|
||||
--prefix-list-name "$PREFIX_LIST_NAME" \
|
||||
--address-family IPv4 \
|
||||
--max-entries 5 \
|
||||
--entries 'Cidr=10.0.0.0/8,Description=corporate')
|
||||
PREFIX_LIST_ID=$(json_get "$out" '.PrefixList.PrefixListId')
|
||||
}
|
||||
|
||||
@test "EC2: create managed prefix list" {
|
||||
run aws_cmd ec2 create-managed-prefix-list \
|
||||
--prefix-list-name "$PREFIX_LIST_NAME" \
|
||||
--address-family IPv4 \
|
||||
--max-entries 5 \
|
||||
--entries 'Cidr=10.0.0.0/8,Description=corporate'
|
||||
assert_success
|
||||
PREFIX_LIST_ID=$(json_get "$output" '.PrefixList.PrefixListId')
|
||||
[ -n "$PREFIX_LIST_ID" ]
|
||||
|
||||
state=$(json_get "$output" '.PrefixList.State')
|
||||
[ "$state" = "create-complete" ]
|
||||
version=$(json_get "$output" '.PrefixList.Version')
|
||||
[ "$version" = "1" ]
|
||||
}
|
||||
|
||||
@test "EC2: describe managed prefix list by id" {
|
||||
create_prefix_list
|
||||
|
||||
run aws_cmd ec2 describe-managed-prefix-lists --prefix-list-ids "$PREFIX_LIST_ID"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.PrefixLists[0].PrefixListName')
|
||||
[ "$name" = "$PREFIX_LIST_NAME" ]
|
||||
}
|
||||
|
||||
@test "EC2: describe managed prefix lists exposes the AWS-managed S3 list" {
|
||||
run aws_cmd ec2 describe-managed-prefix-lists \
|
||||
--filters "Name=prefix-list-name,Values=com.amazonaws.${AWS_DEFAULT_REGION}.s3"
|
||||
assert_success
|
||||
id=$(json_get "$output" '.PrefixLists[0].PrefixListId')
|
||||
[ "$id" = "pl-63a5400a" ]
|
||||
owner=$(json_get "$output" '.PrefixLists[0].OwnerId')
|
||||
[ "$owner" = "AWS" ]
|
||||
}
|
||||
|
||||
@test "EC2: get managed prefix list entries" {
|
||||
create_prefix_list
|
||||
|
||||
run aws_cmd ec2 get-managed-prefix-list-entries --prefix-list-id "$PREFIX_LIST_ID"
|
||||
assert_success
|
||||
cidr=$(json_get "$output" '.Entries[0].Cidr')
|
||||
[ "$cidr" = "10.0.0.0/8" ]
|
||||
desc=$(json_get "$output" '.Entries[0].Description')
|
||||
[ "$desc" = "corporate" ]
|
||||
}
|
||||
|
||||
@test "EC2: modify managed prefix list bumps version and keeps history" {
|
||||
create_prefix_list
|
||||
|
||||
run aws_cmd ec2 modify-managed-prefix-list \
|
||||
--prefix-list-id "$PREFIX_LIST_ID" \
|
||||
--add-entries 'Cidr=192.168.0.0/16,Description=lab'
|
||||
assert_success
|
||||
version=$(json_get "$output" '.PrefixList.Version')
|
||||
[ "$version" = "2" ]
|
||||
|
||||
run aws_cmd ec2 get-managed-prefix-list-entries --prefix-list-id "$PREFIX_LIST_ID"
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Entries | length')
|
||||
[ "$count" = "2" ]
|
||||
|
||||
run aws_cmd ec2 get-managed-prefix-list-entries --prefix-list-id "$PREFIX_LIST_ID" --target-version 1
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Entries | length')
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
@test "EC2: modify with a stale current version is rejected" {
|
||||
create_prefix_list
|
||||
aws_cmd ec2 modify-managed-prefix-list --prefix-list-id "$PREFIX_LIST_ID" \
|
||||
--add-entries 'Cidr=192.168.0.0/16' >/dev/null
|
||||
|
||||
run aws_cmd ec2 modify-managed-prefix-list \
|
||||
--prefix-list-id "$PREFIX_LIST_ID" \
|
||||
--current-version 1 \
|
||||
--add-entries 'Cidr=172.16.0.0/12'
|
||||
assert_failure
|
||||
assert_output --partial "PrefixListVersionMismatch"
|
||||
}
|
||||
|
||||
@test "EC2: AWS-managed prefix list cannot be modified" {
|
||||
run aws_cmd ec2 modify-managed-prefix-list \
|
||||
--prefix-list-id pl-63a5400a \
|
||||
--add-entries 'Cidr=10.1.0.0/16'
|
||||
assert_failure
|
||||
assert_output --partial "UnsupportedOperation"
|
||||
}
|
||||
|
||||
@test "EC2: delete managed prefix list" {
|
||||
create_prefix_list
|
||||
local created_id="$PREFIX_LIST_ID"
|
||||
|
||||
run aws_cmd ec2 delete-managed-prefix-list --prefix-list-id "$created_id"
|
||||
assert_success
|
||||
state=$(json_get "$output" '.PrefixList.State')
|
||||
[ "$state" = "delete-complete" ]
|
||||
PREFIX_LIST_ID=""
|
||||
|
||||
run aws_cmd ec2 describe-managed-prefix-lists --prefix-list-ids "$created_id"
|
||||
assert_failure
|
||||
assert_output --partial "InvalidPrefixListID.NotFound"
|
||||
}
|
||||
|
||||
@test "EC2: legacy describe-prefix-lists still serves the gateway lists" {
|
||||
run aws_cmd ec2 describe-prefix-lists \
|
||||
--filters "Name=prefix-list-name,Values=com.amazonaws.${AWS_DEFAULT_REGION}.s3"
|
||||
assert_success
|
||||
id=$(json_get "$output" '.PrefixLists[0].PrefixListId')
|
||||
[ "$id" = "pl-63a5400a" ]
|
||||
}
|
||||
|
||||
# ─── security group rules sourced from a prefix list ────────────────────────
|
||||
|
||||
@test "EC2: authorize a security group rule from a prefix list" {
|
||||
create_prefix_list
|
||||
local sg
|
||||
sg=$(aws_cmd ec2 create-security-group --group-name "$(unique_name bats-sg)" \
|
||||
--description "prefix list source" --query 'GroupId' --output text)
|
||||
|
||||
run aws_cmd ec2 authorize-security-group-ingress --group-id "$sg" \
|
||||
--ip-permissions "IpProtocol=tcp,FromPort=5432,ToPort=5432,PrefixListIds=[{PrefixListId=$PREFIX_LIST_ID,Description=from-corp}]"
|
||||
assert_success
|
||||
pl=$(json_get "$output" '.SecurityGroupRules[0].PrefixListId')
|
||||
[ "$pl" = "$PREFIX_LIST_ID" ]
|
||||
|
||||
run aws_cmd ec2 describe-security-groups --group-ids "$sg"
|
||||
assert_success
|
||||
nested=$(json_get "$output" '.SecurityGroups[0].IpPermissions[0].PrefixListIds[0].PrefixListId')
|
||||
[ "$nested" = "$PREFIX_LIST_ID" ]
|
||||
|
||||
run aws_cmd ec2 describe-security-group-rules --filters "Name=group-id,Values=$sg"
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg pl "$PREFIX_LIST_ID" '.SecurityGroupRules | any(.PrefixListId == $pl)')
|
||||
[ "$found" = "true" ]
|
||||
|
||||
aws_cmd ec2 delete-security-group --group-id "$sg" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "EC2: authorizing from an unknown prefix list is rejected" {
|
||||
local sg
|
||||
sg=$(aws_cmd ec2 create-security-group --group-name "$(unique_name bats-sg)" \
|
||||
--description "unknown prefix list" --query 'GroupId' --output text)
|
||||
|
||||
run aws_cmd ec2 authorize-security-group-ingress --group-id "$sg" \
|
||||
--ip-permissions 'IpProtocol=tcp,FromPort=1,ToPort=1,PrefixListIds=[{PrefixListId=pl-doesnotexist}]'
|
||||
assert_failure
|
||||
assert_output --partial "InvalidPrefixListID.NotFound"
|
||||
|
||||
aws_cmd ec2 delete-security-group --group-id "$sg" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# Creates a VPC holding a source and a target security group, and sets SG_VPC_ID,
|
||||
# SG_SOURCE_ID and SG_TARGET_ID.
|
||||
create_sg_pair() {
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-vpc --cidr-block 10.0.0.0/16)
|
||||
SG_VPC_ID=$(json_get "$out" '.Vpc.VpcId')
|
||||
out=$(aws_cmd ec2 create-security-group \
|
||||
--group-name "$(unique_name bats-sg-source)" \
|
||||
--description "traffic source" --vpc-id "$SG_VPC_ID")
|
||||
SG_SOURCE_ID=$(json_get "$out" '.GroupId')
|
||||
out=$(aws_cmd ec2 create-security-group \
|
||||
--group-name "$(unique_name bats-sg-target)" \
|
||||
--description "traffic target" --vpc-id "$SG_VPC_ID")
|
||||
SG_TARGET_ID=$(json_get "$out" '.GroupId')
|
||||
}
|
||||
|
||||
@test "EC2: ingress rule keeps its source security group" {
|
||||
create_sg_pair
|
||||
|
||||
# The CLI serializes UserIdGroupPairs under the wire name "Groups", so this exercises the
|
||||
# form the reporter of #2190 actually sent.
|
||||
run aws_cmd ec2 authorize-security-group-ingress --group-id "$SG_TARGET_ID" \
|
||||
--ip-permissions "IpProtocol=tcp,FromPort=443,ToPort=443,UserIdGroupPairs=[{GroupId=$SG_SOURCE_ID,Description=from-source-sg}]"
|
||||
assert_success
|
||||
ref=$(json_get "$output" '.SecurityGroupRules[0].ReferencedGroupInfo.GroupId')
|
||||
[ "$ref" = "$SG_SOURCE_ID" ]
|
||||
|
||||
# Control: the CIDR form of the same rule.
|
||||
aws_cmd ec2 authorize-security-group-ingress --group-id "$SG_TARGET_ID" \
|
||||
--ip-permissions 'IpProtocol=tcp,FromPort=8443,ToPort=8443,IpRanges=[{CidrIp=10.0.0.0/8,Description=control}]' >/dev/null
|
||||
|
||||
run aws_cmd ec2 describe-security-groups --group-ids "$SG_TARGET_ID"
|
||||
assert_success
|
||||
gid=$(json_get "$output" '.SecurityGroups[0].IpPermissions[] | select(.FromPort==443) | .UserIdGroupPairs[0].GroupId')
|
||||
[ "$gid" = "$SG_SOURCE_ID" ]
|
||||
desc=$(json_get "$output" '.SecurityGroups[0].IpPermissions[] | select(.FromPort==443) | .UserIdGroupPairs[0].Description')
|
||||
[ "$desc" = "from-source-sg" ]
|
||||
cidr=$(json_get "$output" '.SecurityGroups[0].IpPermissions[] | select(.FromPort==8443) | .IpRanges[0].CidrIp')
|
||||
[ "$cidr" = "10.0.0.0/8" ]
|
||||
|
||||
run aws_cmd ec2 describe-security-group-rules --filters "Name=group-id,Values=$SG_TARGET_ID"
|
||||
assert_success
|
||||
ref=$(json_get "$output" '.SecurityGroupRules[] | select(.FromPort==443) | .ReferencedGroupInfo.GroupId')
|
||||
[ "$ref" = "$SG_SOURCE_ID" ]
|
||||
desc=$(json_get "$output" '.SecurityGroupRules[] | select(.FromPort==443) | .Description')
|
||||
[ "$desc" = "from-source-sg" ]
|
||||
cidr=$(json_get "$output" '.SecurityGroupRules[] | select(.FromPort==8443) | .CidrIpv4')
|
||||
[ "$cidr" = "10.0.0.0/8" ]
|
||||
}
|
||||
|
||||
# ─── transit gateways ───────────────────────────────────────────────────────
|
||||
|
||||
@test "EC2: create a transit gateway and read it back" {
|
||||
run aws_cmd ec2 create-transit-gateway \
|
||||
--description "bats hub" \
|
||||
--tag-specifications 'ResourceType=transit-gateway,Tags=[{Key=Name,Value=bats-tgw}]'
|
||||
assert_success
|
||||
TRANSIT_GATEWAY_ID=$(json_get "$output" '.TransitGateway.TransitGatewayId')
|
||||
[ "$(json_get "$output" '.TransitGateway.State')" = "available" ]
|
||||
[ "$(json_get "$output" '.TransitGateway.Options.AmazonSideAsn')" = "64512" ]
|
||||
[ "$(json_get "$output" '.TransitGateway.Options.SecurityGroupReferencingSupport')" = "disable" ]
|
||||
|
||||
# The default route table is minted with the gateway, so both ids are already set and equal.
|
||||
assoc=$(json_get "$output" '.TransitGateway.Options.AssociationDefaultRouteTableId')
|
||||
prop=$(json_get "$output" '.TransitGateway.Options.PropagationDefaultRouteTableId')
|
||||
[ "$assoc" = "$prop" ]
|
||||
case "$assoc" in tgw-rtb-*) ;; *) return 1 ;; esac
|
||||
|
||||
run aws_cmd ec2 describe-transit-gateways --transit-gateway-ids "$TRANSIT_GATEWAY_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TransitGateways[0].TransitGatewayId')" = "$TRANSIT_GATEWAY_ID" ]
|
||||
[ "$(json_get "$output" '.TransitGateways[0].Tags[0].Value')" = "bats-tgw" ]
|
||||
}
|
||||
|
||||
@test "EC2: create a transit gateway with CIDR blocks" {
|
||||
run aws_cmd ec2 create-transit-gateway \
|
||||
--options 'TransitGatewayCidrBlocks=[10.99.0.0/16]'
|
||||
assert_success
|
||||
TRANSIT_GATEWAY_ID=$(json_get "$output" '.TransitGateway.TransitGatewayId')
|
||||
[ "$(json_get "$output" '.TransitGateway.Options.TransitGatewayCidrBlocks[0]')" = "10.99.0.0/16" ]
|
||||
|
||||
run aws_cmd ec2 describe-transit-gateways --transit-gateway-ids "$TRANSIT_GATEWAY_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TransitGateways[0].Options.TransitGatewayCidrBlocks[0]')" = "10.99.0.0/16" ]
|
||||
}
|
||||
|
||||
# Tags are changed after creation with create-tags rather than a tag specification, and read back
|
||||
# from describe-transit-gateways, which is how a Terraform tag update converges.
|
||||
@test "EC2: transit gateway tags changed after creation are visible on describe" {
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-transit-gateway --tag-specifications \
|
||||
'ResourceType=transit-gateway,Tags=[{Key=Name,Value=bats-tgw-tags}]')
|
||||
TRANSIT_GATEWAY_ID=$(json_get "$out" '.TransitGateway.TransitGatewayId')
|
||||
|
||||
aws_cmd ec2 create-tags --resources "$TRANSIT_GATEWAY_ID" --tags 'Key=env,Value=prod' >/dev/null
|
||||
|
||||
run aws_cmd ec2 describe-transit-gateways --transit-gateway-ids "$TRANSIT_GATEWAY_ID"
|
||||
assert_success
|
||||
count=$(json_get "$output" '.TransitGateways[0].Tags | length')
|
||||
[ "$count" = "2" ]
|
||||
env_value=$(json_get "$output" '.TransitGateways[0].Tags[] | select(.Key=="env") | .Value')
|
||||
[ "$env_value" = "prod" ]
|
||||
|
||||
aws_cmd ec2 delete-tags --resources "$TRANSIT_GATEWAY_ID" --tags 'Key=env' >/dev/null
|
||||
|
||||
run aws_cmd ec2 describe-transit-gateways --transit-gateway-ids "$TRANSIT_GATEWAY_ID"
|
||||
assert_success
|
||||
count=$(json_get "$output" '.TransitGateways[0].Tags | length')
|
||||
[ "$count" = "1" ]
|
||||
}
|
||||
|
||||
@test "EC2: modify a transit gateway" {
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-transit-gateway --description "before")
|
||||
TRANSIT_GATEWAY_ID=$(json_get "$out" '.TransitGateway.TransitGatewayId')
|
||||
|
||||
run aws_cmd ec2 modify-transit-gateway \
|
||||
--transit-gateway-id "$TRANSIT_GATEWAY_ID" \
|
||||
--description "after" \
|
||||
--options 'DnsSupport=disable,AddTransitGatewayCidrBlocks=[10.100.0.0/16]'
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TransitGateway.Description')" = "after" ]
|
||||
[ "$(json_get "$output" '.TransitGateway.Options.DnsSupport')" = "disable" ]
|
||||
[ "$(json_get "$output" '.TransitGateway.Options.TransitGatewayCidrBlocks[0]')" = "10.100.0.0/16" ]
|
||||
|
||||
# Describe serializes the same list, so read it back through the CLI's own parser too.
|
||||
run aws_cmd ec2 describe-transit-gateways --transit-gateway-ids "$TRANSIT_GATEWAY_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TransitGateways[0].Options.TransitGatewayCidrBlocks[0]')" = "10.100.0.0/16" ]
|
||||
}
|
||||
|
||||
@test "EC2: delete a transit gateway" {
|
||||
local out tgw
|
||||
out=$(aws_cmd ec2 create-transit-gateway --description "short lived")
|
||||
tgw=$(json_get "$out" '.TransitGateway.TransitGatewayId')
|
||||
|
||||
run aws_cmd ec2 delete-transit-gateway --transit-gateway-id "$tgw"
|
||||
assert_success
|
||||
|
||||
run aws_cmd ec2 describe-transit-gateways --transit-gateway-ids "$tgw"
|
||||
assert_failure
|
||||
assert_output --partial "InvalidTransitGatewayID.NotFound"
|
||||
}
|
||||
|
||||
@test "EC2: describing an unknown transit gateway is rejected" {
|
||||
run aws_cmd ec2 describe-transit-gateways --transit-gateway-ids tgw-0123456789abcdef0
|
||||
assert_failure
|
||||
assert_output --partial "InvalidTransitGatewayID.NotFound"
|
||||
}
|
||||
|
||||
# ─── transit gateway VPC attachments ────────────────────────────────────────
|
||||
|
||||
# Creates a gateway, a VPC and one subnet, setting TRANSIT_GATEWAY_ID, TGW_VPC_ID and TGW_SUBNET_ID.
|
||||
create_attachment_fixture() {
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-transit-gateway --description "bats attachment host")
|
||||
TRANSIT_GATEWAY_ID=$(json_get "$out" '.TransitGateway.TransitGatewayId')
|
||||
out=$(aws_cmd ec2 create-vpc --cidr-block 10.80.0.0/16)
|
||||
TGW_VPC_ID=$(json_get "$out" '.Vpc.VpcId')
|
||||
out=$(aws_cmd ec2 create-subnet --vpc-id "$TGW_VPC_ID" --cidr-block 10.80.1.0/24 \
|
||||
--availability-zone "${AWS_DEFAULT_REGION}a")
|
||||
TGW_SUBNET_ID=$(json_get "$out" '.Subnet.SubnetId')
|
||||
}
|
||||
|
||||
@test "EC2: attach a VPC to a transit gateway and read it back both ways" {
|
||||
create_attachment_fixture
|
||||
|
||||
run aws_cmd ec2 create-transit-gateway-vpc-attachment \
|
||||
--transit-gateway-id "$TRANSIT_GATEWAY_ID" \
|
||||
--vpc-id "$TGW_VPC_ID" \
|
||||
--subnet-ids "$TGW_SUBNET_ID" \
|
||||
--tag-specifications 'ResourceType=transit-gateway-attachment,Tags=[{Key=Name,Value=bats-attach}]'
|
||||
assert_success
|
||||
TGW_ATTACHMENT_ID=$(json_get "$output" '.TransitGatewayVpcAttachment.TransitGatewayAttachmentId')
|
||||
[ "$(json_get "$output" '.TransitGatewayVpcAttachment.State')" = "available" ]
|
||||
[ "$(json_get "$output" '.TransitGatewayVpcAttachment.SubnetIds[0]')" = "$TGW_SUBNET_ID" ]
|
||||
# The attachment's own default, which differs from the gateway's.
|
||||
[ "$(json_get "$output" '.TransitGatewayVpcAttachment.Options.SecurityGroupReferencingSupport')" = "enable" ]
|
||||
|
||||
run aws_cmd ec2 describe-transit-gateway-vpc-attachments \
|
||||
--transit-gateway-attachment-ids "$TGW_ATTACHMENT_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TransitGatewayVpcAttachments[0].VpcId')" = "$TGW_VPC_ID" ]
|
||||
[ "$(json_get "$output" '.TransitGatewayVpcAttachments[0].Tags[0].Value')" = "bats-attach" ]
|
||||
|
||||
# The resource-agnostic form is a different shape: typed resource plus the association.
|
||||
run aws_cmd ec2 describe-transit-gateway-attachments \
|
||||
--transit-gateway-attachment-ids "$TGW_ATTACHMENT_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TransitGatewayAttachments[0].ResourceType')" = "vpc" ]
|
||||
[ "$(json_get "$output" '.TransitGatewayAttachments[0].ResourceId')" = "$TGW_VPC_ID" ]
|
||||
[ "$(json_get "$output" '.TransitGatewayAttachments[0].Association.State')" = "associated" ]
|
||||
rtb=$(json_get "$output" '.TransitGatewayAttachments[0].Association.TransitGatewayRouteTableId')
|
||||
case "$rtb" in tgw-rtb-*) ;; *) return 1 ;; esac
|
||||
}
|
||||
|
||||
@test "EC2: modify a transit gateway VPC attachment" {
|
||||
create_attachment_fixture
|
||||
local out subnet_b
|
||||
out=$(aws_cmd ec2 create-transit-gateway-vpc-attachment --transit-gateway-id "$TRANSIT_GATEWAY_ID" \
|
||||
--vpc-id "$TGW_VPC_ID" --subnet-ids "$TGW_SUBNET_ID")
|
||||
TGW_ATTACHMENT_ID=$(json_get "$out" '.TransitGatewayVpcAttachment.TransitGatewayAttachmentId')
|
||||
out=$(aws_cmd ec2 create-subnet --vpc-id "$TGW_VPC_ID" --cidr-block 10.80.2.0/24 \
|
||||
--availability-zone "${AWS_DEFAULT_REGION}b")
|
||||
subnet_b=$(json_get "$out" '.Subnet.SubnetId')
|
||||
|
||||
run aws_cmd ec2 modify-transit-gateway-vpc-attachment \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID" \
|
||||
--add-subnet-ids "$subnet_b" \
|
||||
--options 'DnsSupport=disable'
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TransitGatewayVpcAttachment.Options.DnsSupport')" = "disable" ]
|
||||
count=$(json_get "$output" '.TransitGatewayVpcAttachment.SubnetIds | length')
|
||||
[ "$count" = "2" ]
|
||||
|
||||
run aws_cmd ec2 modify-transit-gateway-vpc-attachment \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID" \
|
||||
--remove-subnet-ids "$TGW_SUBNET_ID" "$subnet_b"
|
||||
assert_failure
|
||||
assert_output --partial "InsufficientSubnetsException"
|
||||
}
|
||||
|
||||
@test "EC2: a transit gateway with an attachment cannot be deleted" {
|
||||
create_attachment_fixture
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-transit-gateway-vpc-attachment --transit-gateway-id "$TRANSIT_GATEWAY_ID" \
|
||||
--vpc-id "$TGW_VPC_ID" --subnet-ids "$TGW_SUBNET_ID")
|
||||
TGW_ATTACHMENT_ID=$(json_get "$out" '.TransitGatewayVpcAttachment.TransitGatewayAttachmentId')
|
||||
|
||||
run aws_cmd ec2 delete-transit-gateway --transit-gateway-id "$TRANSIT_GATEWAY_ID"
|
||||
assert_failure
|
||||
assert_output --partial "IncorrectState"
|
||||
|
||||
run aws_cmd ec2 delete-transit-gateway-vpc-attachment \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID"
|
||||
assert_success
|
||||
TGW_ATTACHMENT_ID=""
|
||||
|
||||
run aws_cmd ec2 delete-transit-gateway --transit-gateway-id "$TRANSIT_GATEWAY_ID"
|
||||
assert_success
|
||||
TRANSIT_GATEWAY_ID=""
|
||||
}
|
||||
|
||||
# ─── transit gateway route tables, associations, propagations and routes ────
|
||||
|
||||
@test "EC2: route table associations move between tables" {
|
||||
create_attachment_fixture
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-transit-gateway-vpc-attachment --transit-gateway-id "$TRANSIT_GATEWAY_ID" \
|
||||
--vpc-id "$TGW_VPC_ID" --subnet-ids "$TGW_SUBNET_ID")
|
||||
TGW_ATTACHMENT_ID=$(json_get "$out" '.TransitGatewayVpcAttachment.TransitGatewayAttachmentId')
|
||||
out=$(aws_cmd ec2 describe-transit-gateways --transit-gateway-ids "$TRANSIT_GATEWAY_ID")
|
||||
local default_rtb
|
||||
default_rtb=$(json_get "$out" '.TransitGateways[0].Options.AssociationDefaultRouteTableId')
|
||||
|
||||
run aws_cmd ec2 create-transit-gateway-route-table --transit-gateway-id "$TRANSIT_GATEWAY_ID" \
|
||||
--tag-specifications 'ResourceType=transit-gateway-route-table,Tags=[{Key=Name,Value=bats-rtb}]'
|
||||
assert_success
|
||||
TGW_ROUTE_TABLE_ID=$(json_get "$output" '.TransitGatewayRouteTable.TransitGatewayRouteTableId')
|
||||
[ "$(json_get "$output" '.TransitGatewayRouteTable.State')" = "available" ]
|
||||
[ "$(json_get "$output" '.TransitGatewayRouteTable.DefaultAssociationRouteTable')" = "false" ]
|
||||
|
||||
# The attachment starts on the gateway's default table, so a second association is refused.
|
||||
run aws_cmd ec2 associate-transit-gateway-route-table \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID"
|
||||
assert_failure
|
||||
assert_output --partial "Resource.AlreadyAssociated"
|
||||
|
||||
run aws_cmd ec2 disassociate-transit-gateway-route-table \
|
||||
--transit-gateway-route-table-id "$default_rtb" \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Association.State')" = "disassociating" ]
|
||||
|
||||
run aws_cmd ec2 associate-transit-gateway-route-table \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Association.State')" = "associating" ]
|
||||
|
||||
run aws_cmd ec2 get-transit-gateway-route-table-associations \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Associations[0].TransitGatewayAttachmentId')" = "$TGW_ATTACHMENT_ID" ]
|
||||
[ "$(json_get "$output" '.Associations[0].ResourceType')" = "vpc" ]
|
||||
}
|
||||
|
||||
@test "EC2: propagation produces a route for the attached VPC" {
|
||||
create_attachment_fixture
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-transit-gateway-vpc-attachment --transit-gateway-id "$TRANSIT_GATEWAY_ID" \
|
||||
--vpc-id "$TGW_VPC_ID" --subnet-ids "$TGW_SUBNET_ID")
|
||||
TGW_ATTACHMENT_ID=$(json_get "$out" '.TransitGatewayVpcAttachment.TransitGatewayAttachmentId')
|
||||
out=$(aws_cmd ec2 create-transit-gateway-route-table --transit-gateway-id "$TRANSIT_GATEWAY_ID")
|
||||
TGW_ROUTE_TABLE_ID=$(json_get "$out" '.TransitGatewayRouteTable.TransitGatewayRouteTableId')
|
||||
|
||||
run aws_cmd ec2 enable-transit-gateway-route-table-propagation \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Propagation.State')" = "enabled" ]
|
||||
|
||||
run aws_cmd ec2 get-transit-gateway-route-table-propagations \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.TransitGatewayRouteTablePropagations[0].State')" = "enabled" ]
|
||||
|
||||
run aws_cmd ec2 search-transit-gateway-routes \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--filters 'Name=type,Values=propagated'
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Routes[0].DestinationCidrBlock')" = "10.80.0.0/16" ]
|
||||
[ "$(json_get "$output" '.Routes[0].Type')" = "propagated" ]
|
||||
[ "$(json_get "$output" '.Routes[0].State')" = "active" ]
|
||||
|
||||
run aws_cmd ec2 enable-transit-gateway-route-table-propagation \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID"
|
||||
assert_failure
|
||||
assert_output --partial "TransitGatewayRouteTablePropagation.Duplicate"
|
||||
}
|
||||
|
||||
@test "EC2: static and blackhole transit gateway routes" {
|
||||
create_attachment_fixture
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-transit-gateway-vpc-attachment --transit-gateway-id "$TRANSIT_GATEWAY_ID" \
|
||||
--vpc-id "$TGW_VPC_ID" --subnet-ids "$TGW_SUBNET_ID")
|
||||
TGW_ATTACHMENT_ID=$(json_get "$out" '.TransitGatewayVpcAttachment.TransitGatewayAttachmentId')
|
||||
out=$(aws_cmd ec2 create-transit-gateway-route-table --transit-gateway-id "$TRANSIT_GATEWAY_ID")
|
||||
TGW_ROUTE_TABLE_ID=$(json_get "$out" '.TransitGatewayRouteTable.TransitGatewayRouteTableId')
|
||||
|
||||
run aws_cmd ec2 create-transit-gateway-route \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--destination-cidr-block 10.60.0.0/16 \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Route.Type')" = "static" ]
|
||||
[ "$(json_get "$output" '.Route.State')" = "active" ]
|
||||
[ "$(json_get "$output" '.Route.TransitGatewayAttachments[0].TransitGatewayAttachmentId')" = "$TGW_ATTACHMENT_ID" ]
|
||||
|
||||
run aws_cmd ec2 create-transit-gateway-route \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--destination-cidr-block 10.61.0.0/16 --blackhole
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Route.State')" = "blackhole" ]
|
||||
# A blackhole carries no attachment, so the key is absent rather than empty.
|
||||
count=$(json_get "$output" '.Route.TransitGatewayAttachments // [] | length')
|
||||
[ "$count" = "0" ]
|
||||
|
||||
run aws_cmd ec2 search-transit-gateway-routes \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--filters 'Name=type,Values=static'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Routes | length')
|
||||
[ "$count" = "2" ]
|
||||
|
||||
run aws_cmd ec2 delete-transit-gateway-route \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--destination-cidr-block 10.61.0.0/16
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Route.State')" = "deleted" ]
|
||||
}
|
||||
|
||||
@test "EC2: replacing a transit gateway route moves its target and upserts" {
|
||||
create_attachment_fixture
|
||||
local out
|
||||
out=$(aws_cmd ec2 create-transit-gateway-vpc-attachment --transit-gateway-id "$TRANSIT_GATEWAY_ID" \
|
||||
--vpc-id "$TGW_VPC_ID" --subnet-ids "$TGW_SUBNET_ID")
|
||||
TGW_ATTACHMENT_ID=$(json_get "$out" '.TransitGatewayVpcAttachment.TransitGatewayAttachmentId')
|
||||
out=$(aws_cmd ec2 create-transit-gateway-route-table --transit-gateway-id "$TRANSIT_GATEWAY_ID")
|
||||
TGW_ROUTE_TABLE_ID=$(json_get "$out" '.TransitGatewayRouteTable.TransitGatewayRouteTableId')
|
||||
aws_cmd ec2 create-transit-gateway-route --transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--destination-cidr-block 10.88.0.0/16 --transit-gateway-attachment-id "$TGW_ATTACHMENT_ID" >/dev/null
|
||||
|
||||
run aws_cmd ec2 replace-transit-gateway-route \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--destination-cidr-block 10.88.0.0/16 --blackhole
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Route.State')" = "blackhole" ]
|
||||
count=$(json_get "$output" '.Route.TransitGatewayAttachments // [] | length')
|
||||
[ "$count" = "0" ]
|
||||
|
||||
run aws_cmd ec2 replace-transit-gateway-route \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--destination-cidr-block 10.88.0.0/16 \
|
||||
--transit-gateway-attachment-id "$TGW_ATTACHMENT_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Route.State')" = "active" ]
|
||||
|
||||
# Replacing a destination the table never held writes it.
|
||||
run aws_cmd ec2 replace-transit-gateway-route \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--destination-cidr-block 10.89.0.0/16 --blackhole
|
||||
assert_success
|
||||
run aws_cmd ec2 search-transit-gateway-routes \
|
||||
--transit-gateway-route-table-id "$TGW_ROUTE_TABLE_ID" \
|
||||
--filters 'Name=type,Values=static'
|
||||
assert_success
|
||||
count=$(json_get "$output" '.Routes | length')
|
||||
[ "$count" = "2" ]
|
||||
}
|
||||
|
||||
# Monitoring goes through the SDK, not just the raw Query wire, because that is the
|
||||
# path aws_instance takes: `monitoring = true` calls MonitorInstances, and the SDK
|
||||
# parses the response rather than reading the XML we happen to emit. AGENTS.md asks
|
||||
# for management APIs to be validated with SDK clients and not only handcrafted HTTP.
|
||||
|
||||
launch_monitor_instance() {
|
||||
run aws_cmd ec2 run-instances \
|
||||
--image-id ami-0abcdef1234567890 --instance-type t3.micro \
|
||||
--count 1
|
||||
assert_success
|
||||
MONITOR_INSTANCE_ID=$(json_get "$output" '.Instances[0].InstanceId')
|
||||
[ -n "$MONITOR_INSTANCE_ID" ]
|
||||
}
|
||||
|
||||
@test "EC2: monitor-instances reports enabled and the state survives to describe" {
|
||||
launch_monitor_instance
|
||||
|
||||
run aws_cmd ec2 monitor-instances --instance-ids "$MONITOR_INSTANCE_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.InstanceMonitorings[0].InstanceId')" = "$MONITOR_INSTANCE_ID" ]
|
||||
[ "$(json_get "$output" '.InstanceMonitorings[0].Monitoring.State')" = "enabled" ]
|
||||
|
||||
# Read it back rather than trusting the mutating call's own response: the bug this
|
||||
# guards against was answering "enabled" while every later read still said disabled.
|
||||
run aws_cmd ec2 describe-instances --instance-ids "$MONITOR_INSTANCE_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Reservations[0].Instances[0].Monitoring.State')" = "enabled" ]
|
||||
}
|
||||
|
||||
@test "EC2: unmonitor-instances reports disabled and the state survives to describe" {
|
||||
launch_monitor_instance
|
||||
run aws_cmd ec2 monitor-instances --instance-ids "$MONITOR_INSTANCE_ID"
|
||||
assert_success
|
||||
|
||||
run aws_cmd ec2 unmonitor-instances --instance-ids "$MONITOR_INSTANCE_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.InstanceMonitorings[0].Monitoring.State')" = "disabled" ]
|
||||
|
||||
run aws_cmd ec2 describe-instances --instance-ids "$MONITOR_INSTANCE_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Reservations[0].Instances[0].Monitoring.State')" = "disabled" ]
|
||||
}
|
||||
|
||||
@test "EC2: monitor-instances rejects an instance that does not exist" {
|
||||
run aws_cmd ec2 monitor-instances --instance-ids i-1234567890abcdef0
|
||||
assert_failure
|
||||
[[ "$output" == *"InvalidInstanceID.NotFound"* ]]
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bats
|
||||
# ECR control-plane integration tests.
|
||||
#
|
||||
# Test-first: this file is committed before the server-side ECR implementation
|
||||
# lands. With ECR unimplemented, every test below should fail.
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
REPO_NAME="floci-it/app-cli-$(unique_name)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
aws_cmd ecr delete-repository --repository-name "$REPO_NAME" --force >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "ECR: create-repository returns loopback URI" {
|
||||
run aws_cmd ecr create-repository --repository-name "$REPO_NAME"
|
||||
assert_success
|
||||
arn=$(json_get "$output" '.repository.repositoryArn')
|
||||
uri=$(json_get "$output" '.repository.repositoryUri')
|
||||
name=$(json_get "$output" '.repository.repositoryName')
|
||||
[ "$name" = "$REPO_NAME" ]
|
||||
[[ "$arn" =~ ^arn:aws:ecr: ]]
|
||||
[[ "$arn" == *":repository/$REPO_NAME" ]]
|
||||
[[ "$uri" == *"localhost:"* ]]
|
||||
[[ "$uri" == *"/$REPO_NAME" ]]
|
||||
}
|
||||
|
||||
@test "ECR: create-repository duplicate fails with RepositoryAlreadyExistsException" {
|
||||
aws_cmd ecr create-repository --repository-name "$REPO_NAME" >/dev/null
|
||||
run aws_cmd ecr create-repository --repository-name "$REPO_NAME"
|
||||
assert_failure
|
||||
[[ "$output" == *"RepositoryAlreadyExistsException"* ]]
|
||||
}
|
||||
|
||||
@test "ECR: describe-repositories returns the created repo" {
|
||||
aws_cmd ecr create-repository --repository-name "$REPO_NAME" >/dev/null
|
||||
run aws_cmd ecr describe-repositories --repository-names "$REPO_NAME"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.repositories[0].repositoryName')
|
||||
[ "$name" = "$REPO_NAME" ]
|
||||
}
|
||||
|
||||
@test "ECR: get-authorization-token returns AWS:<password>" {
|
||||
aws_cmd ecr create-repository --repository-name "$REPO_NAME" >/dev/null
|
||||
run aws_cmd ecr get-authorization-token
|
||||
assert_success
|
||||
token=$(json_get "$output" '.authorizationData[0].authorizationToken')
|
||||
proxy=$(json_get "$output" '.authorizationData[0].proxyEndpoint')
|
||||
[ -n "$token" ]
|
||||
[[ "$proxy" =~ ^https?:// ]]
|
||||
decoded=$(echo "$token" | base64 -d)
|
||||
[[ "$decoded" == AWS:* ]]
|
||||
}
|
||||
|
||||
@test "ECR: list-images on empty repo returns []" {
|
||||
aws_cmd ecr create-repository --repository-name "$REPO_NAME" >/dev/null
|
||||
run aws_cmd ecr list-images --repository-name "$REPO_NAME"
|
||||
assert_success
|
||||
count=$(echo "$output" | jq '.imageIds | length')
|
||||
[ "$count" = "0" ]
|
||||
}
|
||||
|
||||
@test "ECR: put-image-tag-mutability round-trips IMMUTABLE" {
|
||||
aws_cmd ecr create-repository --repository-name "$REPO_NAME" >/dev/null
|
||||
run aws_cmd ecr put-image-tag-mutability --repository-name "$REPO_NAME" --image-tag-mutability IMMUTABLE
|
||||
assert_success
|
||||
mut=$(json_get "$output" '.imageTagMutability')
|
||||
[ "$mut" = "IMMUTABLE" ]
|
||||
}
|
||||
|
||||
@test "ECR: put-lifecycle-policy round-trips" {
|
||||
aws_cmd ecr create-repository --repository-name "$REPO_NAME" >/dev/null
|
||||
policy='{"rules":[{"rulePriority":1,"selection":{"tagStatus":"untagged","countType":"imageCountMoreThan","countNumber":5},"action":{"type":"expire"}}]}'
|
||||
aws_cmd ecr put-lifecycle-policy --repository-name "$REPO_NAME" --lifecycle-policy-text "$policy" >/dev/null
|
||||
run aws_cmd ecr get-lifecycle-policy --repository-name "$REPO_NAME"
|
||||
assert_success
|
||||
got=$(json_get "$output" '.lifecyclePolicyText')
|
||||
[ "$got" = "$policy" ]
|
||||
}
|
||||
|
||||
@test "ECR: set-repository-policy round-trips" {
|
||||
aws_cmd ecr create-repository --repository-name "$REPO_NAME" >/dev/null
|
||||
policy='{"Version":"2012-10-17","Statement":[{"Sid":"AllowAll","Effect":"Allow","Principal":"*","Action":"ecr:*"}]}'
|
||||
aws_cmd ecr set-repository-policy --repository-name "$REPO_NAME" --policy-text "$policy" >/dev/null
|
||||
run aws_cmd ecr get-repository-policy --repository-name "$REPO_NAME"
|
||||
assert_success
|
||||
got=$(json_get "$output" '.policyText')
|
||||
[ "$got" = "$policy" ]
|
||||
}
|
||||
|
||||
@test "ECR: delete-repository force=true removes the repo" {
|
||||
aws_cmd ecr create-repository --repository-name "$REPO_NAME" >/dev/null
|
||||
run aws_cmd ecr delete-repository --repository-name "$REPO_NAME" --force
|
||||
assert_success
|
||||
run aws_cmd ecr describe-repositories --repository-names "$REPO_NAME"
|
||||
assert_failure
|
||||
[[ "$output" == *"RepositoryNotFoundException"* ]]
|
||||
}
|
||||
|
||||
@test "ECR: describe-repositories on missing name fails" {
|
||||
run aws_cmd ecr describe-repositories --repository-names "does-not-exist-cli"
|
||||
assert_failure
|
||||
[[ "$output" == *"RepositoryNotFoundException"* ]]
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env bats
|
||||
# ElastiCache tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
PG="bats-pg-$(unique_name)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
aws_cmd elasticache delete-cache-parameter-group --cache-parameter-group-name "$PG" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "ElastiCache: create, describe, modify and delete a cache parameter group" {
|
||||
run aws_cmd elasticache create-cache-parameter-group \
|
||||
--cache-parameter-group-name "$PG" \
|
||||
--cache-parameter-group-family redis7 \
|
||||
--description "bats parameter group"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.CacheParameterGroup.CacheParameterGroupName')" = "$PG" ]
|
||||
[ "$(json_get "$output" '.CacheParameterGroup.CacheParameterGroupFamily')" = "redis7" ]
|
||||
[ "$(json_get "$output" '.CacheParameterGroup.Description')" = "bats parameter group" ]
|
||||
[ "$(json_get "$output" '.CacheParameterGroup.IsGlobal')" = "false" ]
|
||||
arn=$(json_get "$output" '.CacheParameterGroup.ARN')
|
||||
[[ "$arn" == *":parametergroup:$PG" ]]
|
||||
|
||||
run aws_cmd elasticache describe-cache-parameter-groups --cache-parameter-group-name "$PG"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.CacheParameterGroups | length')" = "1" ]
|
||||
|
||||
run aws_cmd elasticache modify-cache-parameter-group \
|
||||
--cache-parameter-group-name "$PG" \
|
||||
--parameter-name-values 'ParameterName=maxmemory-policy,ParameterValue=allkeys-lru'
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.CacheParameterGroupName')" = "$PG" ]
|
||||
|
||||
run aws_cmd elasticache describe-cache-parameters --cache-parameter-group-name "$PG"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Parameters[0].ParameterName')" = "maxmemory-policy" ]
|
||||
[ "$(json_get "$output" '.Parameters[0].ParameterValue')" = "allkeys-lru" ]
|
||||
[ "$(json_get "$output" '.Parameters[0].Source')" = "user" ]
|
||||
|
||||
run aws_cmd elasticache delete-cache-parameter-group --cache-parameter-group-name "$PG"
|
||||
assert_success
|
||||
|
||||
run aws_cmd elasticache describe-cache-parameter-groups --cache-parameter-group-name "$PG"
|
||||
assert_failure
|
||||
assert_output --partial "CacheParameterGroupNotFound"
|
||||
}
|
||||
|
||||
@test "ElastiCache: the default parameter groups AWS publishes are listed" {
|
||||
run aws_cmd elasticache describe-cache-parameter-groups
|
||||
assert_success
|
||||
names=$(json_get "$output" '.CacheParameterGroups[].CacheParameterGroupName')
|
||||
echo "$names" | grep -q '^default\.redis7$'
|
||||
echo "$names" | grep -q '^default\.redis7\.cluster\.on$'
|
||||
echo "$names" | grep -q '^default\.valkey8$'
|
||||
|
||||
run aws_cmd elasticache describe-cache-parameter-groups --cache-parameter-group-name default.redis7
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.CacheParameterGroups[0].CacheParameterGroupFamily')" = "redis7" ]
|
||||
}
|
||||
|
||||
@test "ElastiCache: a duplicate parameter group is rejected" {
|
||||
aws_cmd elasticache create-cache-parameter-group --cache-parameter-group-name "$PG" \
|
||||
--cache-parameter-group-family redis7 --description first >/dev/null
|
||||
|
||||
run aws_cmd elasticache create-cache-parameter-group --cache-parameter-group-name "$PG" \
|
||||
--cache-parameter-group-family redis7 --description second
|
||||
assert_failure
|
||||
assert_output --partial "CacheParameterGroupAlreadyExists"
|
||||
}
|
||||
|
||||
@test "ElastiCache: an unknown family is rejected" {
|
||||
run aws_cmd elasticache create-cache-parameter-group \
|
||||
--cache-parameter-group-name "$PG" \
|
||||
--cache-parameter-group-family redis99 --description x
|
||||
assert_failure
|
||||
assert_output --partial "not a valid parameter group family"
|
||||
}
|
||||
|
||||
@test "ElastiCache: a default parameter group cannot be deleted" {
|
||||
# AWS rejects it on the identifier rule, since a user-supplied name cannot contain dots.
|
||||
run aws_cmd elasticache delete-cache-parameter-group --cache-parameter-group-name default.redis7
|
||||
assert_failure
|
||||
assert_output --partial "not a valid identifier"
|
||||
}
|
||||
|
||||
@test "ElastiCache: cache subnet group takes its VPC and zones from the subnets" {
|
||||
vpc=$(aws_cmd ec2 create-vpc --cidr-block 10.30.0.0/16 --query 'Vpc.VpcId' --output text)
|
||||
a=$(aws_cmd ec2 create-subnet --vpc-id "$vpc" --cidr-block 10.30.1.0/24 \
|
||||
--availability-zone us-east-1a --query 'Subnet.SubnetId' --output text)
|
||||
b=$(aws_cmd ec2 create-subnet --vpc-id "$vpc" --cidr-block 10.30.2.0/24 \
|
||||
--availability-zone us-east-1b --query 'Subnet.SubnetId' --output text)
|
||||
SNG="bats-sng-$(unique_name)"
|
||||
|
||||
run aws_cmd elasticache create-cache-subnet-group \
|
||||
--cache-subnet-group-name "$SNG" \
|
||||
--cache-subnet-group-description "bats subnet group" \
|
||||
--subnet-ids "$a" "$b"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.CacheSubnetGroup.VpcId')" = "$vpc" ]
|
||||
[ "$(json_get "$output" '.CacheSubnetGroup.Subnets | length')" = "2" ]
|
||||
[ "$(json_get "$output" '.CacheSubnetGroup.Subnets[0].SubnetAvailabilityZone.Name')" = "us-east-1a" ]
|
||||
|
||||
run aws_cmd elasticache describe-cache-subnet-groups --cache-subnet-group-name "$SNG"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.CacheSubnetGroups[0].CacheSubnetGroupName')" = "$SNG" ]
|
||||
|
||||
run aws_cmd elasticache create-cache-subnet-group \
|
||||
--cache-subnet-group-name "bats-absent-$(unique_name)" \
|
||||
--cache-subnet-group-description x --subnet-ids subnet-00000000000000000
|
||||
assert_failure
|
||||
assert_output --partial "are invalid"
|
||||
|
||||
run aws_cmd elasticache delete-cache-subnet-group --cache-subnet-group-name "$SNG"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "ElastiCache: a cache subnet group is built from subnets in the caller's region" {
|
||||
# Subnets exist in the region they were created in. Resolving them in the configured default
|
||||
# region instead rejects a caller's own subnets as invalid.
|
||||
export AWS_DEFAULT_REGION=eu-west-1
|
||||
vpc=$(aws_cmd ec2 create-vpc --cidr-block 10.31.0.0/16 --query 'Vpc.VpcId' --output text)
|
||||
s=$(aws_cmd ec2 create-subnet --vpc-id "$vpc" --cidr-block 10.31.1.0/24 \
|
||||
--availability-zone eu-west-1a --query 'Subnet.SubnetId' --output text)
|
||||
SNG_EU="bats-sng-eu-$(unique_name)"
|
||||
|
||||
run aws_cmd elasticache create-cache-subnet-group \
|
||||
--cache-subnet-group-name "$SNG_EU" \
|
||||
--cache-subnet-group-description "bats other region" \
|
||||
--subnet-ids "$s"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.CacheSubnetGroup.VpcId')" = "$vpc" ]
|
||||
[ "$(json_get "$output" '.CacheSubnetGroup.Subnets[0].SubnetAvailabilityZone.Name')" = "eu-west-1a" ]
|
||||
[[ "$(json_get "$output" '.CacheSubnetGroup.ARN')" == arn:aws:elasticache:eu-west-1:* ]]
|
||||
|
||||
run aws_cmd elasticache delete-cache-subnet-group --cache-subnet-group-name "$SNG_EU"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "ElastiCache: replication group keeps its encryption, snapshot settings and tags" {
|
||||
RG="bats-rg-$(unique_name)"
|
||||
run aws_cmd kms create-key --description "$RG"
|
||||
assert_success
|
||||
KEY_ARN=$(json_get "$output" '.KeyMetadata.Arn')
|
||||
KEY_ID=$(json_get "$output" '.KeyMetadata.KeyId')
|
||||
aws_cmd kms create-alias --alias-name "alias/$RG" --target-key-id "$KEY_ID" >/dev/null
|
||||
|
||||
run aws_cmd elasticache create-replication-group --replication-group-id "$RG" --replication-group-description d \
|
||||
--engine redis --cache-node-type cache.t3.micro --num-cache-clusters 1 \
|
||||
--at-rest-encryption-enabled --kms-key-id "alias/$RG" \
|
||||
--snapshot-retention-limit 7 --snapshot-window 06:30-07:30 --tags "Key=Name,Value=$RG"
|
||||
assert_success
|
||||
|
||||
run aws_cmd elasticache describe-replication-groups --replication-group-id "$RG" \
|
||||
--query 'ReplicationGroups[0].[KmsKeyId,AtRestEncryptionEnabled,SnapshotRetentionLimit,SnapshotWindow,ARN]'
|
||||
assert_success
|
||||
assert_output --partial "$KEY_ARN"
|
||||
assert_output --partial 'true'
|
||||
assert_output --partial '7'
|
||||
assert_output --partial '"06:30-07:30"'
|
||||
assert_output --partial "replicationgroup:$RG"
|
||||
ARN=$(aws_cmd elasticache describe-replication-groups --replication-group-id "$RG" --query 'ReplicationGroups[0].ARN' --output text)
|
||||
|
||||
run aws_cmd elasticache list-tags-for-resource --resource-name "$ARN"
|
||||
assert_success
|
||||
assert_output --partial "\"Value\": \"$RG\""
|
||||
|
||||
run aws_cmd elasticache modify-replication-group --replication-group-id "$RG" --snapshot-retention-limit 3 --snapshot-window 01:00-02:00 --apply-immediately
|
||||
assert_success
|
||||
run aws_cmd elasticache describe-replication-groups --replication-group-id "$RG" --query 'ReplicationGroups[0].[SnapshotRetentionLimit,SnapshotWindow]'
|
||||
assert_success
|
||||
assert_output --partial '3'
|
||||
assert_output --partial '"01:00-02:00"'
|
||||
|
||||
aws_cmd elasticache delete-replication-group --replication-group-id "$RG" >/dev/null 2>&1 || true
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env bats
|
||||
# AWS Fault Injection Service compatibility tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
TEMPLATE_ID=""
|
||||
TEMPLATE_ARN=""
|
||||
EXPERIMENT_ID=""
|
||||
TARGET_ACCOUNT_CONFIGURATION_EXISTS="false"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
# The safety lever is account-global, so always restore its safe default even
|
||||
# when an assertion aborts the lifecycle halfway through.
|
||||
aws_cmd fis update-safety-lever-state \
|
||||
--id default \
|
||||
--state '{"status":"disengaged","reason":"AWS CLI compatibility cleanup"}' \
|
||||
>/dev/null 2>&1 || true
|
||||
|
||||
if [ -n "$EXPERIMENT_ID" ]; then
|
||||
aws_cmd fis stop-experiment --id "$EXPERIMENT_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$TEMPLATE_ID" ] && [ "$TARGET_ACCOUNT_CONFIGURATION_EXISTS" = "true" ]; then
|
||||
aws_cmd fis delete-target-account-configuration \
|
||||
--experiment-template-id "$TEMPLATE_ID" \
|
||||
--account-id 111122223333 >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$TEMPLATE_ID" ]; then
|
||||
aws_cmd fis delete-experiment-template --id "$TEMPLATE_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# This safe lifecycle invokes every AWS FIS management operation. It uses only
|
||||
# the built-in wait action and starts the experiment with actionsMode=skip-all.
|
||||
@test "FIS: all management operations use AWS CLI v2 wire shapes" {
|
||||
local action_id target_resource_type client_token target_account_token
|
||||
local next_token state template_listed account_listed owner experiment_status
|
||||
local experiment_listed account_snapshot_listed
|
||||
|
||||
action_id="aws:fis:wait"
|
||||
target_resource_type="aws:ec2:instance"
|
||||
|
||||
run aws_cmd fis get-action --id "$action_id"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.action.id')" = "$action_id" ]
|
||||
[[ "$(json_get "$output" '.action.arn')" =~ :action/aws:fis:wait$ ]]
|
||||
|
||||
run aws_cmd fis list-actions --max-results 1
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.actions | length')" = "1" ]
|
||||
next_token=$(json_get "$output" '.nextToken')
|
||||
[ -n "$next_token" ]
|
||||
[ "$next_token" != "null" ]
|
||||
|
||||
run aws_cmd fis list-actions --max-results 1 --next-token "$next_token"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.actions | length')" = "1" ]
|
||||
|
||||
run aws_cmd fis get-target-resource-type --resource-type "$target_resource_type"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.targetResourceType.resourceType')" = "$target_resource_type" ]
|
||||
|
||||
run aws_cmd fis list-target-resource-types --max-results 1
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.targetResourceTypes | length')" = "1" ]
|
||||
next_token=$(json_get "$output" '.nextToken')
|
||||
[ -n "$next_token" ]
|
||||
[ "$next_token" != "null" ]
|
||||
|
||||
run aws_cmd fis list-target-resource-types --max-results 1 --next-token "$next_token"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.targetResourceTypes | length')" = "1" ]
|
||||
|
||||
run aws_cmd fis get-safety-lever --id default
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.safetyLever.id')" = "default" ]
|
||||
|
||||
run aws_cmd fis update-safety-lever-state \
|
||||
--id default \
|
||||
--state '{"status":"engaged","reason":"AWS CLI compatibility test"}'
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.safetyLever.state.status')" = "engaged" ]
|
||||
|
||||
run aws_cmd fis update-safety-lever-state \
|
||||
--id default \
|
||||
--state '{"status":"disengaged","reason":"AWS CLI compatibility test ready"}'
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.safetyLever.state.status')" = "disengaged" ]
|
||||
|
||||
client_token=$(unique_name fis-cli-template)
|
||||
run aws_cmd fis create-experiment-template \
|
||||
--client-token "$client_token" \
|
||||
--description "AWS CLI compatibility template" \
|
||||
--stop-conditions '[{"source":"none"}]' \
|
||||
--actions '{"wait":{"actionId":"aws:fis:wait","parameters":{"duration":"PT1M"}}}' \
|
||||
--role-arn "arn:aws:iam::000000000000:role/fis-cli-role" \
|
||||
--tags '{"suite":"awscli"}' \
|
||||
--experiment-options '{"accountTargeting":"multi-account","emptyTargetResolutionMode":"skip"}'
|
||||
assert_success
|
||||
TEMPLATE_ID=$(json_get "$output" '.experimentTemplate.id')
|
||||
TEMPLATE_ARN=$(json_get "$output" '.experimentTemplate.arn')
|
||||
[ -n "$TEMPLATE_ID" ]
|
||||
[[ "$TEMPLATE_ARN" =~ :experiment-template/${TEMPLATE_ID}$ ]]
|
||||
[ "$(json_get "$output" '.experimentTemplate.tags.suite')" = "awscli" ]
|
||||
|
||||
run aws_cmd fis get-experiment-template --id "$TEMPLATE_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.experimentTemplate.id')" = "$TEMPLATE_ID" ]
|
||||
|
||||
run aws_cmd fis update-experiment-template \
|
||||
--id "$TEMPLATE_ID" \
|
||||
--description "Updated through AWS CLI v2"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.experimentTemplate.description')" = "Updated through AWS CLI v2" ]
|
||||
|
||||
run aws_cmd fis list-experiment-templates --max-results 100
|
||||
assert_success
|
||||
template_listed=$(echo "$output" | jq --arg id "$TEMPLATE_ID" \
|
||||
'.experimentTemplates | any(.id == $id)')
|
||||
[ "$template_listed" = "true" ]
|
||||
|
||||
target_account_token=$(unique_name fis-cli-account)
|
||||
run aws_cmd fis create-target-account-configuration \
|
||||
--client-token "$target_account_token" \
|
||||
--experiment-template-id "$TEMPLATE_ID" \
|
||||
--account-id 111122223333 \
|
||||
--role-arn "arn:aws:iam::111122223333:role/fis-cli-target-role" \
|
||||
--description "AWS CLI target account"
|
||||
assert_success
|
||||
TARGET_ACCOUNT_CONFIGURATION_EXISTS="true"
|
||||
[ "$(json_get "$output" '.targetAccountConfiguration.accountId')" = "111122223333" ]
|
||||
|
||||
run aws_cmd fis get-target-account-configuration \
|
||||
--experiment-template-id "$TEMPLATE_ID" \
|
||||
--account-id 111122223333
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.targetAccountConfiguration.description')" = "AWS CLI target account" ]
|
||||
|
||||
run aws_cmd fis update-target-account-configuration \
|
||||
--experiment-template-id "$TEMPLATE_ID" \
|
||||
--account-id 111122223333 \
|
||||
--role-arn "arn:aws:iam::111122223333:role/fis-cli-target-role" \
|
||||
--description "Updated AWS CLI target account"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.targetAccountConfiguration.description')" = \
|
||||
"Updated AWS CLI target account" ]
|
||||
|
||||
run aws_cmd fis list-target-account-configurations \
|
||||
--experiment-template-id "$TEMPLATE_ID" \
|
||||
--max-results 100
|
||||
assert_success
|
||||
account_listed=$(echo "$output" | jq \
|
||||
'.targetAccountConfigurations | any(.accountId == "111122223333")')
|
||||
[ "$account_listed" = "true" ]
|
||||
|
||||
run aws_cmd fis tag-resource \
|
||||
--resource-arn "$TEMPLATE_ARN" \
|
||||
--tags '{"owner":"compatibility"}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd fis list-tags-for-resource --resource-arn "$TEMPLATE_ARN"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.tags.suite')" = "awscli" ]
|
||||
[ "$(json_get "$output" '.tags.owner')" = "compatibility" ]
|
||||
|
||||
run aws_cmd fis untag-resource \
|
||||
--resource-arn "$TEMPLATE_ARN" \
|
||||
--tag-keys owner
|
||||
assert_success
|
||||
|
||||
run aws_cmd fis list-tags-for-resource --resource-arn "$TEMPLATE_ARN"
|
||||
assert_success
|
||||
owner=$(json_get "$output" '.tags.owner // empty')
|
||||
[ -z "$owner" ]
|
||||
[ "$(json_get "$output" '.tags.suite')" = "awscli" ]
|
||||
|
||||
run aws_cmd fis start-experiment \
|
||||
--client-token "$(unique_name fis-cli-experiment)" \
|
||||
--experiment-template-id "$TEMPLATE_ID" \
|
||||
--experiment-options '{"actionsMode":"skip-all"}' \
|
||||
--tags '{"mode":"safe-preview"}'
|
||||
assert_success
|
||||
EXPERIMENT_ID=$(json_get "$output" '.experiment.id')
|
||||
[ -n "$EXPERIMENT_ID" ]
|
||||
[ "$(json_get "$output" '.experiment.experimentTemplateId')" = "$TEMPLATE_ID" ]
|
||||
[ "$(json_get "$output" '.experiment.experimentOptions.actionsMode')" = "skip-all" ]
|
||||
experiment_status=$(json_get "$output" '.experiment.state.status')
|
||||
[[ "$experiment_status" = "running" || "$experiment_status" = "completed" ]]
|
||||
|
||||
run aws_cmd fis get-experiment --id "$EXPERIMENT_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.experiment.id')" = "$EXPERIMENT_ID" ]
|
||||
[ "$(json_get "$output" '.experiment.tags.mode')" = "safe-preview" ]
|
||||
|
||||
run aws_cmd fis list-experiments \
|
||||
--experiment-template-id "$TEMPLATE_ID" \
|
||||
--max-results 100
|
||||
assert_success
|
||||
experiment_listed=$(echo "$output" | jq --arg id "$EXPERIMENT_ID" \
|
||||
'.experiments | any(.id == $id)')
|
||||
[ "$experiment_listed" = "true" ]
|
||||
|
||||
run aws_cmd fis list-experiment-resolved-targets \
|
||||
--experiment-id "$EXPERIMENT_ID" \
|
||||
--max-results 100
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.resolvedTargets | length')" = "0" ]
|
||||
|
||||
run aws_cmd fis get-experiment-target-account-configuration \
|
||||
--experiment-id "$EXPERIMENT_ID" \
|
||||
--account-id 111122223333
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.targetAccountConfiguration.accountId')" = "111122223333" ]
|
||||
|
||||
run aws_cmd fis list-experiment-target-account-configurations \
|
||||
--experiment-id "$EXPERIMENT_ID"
|
||||
assert_success
|
||||
account_snapshot_listed=$(echo "$output" | jq \
|
||||
'.targetAccountConfigurations | any(.accountId == "111122223333")')
|
||||
[ "$account_snapshot_listed" = "true" ]
|
||||
|
||||
run aws_cmd fis stop-experiment --id "$EXPERIMENT_ID"
|
||||
assert_success
|
||||
state=$(json_get "$output" '.experiment.state.status')
|
||||
[[ "$state" = "stopped" || "$state" = "completed" ]]
|
||||
EXPERIMENT_ID=""
|
||||
|
||||
run aws_cmd fis delete-target-account-configuration \
|
||||
--experiment-template-id "$TEMPLATE_ID" \
|
||||
--account-id 111122223333
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.targetAccountConfiguration.accountId')" = "111122223333" ]
|
||||
TARGET_ACCOUNT_CONFIGURATION_EXISTS="false"
|
||||
|
||||
run aws_cmd fis delete-experiment-template --id "$TEMPLATE_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.experimentTemplate.id')" = "$TEMPLATE_ID" ]
|
||||
TEMPLATE_ID=""
|
||||
TEMPLATE_ARN=""
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
DB_NAME="$(unique_name glue-catalog-db)"
|
||||
TABLE_NAME="catalog_table"
|
||||
SECOND_TABLE_NAME="catalog_table_second"
|
||||
FUNCTION_NAME="catalog_function"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
aws_cmd glue delete-user-defined-function \
|
||||
--database-name "$DB_NAME" \
|
||||
--function-name "$FUNCTION_NAME" >/dev/null 2>&1 || true
|
||||
aws_cmd glue delete-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--name "$SECOND_TABLE_NAME" >/dev/null 2>&1 || true
|
||||
aws_cmd glue delete-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--name "$TABLE_NAME" >/dev/null 2>&1 || true
|
||||
aws_cmd glue delete-database \
|
||||
--name "$DB_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
create_database() {
|
||||
aws_cmd glue create-database \
|
||||
--database-input "{\"Name\":\"$DB_NAME\",\"Description\":\"catalog compatibility database\"}" >/dev/null
|
||||
}
|
||||
|
||||
table_input() {
|
||||
local name="${1:-$TABLE_NAME}"
|
||||
local description="$2"
|
||||
jq -n \
|
||||
--arg name "$name" \
|
||||
--arg description "$description" \
|
||||
--arg location "s3://floci-glue-catalog/$DB_NAME/$name/" \
|
||||
'{
|
||||
Name: $name,
|
||||
Description: $description,
|
||||
Parameters: {
|
||||
classification: "json"
|
||||
},
|
||||
StorageDescriptor: {
|
||||
Location: $location,
|
||||
InputFormat: "org.apache.hadoop.mapred.TextInputFormat",
|
||||
OutputFormat: "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat",
|
||||
SerdeInfo: {
|
||||
SerializationLibrary: "org.openx.data.jsonserde.JsonSerDe",
|
||||
Parameters: {
|
||||
"serialization.format": "1"
|
||||
}
|
||||
},
|
||||
Columns: [
|
||||
{
|
||||
Name: "id",
|
||||
Type: "int",
|
||||
Parameters: {
|
||||
comment: "identifier"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
PartitionKeys: [
|
||||
{
|
||||
Name: "year",
|
||||
Type: "int"
|
||||
}
|
||||
]
|
||||
}'
|
||||
}
|
||||
|
||||
create_table() {
|
||||
local name="${1:-$TABLE_NAME}"
|
||||
local description="${2:-created}"
|
||||
aws_cmd glue create-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-input "$(table_input "$name" "$description")" >/dev/null
|
||||
}
|
||||
|
||||
column_statistics_input() {
|
||||
jq -n '[
|
||||
{
|
||||
ColumnName: "id",
|
||||
ColumnType: "int",
|
||||
AnalyzedTime: "2026-06-08T00:00:00Z",
|
||||
StatisticsData: {
|
||||
Type: "LONG",
|
||||
LongColumnStatisticsData: {
|
||||
MinimumValue: 1,
|
||||
MaximumValue: 10,
|
||||
NumberOfNulls: 0,
|
||||
NumberOfDistinctValues: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
]'
|
||||
}
|
||||
|
||||
function_input() {
|
||||
local owner="$1"
|
||||
jq -n \
|
||||
--arg name "$FUNCTION_NAME" \
|
||||
--arg owner "$owner" \
|
||||
'{
|
||||
FunctionName: $name,
|
||||
ClassName: "CatalogFunction",
|
||||
OwnerName: $owner,
|
||||
OwnerType: "USER",
|
||||
ResourceUris: [
|
||||
{
|
||||
ResourceType: "FILE",
|
||||
Uri: "s3://floci-glue-catalog/function.jar"
|
||||
}
|
||||
]
|
||||
}'
|
||||
}
|
||||
|
||||
@test "Glue catalog: database lifecycle" {
|
||||
run create_database
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-database --name "$DB_NAME"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.Database.Name')
|
||||
[ "$name" = "$DB_NAME" ]
|
||||
|
||||
run aws_cmd glue get-databases
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$DB_NAME" '.DatabaseList | any(.Name == $name)')
|
||||
[ "$found" = "true" ]
|
||||
|
||||
run aws_cmd glue update-database \
|
||||
--name "$DB_NAME" \
|
||||
--database-input "{\"Name\":\"$DB_NAME\",\"Description\":\"updated database\",\"LocationUri\":\"s3://floci-glue-catalog/$DB_NAME/\"}"
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-database --name "$DB_NAME"
|
||||
assert_success
|
||||
description=$(json_get "$output" '.Database.Description')
|
||||
location=$(json_get "$output" '.Database.LocationUri')
|
||||
[ "$description" = "updated database" ]
|
||||
[ "$location" = "s3://floci-glue-catalog/$DB_NAME/" ]
|
||||
|
||||
run aws_cmd glue update-database \
|
||||
--name "$DB_NAME" \
|
||||
--database-input "{\"Name\":\"${DB_NAME}-renamed\",\"Description\":\"renamed database\"}"
|
||||
assert_failure
|
||||
[[ "$output" == *"InvalidInputException"* ]]
|
||||
[[ "$output" == *"Database cannot be renamed"* ]]
|
||||
|
||||
run aws_cmd glue delete-database --name "$DB_NAME"
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-database --name "$DB_NAME"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "Glue catalog: table and partition lifecycle" {
|
||||
run create_database
|
||||
assert_success
|
||||
|
||||
run create_table
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--name "$TABLE_NAME"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.Table.Name')
|
||||
version_id=$(json_get "$output" '.Table.VersionId')
|
||||
column_comment=$(json_get "$output" '.Table.StorageDescriptor.Columns[0].Parameters.comment')
|
||||
[ "$name" = "$TABLE_NAME" ]
|
||||
[ "$version_id" = "0" ]
|
||||
[ "$column_comment" = "identifier" ]
|
||||
|
||||
run aws_cmd glue get-tables --database-name "$DB_NAME"
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$TABLE_NAME" '.TableList | any(.Name == $name)')
|
||||
[ "$found" = "true" ]
|
||||
|
||||
run aws_cmd glue update-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--version-id "$version_id" \
|
||||
--table-input "$(table_input "$TABLE_NAME" updated)"
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--name "$TABLE_NAME"
|
||||
assert_success
|
||||
description=$(json_get "$output" '.Table.Description')
|
||||
version_id=$(json_get "$output" '.Table.VersionId')
|
||||
[ "$description" = "updated" ]
|
||||
[ "$version_id" = "1" ]
|
||||
|
||||
run aws_cmd glue get-table-versions \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
version_count=$(echo "$output" | jq '.TableVersions | length')
|
||||
current_version=$(json_get "$output" '.TableVersions[0].VersionId')
|
||||
current_description=$(json_get "$output" '.TableVersions[0].Table.Description')
|
||||
archived_version=$(json_get "$output" '.TableVersions[1].VersionId')
|
||||
archived_description=$(json_get "$output" '.TableVersions[1].Table.Description')
|
||||
[ "$version_count" = "2" ]
|
||||
[ "$current_version" = "1" ]
|
||||
[ "$current_description" = "updated" ]
|
||||
[ "$archived_version" = "0" ]
|
||||
[ "$archived_description" = "created" ]
|
||||
|
||||
run aws_cmd glue update-column-statistics-for-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--column-statistics-list "$(column_statistics_input)"
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-column-statistics-for-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--column-names id missing
|
||||
assert_success
|
||||
column_name=$(json_get "$output" '.ColumnStatisticsList[0].ColumnName')
|
||||
statistics_type=$(json_get "$output" '.ColumnStatisticsList[0].StatisticsData.Type')
|
||||
minimum_value=$(json_get "$output" '.ColumnStatisticsList[0].StatisticsData.LongColumnStatisticsData.MinimumValue')
|
||||
missing_column_name=$(json_get "$output" '.Errors[0].ColumnName')
|
||||
error_code=$(json_get "$output" '.Errors[0].Error.ErrorCode')
|
||||
[ "$column_name" = "id" ]
|
||||
[ "$statistics_type" = "LONG" ]
|
||||
[ "$minimum_value" = "1" ]
|
||||
[ "$missing_column_name" = "missing" ]
|
||||
[ "$error_code" = "EntityNotFoundException" ]
|
||||
|
||||
run aws_cmd glue delete-column-statistics-for-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--column-name id
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue delete-column-statistics-for-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--column-name id
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-column-statistics-for-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--column-names id
|
||||
assert_success
|
||||
deleted_statistics_count=$(json_get "$output" '.ColumnStatisticsList | length')
|
||||
deleted_error_code=$(json_get "$output" '.Errors[0].Error.ErrorCode')
|
||||
[ "$deleted_statistics_count" = "0" ]
|
||||
[ "$deleted_error_code" = "EntityNotFoundException" ]
|
||||
|
||||
run aws_cmd glue create-partition \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--partition-input '{"Values":["2026"]}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue batch-create-partition \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--partition-input-list '[{"Values":["2027"]},{"Values":["2028"]}]'
|
||||
assert_success
|
||||
batch_create_error_count=$(json_get "$output" '.Errors | length')
|
||||
[ "$batch_create_error_count" = "0" ]
|
||||
|
||||
run aws_cmd glue batch-create-partition \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--partition-input-list '[{"Values":["2027"]}]'
|
||||
assert_success
|
||||
duplicate_partition_value=$(json_get "$output" '.Errors[0].PartitionValues[0]')
|
||||
duplicate_error_code=$(json_get "$output" '.Errors[0].ErrorDetail.ErrorCode')
|
||||
[ "$duplicate_partition_value" = "2027" ]
|
||||
[ "$duplicate_error_code" = "AlreadyExistsException" ]
|
||||
|
||||
batch_update_entries='[
|
||||
{"PartitionValueList":["2026"],"PartitionInput":{"Values":["2026"],"Parameters":{"after":"yes"}}},
|
||||
{"PartitionValueList":["missing"],"PartitionInput":{"Values":["missing"]}}
|
||||
]'
|
||||
run aws_cmd glue batch-update-partition \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--entries "$batch_update_entries"
|
||||
assert_success
|
||||
updated_missing_partition_value=$(json_get "$output" '.Errors[0].PartitionValueList[0]')
|
||||
updated_missing_error_code=$(json_get "$output" '.Errors[0].ErrorDetail.ErrorCode')
|
||||
[ "$updated_missing_partition_value" = "missing" ]
|
||||
[ "$updated_missing_error_code" = "EntityNotFoundException" ]
|
||||
|
||||
run aws_cmd glue update-partition \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--partition-value-list 2027 \
|
||||
--partition-input '{"Values":["2027"],"Parameters":{"updated":"yes"}}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue update-partition \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--partition-value-list missing \
|
||||
--partition-input '{"Values":["missing"]}'
|
||||
assert_failure
|
||||
[[ "$output" == *"EntityNotFoundException"* ]]
|
||||
|
||||
run aws_cmd glue get-partitions \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME"
|
||||
assert_success
|
||||
value=$(json_get "$output" '.Partitions[0].Values[0]')
|
||||
parameter=$(json_get "$output" '.Partitions[0].Parameters.after')
|
||||
[ "$value" = "2026" ]
|
||||
[ "$parameter" = "yes" ]
|
||||
|
||||
run aws_cmd glue get-partitions \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--expression "year >= 2026 AND year <= 2027"
|
||||
assert_success
|
||||
filtered_count=$(json_get "$output" '.Partitions | length')
|
||||
[ "$filtered_count" = "2" ]
|
||||
|
||||
run aws_cmd glue get-partitions \
|
||||
--database-name "$DB_NAME" \
|
||||
--table-name "$TABLE_NAME" \
|
||||
--expression "year in (2026, 2028)"
|
||||
assert_success
|
||||
filtered_count=$(json_get "$output" '.Partitions | length')
|
||||
[ "$filtered_count" = "2" ]
|
||||
|
||||
run aws_cmd glue delete-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--name "$TABLE_NAME"
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--name "$TABLE_NAME"
|
||||
assert_failure
|
||||
|
||||
run aws_cmd glue delete-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--name "$TABLE_NAME"
|
||||
assert_failure
|
||||
[[ "$output" == *"EntityNotFoundException"* ]]
|
||||
}
|
||||
|
||||
@test "Glue catalog: batch delete table" {
|
||||
run create_database
|
||||
assert_success
|
||||
|
||||
run create_table "$TABLE_NAME" "batch delete first table"
|
||||
assert_success
|
||||
|
||||
run create_table "$SECOND_TABLE_NAME" "batch delete second table"
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue batch-delete-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--tables-to-delete "$TABLE_NAME" "$SECOND_TABLE_NAME"
|
||||
assert_success
|
||||
error_count=$(echo "$output" | jq '.Errors | length')
|
||||
[ "$error_count" = "0" ]
|
||||
|
||||
run aws_cmd glue get-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--name "$TABLE_NAME"
|
||||
assert_failure
|
||||
|
||||
run aws_cmd glue get-table \
|
||||
--database-name "$DB_NAME" \
|
||||
--name "$SECOND_TABLE_NAME"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "Glue catalog: user-defined function lifecycle" {
|
||||
run create_database
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue create-user-defined-function \
|
||||
--database-name "$DB_NAME" \
|
||||
--function-input "$(function_input created-owner)"
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-user-defined-function \
|
||||
--database-name "$DB_NAME" \
|
||||
--function-name "$FUNCTION_NAME"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.UserDefinedFunction.FunctionName')
|
||||
owner=$(json_get "$output" '.UserDefinedFunction.OwnerName')
|
||||
[ "$name" = "$FUNCTION_NAME" ]
|
||||
[ "$owner" = "created-owner" ]
|
||||
|
||||
run aws_cmd glue get-user-defined-functions \
|
||||
--database-name "$DB_NAME" \
|
||||
--pattern "catalog_.*"
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$FUNCTION_NAME" '.UserDefinedFunctions | any(.FunctionName == $name)')
|
||||
[ "$found" = "true" ]
|
||||
|
||||
run aws_cmd glue update-user-defined-function \
|
||||
--database-name "$DB_NAME" \
|
||||
--function-name "$FUNCTION_NAME" \
|
||||
--function-input "$(function_input updated-owner)"
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-user-defined-function \
|
||||
--database-name "$DB_NAME" \
|
||||
--function-name "$FUNCTION_NAME"
|
||||
assert_success
|
||||
owner=$(json_get "$output" '.UserDefinedFunction.OwnerName')
|
||||
[ "$owner" = "updated-owner" ]
|
||||
|
||||
run aws_cmd glue delete-user-defined-function \
|
||||
--database-name "$DB_NAME" \
|
||||
--function-name "$FUNCTION_NAME"
|
||||
assert_success
|
||||
|
||||
run aws_cmd glue get-user-defined-function \
|
||||
--database-name "$DB_NAME" \
|
||||
--function-name "$FUNCTION_NAME"
|
||||
assert_failure
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env bats
|
||||
# IAM tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
ROLE_NAME="bats-test-role-$(unique_name)"
|
||||
POLICY_ARN=""
|
||||
ACCOUNT_ALIAS=""
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ -n "$POLICY_ARN" ]; then
|
||||
aws_cmd iam detach-role-policy --role-name "$ROLE_NAME" --policy-arn "$POLICY_ARN" >/dev/null 2>&1 || true
|
||||
aws_cmd iam delete-policy --policy-arn "$POLICY_ARN" >/dev/null 2>&1 || true
|
||||
fi
|
||||
aws_cmd iam delete-role --role-name "$ROLE_NAME" >/dev/null 2>&1 || true
|
||||
# An account holds one alias, so a leaked one would fail every later create.
|
||||
if [ -n "$ACCOUNT_ALIAS" ]; then
|
||||
aws_cmd iam delete-account-alias --account-alias "$ACCOUNT_ALIAS" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
@test "IAM: create role" {
|
||||
local policy_doc='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
||||
|
||||
run aws_cmd iam create-role \
|
||||
--role-name "$ROLE_NAME" \
|
||||
--assume-role-policy-document "$policy_doc"
|
||||
assert_success
|
||||
arn=$(json_get "$output" '.Role.Arn')
|
||||
[ -n "$arn" ]
|
||||
}
|
||||
|
||||
@test "IAM: get role" {
|
||||
local policy_doc='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
||||
aws_cmd iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document "$policy_doc" >/dev/null
|
||||
|
||||
run aws_cmd iam get-role --role-name "$ROLE_NAME"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.Role.RoleName')
|
||||
[ "$name" = "$ROLE_NAME" ]
|
||||
}
|
||||
|
||||
@test "IAM: list roles" {
|
||||
local policy_doc='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
||||
aws_cmd iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document "$policy_doc" >/dev/null
|
||||
|
||||
run aws_cmd iam list-roles
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$ROLE_NAME" '.Roles | any(.RoleName == $name)')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "IAM: create and delete policy" {
|
||||
local policy_doc='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}'
|
||||
|
||||
run aws_cmd iam create-policy \
|
||||
--policy-name "bats-test-policy-$(unique_name)" \
|
||||
--policy-document "$policy_doc"
|
||||
assert_success
|
||||
POLICY_ARN=$(json_get "$output" '.Policy.Arn')
|
||||
[ -n "$POLICY_ARN" ]
|
||||
|
||||
run aws_cmd iam delete-policy --policy-arn "$POLICY_ARN"
|
||||
assert_success
|
||||
POLICY_ARN=""
|
||||
}
|
||||
|
||||
@test "IAM: attach and detach role policy" {
|
||||
local role_policy_doc='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
||||
local policy_doc='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}'
|
||||
|
||||
aws_cmd iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document "$role_policy_doc" >/dev/null
|
||||
|
||||
out=$(aws_cmd iam create-policy --policy-name "bats-test-policy-$(unique_name)" --policy-document "$policy_doc")
|
||||
POLICY_ARN=$(json_get "$out" '.Policy.Arn')
|
||||
|
||||
run aws_cmd iam attach-role-policy --role-name "$ROLE_NAME" --policy-arn "$POLICY_ARN"
|
||||
assert_success
|
||||
|
||||
run aws_cmd iam detach-role-policy --role-name "$ROLE_NAME" --policy-arn "$POLICY_ARN"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "IAM: delete role" {
|
||||
local policy_doc='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
||||
aws_cmd iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document "$policy_doc" >/dev/null
|
||||
|
||||
run aws_cmd iam delete-role --role-name "$ROLE_NAME"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# ─── OIDC identity providers ────────────────────────────────────────────────
|
||||
|
||||
# Creates a provider and sets OIDC_ARN. Each test uses a unique URL so the suite
|
||||
# stays safe when bats runs test files in parallel.
|
||||
create_oidc_provider() {
|
||||
local url="https://oidc.bats.example.com/id/$(date +%s)-$$-${1:-0}"
|
||||
local out
|
||||
out=$(aws_cmd iam create-open-id-connect-provider \
|
||||
--url "$url" \
|
||||
--client-id-list sts.amazonaws.com \
|
||||
--thumbprint-list 9e99a48a9960b14926bb7f3b02e22da2b0ab7280)
|
||||
OIDC_ARN=$(json_get "$out" '.OpenIDConnectProviderArn')
|
||||
OIDC_HOST="${url#https://}"
|
||||
}
|
||||
|
||||
@test "IAM: create and get OIDC provider" {
|
||||
create_oidc_provider 1
|
||||
[ -n "$OIDC_ARN" ]
|
||||
|
||||
run aws_cmd iam get-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN"
|
||||
assert_success
|
||||
# AWS reports the URL without its scheme.
|
||||
url=$(json_get "$output" '.Url')
|
||||
[ "$url" = "$OIDC_HOST" ]
|
||||
client=$(json_get "$output" '.ClientIDList[0]')
|
||||
[ "$client" = "sts.amazonaws.com" ]
|
||||
|
||||
aws_cmd iam delete-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN" >/dev/null
|
||||
}
|
||||
|
||||
@test "IAM: list OIDC providers includes the created provider" {
|
||||
create_oidc_provider 2
|
||||
|
||||
run aws_cmd iam list-open-id-connect-providers
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg arn "$OIDC_ARN" '.OpenIDConnectProviderList | any(.Arn == $arn)')
|
||||
[ "$found" = "true" ]
|
||||
|
||||
aws_cmd iam delete-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN" >/dev/null
|
||||
}
|
||||
|
||||
@test "IAM: add and remove an OIDC client id" {
|
||||
create_oidc_provider 3
|
||||
|
||||
run aws_cmd iam add-client-id-to-open-id-connect-provider \
|
||||
--open-id-connect-provider-arn "$OIDC_ARN" --client-id extra.audience
|
||||
assert_success
|
||||
|
||||
run aws_cmd iam get-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN"
|
||||
assert_success
|
||||
count=$(json_get "$output" '.ClientIDList | length')
|
||||
[ "$count" = "2" ]
|
||||
|
||||
run aws_cmd iam remove-client-id-from-open-id-connect-provider \
|
||||
--open-id-connect-provider-arn "$OIDC_ARN" --client-id extra.audience
|
||||
assert_success
|
||||
|
||||
aws_cmd iam delete-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN" >/dev/null
|
||||
}
|
||||
|
||||
@test "IAM: update OIDC provider thumbprint" {
|
||||
create_oidc_provider 4
|
||||
|
||||
run aws_cmd iam update-open-id-connect-provider-thumbprint \
|
||||
--open-id-connect-provider-arn "$OIDC_ARN" \
|
||||
--thumbprint-list 1111111111111111111111111111111111111111
|
||||
assert_success
|
||||
|
||||
run aws_cmd iam get-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN"
|
||||
assert_success
|
||||
thumb=$(json_get "$output" '.ThumbprintList[0]')
|
||||
[ "$thumb" = "1111111111111111111111111111111111111111" ]
|
||||
|
||||
aws_cmd iam delete-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN" >/dev/null
|
||||
}
|
||||
|
||||
@test "IAM: delete OIDC provider" {
|
||||
create_oidc_provider 5
|
||||
|
||||
run aws_cmd iam delete-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN"
|
||||
assert_success
|
||||
|
||||
run aws_cmd iam get-open-id-connect-provider --open-id-connect-provider-arn "$OIDC_ARN"
|
||||
assert_failure
|
||||
assert_output --partial "NoSuchEntity"
|
||||
}
|
||||
|
||||
@test "IAM: OIDC provider url must be https" {
|
||||
run aws_cmd iam create-open-id-connect-provider \
|
||||
--url http://oidc.bats.example.com/id/insecure \
|
||||
--thumbprint-list 9e99a48a9960b14926bb7f3b02e22da2b0ab7280
|
||||
assert_failure
|
||||
assert_output --partial "ValidationError"
|
||||
}
|
||||
|
||||
@test "IAM: list account aliases is empty by default" {
|
||||
run aws_cmd iam list-account-aliases
|
||||
assert_success
|
||||
count=$(json_get "$output" '.AccountAliases | length')
|
||||
[ "$count" = "0" ]
|
||||
}
|
||||
|
||||
@test "IAM: create and list account alias" {
|
||||
ACCOUNT_ALIAS="$(unique_name bats-alias)"
|
||||
|
||||
run aws_cmd iam create-account-alias --account-alias "$ACCOUNT_ALIAS"
|
||||
assert_success
|
||||
|
||||
run aws_cmd iam list-account-aliases
|
||||
assert_success
|
||||
alias=$(json_get "$output" '.AccountAliases[0]')
|
||||
[ "$alias" = "$ACCOUNT_ALIAS" ]
|
||||
}
|
||||
|
||||
@test "IAM: creating another account alias replaces the current one" {
|
||||
ACCOUNT_ALIAS="$(unique_name bats-alias)"
|
||||
aws_cmd iam create-account-alias --account-alias "$ACCOUNT_ALIAS" >/dev/null
|
||||
|
||||
local replacement
|
||||
replacement="$(unique_name bats-alias-two)"
|
||||
run aws_cmd iam create-account-alias --account-alias "$replacement"
|
||||
assert_success
|
||||
ACCOUNT_ALIAS="$replacement"
|
||||
|
||||
run aws_cmd iam list-account-aliases
|
||||
assert_success
|
||||
alias=$(json_get "$output" '.AccountAliases[0]')
|
||||
[ "$alias" = "$replacement" ]
|
||||
}
|
||||
|
||||
@test "IAM: re-creating the alias the account already holds fails" {
|
||||
ACCOUNT_ALIAS="$(unique_name bats-alias)"
|
||||
aws_cmd iam create-account-alias --account-alias "$ACCOUNT_ALIAS" >/dev/null
|
||||
|
||||
run aws_cmd iam create-account-alias --account-alias "$ACCOUNT_ALIAS"
|
||||
assert_failure
|
||||
assert_output --partial "EntityAlreadyExists"
|
||||
}
|
||||
|
||||
@test "IAM: deleting a mismatched account alias fails" {
|
||||
ACCOUNT_ALIAS="$(unique_name bats-alias)"
|
||||
aws_cmd iam create-account-alias --account-alias "$ACCOUNT_ALIAS" >/dev/null
|
||||
|
||||
run aws_cmd iam delete-account-alias --account-alias "bats-alias-not-set-$$"
|
||||
assert_failure
|
||||
assert_output --partial "NoSuchEntity"
|
||||
}
|
||||
|
||||
@test "IAM: delete account alias" {
|
||||
ACCOUNT_ALIAS="$(unique_name bats-alias)"
|
||||
aws_cmd iam create-account-alias --account-alias "$ACCOUNT_ALIAS" >/dev/null
|
||||
|
||||
run aws_cmd iam delete-account-alias --account-alias "$ACCOUNT_ALIAS"
|
||||
assert_success
|
||||
|
||||
run aws_cmd iam list-account-aliases
|
||||
assert_success
|
||||
count=$(json_get "$output" '.AccountAliases | length')
|
||||
[ "$count" = "0" ]
|
||||
}
|
||||
|
||||
@test "IAM: malformed account alias is rejected" {
|
||||
run aws_cmd iam create-account-alias --account-alias "Upper-Case"
|
||||
assert_failure
|
||||
assert_output --partial "ValidationError"
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
#!/usr/bin/env bats
|
||||
# KMS tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
KEY_ID=""
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ -n "$KEY_ID" ]; then
|
||||
aws_cmd kms schedule-key-deletion --key-id "$KEY_ID" --pending-window-in-days 7 >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
@test "KMS: create key" {
|
||||
run aws_cmd kms create-key --description "bats-test-key"
|
||||
assert_success
|
||||
KEY_ID=$(json_get "$output" '.KeyMetadata.KeyId')
|
||||
[ -n "$KEY_ID" ]
|
||||
}
|
||||
|
||||
@test "KMS: describe key" {
|
||||
out=$(aws_cmd kms create-key --description "bats-test-key")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
run aws_cmd kms describe-key --key-id "$KEY_ID"
|
||||
assert_success
|
||||
enabled=$(json_get "$output" '.KeyMetadata.Enabled')
|
||||
[ "$enabled" = "true" ]
|
||||
}
|
||||
|
||||
@test "KMS: list keys" {
|
||||
out=$(aws_cmd kms create-key --description "bats-test-key")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
run aws_cmd kms list-keys
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg id "$KEY_ID" '.Keys | any(.KeyId == $id)')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "KMS: list grants" {
|
||||
out=$(aws_cmd kms create-key --description "bats-test-key")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
[ -n "$KEY_ID" ]
|
||||
|
||||
run aws_cmd kms list-grants --key-id "$KEY_ID"
|
||||
assert_success
|
||||
has_grants=$(echo "$output" | jq 'has("Grants") and (.Grants | type == "array")')
|
||||
[ "$has_grants" = "true" ]
|
||||
}
|
||||
|
||||
@test "KMS: encrypt and decrypt" {
|
||||
out=$(aws_cmd kms create-key --description "bats-test-key")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
# Create a temp file with plaintext
|
||||
local plaintext_file ciphertext_file decrypted_file
|
||||
plaintext_file=$(mktemp)
|
||||
ciphertext_file=$(mktemp)
|
||||
decrypted_file=$(mktemp)
|
||||
echo -n "hello-kms-bats" > "$plaintext_file"
|
||||
|
||||
run aws_cmd kms encrypt \
|
||||
--key-id "$KEY_ID" \
|
||||
--plaintext "fileb://$plaintext_file" \
|
||||
--output text \
|
||||
--query CiphertextBlob
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$ciphertext_file"
|
||||
|
||||
run aws_cmd kms decrypt \
|
||||
--ciphertext-blob "fileb://$ciphertext_file" \
|
||||
--output text \
|
||||
--query Plaintext
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$decrypted_file"
|
||||
|
||||
decrypted=$(cat "$decrypted_file")
|
||||
[ "$decrypted" = "hello-kms-bats" ]
|
||||
|
||||
rm -f "$plaintext_file" "$ciphertext_file" "$decrypted_file"
|
||||
}
|
||||
|
||||
@test "KMS: generate data key" {
|
||||
out=$(aws_cmd kms create-key --description "bats-test-key")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
run aws_cmd kms generate-data-key --key-id "$KEY_ID" --key-spec AES_256
|
||||
assert_success
|
||||
plaintext=$(json_get "$output" '.Plaintext')
|
||||
[ -n "$plaintext" ]
|
||||
ciphertext=$(json_get "$output" '.CiphertextBlob')
|
||||
[ -n "$ciphertext" ]
|
||||
}
|
||||
|
||||
# --- KMS Alias Tests ---
|
||||
|
||||
@test "KMS: create alias" {
|
||||
out=$(aws_cmd kms create-key --description "bats-test-key")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
alias_name="alias/bats-test-$(unique_name)"
|
||||
|
||||
run aws_cmd kms create-alias --alias-name "$alias_name" --target-key-id "$KEY_ID"
|
||||
assert_success
|
||||
|
||||
# Cleanup alias
|
||||
aws_cmd kms delete-alias --alias-name "$alias_name" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "KMS: list aliases" {
|
||||
out=$(aws_cmd kms create-key --description "bats-test-key")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
alias_name="alias/bats-test-$(unique_name)"
|
||||
|
||||
aws_cmd kms create-alias --alias-name "$alias_name" --target-key-id "$KEY_ID" >/dev/null
|
||||
|
||||
run aws_cmd kms list-aliases
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$alias_name" '.Aliases | any(.AliasName == $name)')
|
||||
[ "$found" = "true" ]
|
||||
|
||||
# Cleanup alias
|
||||
aws_cmd kms delete-alias --alias-name "$alias_name" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "KMS: create HMAC key and describe returns MacAlgorithms" {
|
||||
out=$(aws_cmd kms create-key \
|
||||
--description "bats-hmac-$(unique_name)" \
|
||||
--key-spec HMAC_256 \
|
||||
--key-usage GENERATE_VERIFY_MAC)
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
[ -n "$KEY_ID" ]
|
||||
|
||||
spec=$(json_get "$out" '.KeyMetadata.KeySpec')
|
||||
[ "$spec" = "HMAC_256" ]
|
||||
|
||||
run aws_cmd kms describe-key --key-id "$KEY_ID"
|
||||
assert_success
|
||||
macs=$(echo "$output" | jq -r '.KeyMetadata.MacAlgorithms[0]')
|
||||
[ "$macs" = "HMAC_SHA_256" ]
|
||||
}
|
||||
|
||||
@test "KMS: delete alias" {
|
||||
out=$(aws_cmd kms create-key --description "bats-test-key")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
alias_name="alias/bats-test-$(unique_name)"
|
||||
|
||||
aws_cmd kms create-alias --alias-name "$alias_name" --target-key-id "$KEY_ID" >/dev/null
|
||||
|
||||
run aws_cmd kms delete-alias --alias-name "$alias_name"
|
||||
assert_success
|
||||
|
||||
# Verify alias is deleted
|
||||
run aws_cmd kms list-aliases
|
||||
found=$(echo "$output" | jq --arg name "$alias_name" '.Aliases | any(.AliasName == $name)')
|
||||
[ "$found" = "false" ]
|
||||
}
|
||||
|
||||
@test "KMS: create key with reserved override tag uses pinned ID and strips reserved tag" {
|
||||
run aws_cmd kms create-key \
|
||||
--description "override-key" \
|
||||
--tags TagKey=floci:override-id,TagValue=bats-pinned-key TagKey=env,TagValue=test
|
||||
assert_success
|
||||
KEY_ID=$(json_get "$output" '.KeyMetadata.KeyId')
|
||||
[ "$KEY_ID" = "bats-pinned-key" ]
|
||||
|
||||
run aws_cmd kms list-resource-tags --key-id "$KEY_ID"
|
||||
assert_success
|
||||
has_reserved=$(echo "$output" | jq 'any(.Tags[]?; .TagKey == "floci:override-id")')
|
||||
[ "$has_reserved" = "false" ]
|
||||
env_value=$(echo "$output" | jq -r '.Tags[] | select(.TagKey == "env") | .TagValue')
|
||||
[ "$env_value" = "test" ]
|
||||
}
|
||||
|
||||
@test "KMS: duplicate reserved override tag fails with AlreadyExistsException" {
|
||||
out=$(aws_cmd kms create-key \
|
||||
--description "override-key" \
|
||||
--tags TagKey=floci:override-id,TagValue=bats-duplicate-key)
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
run aws_cmd kms create-key \
|
||||
--description "override-key-2" \
|
||||
--tags TagKey=floci:override-id,TagValue=bats-duplicate-key
|
||||
assert_failure
|
||||
[[ "$output" == *"AlreadyExistsException"* ]]
|
||||
}
|
||||
|
||||
@test "KMS: tag-resource rejects reserved override tag after creation" {
|
||||
out=$(aws_cmd kms create-key --description "bats-test-key")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
run aws_cmd kms tag-resource \
|
||||
--key-id "$KEY_ID" \
|
||||
--tags TagKey=floci:override-id,TagValue=late-id
|
||||
assert_failure
|
||||
[[ "$output" == *"ValidationException"* ]]
|
||||
}
|
||||
|
||||
# Issue #1844: Decrypt must enforce KeyId against wrapping key
|
||||
@test "KMS: decrypt with matching KeyId returns plaintext" {
|
||||
out=$(aws_cmd kms create-key --description "bats-1844-match")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
plaintext_file=$(mktemp)
|
||||
ciphertext_file=$(mktemp)
|
||||
echo -n "secret data" > "$plaintext_file"
|
||||
|
||||
run aws_cmd kms encrypt \
|
||||
--key-id "$KEY_ID" \
|
||||
--plaintext "fileb://$plaintext_file" \
|
||||
--output text \
|
||||
--query CiphertextBlob
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$ciphertext_file"
|
||||
|
||||
run aws_cmd kms decrypt \
|
||||
--ciphertext-blob "fileb://$ciphertext_file" \
|
||||
--key-id "$KEY_ID" \
|
||||
--output text \
|
||||
--query Plaintext
|
||||
assert_success
|
||||
[ "$(echo "$output" | base64 -d)" = "secret data" ]
|
||||
|
||||
rm -f "$plaintext_file" "$ciphertext_file"
|
||||
}
|
||||
|
||||
@test "KMS: decrypt with mismatched KeyId returns IncorrectKeyException" {
|
||||
out_a=$(aws_cmd kms create-key --description "bats-1844-a")
|
||||
key_a=$(json_get "$out_a" '.KeyMetadata.KeyId')
|
||||
out_b=$(aws_cmd kms create-key --description "bats-1844-b")
|
||||
key_b=$(json_get "$out_b" '.KeyMetadata.KeyId')
|
||||
|
||||
plaintext_file=$(mktemp)
|
||||
ciphertext_file=$(mktemp)
|
||||
echo -n "secret data" > "$plaintext_file"
|
||||
|
||||
run aws_cmd kms encrypt \
|
||||
--key-id "$key_a" \
|
||||
--plaintext "fileb://$plaintext_file" \
|
||||
--output text \
|
||||
--query CiphertextBlob
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$ciphertext_file"
|
||||
|
||||
run aws_cmd kms decrypt \
|
||||
--ciphertext-blob "fileb://$ciphertext_file" \
|
||||
--key-id "$key_b"
|
||||
assert_failure
|
||||
[[ "$output" == *"IncorrectKeyException"* ]]
|
||||
|
||||
rm -f "$plaintext_file" "$ciphertext_file"
|
||||
aws_cmd kms schedule-key-deletion --key-id "$key_a" --pending-window-in-days 7 >/dev/null 2>&1 || true
|
||||
aws_cmd kms schedule-key-deletion --key-id "$key_b" --pending-window-in-days 7 >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "KMS: reencrypt with matching SourceKeyId re-wraps ciphertext" {
|
||||
out_a=$(aws_cmd kms create-key --description "bats-reencrypt-match-a")
|
||||
key_a=$(json_get "$out_a" '.KeyMetadata.KeyId')
|
||||
out_b=$(aws_cmd kms create-key --description "bats-reencrypt-match-b")
|
||||
key_b=$(json_get "$out_b" '.KeyMetadata.KeyId')
|
||||
|
||||
plaintext_file=$(mktemp)
|
||||
ciphertext_file=$(mktemp)
|
||||
reencrypted_file=$(mktemp)
|
||||
decrypted_file=$(mktemp)
|
||||
echo -n "reencrypt test data" > "$plaintext_file"
|
||||
|
||||
run aws_cmd kms encrypt \
|
||||
--key-id "$key_a" \
|
||||
--plaintext "fileb://$plaintext_file" \
|
||||
--output text \
|
||||
--query CiphertextBlob
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$ciphertext_file"
|
||||
|
||||
run aws_cmd kms re-encrypt \
|
||||
--ciphertext-blob "fileb://$ciphertext_file" \
|
||||
--source-key-id "$key_a" \
|
||||
--destination-key-id "$key_b" \
|
||||
--output text \
|
||||
--query CiphertextBlob
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$reencrypted_file"
|
||||
|
||||
run aws_cmd kms decrypt \
|
||||
--ciphertext-blob "fileb://$reencrypted_file" \
|
||||
--output text \
|
||||
--query Plaintext
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$decrypted_file"
|
||||
[ "$(cat "$decrypted_file")" = "reencrypt test data" ]
|
||||
|
||||
rm -f "$plaintext_file" "$ciphertext_file" "$reencrypted_file" "$decrypted_file"
|
||||
aws_cmd kms schedule-key-deletion --key-id "$key_a" --pending-window-in-days 7 >/dev/null 2>&1 || true
|
||||
aws_cmd kms schedule-key-deletion --key-id "$key_b" --pending-window-in-days 7 >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "KMS: reencrypt with mismatched SourceKeyId returns IncorrectKeyException" {
|
||||
out_a=$(aws_cmd kms create-key --description "bats-reencrypt-mismatch-a")
|
||||
key_a=$(json_get "$out_a" '.KeyMetadata.KeyId')
|
||||
out_b=$(aws_cmd kms create-key --description "bats-reencrypt-mismatch-b")
|
||||
key_b=$(json_get "$out_b" '.KeyMetadata.KeyId')
|
||||
|
||||
plaintext_file=$(mktemp)
|
||||
ciphertext_file=$(mktemp)
|
||||
echo -n "reencrypt test data" > "$plaintext_file"
|
||||
|
||||
run aws_cmd kms encrypt \
|
||||
--key-id "$key_a" \
|
||||
--plaintext "fileb://$plaintext_file" \
|
||||
--output text \
|
||||
--query CiphertextBlob
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$ciphertext_file"
|
||||
|
||||
run aws_cmd kms re-encrypt \
|
||||
--ciphertext-blob "fileb://$ciphertext_file" \
|
||||
--source-key-id "$key_b" \
|
||||
--destination-key-id "$key_a"
|
||||
assert_failure
|
||||
[[ "$output" == *"IncorrectKeyException"* ]]
|
||||
|
||||
rm -f "$plaintext_file" "$ciphertext_file"
|
||||
aws_cmd kms schedule-key-deletion --key-id "$key_a" --pending-window-in-days 7 >/dev/null 2>&1 || true
|
||||
aws_cmd kms schedule-key-deletion --key-id "$key_b" --pending-window-in-days 7 >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "KMS: decrypt with disabled key returns DisabledException" {
|
||||
out=$(aws_cmd kms create-key --description "bats-disabled-decrypt")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
plaintext_file=$(mktemp)
|
||||
ciphertext_file=$(mktemp)
|
||||
echo -n "secret data" > "$plaintext_file"
|
||||
|
||||
run aws_cmd kms encrypt \
|
||||
--key-id "$KEY_ID" \
|
||||
--plaintext "fileb://$plaintext_file" \
|
||||
--output text \
|
||||
--query CiphertextBlob
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$ciphertext_file"
|
||||
|
||||
run aws_cmd kms disable-key --key-id "$KEY_ID"
|
||||
assert_success
|
||||
|
||||
run aws_cmd kms decrypt \
|
||||
--ciphertext-blob "fileb://$ciphertext_file" \
|
||||
--key-id "$KEY_ID"
|
||||
assert_failure
|
||||
[[ "$output" == *"DisabledException"* ]]
|
||||
|
||||
rm -f "$plaintext_file" "$ciphertext_file"
|
||||
}
|
||||
|
||||
@test "KMS: encrypt with disabled key returns DisabledException" {
|
||||
out=$(aws_cmd kms create-key --description "bats-disabled-encrypt")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
run aws_cmd kms disable-key --key-id "$KEY_ID"
|
||||
assert_success
|
||||
|
||||
plaintext_file=$(mktemp)
|
||||
echo -n "secret data" > "$plaintext_file"
|
||||
|
||||
run aws_cmd kms encrypt --key-id "$KEY_ID" --plaintext "fileb://$plaintext_file"
|
||||
assert_failure
|
||||
[[ "$output" == *"DisabledException"* ]]
|
||||
|
||||
rm -f "$plaintext_file"
|
||||
}
|
||||
|
||||
@test "KMS: decrypt with pending deletion key returns KmsInvalidStateException" {
|
||||
out=$(aws_cmd kms create-key --description "bats-pending-decrypt")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
plaintext_file=$(mktemp)
|
||||
ciphertext_file=$(mktemp)
|
||||
echo -n "secret data" > "$plaintext_file"
|
||||
|
||||
run aws_cmd kms encrypt \
|
||||
--key-id "$KEY_ID" \
|
||||
--plaintext "fileb://$plaintext_file" \
|
||||
--output text \
|
||||
--query CiphertextBlob
|
||||
assert_success
|
||||
echo "$output" | base64 -d > "$ciphertext_file"
|
||||
|
||||
run aws_cmd kms schedule-key-deletion --key-id "$KEY_ID" --pending-window-in-days 7
|
||||
assert_success
|
||||
|
||||
run aws_cmd kms decrypt \
|
||||
--ciphertext-blob "fileb://$ciphertext_file" \
|
||||
--key-id "$KEY_ID"
|
||||
assert_failure
|
||||
[[ "$output" == *"KMSInvalidStateException"* ]]
|
||||
|
||||
rm -f "$plaintext_file" "$ciphertext_file"
|
||||
}
|
||||
|
||||
@test "KMS: encrypt with pending deletion key returns KmsInvalidStateException" {
|
||||
out=$(aws_cmd kms create-key --description "bats-pending-encrypt")
|
||||
KEY_ID=$(json_get "$out" '.KeyMetadata.KeyId')
|
||||
|
||||
run aws_cmd kms schedule-key-deletion --key-id "$KEY_ID" --pending-window-in-days 7
|
||||
assert_success
|
||||
|
||||
plaintext_file=$(mktemp)
|
||||
echo -n "secret data" > "$plaintext_file"
|
||||
|
||||
run aws_cmd kms encrypt --key-id "$KEY_ID" --plaintext "fileb://$plaintext_file"
|
||||
assert_failure
|
||||
[[ "$output" == *"KMSInvalidStateException"* ]]
|
||||
|
||||
rm -f "$plaintext_file"
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env bats
|
||||
# Lambda MicroVMs integration tests
|
||||
#
|
||||
# Two AWS service models sit behind these commands: `lambda-microvms` (images
|
||||
# and MicroVMs) and `lambda-core` (network connectors). Both sign as lambda.
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
IMAGE_NAME=""
|
||||
MICROVM_ID=""
|
||||
CONNECTOR_ID=""
|
||||
BASE_IMAGE_ARN="arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1"
|
||||
BUILD_ROLE_ARN="arn:aws:iam::000000000000:role/microvm-build"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ -n "$MICROVM_ID" ]; then
|
||||
aws_cmd lambda-microvms terminate-microvm --microvm-identifier "$MICROVM_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$IMAGE_NAME" ]; then
|
||||
aws_cmd lambda-microvms delete-microvm-image --image-identifier "$IMAGE_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$CONNECTOR_ID" ]; then
|
||||
aws_cmd lambda-core delete-network-connector --id "$CONNECTOR_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
create_image() {
|
||||
local name="$1"
|
||||
aws_cmd lambda-microvms create-microvm-image \
|
||||
--name "$name" \
|
||||
--base-image-arn "$BASE_IMAGE_ARN" \
|
||||
--build-role-arn "$BUILD_ROLE_ARN" \
|
||||
--code-artifact "uri=s3://bucket/code.zip"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Image lifecycle
|
||||
# ============================================
|
||||
|
||||
@test "MicroVMs: create image" {
|
||||
IMAGE_NAME=$(unique_name "cli-img")
|
||||
run create_image "$IMAGE_NAME"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.name')
|
||||
state=$(json_get "$output" '.state')
|
||||
version=$(json_get "$output" '.imageVersion')
|
||||
arn=$(json_get "$output" '.imageArn')
|
||||
[ "$name" = "$IMAGE_NAME" ]
|
||||
[ "$state" = "CREATING" ]
|
||||
[ "$version" = "1.0" ]
|
||||
[[ "$arn" =~ ^arn:aws:lambda: ]]
|
||||
}
|
||||
|
||||
@test "MicroVMs: image settles to CREATED with an active version" {
|
||||
IMAGE_NAME=$(unique_name "cli-img")
|
||||
create_image "$IMAGE_NAME" >/dev/null
|
||||
|
||||
run aws_cmd lambda-microvms get-microvm-image --image-identifier "$IMAGE_NAME"
|
||||
assert_success
|
||||
state=$(json_get "$output" '.state')
|
||||
latest=$(json_get "$output" '.latestActiveImageVersion')
|
||||
[ "$state" = "CREATED" ]
|
||||
[ "$latest" = "1.0" ]
|
||||
}
|
||||
|
||||
@test "MicroVMs: list images includes the created image" {
|
||||
IMAGE_NAME=$(unique_name "cli-img")
|
||||
create_image "$IMAGE_NAME" >/dev/null
|
||||
|
||||
run aws_cmd lambda-microvms list-microvm-images
|
||||
assert_success
|
||||
assert_output --partial "$IMAGE_NAME"
|
||||
}
|
||||
|
||||
@test "MicroVMs: version and build converge to SUCCESSFUL" {
|
||||
IMAGE_NAME=$(unique_name "cli-img")
|
||||
create_image "$IMAGE_NAME" >/dev/null
|
||||
|
||||
run aws_cmd lambda-microvms list-microvm-image-builds \
|
||||
--image-identifier "$IMAGE_NAME" --image-version 1.0
|
||||
assert_success
|
||||
build_id=$(json_get "$output" '.items[0].buildId')
|
||||
build_state=$(json_get "$output" '.items[0].buildState')
|
||||
[ "$build_state" = "SUCCESSFUL" ]
|
||||
[ -n "$build_id" ]
|
||||
|
||||
run aws_cmd lambda-microvms get-microvm-image-build \
|
||||
--image-identifier "$IMAGE_NAME" --image-version 1.0 --build-id "$build_id"
|
||||
assert_success
|
||||
arch=$(json_get "$output" '.architecture')
|
||||
[ "$arch" = "ARM_64" ]
|
||||
|
||||
run aws_cmd lambda-microvms get-microvm-image-version \
|
||||
--image-identifier "$IMAGE_NAME" --image-version 1.0
|
||||
assert_success
|
||||
status=$(json_get "$output" '.status')
|
||||
[ "$status" = "ACTIVE" ]
|
||||
}
|
||||
|
||||
@test "MicroVMs: invalid image name is rejected" {
|
||||
run create_image "bad name!"
|
||||
assert_failure
|
||||
assert_output --partial "ValidationException"
|
||||
}
|
||||
|
||||
@test "MicroVMs: get a nonexistent image returns not found" {
|
||||
run aws_cmd lambda-microvms get-microvm-image --image-identifier "cli-no-such-image"
|
||||
assert_failure
|
||||
assert_output --partial "ResourceNotFound"
|
||||
}
|
||||
|
||||
@test "MicroVMs: managed catalog lists a base image" {
|
||||
run aws_cmd lambda-microvms list-managed-microvm-images
|
||||
assert_success
|
||||
arn=$(json_get "$output" '.items[0].imageArn')
|
||||
[ -n "$arn" ]
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# MicroVM lifecycle
|
||||
# ============================================
|
||||
|
||||
@test "MicroVMs: run a MicroVM and read it back running" {
|
||||
IMAGE_NAME=$(unique_name "cli-img")
|
||||
create_image "$IMAGE_NAME" >/dev/null
|
||||
|
||||
run aws_cmd lambda-microvms run-microvm --image-identifier "$IMAGE_NAME"
|
||||
assert_success
|
||||
MICROVM_ID=$(json_get "$output" '.microvmId')
|
||||
state=$(json_get "$output" '.state')
|
||||
duration=$(json_get "$output" '.maximumDurationInSeconds')
|
||||
[[ "$MICROVM_ID" =~ ^microvm- ]]
|
||||
[ "$state" = "PENDING" ]
|
||||
[ "$duration" = "28800" ]
|
||||
|
||||
run aws_cmd lambda-microvms get-microvm --microvm-identifier "$MICROVM_ID"
|
||||
assert_success
|
||||
state=$(json_get "$output" '.state')
|
||||
[ "$state" = "RUNNING" ]
|
||||
}
|
||||
|
||||
@test "MicroVMs: run against a missing image returns not found" {
|
||||
run aws_cmd lambda-microvms run-microvm --image-identifier "cli-no-such-image"
|
||||
assert_failure
|
||||
assert_output --partial "ResourceNotFound"
|
||||
}
|
||||
|
||||
@test "MicroVMs: an image with a running MicroVM refuses delete" {
|
||||
IMAGE_NAME=$(unique_name "cli-img")
|
||||
create_image "$IMAGE_NAME" >/dev/null
|
||||
out=$(aws_cmd lambda-microvms run-microvm --image-identifier "$IMAGE_NAME")
|
||||
MICROVM_ID=$(json_get "$out" '.microvmId')
|
||||
|
||||
run aws_cmd lambda-microvms delete-microvm-image --image-identifier "$IMAGE_NAME"
|
||||
assert_failure
|
||||
assert_output --partial "running microvms"
|
||||
}
|
||||
|
||||
@test "MicroVMs: terminate is idempotent only once" {
|
||||
IMAGE_NAME=$(unique_name "cli-img")
|
||||
create_image "$IMAGE_NAME" >/dev/null
|
||||
out=$(aws_cmd lambda-microvms run-microvm --image-identifier "$IMAGE_NAME")
|
||||
microvm_id=$(json_get "$out" '.microvmId')
|
||||
|
||||
run aws_cmd lambda-microvms terminate-microvm --microvm-identifier "$microvm_id"
|
||||
assert_success
|
||||
|
||||
run aws_cmd lambda-microvms get-microvm --microvm-identifier "$microvm_id"
|
||||
assert_success
|
||||
state=$(json_get "$output" '.state')
|
||||
[ "$state" = "TERMINATED" ]
|
||||
|
||||
# Terminal-state mutations are a ValidationException, not a second success.
|
||||
run aws_cmd lambda-microvms terminate-microvm --microvm-identifier "$microvm_id"
|
||||
assert_failure
|
||||
assert_output --partial "state cannot be changed"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Tagging
|
||||
# ============================================
|
||||
|
||||
@test "MicroVMs: tag, read and untag an image ARN" {
|
||||
IMAGE_NAME=$(unique_name "cli-img")
|
||||
out=$(create_image "$IMAGE_NAME")
|
||||
image_arn=$(json_get "$out" '.imageArn')
|
||||
|
||||
run aws_cmd lambda-microvms tag-resource --resource "$image_arn" --tags team=conformance
|
||||
assert_success
|
||||
|
||||
run aws_cmd lambda-microvms list-tags --resource "$image_arn"
|
||||
assert_success
|
||||
team=$(json_get "$output" '.Tags.team')
|
||||
[ "$team" = "conformance" ]
|
||||
|
||||
run aws_cmd lambda-microvms untag-resource --resource "$image_arn" --tag-keys team
|
||||
assert_success
|
||||
|
||||
run aws_cmd lambda-microvms list-tags --resource "$image_arn"
|
||||
assert_success
|
||||
team=$(json_get "$output" '.Tags.team')
|
||||
[ "$team" = "null" ] || [ -z "$team" ]
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Network connectors (lambda-core)
|
||||
# ============================================
|
||||
|
||||
@test "MicroVMs: network connector lifecycle" {
|
||||
name=$(unique_name "cli-conn")
|
||||
run aws_cmd lambda-core create-network-connector \
|
||||
--name "$name" \
|
||||
--operator-role "arn:aws:iam::000000000000:role/microvm-connector-operator" \
|
||||
--configuration "VpcEgressConfiguration={AssociatedComputeResourceTypes=MicroVm,NetworkProtocol=IPv4,SubnetIds=subnet-0000000000000cli1,SecurityGroupIds=sg-0000000000000cli1}"
|
||||
assert_success
|
||||
CONNECTOR_ID=$(json_get "$output" '.Id')
|
||||
state=$(json_get "$output" '.State')
|
||||
[ -n "$CONNECTOR_ID" ]
|
||||
[ "$state" = "PENDING" ]
|
||||
|
||||
run aws_cmd lambda-core get-network-connector --id "$CONNECTOR_ID"
|
||||
assert_success
|
||||
state=$(json_get "$output" '.State')
|
||||
got_name=$(json_get "$output" '.Name')
|
||||
[ "$state" = "ACTIVE" ]
|
||||
[ "$got_name" = "$name" ]
|
||||
|
||||
run aws_cmd lambda-core list-network-connectors
|
||||
assert_success
|
||||
assert_output --partial "$CONNECTOR_ID"
|
||||
|
||||
run aws_cmd lambda-core delete-network-connector --id "$CONNECTOR_ID"
|
||||
assert_success
|
||||
CONNECTOR_ID=""
|
||||
}
|
||||
|
||||
@test "MicroVMs: get a nonexistent connector returns not found" {
|
||||
run aws_cmd lambda-core get-network-connector --id "nc-00000000000000cli"
|
||||
assert_failure
|
||||
assert_output --partial "ResourceNotFound"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Region isolation
|
||||
# ============================================
|
||||
|
||||
@test "MicroVMs: an image created in one region is not visible in another" {
|
||||
IMAGE_NAME=$(unique_name "cli-img")
|
||||
create_image "$IMAGE_NAME" >/dev/null
|
||||
|
||||
run aws --endpoint-url "$FLOCI_ENDPOINT" --region eu-west-1 --output json \
|
||||
lambda-microvms get-microvm-image --image-identifier "$IMAGE_NAME" 2>&1
|
||||
assert_failure
|
||||
assert_output --partial "ResourceNotFound"
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bats
|
||||
# Lambda tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
FUNC_NAME=""
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ -n "$FUNC_NAME" ]; then
|
||||
aws_cmd lambda delete-function --function-name "$FUNC_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
_role="arn:aws:iam::000000000000:role/lambda-role"
|
||||
_image_uri="000000000000.dkr.ecr.us-east-1.amazonaws.com/fake-repo:latest"
|
||||
|
||||
@test "Lambda: ImageConfig.WorkingDirectory is returned in CreateFunction response" {
|
||||
FUNC_NAME="bats-imgwd-create-$(unique_name)"
|
||||
|
||||
run aws_cmd lambda create-function \
|
||||
--function-name "$FUNC_NAME" \
|
||||
--package-type Image \
|
||||
--role "$_role" \
|
||||
--code "ImageUri=$_image_uri" \
|
||||
--image-config 'WorkingDirectory=/app'
|
||||
assert_success
|
||||
|
||||
wd=$(json_get "$output" '.ImageConfigResponse.ImageConfig.WorkingDirectory')
|
||||
[ "$wd" = "/app" ] || { echo "expected WorkingDirectory=/app, got: $wd"; return 1; }
|
||||
}
|
||||
|
||||
@test "Lambda: ImageConfig.WorkingDirectory is persisted and returned by get-function-configuration" {
|
||||
FUNC_NAME="bats-imgwd-get-$(unique_name)"
|
||||
|
||||
aws_cmd lambda create-function \
|
||||
--function-name "$FUNC_NAME" \
|
||||
--package-type Image \
|
||||
--role "$_role" \
|
||||
--code "ImageUri=$_image_uri" \
|
||||
--image-config 'WorkingDirectory=/workspace' >/dev/null
|
||||
|
||||
run aws_cmd lambda get-function-configuration --function-name "$FUNC_NAME"
|
||||
assert_success
|
||||
|
||||
wd=$(json_get "$output" '.ImageConfigResponse.ImageConfig.WorkingDirectory')
|
||||
[ "$wd" = "/workspace" ] || { echo "expected WorkingDirectory=/workspace, got: $wd"; return 1; }
|
||||
}
|
||||
|
||||
@test "Lambda: ImageConfig.WorkingDirectory is updated by update-function-configuration" {
|
||||
FUNC_NAME="bats-imgwd-upd-$(unique_name)"
|
||||
|
||||
aws_cmd lambda create-function \
|
||||
--function-name "$FUNC_NAME" \
|
||||
--package-type Image \
|
||||
--role "$_role" \
|
||||
--code "ImageUri=$_image_uri" \
|
||||
--image-config 'WorkingDirectory=/initial' >/dev/null
|
||||
|
||||
run aws_cmd lambda update-function-configuration \
|
||||
--function-name "$FUNC_NAME" \
|
||||
--image-config 'WorkingDirectory=/updated'
|
||||
assert_success
|
||||
|
||||
wd=$(json_get "$output" '.ImageConfigResponse.ImageConfig.WorkingDirectory')
|
||||
[ "$wd" = "/updated" ] || { echo "expected WorkingDirectory=/updated, got: $wd"; return 1; }
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env bats
|
||||
# Neptune cluster and instance management tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
CLUSTER_ID="bats-neptune-$(unique_name)"
|
||||
INSTANCE_ID="bats-neptune-inst-$(unique_name)"
|
||||
CLUSTER_CREATED=false
|
||||
INSTANCE_CREATED=false
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ "$INSTANCE_CREATED" = "true" ]; then
|
||||
aws_cmd neptune delete-db-instance \
|
||||
--db-instance-identifier "$INSTANCE_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ "$CLUSTER_CREATED" = "true" ]; then
|
||||
aws_cmd neptune delete-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--skip-final-snapshot >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
@test "Neptune: create DB cluster" {
|
||||
run aws_cmd neptune create-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine neptune
|
||||
assert_success
|
||||
CLUSTER_CREATED=true
|
||||
|
||||
id=$(json_get "$output" '.DBCluster.DBClusterIdentifier')
|
||||
engine=$(json_get "$output" '.DBCluster.Engine')
|
||||
status=$(json_get "$output" '.DBCluster.Status')
|
||||
arn=$(json_get "$output" '.DBCluster.DBClusterArn')
|
||||
port=$(json_get "$output" '.DBCluster.Port')
|
||||
|
||||
[ "$id" = "$CLUSTER_ID" ]
|
||||
[ "$engine" = "neptune" ]
|
||||
[ "$status" = "available" ]
|
||||
[[ "$arn" == arn:aws:neptune:* ]]
|
||||
[ "$port" -gt 0 ]
|
||||
}
|
||||
|
||||
@test "Neptune: create duplicate cluster fails" {
|
||||
aws_cmd neptune create-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine neptune >/dev/null
|
||||
CLUSTER_CREATED=true
|
||||
|
||||
run aws_cmd neptune create-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine neptune
|
||||
assert_failure
|
||||
[[ "$output" == *"DBClusterAlreadyExistsFault"* ]]
|
||||
}
|
||||
|
||||
@test "Neptune: describe DB cluster by ID" {
|
||||
aws_cmd neptune create-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine neptune >/dev/null
|
||||
CLUSTER_CREATED=true
|
||||
|
||||
run aws_cmd neptune describe-db-clusters \
|
||||
--db-cluster-identifier "$CLUSTER_ID"
|
||||
assert_success
|
||||
|
||||
id=$(json_get "$output" '.DBClusters[0].DBClusterIdentifier')
|
||||
[ "$id" = "$CLUSTER_ID" ]
|
||||
}
|
||||
|
||||
@test "Neptune: modify DB cluster IAM auth" {
|
||||
aws_cmd neptune create-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine neptune >/dev/null
|
||||
CLUSTER_CREATED=true
|
||||
|
||||
run aws_cmd neptune modify-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--enable-iam-database-authentication
|
||||
assert_success
|
||||
|
||||
iam=$(json_get "$output" '.DBCluster.IAMDatabaseAuthenticationEnabled')
|
||||
[ "$iam" = "true" ]
|
||||
}
|
||||
|
||||
@test "Neptune: create DB instance in cluster" {
|
||||
aws_cmd neptune create-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine neptune >/dev/null
|
||||
CLUSTER_CREATED=true
|
||||
|
||||
run aws_cmd neptune create-db-instance \
|
||||
--db-instance-identifier "$INSTANCE_ID" \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--db-instance-class db.r5.large \
|
||||
--engine neptune
|
||||
assert_success
|
||||
INSTANCE_CREATED=true
|
||||
|
||||
id=$(json_get "$output" '.DBInstance.DBInstanceIdentifier')
|
||||
cluster=$(json_get "$output" '.DBInstance.DBClusterIdentifier')
|
||||
status=$(json_get "$output" '.DBInstance.DBInstanceStatus')
|
||||
|
||||
[ "$id" = "$INSTANCE_ID" ]
|
||||
[ "$cluster" = "$CLUSTER_ID" ]
|
||||
[ "$status" = "available" ]
|
||||
}
|
||||
|
||||
@test "Neptune: describe DB instance by ID" {
|
||||
aws_cmd neptune create-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine neptune >/dev/null
|
||||
CLUSTER_CREATED=true
|
||||
aws_cmd neptune create-db-instance \
|
||||
--db-instance-identifier "$INSTANCE_ID" \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--db-instance-class db.r5.large \
|
||||
--engine neptune >/dev/null
|
||||
INSTANCE_CREATED=true
|
||||
|
||||
run aws_cmd neptune describe-db-instances \
|
||||
--db-instance-identifier "$INSTANCE_ID"
|
||||
assert_success
|
||||
|
||||
id=$(json_get "$output" '.DBInstances[0].DBInstanceIdentifier')
|
||||
[ "$id" = "$INSTANCE_ID" ]
|
||||
}
|
||||
|
||||
@test "Neptune: delete cluster with instance fails" {
|
||||
aws_cmd neptune create-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine neptune >/dev/null
|
||||
CLUSTER_CREATED=true
|
||||
aws_cmd neptune create-db-instance \
|
||||
--db-instance-identifier "$INSTANCE_ID" \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--db-instance-class db.r5.large \
|
||||
--engine neptune >/dev/null
|
||||
INSTANCE_CREATED=true
|
||||
|
||||
run aws_cmd neptune delete-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--skip-final-snapshot
|
||||
assert_failure
|
||||
[[ "$output" == *"InvalidDBClusterStateFault"* ]]
|
||||
}
|
||||
|
||||
@test "Neptune: delete DB instance then cluster" {
|
||||
aws_cmd neptune create-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine neptune >/dev/null
|
||||
CLUSTER_CREATED=true
|
||||
aws_cmd neptune create-db-instance \
|
||||
--db-instance-identifier "$INSTANCE_ID" \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--db-instance-class db.r5.large \
|
||||
--engine neptune >/dev/null
|
||||
INSTANCE_CREATED=true
|
||||
|
||||
run aws_cmd neptune delete-db-instance \
|
||||
--db-instance-identifier "$INSTANCE_ID"
|
||||
assert_success
|
||||
INSTANCE_CREATED=false
|
||||
|
||||
run aws_cmd neptune delete-db-cluster \
|
||||
--db-cluster-identifier "$CLUSTER_ID" \
|
||||
--skip-final-snapshot
|
||||
assert_success
|
||||
CLUSTER_CREATED=false
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env bats
|
||||
# EventBridge Pipes tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
PIPE_NAME="bats-pipe-$(unique_name)"
|
||||
SOURCE_QUEUE="bats-pipe-src-$(unique_name)"
|
||||
TARGET_QUEUE="bats-pipe-tgt-$(unique_name)"
|
||||
SOURCE_QUEUE_URL=""
|
||||
TARGET_QUEUE_URL=""
|
||||
ROLE_ARN="arn:aws:iam::000000000000:role/pipe-role"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
aws_cmd pipes delete-pipe --name "$PIPE_NAME" >/dev/null 2>&1 || true
|
||||
if [ -n "$SOURCE_QUEUE_URL" ]; then
|
||||
aws_cmd sqs delete-queue --queue-url "$SOURCE_QUEUE_URL" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "$TARGET_QUEUE_URL" ]; then
|
||||
aws_cmd sqs delete-queue --queue-url "$TARGET_QUEUE_URL" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
create_queues() {
|
||||
out=$(aws_cmd sqs create-queue --queue-name "$SOURCE_QUEUE")
|
||||
SOURCE_QUEUE_URL=$(json_get "$out" '.QueueUrl')
|
||||
out=$(aws_cmd sqs create-queue --queue-name "$TARGET_QUEUE")
|
||||
TARGET_QUEUE_URL=$(json_get "$out" '.QueueUrl')
|
||||
}
|
||||
|
||||
source_arn() {
|
||||
echo "arn:aws:sqs:${AWS_DEFAULT_REGION}:000000000000:${SOURCE_QUEUE}"
|
||||
}
|
||||
|
||||
target_arn() {
|
||||
echo "arn:aws:sqs:${AWS_DEFAULT_REGION}:000000000000:${TARGET_QUEUE}"
|
||||
}
|
||||
|
||||
@test "Pipes: create pipe in STOPPED state" {
|
||||
create_queues
|
||||
|
||||
run aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state STOPPED
|
||||
assert_success
|
||||
|
||||
state=$(json_get "$output" '.CurrentState')
|
||||
[ "$state" = "STOPPED" ]
|
||||
|
||||
name=$(json_get "$output" '.Name')
|
||||
[ "$name" = "$PIPE_NAME" ]
|
||||
}
|
||||
|
||||
@test "Pipes: describe pipe" {
|
||||
create_queues
|
||||
aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state STOPPED >/dev/null
|
||||
|
||||
run aws_cmd pipes describe-pipe --name "$PIPE_NAME"
|
||||
assert_success
|
||||
|
||||
name=$(json_get "$output" '.Name')
|
||||
[ "$name" = "$PIPE_NAME" ]
|
||||
|
||||
source=$(json_get "$output" '.Source')
|
||||
[ "$source" = "$(source_arn)" ]
|
||||
|
||||
target=$(json_get "$output" '.Target')
|
||||
[ "$target" = "$(target_arn)" ]
|
||||
}
|
||||
|
||||
@test "Pipes: list pipes" {
|
||||
create_queues
|
||||
aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state STOPPED >/dev/null
|
||||
|
||||
run aws_cmd pipes list-pipes
|
||||
assert_success
|
||||
|
||||
found=$(echo "$output" | jq --arg name "$PIPE_NAME" '.Pipes | any(.Name == $name)')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "Pipes: update pipe" {
|
||||
create_queues
|
||||
aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state STOPPED >/dev/null
|
||||
|
||||
run aws_cmd pipes update-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--description "updated description" \
|
||||
--desired-state STOPPED
|
||||
assert_success
|
||||
|
||||
run aws_cmd pipes describe-pipe --name "$PIPE_NAME"
|
||||
assert_success
|
||||
desc=$(json_get "$output" '.Description')
|
||||
[ "$desc" = "updated description" ]
|
||||
}
|
||||
|
||||
@test "Pipes: start and stop pipe" {
|
||||
create_queues
|
||||
aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state STOPPED >/dev/null
|
||||
|
||||
run aws_cmd pipes start-pipe --name "$PIPE_NAME"
|
||||
assert_success
|
||||
state=$(json_get "$output" '.CurrentState')
|
||||
[ "$state" = "RUNNING" ]
|
||||
|
||||
run aws_cmd pipes stop-pipe --name "$PIPE_NAME"
|
||||
assert_success
|
||||
state=$(json_get "$output" '.CurrentState')
|
||||
[ "$state" = "STOPPED" ]
|
||||
}
|
||||
|
||||
@test "Pipes: delete pipe" {
|
||||
create_queues
|
||||
aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state STOPPED >/dev/null
|
||||
|
||||
run aws_cmd pipes delete-pipe --name "$PIPE_NAME"
|
||||
assert_success
|
||||
|
||||
run aws_cmd pipes describe-pipe --name "$PIPE_NAME"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "Pipes: describe non-existent pipe returns error" {
|
||||
run aws_cmd pipes describe-pipe --name "non-existent-pipe"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "Pipes: SQS to SQS forwarding" {
|
||||
create_queues
|
||||
aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state RUNNING >/dev/null
|
||||
|
||||
aws_cmd sqs send-message \
|
||||
--queue-url "$SOURCE_QUEUE_URL" \
|
||||
--message-body "hello from pipes" >/dev/null
|
||||
|
||||
# Poll target queue for forwarded message
|
||||
local found=false
|
||||
for i in $(seq 1 15); do
|
||||
sleep 1
|
||||
out=$(aws_cmd sqs receive-message \
|
||||
--queue-url "$TARGET_QUEUE_URL" \
|
||||
--max-number-of-messages 1 \
|
||||
--wait-time-seconds 1)
|
||||
if echo "$out" | grep -q "hello from pipes"; then
|
||||
found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "Pipes: FilterCriteria filters messages" {
|
||||
create_queues
|
||||
aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state RUNNING \
|
||||
--source-parameters '{"FilterCriteria":{"Filters":[{"Pattern":"{\"body\": {\"status\": [\"active\"]}}"}]}}' >/dev/null
|
||||
|
||||
aws_cmd sqs send-message \
|
||||
--queue-url "$SOURCE_QUEUE_URL" \
|
||||
--message-body '{"status": "active", "id": "match-1"}' >/dev/null
|
||||
|
||||
aws_cmd sqs send-message \
|
||||
--queue-url "$SOURCE_QUEUE_URL" \
|
||||
--message-body '{"status": "inactive", "id": "no-match"}' >/dev/null
|
||||
|
||||
# Poll target queue for matching message
|
||||
local found=false
|
||||
for i in $(seq 1 15); do
|
||||
sleep 1
|
||||
out=$(aws_cmd sqs receive-message \
|
||||
--queue-url "$TARGET_QUEUE_URL" \
|
||||
--max-number-of-messages 10 \
|
||||
--wait-time-seconds 1)
|
||||
if echo "$out" | grep -q "match-1"; then
|
||||
# Verify no non-matching message arrived
|
||||
if echo "$out" | grep -q "no-match"; then
|
||||
fail "non-matching message should not be forwarded"
|
||||
fi
|
||||
found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$found" = "true" ]
|
||||
|
||||
# Source queue should be drained (non-matching messages deleted per AWS behavior)
|
||||
out=$(aws_cmd sqs get-queue-attributes \
|
||||
--queue-url "$SOURCE_QUEUE_URL" \
|
||||
--attribute-names ApproximateNumberOfMessages)
|
||||
count=$(json_get "$out" '.Attributes.ApproximateNumberOfMessages')
|
||||
[ "$count" = "0" ]
|
||||
}
|
||||
|
||||
@test "Pipes: BatchSize in SourceParameters" {
|
||||
create_queues
|
||||
aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state RUNNING \
|
||||
--source-parameters '{"SqsQueueParameters":{"BatchSize":1}}' >/dev/null
|
||||
|
||||
for i in 1 2 3; do
|
||||
aws_cmd sqs send-message \
|
||||
--queue-url "$SOURCE_QUEUE_URL" \
|
||||
--message-body "batch-msg-$i" >/dev/null
|
||||
done
|
||||
|
||||
# Poll target queue until all 3 messages arrive
|
||||
local found_count=0
|
||||
local found1=false found2=false found3=false
|
||||
for i in $(seq 1 20); do
|
||||
sleep 1
|
||||
out=$(aws_cmd sqs receive-message \
|
||||
--queue-url "$TARGET_QUEUE_URL" \
|
||||
--max-number-of-messages 10 \
|
||||
--wait-time-seconds 1)
|
||||
if [ "$found1" = "false" ] && echo "$out" | grep -q "batch-msg-1"; then
|
||||
found1=true
|
||||
fi
|
||||
if [ "$found2" = "false" ] && echo "$out" | grep -q "batch-msg-2"; then
|
||||
found2=true
|
||||
fi
|
||||
if [ "$found3" = "false" ] && echo "$out" | grep -q "batch-msg-3"; then
|
||||
found3=true
|
||||
fi
|
||||
if [ "$found1" = "true" ] && [ "$found2" = "true" ] && [ "$found3" = "true" ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$found1" = "true" ]
|
||||
[ "$found2" = "true" ]
|
||||
[ "$found3" = "true" ]
|
||||
}
|
||||
|
||||
@test "Pipes: stopped pipe does not forward messages" {
|
||||
create_queues
|
||||
aws_cmd pipes create-pipe \
|
||||
--name "$PIPE_NAME" \
|
||||
--source "$(source_arn)" \
|
||||
--target "$(target_arn)" \
|
||||
--role-arn "$ROLE_ARN" \
|
||||
--desired-state STOPPED >/dev/null
|
||||
|
||||
aws_cmd sqs send-message \
|
||||
--queue-url "$SOURCE_QUEUE_URL" \
|
||||
--message-body "should not forward" >/dev/null
|
||||
|
||||
sleep 3
|
||||
|
||||
# Source should still have the message
|
||||
out=$(aws_cmd sqs get-queue-attributes \
|
||||
--queue-url "$SOURCE_QUEUE_URL" \
|
||||
--attribute-names ApproximateNumberOfMessages)
|
||||
count=$(json_get "$out" '.Attributes.ApproximateNumberOfMessages')
|
||||
[ "$count" = "1" ]
|
||||
|
||||
# Target should be empty
|
||||
out=$(aws_cmd sqs receive-message \
|
||||
--queue-url "$TARGET_QUEUE_URL" \
|
||||
--max-number-of-messages 1)
|
||||
msgs=$(echo "${out:-"{}"}" | jq '.Messages // [] | length')
|
||||
[ "$msgs" = "0" ]
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
#!/usr/bin/env bats
|
||||
# RDS integration tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
DB_ID="bats-rds-$(unique_name)"
|
||||
DB_ID_2="bats-rds-2-$(unique_name)"
|
||||
RDS_CLUSTER_ID="bats-rds-cluster-$(unique_name)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
aws_cmd rds delete-db-instance --db-instance-identifier "$DB_ID" --skip-final-snapshot >/dev/null 2>&1 || true
|
||||
aws_cmd rds delete-db-instance --db-instance-identifier "$DB_ID_2" --skip-final-snapshot >/dev/null 2>&1 || true
|
||||
aws_cmd rds delete-db-cluster --db-cluster-identifier "$RDS_CLUSTER_ID" --skip-final-snapshot >/dev/null 2>&1 || true
|
||||
if [ -n "${MANAGED_SECRET_ARN:-}" ]; then
|
||||
aws_cmd secretsmanager delete-secret --secret-id "$MANAGED_SECRET_ARN" \
|
||||
--force-delete-without-recovery >/dev/null 2>&1 || true
|
||||
fi
|
||||
# EC2 fixtures of the docdb security-group case, whether or not its assertions passed
|
||||
if [ -n "${SG_ID:-}" ]; then
|
||||
aws_cmd docdb delete-db-cluster --db-cluster-identifier "$CLUSTER_ID" --skip-final-snapshot >/dev/null 2>&1 || true
|
||||
aws_cmd ec2 delete-security-group --group-id "$SG_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "${VPC_ID:-}" ]; then
|
||||
aws_cmd ec2 delete-vpc --vpc-id "$VPC_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
@test "RDS: create db instance returns resource identifiers" {
|
||||
run aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10
|
||||
|
||||
assert_success
|
||||
|
||||
dbi_resource_id=$(json_get "$output" '.DBInstance.DbiResourceId')
|
||||
db_instance_arn=$(json_get "$output" '.DBInstance.DBInstanceArn')
|
||||
|
||||
[ -n "$dbi_resource_id" ]
|
||||
[[ "$dbi_resource_id" =~ ^db- ]]
|
||||
|
||||
[ -n "$db_instance_arn" ]
|
||||
[[ "$db_instance_arn" == *":db:$DB_ID" ]]
|
||||
}
|
||||
|
||||
@test "RDS: describe db instances filters by identifier" {
|
||||
aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10
|
||||
|
||||
run aws_cmd rds describe-db-instances --db-instance-identifier "$DB_ID"
|
||||
assert_success
|
||||
|
||||
count=$(echo "$output" | jq '.DBInstances | length')
|
||||
[ "$count" -eq 1 ]
|
||||
|
||||
id=$(json_get "$output" '.DBInstances[0].DBInstanceIdentifier')
|
||||
[ "$id" = "$DB_ID" ]
|
||||
}
|
||||
|
||||
@test "RDS: describe db instances is case-insensitive" {
|
||||
aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10
|
||||
|
||||
# shellcheck disable=SC2155
|
||||
local upper_id=$(echo "$DB_ID" | tr '[:lower:]' '[:upper:]')
|
||||
run aws_cmd rds describe-db-instances --db-instance-identifier "$upper_id"
|
||||
assert_success
|
||||
|
||||
count=$(echo "$output" | jq '.DBInstances | length')
|
||||
[ "$count" -eq 1 ]
|
||||
|
||||
id=$(json_get "$output" '.DBInstances[0].DBInstanceIdentifier')
|
||||
[ "$id" = "$DB_ID" ]
|
||||
}
|
||||
|
||||
@test "RDS: describe db instances returns all when no filter" {
|
||||
aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10
|
||||
|
||||
aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID_2" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10
|
||||
|
||||
run aws_cmd rds describe-db-instances
|
||||
assert_success
|
||||
|
||||
# Might have more from other tests, but at least 2
|
||||
count=$(echo "$output" | jq '.DBInstances | length')
|
||||
[ "$count" -ge 2 ]
|
||||
}
|
||||
|
||||
@test "RDS: managed master user secret is owned by rds and rotates without a Lambda" {
|
||||
run aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10 \
|
||||
--master-username admin \
|
||||
--manage-master-user-password
|
||||
assert_success
|
||||
|
||||
MANAGED_SECRET_ARN=$(json_get "$output" '.DBInstance.MasterUserSecret.SecretArn')
|
||||
[ -n "$MANAGED_SECRET_ARN" ]
|
||||
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "$MANAGED_SECRET_ARN"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.OwningService')" = "rds" ]
|
||||
|
||||
# RDS rotates this secret itself, so a rotation Lambda is not accepted for it.
|
||||
run aws_cmd secretsmanager rotate-secret \
|
||||
--secret-id "$MANAGED_SECRET_ARN" \
|
||||
--rotation-lambda-arn "arn:aws:lambda:$AWS_DEFAULT_REGION:000000000000:function:absent"
|
||||
assert_failure
|
||||
assert_output --partial "not supported for a service-managed secret"
|
||||
|
||||
# The call terraform's aws_secretsmanager_secret_rotation makes: rules, no Lambda ARN.
|
||||
run aws_cmd secretsmanager rotate-secret \
|
||||
--secret-id "$MANAGED_SECRET_ARN" \
|
||||
--rotation-rules 'AutomaticallyAfterDays=7'
|
||||
assert_success
|
||||
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "$MANAGED_SECRET_ARN"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.RotationEnabled')" = "true" ]
|
||||
[ "$(json_get "$output" '.RotationRules.AutomaticallyAfterDays')" = "7" ]
|
||||
}
|
||||
|
||||
@test "RDS: Aurora cluster with managed master user password populates MasterUserSecret" {
|
||||
run aws_cmd rds create-db-cluster \
|
||||
--db-cluster-identifier "$RDS_CLUSTER_ID" \
|
||||
--engine aurora-postgresql \
|
||||
--master-username omni_admin \
|
||||
--manage-master-user-password
|
||||
assert_success
|
||||
|
||||
MANAGED_SECRET_ARN=$(json_get "$output" '.DBCluster.MasterUserSecret.SecretArn')
|
||||
[ -n "$MANAGED_SECRET_ARN" ]
|
||||
[ "$(json_get "$output" '.DBCluster.MasterUserSecret.SecretStatus')" = "active" ]
|
||||
|
||||
run aws_cmd rds describe-db-clusters --db-cluster-identifier "$RDS_CLUSTER_ID"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.DBClusters[0].MasterUserSecret.SecretArn')" = "$MANAGED_SECRET_ARN" ]
|
||||
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "$MANAGED_SECRET_ARN"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.OwningService')" = "rds" ]
|
||||
}
|
||||
|
||||
@test "RDS: describe global clusters returns an empty list" {
|
||||
run aws_cmd rds describe-global-clusters
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.GlobalClusters | length')" = "0" ]
|
||||
}
|
||||
|
||||
@test "DocDB: describe global clusters answers the read every cluster read makes" {
|
||||
# DocumentDB signs with the rds scope, so this is the same handler the CLI reaches
|
||||
# for either service. Without an answer here a created cluster cannot be read back.
|
||||
run aws_cmd docdb describe-global-clusters
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.GlobalClusters | length')" = "0" ]
|
||||
|
||||
run aws_cmd docdb describe-global-clusters --global-cluster-identifier "bats-absent-gc"
|
||||
assert_failure
|
||||
assert_output --partial "GlobalClusterNotFoundFault"
|
||||
}
|
||||
|
||||
@test "RDS: cluster parameter group reports its ARN and carries tags" {
|
||||
CPG="bats-cpg-$(unique_name)"
|
||||
run aws_cmd rds create-db-cluster-parameter-group --db-cluster-parameter-group-name "$CPG" \
|
||||
--db-parameter-group-family aurora-postgresql15 --description "bats" \
|
||||
--tags Key=team,Value=data
|
||||
assert_success
|
||||
arn=$(json_get "$output" '.DBClusterParameterGroup.DBClusterParameterGroupArn')
|
||||
[ -n "$arn" ]
|
||||
[[ "$arn" == *":cluster-pg:$CPG" ]]
|
||||
|
||||
run aws_cmd rds list-tags-for-resource --resource-name "$arn"
|
||||
assert_success
|
||||
assert_output --partial '"Key": "team"'
|
||||
|
||||
aws_cmd rds delete-db-cluster-parameter-group --db-cluster-parameter-group-name "$CPG" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "DocDB: cluster tags survive create and can be added and removed" {
|
||||
# The tag actions carry only the resource ARN, so this is also the check that they reach
|
||||
# DocumentDB at all rather than the RDS handler, which does not hold its records.
|
||||
CLUSTER_ID="bats-docdb-$(unique_name)"
|
||||
run aws_cmd docdb create-db-cluster --db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine docdb --master-username docdbadmin --master-user-password "secret99password" \
|
||||
--tags Key=env,Value=bats
|
||||
assert_success
|
||||
arn=$(json_get "$output" '.DBCluster.DBClusterArn')
|
||||
|
||||
run aws_cmd docdb list-tags-for-resource --resource-name "$arn"
|
||||
assert_success
|
||||
assert_output --partial '"Key": "env"'
|
||||
|
||||
run aws_cmd docdb add-tags-to-resource --resource-name "$arn" --tags Key=env,Value=changed Key=extra,Value=yes
|
||||
assert_success
|
||||
run aws_cmd docdb list-tags-for-resource --resource-name "$arn"
|
||||
assert_success
|
||||
assert_output --partial '"Value": "changed"'
|
||||
|
||||
# Removing a key that is not there is not an error on a live account.
|
||||
run aws_cmd docdb remove-tags-from-resource --resource-name "$arn" --tag-keys extra absent
|
||||
assert_success
|
||||
run aws_cmd docdb list-tags-for-resource --resource-name "$arn"
|
||||
assert_success
|
||||
refute_output --partial '"Key": "extra"'
|
||||
|
||||
aws_cmd docdb delete-db-cluster --db-cluster-identifier "$CLUSTER_ID" --skip-final-snapshot >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "rds: tags given to create-db-subnet-group are readable back" {
|
||||
VPC_ID=$(aws_cmd ec2 create-vpc --cidr-block 10.77.0.0/16 --query Vpc.VpcId --output text)
|
||||
S1=$(aws_cmd ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.77.1.0/24 --availability-zone us-east-1a --query Subnet.SubnetId --output text)
|
||||
S2=$(aws_cmd ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.77.2.0/24 --availability-zone us-east-1b --query Subnet.SubnetId --output text)
|
||||
GROUP="bats-sng-$(unique_name)"
|
||||
|
||||
run aws_cmd rds create-db-subnet-group --db-subnet-group-name "$GROUP" --db-subnet-group-description d \
|
||||
--subnet-ids "$S1" "$S2" --tags "Key=Name,Value=$GROUP" Key=env,Value=tst
|
||||
assert_success
|
||||
arn=$(json_get "$output" '.DBSubnetGroup.DBSubnetGroupArn')
|
||||
|
||||
run aws_cmd rds list-tags-for-resource --resource-name "$arn"
|
||||
assert_success
|
||||
assert_output --partial '"Key": "env"'
|
||||
assert_output --partial "\"Value\": \"$GROUP\""
|
||||
|
||||
# the DocumentDB CLI reaches the same group through the same scope
|
||||
run aws_cmd docdb list-tags-for-resource --resource-name "$arn"
|
||||
assert_success
|
||||
assert_output --partial '"Key": "env"'
|
||||
|
||||
aws_cmd rds delete-db-subnet-group --db-subnet-group-name "$GROUP" >/dev/null 2>&1 || true
|
||||
aws_cmd ec2 delete-subnet --subnet-id "$S1" >/dev/null 2>&1 || true
|
||||
aws_cmd ec2 delete-subnet --subnet-id "$S2" >/dev/null 2>&1 || true
|
||||
aws_cmd ec2 delete-vpc --vpc-id "$VPC_ID" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "docdb: an engine version a live account does not list is refused" {
|
||||
CLUSTER_ID="bats-docdb-version-$(unique_name)"
|
||||
run aws_cmd docdb create-db-cluster --db-cluster-identifier "$CLUSTER_ID" --engine docdb \
|
||||
--engine-version 9.9.9 --master-username u --master-user-password pw12345678
|
||||
assert_failure
|
||||
assert_output --partial 'InvalidParameterCombination'
|
||||
assert_output --partial 'Cannot find version 9.9.9 for docdb'
|
||||
|
||||
run aws_cmd docdb describe-db-clusters --db-cluster-identifier "$CLUSTER_ID"
|
||||
assert_failure
|
||||
assert_output --partial 'DBClusterNotFoundFault'
|
||||
}
|
||||
|
||||
@test "docdb: a security group that exists is accepted on create" {
|
||||
CLUSTER_ID="bats-docdb-sg-$(unique_name)"
|
||||
run aws_cmd ec2 create-vpc --cidr-block 10.42.0.0/16
|
||||
assert_success
|
||||
VPC_ID=$(json_get "$output" '.Vpc.VpcId')
|
||||
run aws_cmd ec2 create-security-group --group-name "$CLUSTER_ID" --description d --vpc-id "$VPC_ID"
|
||||
assert_success
|
||||
SG_ID=$(json_get "$output" '.GroupId')
|
||||
|
||||
run aws_cmd docdb create-db-cluster --db-cluster-identifier "$CLUSTER_ID" --engine docdb \
|
||||
--master-username docdbadmin --master-user-password "secret99password" \
|
||||
--vpc-security-group-ids "$SG_ID"
|
||||
assert_success
|
||||
run aws_cmd docdb describe-db-clusters --db-cluster-identifier "$CLUSTER_ID"
|
||||
assert_success
|
||||
assert_output --partial "\"VpcSecurityGroupId\": \"$SG_ID\""
|
||||
}
|
||||
|
||||
@test "docdb: cluster settings given on create are returned by describe" {
|
||||
CLUSTER_ID="bats-docdb-settings-$(unique_name)"
|
||||
run aws_cmd kms create-key --description "$CLUSTER_ID"
|
||||
assert_success
|
||||
KEY_ARN=$(json_get "$output" '.KeyMetadata.Arn')
|
||||
|
||||
run aws_cmd docdb create-db-cluster --db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine docdb --master-username docdbadmin --master-user-password "secret99password" \
|
||||
--storage-encrypted --kms-key-id "$KEY_ARN" --backup-retention-period 5 \
|
||||
--preferred-backup-window 23:30-00:00 --preferred-maintenance-window sun:03:00-sun:04:00 \
|
||||
--tags "Key=Name,Value=$CLUSTER_ID"
|
||||
assert_success
|
||||
|
||||
run aws_cmd docdb describe-db-clusters --db-cluster-identifier "$CLUSTER_ID" \
|
||||
--query 'DBClusters[0].[DBSubnetGroup,DBClusterParameterGroup,StorageEncrypted,KmsKeyId,BackupRetentionPeriod,PreferredBackupWindow,PreferredMaintenanceWindow,VpcSecurityGroups[0].VpcSecurityGroupId]'
|
||||
assert_success
|
||||
assert_output --partial '"default"'
|
||||
assert_output --partial '"default.docdb5.0"'
|
||||
assert_output --partial 'true'
|
||||
assert_output --partial "$KEY_ARN"
|
||||
assert_output --partial '5'
|
||||
assert_output --partial '"23:30-00:00"'
|
||||
assert_output --partial '"sun:03:00-sun:04:00"'
|
||||
assert_output --partial '"sg-'
|
||||
|
||||
run aws_cmd docdb create-db-cluster --db-cluster-identifier "$CLUSTER_ID-x" \
|
||||
--engine docdb --master-username docdbadmin --master-user-password "secret99password" \
|
||||
--kms-key-id "$KEY_ARN"
|
||||
assert_failure
|
||||
assert_output --partial 'You cannot specify KMS key for unencrypted clusters.'
|
||||
|
||||
aws_cmd docdb delete-db-cluster --db-cluster-identifier "$CLUSTER_ID" --skip-final-snapshot >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "docdb: a DocumentDB cluster is listed by describe-db-clusters on both endpoints" {
|
||||
CLUSTER_ID="bats-docdb-list-$(unique_name)"
|
||||
aws_cmd docdb create-db-cluster --db-cluster-identifier "$CLUSTER_ID" \
|
||||
--engine docdb --master-username docdbadmin --master-user-password "secret99password" >/dev/null
|
||||
|
||||
run aws_cmd docdb describe-db-clusters --query 'DBClusters[].DBClusterIdentifier'
|
||||
assert_success
|
||||
assert_output --partial "$CLUSTER_ID"
|
||||
|
||||
run aws_cmd rds describe-db-clusters --query 'DBClusters[].DBClusterIdentifier'
|
||||
assert_success
|
||||
assert_output --partial "$CLUSTER_ID"
|
||||
|
||||
run aws_cmd docdb describe-db-clusters --filters Name=engine,Values=docdb --query 'DBClusters[].DBClusterIdentifier'
|
||||
assert_success
|
||||
assert_output --partial "$CLUSTER_ID"
|
||||
|
||||
run aws_cmd rds describe-db-clusters --filters Name=engine,Values=aurora-postgresql --query 'DBClusters[].DBClusterIdentifier'
|
||||
assert_success
|
||||
refute_output --partial "$CLUSTER_ID"
|
||||
|
||||
run aws_cmd rds describe-db-clusters --filters Name=engine,Values=nothing
|
||||
assert_failure
|
||||
assert_output --partial 'Unrecognized engine name: nothing'
|
||||
|
||||
aws_cmd docdb delete-db-cluster --db-cluster-identifier "$CLUSTER_ID" --skip-final-snapshot >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "rds: storage and backup settings given on create are returned by describe and modify" {
|
||||
run aws_cmd kms create-key --description "$DB_ID"
|
||||
assert_success
|
||||
KEY_ARN=$(json_get "$output" '.KeyMetadata.Arn')
|
||||
KEY_ID=$(json_get "$output" '.KeyMetadata.KeyId')
|
||||
aws_cmd kms create-alias --alias-name "alias/$DB_ID" --target-key-id "$KEY_ID" >/dev/null
|
||||
|
||||
# the key is given as an alias and must come back as the key ARN, as on AWS
|
||||
run aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10 \
|
||||
--storage-encrypted --kms-key-id "alias/$DB_ID" \
|
||||
--backup-retention-period 7 --copy-tags-to-snapshot \
|
||||
--preferred-backup-window 23:30-00:00 --preferred-maintenance-window Sun:03:08-Sun:03:38
|
||||
assert_success
|
||||
|
||||
run aws_cmd rds describe-db-instances --db-instance-identifier "$DB_ID" \
|
||||
--query 'DBInstances[0].[StorageEncrypted,KmsKeyId,BackupRetentionPeriod,PreferredBackupWindow,CopyTagsToSnapshot,PreferredMaintenanceWindow]'
|
||||
assert_success
|
||||
assert_output --partial 'true'
|
||||
assert_output --partial "$KEY_ARN"
|
||||
assert_output --partial '7'
|
||||
assert_output --partial '"23:30-00:00"'
|
||||
assert_output --partial '"sun:03:08-sun:03:38"'
|
||||
|
||||
run aws_cmd rds modify-db-instance --db-instance-identifier "$DB_ID" \
|
||||
--backup-retention-period 3 --preferred-backup-window 01:00-01:30 --apply-immediately
|
||||
assert_success
|
||||
|
||||
run aws_cmd rds describe-db-instances --db-instance-identifier "$DB_ID" \
|
||||
--query 'DBInstances[0].[BackupRetentionPeriod,PreferredBackupWindow,CopyTagsToSnapshot]'
|
||||
assert_success
|
||||
assert_output --partial '3'
|
||||
assert_output --partial '"01:00-01:30"'
|
||||
assert_output --partial 'true'
|
||||
}
|
||||
|
||||
@test "rds: a KmsKeyId that names no key is refused as on AWS" {
|
||||
run aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10 \
|
||||
--storage-encrypted --kms-key-id "alias/does-not-exist-$DB_ID"
|
||||
assert_failure
|
||||
assert_output --partial 'KMSKeyNotAccessibleFault'
|
||||
}
|
||||
|
||||
@test "rds: KmsKeyId without StorageEncrypted is refused as on AWS" {
|
||||
run aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10 \
|
||||
--no-storage-encrypted --kms-key-id "arn:aws:kms:us-east-1:000000000000:key/00000000-0000-0000-0000-000000000000"
|
||||
assert_failure
|
||||
assert_output --partial 'InvalidParameterCombination'
|
||||
}
|
||||
|
||||
@test "rds: a master username longer than 16 characters is accepted on postgres, as on AWS" {
|
||||
run aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10 \
|
||||
--master-username application_db_admin_user \
|
||||
--master-user-password "secret99password"
|
||||
assert_success
|
||||
|
||||
run aws_cmd rds describe-db-instances --db-instance-identifier "$DB_ID" \
|
||||
--query 'DBInstances[0].MasterUsername'
|
||||
assert_success
|
||||
assert_output --partial 'application_db_admin_user'
|
||||
|
||||
# 64 characters is one past what PostgreSQL accepts on a live account
|
||||
long_username="a$(printf 'b%.0s' $(seq 63))"
|
||||
run aws_cmd rds create-db-instance \
|
||||
--db-instance-identifier "$DB_ID_2" \
|
||||
--engine postgres \
|
||||
--db-instance-class db.t3.micro \
|
||||
--allocated-storage 10 \
|
||||
--master-username "$long_username" \
|
||||
--master-user-password "secret99password"
|
||||
assert_failure
|
||||
assert_output --partial 'InvalidParameterValue'
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bats
|
||||
# S3 Notification Filter integration tests
|
||||
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
setup_file() {
|
||||
export TEST_BUCKET="$(unique_name s3-notif-filter-bucket)"
|
||||
export TEST_QUEUE="$(unique_name s3-notif-filter-queue)"
|
||||
export TEST_TOPIC="$(unique_name s3-notif-filter-topic)"
|
||||
export ACCOUNT_ID="000000000000"
|
||||
export QUEUE_ARN="arn:aws:sqs:us-east-1:${ACCOUNT_ID}:${TEST_QUEUE}"
|
||||
|
||||
# Create SQS queue
|
||||
aws_cmd sqs create-queue --queue-name "$TEST_QUEUE" >/dev/null 2>&1
|
||||
|
||||
# Create SNS topic and capture ARN
|
||||
local topic_out
|
||||
topic_out=$(aws_cmd sns create-topic --name "$TEST_TOPIC")
|
||||
export TOPIC_ARN=$(json_get "$topic_out" '.TopicArn')
|
||||
|
||||
# Create S3 bucket
|
||||
aws_cmd s3api create-bucket --bucket "$TEST_BUCKET" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
# Cleanup resources
|
||||
aws_cmd s3api delete-bucket --bucket "$TEST_BUCKET" >/dev/null 2>&1 || true
|
||||
aws_cmd sqs delete-queue --queue-url "${FLOCI_ENDPOINT}/${ACCOUNT_ID}/${TEST_QUEUE}" >/dev/null 2>&1 || true
|
||||
aws_cmd sns delete-topic --topic-arn "$TOPIC_ARN" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "S3 Notifications: put bucket notification configuration with filter" {
|
||||
local notif_config
|
||||
notif_config=$(cat <<EOF
|
||||
{
|
||||
"QueueConfigurations": [{
|
||||
"Id": "sqs-filtered",
|
||||
"QueueArn": "${QUEUE_ARN}",
|
||||
"Events": ["s3:ObjectCreated:*"],
|
||||
"Filter": {
|
||||
"Key": {
|
||||
"FilterRules": [
|
||||
{"Name": "prefix", "Value": "incoming/"},
|
||||
{"Name": "suffix", "Value": ".csv"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}],
|
||||
"TopicConfigurations": [{
|
||||
"Id": "sns-filtered",
|
||||
"TopicArn": "${TOPIC_ARN}",
|
||||
"Events": ["s3:ObjectRemoved:*"],
|
||||
"Filter": {
|
||||
"Key": {
|
||||
"FilterRules": [
|
||||
{"Name": "prefix", "Value": ""},
|
||||
{"Name": "suffix", "Value": ".txt"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
run aws_cmd s3api put-bucket-notification-configuration \
|
||||
--bucket "$TEST_BUCKET" \
|
||||
--notification-configuration "$notif_config"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "S3 Notifications: get bucket notification configuration - queue filter round-trip" {
|
||||
run aws_cmd s3api get-bucket-notification-configuration --bucket "$TEST_BUCKET"
|
||||
assert_success
|
||||
|
||||
# Verify queue configuration has 2 filter rules
|
||||
local queue_filter_count
|
||||
queue_filter_count=$(echo "$output" | jq "[.QueueConfigurations[] | select(.QueueArn == \"${QUEUE_ARN}\")][0].Filter.Key.FilterRules | length" 2>/dev/null || echo "0")
|
||||
[ "$queue_filter_count" = "2" ]
|
||||
}
|
||||
|
||||
@test "S3 Notifications: get bucket notification configuration - topic filter round-trip" {
|
||||
run aws_cmd s3api get-bucket-notification-configuration --bucket "$TEST_BUCKET"
|
||||
assert_success
|
||||
|
||||
# Verify topic configuration has 2 filter rules
|
||||
local topic_filter_count
|
||||
topic_filter_count=$(echo "$output" | jq "[.TopicConfigurations[] | select(.TopicArn == \"${TOPIC_ARN}\")][0].Filter.Key.FilterRules | length" 2>/dev/null || echo "0")
|
||||
[ "$topic_filter_count" = "2" ]
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
#!/usr/bin/env bats
|
||||
# S3 tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
BUCKET="bats-test-bucket-$(unique_name)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
# Clean up all objects in bucket
|
||||
aws_cmd s3 rm "s3://$BUCKET" --recursive >/dev/null 2>&1 || true
|
||||
aws_cmd s3api delete-bucket --bucket "$BUCKET" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "S3: create bucket" {
|
||||
run aws_cmd s3api create-bucket --bucket "$BUCKET"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "S3: list buckets" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
run aws_cmd s3api list-buckets
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$BUCKET" '.Buckets | any(.Name == $name)')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "S3: put object" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
local body_file
|
||||
body_file=$(mktemp)
|
||||
echo -n "hello-s3-bats" > "$body_file"
|
||||
|
||||
run aws_cmd s3api put-object --bucket "$BUCKET" --key "test.txt" --body "$body_file"
|
||||
assert_success
|
||||
rm -f "$body_file"
|
||||
}
|
||||
|
||||
@test "S3: get object" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
local body_file get_file
|
||||
body_file=$(mktemp)
|
||||
get_file=$(mktemp)
|
||||
echo -n "hello-s3-bats" > "$body_file"
|
||||
|
||||
aws_cmd s3api put-object --bucket "$BUCKET" --key "test.txt" --body "$body_file" >/dev/null
|
||||
|
||||
run aws_cmd s3api get-object --bucket "$BUCKET" --key "test.txt" "$get_file"
|
||||
assert_success
|
||||
|
||||
content=$(cat "$get_file")
|
||||
[ "$content" = "hello-s3-bats" ]
|
||||
|
||||
rm -f "$body_file" "$get_file"
|
||||
}
|
||||
|
||||
@test "S3: head object" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
local body_file
|
||||
body_file=$(mktemp)
|
||||
echo -n "hello-s3-bats" > "$body_file"
|
||||
aws_cmd s3api put-object --bucket "$BUCKET" --key "test.txt" --body "$body_file" >/dev/null
|
||||
|
||||
run aws_cmd s3api head-object --bucket "$BUCKET" --key "test.txt"
|
||||
assert_success
|
||||
length=$(json_get "$output" '.ContentLength')
|
||||
[ "$length" = "13" ]
|
||||
|
||||
rm -f "$body_file"
|
||||
}
|
||||
|
||||
@test "S3: list objects" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
local body_file
|
||||
body_file=$(mktemp)
|
||||
echo -n "hello" > "$body_file"
|
||||
aws_cmd s3api put-object --bucket "$BUCKET" --key "test.txt" --body "$body_file" >/dev/null
|
||||
|
||||
run aws_cmd s3api list-objects-v2 --bucket "$BUCKET"
|
||||
assert_success
|
||||
found=$(echo "$output" | jq '.Contents | any(.Key == "test.txt")')
|
||||
[ "$found" = "true" ]
|
||||
|
||||
rm -f "$body_file"
|
||||
}
|
||||
|
||||
@test "S3: copy object" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
local body_file
|
||||
body_file=$(mktemp)
|
||||
echo -n "hello" > "$body_file"
|
||||
aws_cmd s3api put-object --bucket "$BUCKET" --key "src.txt" --body "$body_file" >/dev/null
|
||||
|
||||
run aws_cmd s3api copy-object --bucket "$BUCKET" --copy-source "$BUCKET/src.txt" --key "dst.txt"
|
||||
assert_success
|
||||
|
||||
rm -f "$body_file"
|
||||
}
|
||||
|
||||
@test "S3: put and get object tagging" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
local body_file
|
||||
body_file=$(mktemp)
|
||||
echo -n "hello" > "$body_file"
|
||||
aws_cmd s3api put-object --bucket "$BUCKET" --key "test.txt" --body "$body_file" >/dev/null
|
||||
|
||||
run aws_cmd s3api put-object-tagging \
|
||||
--bucket "$BUCKET" \
|
||||
--key "test.txt" \
|
||||
--tagging 'TagSet=[{Key=env,Value=test}]'
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api get-object-tagging --bucket "$BUCKET" --key "test.txt"
|
||||
assert_success
|
||||
found=$(echo "$output" | jq '.TagSet | any(.Key == "env" and .Value == "test")')
|
||||
[ "$found" = "true" ]
|
||||
|
||||
rm -f "$body_file"
|
||||
}
|
||||
|
||||
@test "S3: delete object" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
local body_file
|
||||
body_file=$(mktemp)
|
||||
echo -n "hello" > "$body_file"
|
||||
aws_cmd s3api put-object --bucket "$BUCKET" --key "test.txt" --body "$body_file" >/dev/null
|
||||
|
||||
run aws_cmd s3api delete-object --bucket "$BUCKET" --key "test.txt"
|
||||
assert_success
|
||||
|
||||
rm -f "$body_file"
|
||||
}
|
||||
|
||||
@test "S3: delete bucket" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
run aws_cmd s3api delete-bucket --bucket "$BUCKET"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# --- S3 Versioning Tests ---
|
||||
|
||||
@test "S3: put bucket versioning" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
run aws_cmd s3api put-bucket-versioning \
|
||||
--bucket "$BUCKET" \
|
||||
--versioning-configuration Status=Enabled
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "S3: versioned objects have version IDs" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
aws_cmd s3api put-bucket-versioning \
|
||||
--bucket "$BUCKET" \
|
||||
--versioning-configuration Status=Enabled >/dev/null
|
||||
|
||||
local body_file
|
||||
body_file=$(mktemp)
|
||||
echo -n "version-one" > "$body_file"
|
||||
|
||||
run aws_cmd s3api put-object --bucket "$BUCKET" --key "ver.txt" --body "$body_file"
|
||||
assert_success
|
||||
v1=$(json_get "$output" '.VersionId')
|
||||
[ -n "$v1" ]
|
||||
|
||||
echo -n "version-two" > "$body_file"
|
||||
run aws_cmd s3api put-object --bucket "$BUCKET" --key "ver.txt" --body "$body_file"
|
||||
assert_success
|
||||
v2=$(json_get "$output" '.VersionId')
|
||||
[ -n "$v2" ]
|
||||
[ "$v1" != "$v2" ]
|
||||
|
||||
rm -f "$body_file"
|
||||
}
|
||||
|
||||
# --- S3 Multipart Upload Tests ---
|
||||
|
||||
@test "S3: create multipart upload" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
run aws_cmd s3api create-multipart-upload \
|
||||
--bucket "$BUCKET" \
|
||||
--key "multipart.bin"
|
||||
assert_success
|
||||
upload_id=$(json_get "$output" '.UploadId')
|
||||
[ -n "$upload_id" ]
|
||||
|
||||
# Cleanup: abort the upload
|
||||
aws_cmd s3api abort-multipart-upload \
|
||||
--bucket "$BUCKET" \
|
||||
--key "multipart.bin" \
|
||||
--upload-id "$upload_id" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "S3: upload part" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
output=$(aws_cmd s3api create-multipart-upload \
|
||||
--bucket "$BUCKET" \
|
||||
--key "multipart.bin")
|
||||
upload_id=$(json_get "$output" '.UploadId')
|
||||
|
||||
local part_file
|
||||
part_file=$(mktemp)
|
||||
echo -n "part-one-data" > "$part_file"
|
||||
|
||||
run aws_cmd s3api upload-part \
|
||||
--bucket "$BUCKET" \
|
||||
--key "multipart.bin" \
|
||||
--upload-id "$upload_id" \
|
||||
--part-number 1 \
|
||||
--body "$part_file"
|
||||
assert_success
|
||||
etag=$(json_get "$output" '.ETag')
|
||||
[ -n "$etag" ]
|
||||
|
||||
rm -f "$part_file"
|
||||
aws_cmd s3api abort-multipart-upload \
|
||||
--bucket "$BUCKET" \
|
||||
--key "multipart.bin" \
|
||||
--upload-id "$upload_id" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "S3: complete multipart upload" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
output=$(aws_cmd s3api create-multipart-upload \
|
||||
--bucket "$BUCKET" \
|
||||
--key "multipart.bin")
|
||||
upload_id=$(json_get "$output" '.UploadId')
|
||||
|
||||
local part1_file part2_file
|
||||
part1_file=$(mktemp)
|
||||
part2_file=$(mktemp)
|
||||
echo -n "part-one" > "$part1_file"
|
||||
echo -n "part-two" > "$part2_file"
|
||||
|
||||
output=$(aws_cmd s3api upload-part \
|
||||
--bucket "$BUCKET" \
|
||||
--key "multipart.bin" \
|
||||
--upload-id "$upload_id" \
|
||||
--part-number 1 \
|
||||
--body "$part1_file")
|
||||
etag1=$(json_get "$output" '.ETag')
|
||||
|
||||
output=$(aws_cmd s3api upload-part \
|
||||
--bucket "$BUCKET" \
|
||||
--key "multipart.bin" \
|
||||
--upload-id "$upload_id" \
|
||||
--part-number 2 \
|
||||
--body "$part2_file")
|
||||
etag2=$(json_get "$output" '.ETag')
|
||||
|
||||
local mp_file
|
||||
mp_file=$(mktemp)
|
||||
cat > "$mp_file" <<EOF
|
||||
{
|
||||
"Parts": [
|
||||
{ "ETag": $etag1, "PartNumber": 1 },
|
||||
{ "ETag": $etag2, "PartNumber": 2 }
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
run aws_cmd s3api complete-multipart-upload \
|
||||
--bucket "$BUCKET" \
|
||||
--key "multipart.bin" \
|
||||
--upload-id "$upload_id" \
|
||||
--multipart-upload "file://$mp_file"
|
||||
assert_success
|
||||
|
||||
rm -f "$part1_file" "$part2_file" "$mp_file"
|
||||
}
|
||||
|
||||
# --- S3 Large File Test ---
|
||||
|
||||
@test "S3: put object 25 MB" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
local large_file
|
||||
large_file=$(mktemp)
|
||||
dd if=/dev/zero of="$large_file" bs=1048576 count=25 2>/dev/null
|
||||
|
||||
run aws_cmd s3api put-object \
|
||||
--bucket "$BUCKET" \
|
||||
--key "large-25mb.bin" \
|
||||
--body "$large_file"
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api head-object \
|
||||
--bucket "$BUCKET" \
|
||||
--key "large-25mb.bin"
|
||||
assert_success
|
||||
length=$(json_get "$output" '.ContentLength')
|
||||
[ "$length" = "26214400" ]
|
||||
|
||||
rm -f "$large_file"
|
||||
}
|
||||
|
||||
@test "S3: metrics configurations round-trip and never touch the bucket" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
run aws_cmd s3api put-bucket-metrics-configuration \
|
||||
--bucket "$BUCKET" --id EntireBucket --metrics-configuration 'Id=EntireBucket'
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api get-bucket-metrics-configuration --bucket "$BUCKET" --id EntireBucket
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.MetricsConfiguration.Id')" = "EntireBucket" ]
|
||||
|
||||
run aws_cmd s3api put-bucket-metrics-configuration \
|
||||
--bucket "$BUCKET" --id Filtered \
|
||||
--metrics-configuration 'Id=Filtered,Filter={And={Prefix=logs/,Tags=[{Key=env,Value=prod}]}}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api list-bucket-metrics-configurations --bucket "$BUCKET"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.MetricsConfigurationList | length')" = "2" ]
|
||||
[ "$(json_get "$output" '.MetricsConfigurationList[1].Filter.And.Prefix')" = "logs/" ]
|
||||
[ "$(json_get "$output" '.MetricsConfigurationList[1].Filter.And.Tags[0].Key')" = "env" ]
|
||||
|
||||
# A sub-resource DELETE removes only that configuration; the bucket must survive it.
|
||||
run aws_cmd s3api delete-bucket-metrics-configuration --bucket "$BUCKET" --id EntireBucket
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api head-bucket --bucket "$BUCKET"
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api get-bucket-metrics-configuration --bucket "$BUCKET" --id EntireBucket
|
||||
assert_failure
|
||||
assert_output --partial "NoSuchConfiguration"
|
||||
}
|
||||
|
||||
@test "S3: intelligent-tiering configurations round-trip and never touch the bucket" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
|
||||
run aws_cmd s3api put-bucket-intelligent-tiering-configuration \
|
||||
--bucket "$BUCKET" --id EntireBucket \
|
||||
--intelligent-tiering-configuration 'Id=EntireBucket,Status=Enabled,Tierings=[{AccessTier=ARCHIVE_ACCESS,Days=90}]'
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api get-bucket-intelligent-tiering-configuration --bucket "$BUCKET" --id EntireBucket
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfiguration.Id')" = "EntireBucket" ]
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfiguration.Status')" = "Enabled" ]
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfiguration.Tierings[0].AccessTier')" = "ARCHIVE_ACCESS" ]
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfiguration.Tierings[0].Days')" = "90" ]
|
||||
|
||||
run aws_cmd s3api put-bucket-intelligent-tiering-configuration \
|
||||
--bucket "$BUCKET" --id Filtered \
|
||||
--intelligent-tiering-configuration 'Id=Filtered,Filter={And={Prefix=logs/,Tags=[{Key=env,Value=prod}]}},Status=Disabled,Tierings=[{AccessTier=ARCHIVE_ACCESS,Days=90},{AccessTier=DEEP_ARCHIVE_ACCESS,Days=180}]'
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api list-bucket-intelligent-tiering-configurations --bucket "$BUCKET"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfigurationList | length')" = "2" ]
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfigurationList[1].Filter.And.Prefix')" = "logs/" ]
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfigurationList[1].Filter.And.Tags[0].Key')" = "env" ]
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfigurationList[1].Status')" = "Disabled" ]
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfigurationList[1].Tierings | length')" = "2" ]
|
||||
|
||||
# Putting the same id replaces rather than duplicates.
|
||||
run aws_cmd s3api put-bucket-intelligent-tiering-configuration \
|
||||
--bucket "$BUCKET" --id EntireBucket \
|
||||
--intelligent-tiering-configuration 'Id=EntireBucket,Status=Disabled,Tierings=[{AccessTier=DEEP_ARCHIVE_ACCESS,Days=180}]'
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api get-bucket-intelligent-tiering-configuration --bucket "$BUCKET" --id EntireBucket
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfiguration.Status')" = "Disabled" ]
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfiguration.Tierings[0].AccessTier')" = "DEEP_ARCHIVE_ACCESS" ]
|
||||
|
||||
run aws_cmd s3api list-bucket-intelligent-tiering-configurations --bucket "$BUCKET"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.IntelligentTieringConfigurationList | length')" = "2" ]
|
||||
|
||||
# A sub-resource DELETE removes only that configuration; the bucket must survive it.
|
||||
run aws_cmd s3api delete-bucket-intelligent-tiering-configuration --bucket "$BUCKET" --id EntireBucket
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api head-bucket --bucket "$BUCKET"
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api get-bucket-intelligent-tiering-configuration --bucket "$BUCKET" --id EntireBucket
|
||||
assert_failure
|
||||
assert_output --partial "NoSuchConfiguration"
|
||||
}
|
||||
|
||||
# Checksum helpers: Base64 SHA256 of a file, and the composite S3 stores for a multipart upload
|
||||
# (SHA256 over the concatenated binary part digests, suffixed with the part count).
|
||||
sha256_b64() {
|
||||
openssl dgst -sha256 -binary "$1" | base64 | tr -d '\n'
|
||||
}
|
||||
|
||||
sha256_composite() {
|
||||
local count=$#
|
||||
for part in "$@"; do openssl dgst -sha256 -binary "$part"; done \
|
||||
| openssl dgst -sha256 -binary | base64 | tr -d '\n'
|
||||
echo "-$count"
|
||||
}
|
||||
|
||||
@test "S3: multipart upload with SHA256 stores the composite checksum" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
local dir
|
||||
dir=$(mktemp -d)
|
||||
head -c $((5 * 1024 * 1024)) /dev/urandom > "$dir/part1"
|
||||
head -c $((1024 * 1024)) /dev/urandom > "$dir/part2"
|
||||
local sha1 sha2 composite
|
||||
sha1=$(sha256_b64 "$dir/part1")
|
||||
sha2=$(sha256_b64 "$dir/part2")
|
||||
composite=$(sha256_composite "$dir/part1" "$dir/part2")
|
||||
|
||||
output=$(aws_cmd s3api create-multipart-upload --bucket "$BUCKET" --key "composite.bin" \
|
||||
--checksum-algorithm SHA256)
|
||||
upload_id=$(json_get "$output" '.UploadId')
|
||||
[ "$(json_get "$output" '.ChecksumAlgorithm')" = "SHA256" ]
|
||||
[ "$(json_get "$output" '.ChecksumType')" = "COMPOSITE" ]
|
||||
|
||||
output=$(aws_cmd s3api upload-part --bucket "$BUCKET" --key "composite.bin" --upload-id "$upload_id" \
|
||||
--part-number 1 --body "$dir/part1" --checksum-sha256 "$sha1")
|
||||
etag1=$(json_get "$output" '.ETag')
|
||||
# UploadPart echoes the checksum of the part
|
||||
[ "$(json_get "$output" '.ChecksumSHA256')" = "$sha1" ]
|
||||
output=$(aws_cmd s3api upload-part --bucket "$BUCKET" --key "composite.bin" --upload-id "$upload_id" \
|
||||
--part-number 2 --body "$dir/part2" --checksum-sha256 "$sha2")
|
||||
etag2=$(json_get "$output" '.ETag')
|
||||
|
||||
local parts_json
|
||||
parts_json=$(jq -n --arg e1 "$etag1" --arg s1 "$sha1" --arg e2 "$etag2" --arg s2 "$sha2" \
|
||||
'{Parts: [{PartNumber: 1, ETag: $e1, ChecksumSHA256: $s1}, {PartNumber: 2, ETag: $e2, ChecksumSHA256: $s2}]}')
|
||||
run aws_cmd s3api complete-multipart-upload --bucket "$BUCKET" --key "composite.bin" \
|
||||
--upload-id "$upload_id" --multipart-upload "$parts_json"
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.ChecksumSHA256')" = "$composite" ]
|
||||
[ "$(json_get "$output" '.ChecksumType')" = "COMPOSITE" ]
|
||||
|
||||
run aws_cmd s3api head-object --bucket "$BUCKET" --key "composite.bin" --checksum-mode ENABLED
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.ChecksumSHA256')" = "$composite" ]
|
||||
[ "$(json_get "$output" '.ChecksumType')" = "COMPOSITE" ]
|
||||
|
||||
# GetObjectAttributes reports the composite without the "-2" suffix and the ETag without quotes
|
||||
run aws_cmd s3api get-object-attributes --bucket "$BUCKET" --key "composite.bin" \
|
||||
--object-attributes Checksum ObjectParts ETag
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.Checksum.ChecksumSHA256')" = "${composite%-2}" ]
|
||||
[ "$(json_get "$output" '.ObjectParts.TotalPartsCount')" = "2" ]
|
||||
[ "$(json_get "$output" '.ObjectParts.Parts[0].ChecksumSHA256')" = "$sha1" ]
|
||||
[[ "$(json_get "$output" '.ETag')" =~ ^[0-9a-f]{32}-2$ ]]
|
||||
rm -rf "$dir"
|
||||
}
|
||||
|
||||
@test "S3: complete-multipart-upload without part checksums is rejected on a SHA256 upload" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
local part_file
|
||||
part_file=$(mktemp)
|
||||
echo -n "part-one" > "$part_file"
|
||||
output=$(aws_cmd s3api create-multipart-upload --bucket "$BUCKET" --key "strict.bin" --checksum-algorithm SHA256)
|
||||
upload_id=$(json_get "$output" '.UploadId')
|
||||
output=$(aws_cmd s3api upload-part --bucket "$BUCKET" --key "strict.bin" --upload-id "$upload_id" \
|
||||
--part-number 1 --body "$part_file" --checksum-sha256 "$(sha256_b64 "$part_file")")
|
||||
etag1=$(json_get "$output" '.ETag')
|
||||
|
||||
run aws_cmd s3api complete-multipart-upload --bucket "$BUCKET" --key "strict.bin" --upload-id "$upload_id" \
|
||||
--multipart-upload "$(jq -n --arg e1 "$etag1" '{Parts: [{PartNumber: 1, ETag: $e1}]}')"
|
||||
assert_failure
|
||||
assert_output --partial "InvalidRequest"
|
||||
assert_output --partial "must include the checksum for each part"
|
||||
|
||||
aws_cmd s3api abort-multipart-upload --bucket "$BUCKET" --key "strict.bin" --upload-id "$upload_id" >/dev/null 2>&1 || true
|
||||
rm -f "$part_file"
|
||||
}
|
||||
|
||||
@test "S3: aws s3 cp --checksum-algorithm uploads a large file with a composite checksum" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
local dir
|
||||
dir=$(mktemp -d)
|
||||
head -c $((12 * 1024 * 1024)) /dev/urandom > "$dir/large.bin"
|
||||
# the CLI splits at its default 8 MiB part size, so the composite is over two parts
|
||||
split -b 8m "$dir/large.bin" "$dir/piece."
|
||||
local composite
|
||||
composite=$(sha256_composite "$dir"/piece.*)
|
||||
[[ "$composite" == *-2 ]]
|
||||
|
||||
run aws_cmd s3 cp "$dir/large.bin" "s3://$BUCKET/large.bin" --checksum-algorithm SHA256
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api head-object --bucket "$BUCKET" --key "large.bin" --checksum-mode ENABLED
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.ChecksumSHA256')" = "$composite" ]
|
||||
[ "$(json_get "$output" '.ChecksumType')" = "COMPOSITE" ]
|
||||
|
||||
run aws_cmd s3 cp "s3://$BUCKET/large.bin" "$dir/roundtrip.bin"
|
||||
assert_success
|
||||
same_content "$dir/large.bin" "$dir/roundtrip.bin"
|
||||
rm -rf "$dir"
|
||||
}
|
||||
|
||||
# CRC32 (IEEE) checksums as S3 encodes them (Base64 of the big-endian value); python3 ships in the CLI image.
|
||||
crc32_b64() {
|
||||
python3 -c 'import sys, zlib, base64
|
||||
print(base64.b64encode((zlib.crc32(open(sys.argv[1], "rb").read()) & 0xffffffff).to_bytes(4, "big")).decode())' "$1"
|
||||
}
|
||||
|
||||
crc32_composite() {
|
||||
python3 -c 'import sys, zlib, base64
|
||||
def crc(data): return (zlib.crc32(data) & 0xffffffff).to_bytes(4, "big")
|
||||
parts = [open(p, "rb").read() for p in sys.argv[1:]]
|
||||
print(base64.b64encode(crc(b"".join(crc(p) for p in parts))).decode() + f"-{len(parts)}")' "$@"
|
||||
}
|
||||
|
||||
same_content() {
|
||||
[ "$(openssl dgst -sha256 -r "$1" | cut -d" " -f1)" = "$(openssl dgst -sha256 -r "$2" | cut -d" " -f1)" ]
|
||||
}
|
||||
|
||||
@test "S3: plain aws s3 cp of a large file uploads in parts and stores the CLI's default checksum" {
|
||||
aws_cmd s3api create-bucket --bucket "$BUCKET" >/dev/null
|
||||
local dir
|
||||
dir=$(mktemp -d)
|
||||
head -c $((12 * 1024 * 1024)) /dev/urandom > "$dir/large.bin"
|
||||
split -b 8m "$dir/large.bin" "$dir/piece."
|
||||
|
||||
# no --checksum-algorithm: the CLI multipart-uploads at 8 MiB parts with whatever its version declares
|
||||
run aws_cmd s3 cp "$dir/large.bin" "s3://$BUCKET/large-default.bin"
|
||||
assert_success
|
||||
|
||||
run aws_cmd s3api head-object --bucket "$BUCKET" --key "large-default.bin" --checksum-mode ENABLED
|
||||
assert_success
|
||||
local etag type crc32
|
||||
etag=$(json_get "$output" '.ETag')
|
||||
type=$(json_get "$output" '.ChecksumType')
|
||||
crc32=$(json_get "$output" '.ChecksumCRC32')
|
||||
[[ "$etag" =~ -2\"$ ]]
|
||||
if [ -n "$crc32" ] && [ "$crc32" != "null" ]; then
|
||||
# current CLIs declare CRC32, which S3 stores as COMPOSITE (or FULL_OBJECT when the CLI asks for it)
|
||||
if [ "$type" = "COMPOSITE" ]; then
|
||||
[ "$crc32" = "$(crc32_composite "$dir"/piece.*)" ]
|
||||
else
|
||||
[ "$type" = "FULL_OBJECT" ]
|
||||
[ "$crc32" = "$(crc32_b64 "$dir/large.bin")" ]
|
||||
fi
|
||||
else
|
||||
# a CLI that declares no algorithm gets the CRC64NVME full-object checksum S3 attaches itself
|
||||
[ "$type" = "FULL_OBJECT" ]
|
||||
[ -n "$(json_get "$output" '.ChecksumCRC64NVME')" ]
|
||||
fi
|
||||
|
||||
run aws_cmd s3api get-object-attributes --bucket "$BUCKET" --key "large-default.bin" --object-attributes ObjectParts
|
||||
assert_success
|
||||
[ "$(json_get "$output" '.ObjectParts.TotalPartsCount')" = "2" ]
|
||||
|
||||
run aws_cmd s3 cp "s3://$BUCKET/large-default.bin" "$dir/roundtrip.bin"
|
||||
assert_success
|
||||
same_content "$dir/large.bin" "$dir/roundtrip.bin"
|
||||
rm -rf "$dir"
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env bats
|
||||
# Secrets Manager tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
SECRET_NAME="bats/test/secret-$(unique_name)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
aws_cmd secretsmanager delete-secret \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--force-delete-without-recovery >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "Secrets Manager: create secret" {
|
||||
run aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}'
|
||||
assert_success
|
||||
arn=$(json_get "$output" '.ARN')
|
||||
[ -n "$arn" ]
|
||||
}
|
||||
|
||||
@test "Secrets Manager: get secret value" {
|
||||
aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}' >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager get-secret-value --secret-id "$SECRET_NAME"
|
||||
assert_success
|
||||
value=$(json_get "$output" '.SecretString')
|
||||
[ "$value" = '{"key":"value"}' ]
|
||||
}
|
||||
|
||||
@test "Secrets Manager: update secret" {
|
||||
aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}' >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager update-secret \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--secret-string '{"key":"updated"}'
|
||||
assert_success
|
||||
|
||||
run aws_cmd secretsmanager get-secret-value --secret-id "$SECRET_NAME"
|
||||
assert_success
|
||||
value=$(json_get "$output" '.SecretString')
|
||||
[ "$value" = '{"key":"updated"}' ]
|
||||
}
|
||||
|
||||
@test "Secrets Manager: list secrets" {
|
||||
aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}' >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager list-secrets
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$SECRET_NAME" '.SecretList | any(.Name == $name)')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "Secrets Manager: delete secret" {
|
||||
aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}' >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager delete-secret \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--force-delete-without-recovery
|
||||
assert_success
|
||||
}
|
||||
|
||||
# --- Secrets Manager Tagging Tests ---
|
||||
|
||||
@test "Secrets Manager: tag resource" {
|
||||
out=$(aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}')
|
||||
arn=$(json_get "$out" '.ARN')
|
||||
|
||||
run aws_cmd secretsmanager tag-resource \
|
||||
--secret-id "$arn" \
|
||||
--tags Key=Environment,Value=test Key=Project,Value=bats
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "Secrets Manager: describe secret with tags" {
|
||||
out=$(aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}')
|
||||
arn=$(json_get "$out" '.ARN')
|
||||
|
||||
aws_cmd secretsmanager tag-resource \
|
||||
--secret-id "$arn" \
|
||||
--tags Key=Environment,Value=test >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "$SECRET_NAME"
|
||||
assert_success
|
||||
found=$(echo "$output" | jq '.Tags | any(.Key == "Environment" and .Value == "test")')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "Secrets Manager: untag resource" {
|
||||
out=$(aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}')
|
||||
arn=$(json_get "$out" '.ARN')
|
||||
|
||||
aws_cmd secretsmanager tag-resource \
|
||||
--secret-id "$arn" \
|
||||
--tags Key=Environment,Value=test >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager untag-resource \
|
||||
--secret-id "$arn" \
|
||||
--tag-keys Environment
|
||||
assert_success
|
||||
|
||||
# Verify tag is removed
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "$SECRET_NAME"
|
||||
found=$(echo "$output" | jq '.Tags // [] | any(.Key == "Environment")')
|
||||
[ "$found" = "false" ]
|
||||
}
|
||||
|
||||
# --- Secrets Manager Version Stage Tests ---
|
||||
|
||||
@test "Secrets Manager: add custom stage to version" {
|
||||
out=$(aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}')
|
||||
v1=$(json_get "$out" '.VersionId')
|
||||
|
||||
run aws_cmd secretsmanager update-secret-version-stage \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--version-stage MYSTAGE \
|
||||
--move-to-version-id "$v1"
|
||||
assert_success
|
||||
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "$SECRET_NAME"
|
||||
assert_success
|
||||
has_mystage=$(echo "$output" | jq --arg vid "$v1" '.VersionIdsToStages[$vid] | any(. == "MYSTAGE")')
|
||||
[ "$has_mystage" = "true" ]
|
||||
has_current=$(echo "$output" | jq --arg vid "$v1" '.VersionIdsToStages[$vid] | any(. == "AWSCURRENT")')
|
||||
[ "$has_current" = "true" ]
|
||||
}
|
||||
|
||||
@test "Secrets Manager: move custom stage between versions" {
|
||||
out=$(aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"v1"}')
|
||||
v1=$(json_get "$out" '.VersionId')
|
||||
|
||||
out=$(aws_cmd secretsmanager update-secret \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--secret-string '{"key":"v2"}')
|
||||
v2=$(json_get "$out" '.VersionId')
|
||||
|
||||
aws_cmd secretsmanager update-secret-version-stage \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--version-stage MYSTAGE \
|
||||
--move-to-version-id "$v1" >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager update-secret-version-stage \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--version-stage MYSTAGE \
|
||||
--move-to-version-id "$v2" \
|
||||
--remove-from-version-id "$v1"
|
||||
assert_success
|
||||
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "$SECRET_NAME"
|
||||
assert_success
|
||||
on_v2=$(echo "$output" | jq --arg vid "$v2" '.VersionIdsToStages[$vid] | any(. == "MYSTAGE")')
|
||||
[ "$on_v2" = "true" ]
|
||||
on_v1=$(echo "$output" | jq --arg vid "$v1" '(.VersionIdsToStages[$vid] // []) | any(. == "MYSTAGE")')
|
||||
[ "$on_v1" = "false" ]
|
||||
}
|
||||
|
||||
@test "Secrets Manager: move AWSCURRENT updates AWSPREVIOUS" {
|
||||
out=$(aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"v1"}')
|
||||
v1=$(json_get "$out" '.VersionId')
|
||||
|
||||
out=$(aws_cmd secretsmanager update-secret \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--secret-string '{"key":"v2"}')
|
||||
v2=$(json_get "$out" '.VersionId')
|
||||
|
||||
run aws_cmd secretsmanager update-secret-version-stage \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--version-stage AWSCURRENT \
|
||||
--move-to-version-id "$v1" \
|
||||
--remove-from-version-id "$v2"
|
||||
assert_success
|
||||
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "$SECRET_NAME"
|
||||
assert_success
|
||||
v1_current=$(echo "$output" | jq --arg vid "$v1" '.VersionIdsToStages[$vid] | any(. == "AWSCURRENT")')
|
||||
[ "$v1_current" = "true" ]
|
||||
v2_previous=$(echo "$output" | jq --arg vid "$v2" '.VersionIdsToStages[$vid] | any(. == "AWSPREVIOUS")')
|
||||
[ "$v2_previous" = "true" ]
|
||||
}
|
||||
|
||||
@test "Secrets Manager: remove custom stage from version" {
|
||||
out=$(aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}')
|
||||
v1=$(json_get "$out" '.VersionId')
|
||||
|
||||
aws_cmd secretsmanager update-secret-version-stage \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--version-stage MYSTAGE \
|
||||
--move-to-version-id "$v1" >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager update-secret-version-stage \
|
||||
--secret-id "$SECRET_NAME" \
|
||||
--version-stage MYSTAGE \
|
||||
--remove-from-version-id "$v1"
|
||||
assert_success
|
||||
|
||||
run aws_cmd secretsmanager describe-secret --secret-id "$SECRET_NAME"
|
||||
assert_success
|
||||
has_mystage=$(echo "$output" | jq --arg vid "$v1" '(.VersionIdsToStages[$vid] // []) | any(. == "MYSTAGE")')
|
||||
[ "$has_mystage" = "false" ]
|
||||
has_current=$(echo "$output" | jq --arg vid "$v1" '.VersionIdsToStages[$vid] | any(. == "AWSCURRENT")')
|
||||
[ "$has_current" = "true" ]
|
||||
}
|
||||
|
||||
# --- Batch Get Secret Value Tests ---
|
||||
|
||||
@test "Secrets Manager: batch-get-secret-value returns all found secrets" {
|
||||
S1="${SECRET_NAME}-batch-a"
|
||||
S2="${SECRET_NAME}-batch-b"
|
||||
|
||||
aws_cmd secretsmanager create-secret \
|
||||
--name "$S1" \
|
||||
--secret-string '{"key":"val-a"}' >/dev/null
|
||||
aws_cmd secretsmanager create-secret \
|
||||
--name "$S2" \
|
||||
--secret-string '{"key":"val-b"}' >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager batch-get-secret-value \
|
||||
--secret-id-list "$S1" "$S2"
|
||||
assert_success
|
||||
|
||||
count=$(echo "$output" | jq '.SecretValues | length')
|
||||
[ "$count" = "2" ]
|
||||
errors=$(echo "$output" | jq '.Errors | length')
|
||||
[ "$errors" = "0" ]
|
||||
|
||||
aws_cmd secretsmanager delete-secret --secret-id "$S1" --force-delete-without-recovery >/dev/null 2>&1 || true
|
||||
aws_cmd secretsmanager delete-secret --secret-id "$S2" --force-delete-without-recovery >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "Secrets Manager: batch-get-secret-value with one missing returns partial Errors list" {
|
||||
aws_cmd secretsmanager create-secret \
|
||||
--name "$SECRET_NAME" \
|
||||
--secret-string '{"key":"value"}' >/dev/null
|
||||
|
||||
run aws_cmd secretsmanager batch-get-secret-value \
|
||||
--secret-id-list "$SECRET_NAME" "not-exists"
|
||||
assert_success
|
||||
|
||||
values=$(echo "$output" | jq '.SecretValues | length')
|
||||
[ "$values" = "1" ]
|
||||
errors=$(echo "$output" | jq '.Errors | length')
|
||||
[ "$errors" = "1" ]
|
||||
error_code=$(echo "$output" | jq -r '.Errors[0].ErrorCode')
|
||||
[ "$error_code" = "ResourceNotFoundException" ]
|
||||
secret_id=$(echo "$output" | jq -r '.Errors[0].SecretId')
|
||||
[ "$secret_id" = "not-exists" ]
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env bats
|
||||
# SES integration tests
|
||||
|
||||
load 'test_helper/common-setup'
|
||||
|
||||
setup_file() {
|
||||
export TEST_EMAIL="$(unique_name test)@example.com"
|
||||
export TEST_DOMAIN="$(unique_name test).example.com"
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
# Cleanup identities
|
||||
aws_cmd ses delete-identity --identity "$TEST_EMAIL" >/dev/null 2>&1 || true
|
||||
aws_cmd ses delete-identity --identity "$TEST_DOMAIN" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "SES: verify email identity" {
|
||||
run aws_cmd ses verify-email-identity --email-address "$TEST_EMAIL"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "SES: verify domain identity" {
|
||||
run aws_cmd ses verify-domain-identity --domain "$TEST_DOMAIN"
|
||||
assert_success
|
||||
token=$(json_get "$output" '.VerificationToken')
|
||||
[ -n "$token" ]
|
||||
}
|
||||
|
||||
@test "SES: list identities" {
|
||||
run aws_cmd ses list-identities
|
||||
assert_success
|
||||
assert_output --partial "$TEST_EMAIL"
|
||||
assert_output --partial "$TEST_DOMAIN"
|
||||
}
|
||||
|
||||
@test "SES: list identities by type EmailAddress" {
|
||||
run aws_cmd ses list-identities --identity-type EmailAddress
|
||||
assert_success
|
||||
assert_output --partial "$TEST_EMAIL"
|
||||
refute_output --partial "$TEST_DOMAIN"
|
||||
}
|
||||
|
||||
@test "SES: get identity verification attributes" {
|
||||
run aws_cmd ses get-identity-verification-attributes --identities "$TEST_EMAIL"
|
||||
assert_success
|
||||
status=$(json_get "$output" ".VerificationAttributes.\"$TEST_EMAIL\".VerificationStatus")
|
||||
[ "$status" = "Success" ]
|
||||
}
|
||||
|
||||
@test "SES: send email" {
|
||||
run aws_cmd ses send-email \
|
||||
--from "$TEST_EMAIL" \
|
||||
--destination "ToAddresses=recipient@example.com" \
|
||||
--message "Subject={Data=Test Subject},Body={Text={Data=Hello from SES test}}"
|
||||
assert_success
|
||||
message_id=$(json_get "$output" '.MessageId')
|
||||
[ -n "$message_id" ]
|
||||
}
|
||||
|
||||
@test "SES: send raw email" {
|
||||
local raw_file
|
||||
raw_file=$(mktemp)
|
||||
printf 'From: %s\r\nTo: recipient@example.com\r\nSubject: Raw Test\r\n\r\nRaw body' "$TEST_EMAIL" > "$raw_file"
|
||||
local raw_b64
|
||||
raw_b64=$(python3 -c "import base64,sys; print(base64.b64encode(open(sys.argv[1],'rb').read()).decode())" "$raw_file")
|
||||
|
||||
run aws_cmd ses send-raw-email \
|
||||
--source "$TEST_EMAIL" \
|
||||
--destinations "recipient@example.com" \
|
||||
--raw-message "Data=$raw_b64"
|
||||
rm -f "$raw_file"
|
||||
|
||||
assert_success
|
||||
message_id=$(json_get "$output" '.MessageId')
|
||||
[ -n "$message_id" ]
|
||||
}
|
||||
|
||||
@test "SES: get send quota" {
|
||||
run aws_cmd ses get-send-quota
|
||||
assert_success
|
||||
max_24h=$(json_get "$output" '.Max24HourSend')
|
||||
max_rate=$(json_get "$output" '.MaxSendRate')
|
||||
[ "$(echo "$max_24h > 0" | bc)" -eq 1 ]
|
||||
[ "$(echo "$max_rate > 0" | bc)" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "SES: get send statistics" {
|
||||
run aws_cmd ses get-send-statistics
|
||||
assert_success
|
||||
# Should have SendDataPoints array (may be empty)
|
||||
run jq -e '.SendDataPoints' <<< "$output"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "SES: get account sending enabled" {
|
||||
run aws_cmd ses get-account-sending-enabled
|
||||
assert_success
|
||||
enabled=$(json_get "$output" '.Enabled')
|
||||
[ "$enabled" = "true" ]
|
||||
}
|
||||
|
||||
@test "SES: get identity dkim attributes" {
|
||||
run aws_cmd ses get-identity-dkim-attributes --identities "$TEST_DOMAIN"
|
||||
assert_success
|
||||
# Should have DkimAttributes for our domain
|
||||
run jq -e ".DkimAttributes.\"$TEST_DOMAIN\"" <<< "$output"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "SES: set identity notification topic" {
|
||||
# This test checks that the command is accepted (may have parser quirks)
|
||||
run aws_cmd ses set-identity-notification-topic \
|
||||
--identity "$TEST_EMAIL" \
|
||||
--notification-type Bounce \
|
||||
--sns-topic arn:aws:sns:us-east-1:000000000000:bounce-topic
|
||||
# Accept success or known parser bug output
|
||||
[[ $status -eq 0 ]] || [[ "$output" == *"SetIdentityNotificationTopicResult"* ]]
|
||||
}
|
||||
|
||||
@test "SES: get identity notification attributes" {
|
||||
run aws_cmd ses get-identity-notification-attributes --identities "$TEST_EMAIL"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "SES: delete identity email" {
|
||||
run aws_cmd ses delete-identity --identity "$TEST_EMAIL"
|
||||
# Accept success or known parser bug output
|
||||
[[ $status -eq 0 ]] || [[ "$output" == *"DeleteIdentityResult"* ]]
|
||||
}
|
||||
|
||||
@test "SES: delete identity domain" {
|
||||
run aws_cmd ses delete-identity --identity "$TEST_DOMAIN"
|
||||
# Accept success or known parser bug output
|
||||
[[ $status -eq 0 ]] || [[ "$output" == *"DeleteIdentityResult"* ]]
|
||||
}
|
||||
|
||||
@test "SES: verify identities deleted" {
|
||||
run aws_cmd ses list-identities
|
||||
assert_success
|
||||
refute_output --partial "$TEST_EMAIL"
|
||||
refute_output --partial "$TEST_DOMAIN"
|
||||
}
|
||||
|
||||
# ──────────────── Template CRUD ────────────────
|
||||
|
||||
@test "SES: create template" {
|
||||
local tpl_json
|
||||
tpl_json=$(cat <<TMPL
|
||||
{"TemplateName":"cli-tpl-${BATS_ROOT_PID}","SubjectPart":"Hello {{name}}","TextPart":"Hi {{name}} from {{team}}","HtmlPart":"<p>Hi {{name}}</p>"}
|
||||
TMPL
|
||||
)
|
||||
run aws_cmd ses create-template --template "$tpl_json"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "SES: create template - duplicate rejected" {
|
||||
local tpl_json
|
||||
tpl_json=$(cat <<TMPL
|
||||
{"TemplateName":"cli-tpl-${BATS_ROOT_PID}","SubjectPart":"Dup","TextPart":"Dup"}
|
||||
TMPL
|
||||
)
|
||||
run aws_cmd ses create-template --template "$tpl_json"
|
||||
assert_failure
|
||||
assert_output --partial "AlreadyExists"
|
||||
}
|
||||
|
||||
@test "SES: get template" {
|
||||
run aws_cmd ses get-template --template-name "cli-tpl-${BATS_ROOT_PID}"
|
||||
assert_success
|
||||
name=$(json_get "$output" '.Template.TemplateName')
|
||||
subject=$(json_get "$output" '.Template.SubjectPart')
|
||||
[ "$name" = "cli-tpl-${BATS_ROOT_PID}" ]
|
||||
[ "$subject" = "Hello {{name}}" ]
|
||||
}
|
||||
|
||||
@test "SES: get template - not found" {
|
||||
run aws_cmd ses get-template --template-name "nonexistent-tpl"
|
||||
assert_failure
|
||||
assert_output --partial "TemplateDoesNotExist"
|
||||
}
|
||||
|
||||
@test "SES: update template" {
|
||||
local tpl_json
|
||||
tpl_json=$(cat <<TMPL
|
||||
{"TemplateName":"cli-tpl-${BATS_ROOT_PID}","SubjectPart":"Updated {{name}}","TextPart":"Updated {{name}} {{team}}","HtmlPart":"<p>Updated {{name}}</p>"}
|
||||
TMPL
|
||||
)
|
||||
run aws_cmd ses update-template --template "$tpl_json"
|
||||
assert_success
|
||||
|
||||
run aws_cmd ses get-template --template-name "cli-tpl-${BATS_ROOT_PID}"
|
||||
assert_success
|
||||
subject=$(json_get "$output" '.Template.SubjectPart')
|
||||
[ "$subject" = "Updated {{name}}" ]
|
||||
}
|
||||
|
||||
@test "SES: list templates includes created" {
|
||||
run aws_cmd ses list-templates
|
||||
assert_success
|
||||
assert_output --partial "cli-tpl-${BATS_ROOT_PID}"
|
||||
}
|
||||
|
||||
@test "SES: send templated email" {
|
||||
# Ensure an identity exists for sending
|
||||
aws_cmd ses verify-email-identity --email-address "tpl-sender@example.com" >/dev/null 2>&1 || true
|
||||
|
||||
run aws_cmd ses send-templated-email \
|
||||
--source "tpl-sender@example.com" \
|
||||
--destination "ToAddresses=recipient@example.com" \
|
||||
--template "cli-tpl-${BATS_ROOT_PID}" \
|
||||
--template-data '{"name":"Alice","team":"floci"}'
|
||||
assert_success
|
||||
message_id=$(json_get "$output" '.MessageId')
|
||||
[ -n "$message_id" ]
|
||||
}
|
||||
|
||||
@test "SES: send templated email - unknown template" {
|
||||
run aws_cmd ses send-templated-email \
|
||||
--source "tpl-sender@example.com" \
|
||||
--destination "ToAddresses=recipient@example.com" \
|
||||
--template "nonexistent-tpl" \
|
||||
--template-data '{}'
|
||||
assert_failure
|
||||
assert_output --partial "TemplateDoesNotExist"
|
||||
}
|
||||
|
||||
@test "SES: delete template" {
|
||||
run aws_cmd ses delete-template --template-name "cli-tpl-${BATS_ROOT_PID}"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "SES: delete template - not found" {
|
||||
run aws_cmd ses delete-template --template-name "nonexistent-tpl"
|
||||
assert_failure
|
||||
assert_output --partial "TemplateDoesNotExist"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bats
|
||||
# SNS tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
TOPIC_NAME="bats-test-topic-$(unique_name)"
|
||||
TOPIC_ARN=""
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ -n "$TOPIC_ARN" ]; then
|
||||
aws_cmd sns delete-topic --topic-arn "$TOPIC_ARN" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
@test "SNS: create topic" {
|
||||
run aws_cmd sns create-topic --name "$TOPIC_NAME"
|
||||
assert_success
|
||||
TOPIC_ARN=$(json_get "$output" '.TopicArn')
|
||||
[ -n "$TOPIC_ARN" ]
|
||||
}
|
||||
|
||||
@test "SNS: list topics" {
|
||||
out=$(aws_cmd sns create-topic --name "$TOPIC_NAME")
|
||||
TOPIC_ARN=$(json_get "$out" '.TopicArn')
|
||||
|
||||
run aws_cmd sns list-topics
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$TOPIC_NAME" '.Topics | any(.TopicArn | contains($name))')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "SNS: get topic attributes" {
|
||||
out=$(aws_cmd sns create-topic --name "$TOPIC_NAME")
|
||||
TOPIC_ARN=$(json_get "$out" '.TopicArn')
|
||||
|
||||
run aws_cmd sns get-topic-attributes --topic-arn "$TOPIC_ARN"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "SNS: publish message" {
|
||||
out=$(aws_cmd sns create-topic --name "$TOPIC_NAME")
|
||||
TOPIC_ARN=$(json_get "$out" '.TopicArn')
|
||||
|
||||
run aws_cmd sns publish --topic-arn "$TOPIC_ARN" --message "hello-bats"
|
||||
assert_success
|
||||
msg_id=$(json_get "$output" '.MessageId')
|
||||
[ -n "$msg_id" ]
|
||||
}
|
||||
|
||||
@test "SNS: delete topic" {
|
||||
out=$(aws_cmd sns create-topic --name "$TOPIC_NAME")
|
||||
TOPIC_ARN=$(json_get "$out" '.TopicArn')
|
||||
|
||||
run aws_cmd sns delete-topic --topic-arn "$TOPIC_ARN"
|
||||
assert_success
|
||||
TOPIC_ARN=""
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bats
|
||||
# SQS tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
QUEUE_NAME="bats-test-queue-$(unique_name)"
|
||||
QUEUE_URL=""
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [ -n "$QUEUE_URL" ]; then
|
||||
aws_cmd sqs delete-queue --queue-url "$QUEUE_URL" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
@test "SQS: create queue" {
|
||||
run aws_cmd sqs create-queue --queue-name "$QUEUE_NAME"
|
||||
assert_success
|
||||
QUEUE_URL=$(json_get "$output" '.QueueUrl')
|
||||
[ -n "$QUEUE_URL" ]
|
||||
}
|
||||
|
||||
@test "SQS: get queue URL" {
|
||||
out=$(aws_cmd sqs create-queue --queue-name "$QUEUE_NAME")
|
||||
QUEUE_URL=$(json_get "$out" '.QueueUrl')
|
||||
|
||||
run aws_cmd sqs get-queue-url --queue-name "$QUEUE_NAME"
|
||||
assert_success
|
||||
got_url=$(json_get "$output" '.QueueUrl')
|
||||
[ "$got_url" = "$QUEUE_URL" ]
|
||||
}
|
||||
|
||||
@test "SQS: list queues" {
|
||||
out=$(aws_cmd sqs create-queue --queue-name "$QUEUE_NAME")
|
||||
QUEUE_URL=$(json_get "$out" '.QueueUrl')
|
||||
|
||||
run aws_cmd sqs list-queues --queue-name-prefix "bats-test"
|
||||
assert_success
|
||||
found=$(echo "$output" | jq --arg name "$QUEUE_NAME" '.QueueUrls | any(contains($name))')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "SQS: send and receive message" {
|
||||
out=$(aws_cmd sqs create-queue --queue-name "$QUEUE_NAME")
|
||||
QUEUE_URL=$(json_get "$out" '.QueueUrl')
|
||||
|
||||
run aws_cmd sqs send-message --queue-url "$QUEUE_URL" --message-body "hello-bats"
|
||||
assert_success
|
||||
msg_id=$(json_get "$output" '.MessageId')
|
||||
[ -n "$msg_id" ]
|
||||
|
||||
run aws_cmd sqs receive-message --queue-url "$QUEUE_URL" --max-number-of-messages 1 --wait-time-seconds 1
|
||||
assert_success
|
||||
body=$(json_get "$output" '.Messages[0].Body')
|
||||
[ "$body" = "hello-bats" ]
|
||||
}
|
||||
|
||||
@test "SQS: delete message" {
|
||||
out=$(aws_cmd sqs create-queue --queue-name "$QUEUE_NAME")
|
||||
QUEUE_URL=$(json_get "$out" '.QueueUrl')
|
||||
|
||||
aws_cmd sqs send-message --queue-url "$QUEUE_URL" --message-body "hello-bats" >/dev/null
|
||||
|
||||
out=$(aws_cmd sqs receive-message --queue-url "$QUEUE_URL" --max-number-of-messages 1 --wait-time-seconds 1)
|
||||
receipt=$(json_get "$out" '.Messages[0].ReceiptHandle')
|
||||
|
||||
run aws_cmd sqs delete-message --queue-url "$QUEUE_URL" --receipt-handle "$receipt"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "SQS: get queue attributes" {
|
||||
out=$(aws_cmd sqs create-queue --queue-name "$QUEUE_NAME")
|
||||
QUEUE_URL=$(json_get "$out" '.QueueUrl')
|
||||
|
||||
run aws_cmd sqs get-queue-attributes --queue-url "$QUEUE_URL" --attribute-names ApproximateNumberOfMessages
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "SQS: delete queue" {
|
||||
out=$(aws_cmd sqs create-queue --queue-name "$QUEUE_NAME")
|
||||
QUEUE_URL=$(json_get "$out" '.QueueUrl')
|
||||
|
||||
run aws_cmd sqs delete-queue --queue-url "$QUEUE_URL"
|
||||
assert_success
|
||||
QUEUE_URL=""
|
||||
}
|
||||
|
||||
@test "SQS: tags set at CreateQueue are returned by ListQueueTags" {
|
||||
# Regression test for https://github.com/floci-io/floci/issues/699
|
||||
run aws_cmd sqs create-queue --queue-name "$QUEUE_NAME" --tags "k1=v1,k2=v2"
|
||||
assert_success
|
||||
QUEUE_URL=$(json_get "$output" '.QueueUrl')
|
||||
[ -n "$QUEUE_URL" ]
|
||||
|
||||
run aws_cmd sqs list-queue-tags --queue-url "$QUEUE_URL"
|
||||
assert_success
|
||||
v1=$(json_get "$output" '.Tags.k1')
|
||||
v2=$(json_get "$output" '.Tags.k2')
|
||||
[ "$v1" = "v1" ]
|
||||
[ "$v2" = "v2" ]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bats
|
||||
# SSM Parameter Store tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
PARAM_NAME="/bats-test/param-$(unique_name)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
aws_cmd ssm delete-parameter --name "$PARAM_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
@test "SSM: put parameter" {
|
||||
run aws_cmd ssm put-parameter --name "$PARAM_NAME" --value "test-value" --type String
|
||||
assert_success
|
||||
version=$(json_get "$output" '.Version')
|
||||
[ "$version" -gt 0 ]
|
||||
}
|
||||
|
||||
@test "SSM: get parameter" {
|
||||
aws_cmd ssm put-parameter --name "$PARAM_NAME" --value "test-value" --type String >/dev/null
|
||||
|
||||
run aws_cmd ssm get-parameter --name "$PARAM_NAME" --no-with-decryption
|
||||
assert_success
|
||||
value=$(json_get "$output" '.Parameter.Value')
|
||||
[ "$value" = "test-value" ]
|
||||
}
|
||||
|
||||
@test "SSM: get parameters by path" {
|
||||
aws_cmd ssm put-parameter --name "$PARAM_NAME" --value "test-value" --type String >/dev/null
|
||||
|
||||
run aws_cmd ssm get-parameters-by-path --path "/bats-test"
|
||||
assert_success
|
||||
# Check that parameters array is not empty
|
||||
count=$(echo "$output" | jq '.Parameters | length')
|
||||
[ "$count" -gt 0 ]
|
||||
}
|
||||
|
||||
@test "SSM: add and list tags" {
|
||||
aws_cmd ssm put-parameter --name "$PARAM_NAME" --value "test-value" --type String >/dev/null
|
||||
|
||||
run aws_cmd ssm add-tags-to-resource \
|
||||
--resource-type Parameter \
|
||||
--resource-id "$PARAM_NAME" \
|
||||
--tags Key=env,Value=test
|
||||
assert_success
|
||||
|
||||
run aws_cmd ssm list-tags-for-resource \
|
||||
--resource-type Parameter \
|
||||
--resource-id "$PARAM_NAME"
|
||||
assert_success
|
||||
found=$(echo "$output" | jq '.TagList | any(.Key == "env" and .Value == "test")')
|
||||
[ "$found" = "true" ]
|
||||
}
|
||||
|
||||
@test "SSM: overwrite parameter" {
|
||||
aws_cmd ssm put-parameter --name "$PARAM_NAME" --value "original" --type String >/dev/null
|
||||
|
||||
run aws_cmd ssm put-parameter --name "$PARAM_NAME" --value "updated" --type String --overwrite
|
||||
assert_success
|
||||
|
||||
run aws_cmd ssm get-parameter --name "$PARAM_NAME" --no-with-decryption
|
||||
assert_success
|
||||
value=$(json_get "$output" '.Parameter.Value')
|
||||
[ "$value" = "updated" ]
|
||||
}
|
||||
|
||||
@test "SSM: delete parameter" {
|
||||
aws_cmd ssm put-parameter --name "$PARAM_NAME" --value "test-value" --type String >/dev/null
|
||||
|
||||
run aws_cmd ssm delete-parameter --name "$PARAM_NAME"
|
||||
assert_success
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bats
|
||||
# STS tests
|
||||
|
||||
setup() {
|
||||
load 'test_helper/common-setup'
|
||||
}
|
||||
|
||||
@test "STS: get caller identity" {
|
||||
run aws_cmd sts get-caller-identity
|
||||
assert_success
|
||||
account=$(json_get "$output" '.Account')
|
||||
[ -n "$account" ]
|
||||
user_id=$(json_get "$output" '.UserId')
|
||||
[ -n "$user_id" ]
|
||||
}
|
||||
|
||||
@test "STS: assume role" {
|
||||
local role_arn="arn:aws:iam::000000000000:role/test-role"
|
||||
|
||||
run aws_cmd sts assume-role \
|
||||
--role-arn "$role_arn" \
|
||||
--role-session-name "bats-test-session"
|
||||
assert_success
|
||||
access_key=$(json_get "$output" '.Credentials.AccessKeyId')
|
||||
[ -n "$access_key" ]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Common setup for bats tests
|
||||
|
||||
# Load bats-support and bats-assert
|
||||
# Supports both local lib/ directory and BATS_LIB_PATH for Docker
|
||||
_COMMON_SETUP_DIR="${BATS_TEST_DIRNAME}"
|
||||
if [[ -d "${_COMMON_SETUP_DIR}/../../lib/bats-support" ]]; then
|
||||
load "${_COMMON_SETUP_DIR}/../../lib/bats-support/load.bash"
|
||||
load "${_COMMON_SETUP_DIR}/../../lib/bats-assert/load.bash"
|
||||
elif [[ -n "${BATS_LIB_PATH}" ]]; then
|
||||
load "${BATS_LIB_PATH}/bats-support/load.bash"
|
||||
load "${BATS_LIB_PATH}/bats-assert/load.bash"
|
||||
else
|
||||
echo "Error: Cannot find bats-support/bats-assert libraries" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Environment configuration
|
||||
export FLOCI_ENDPOINT="${FLOCI_ENDPOINT:-http://localhost:4566}"
|
||||
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-us-east-1}"
|
||||
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-test}"
|
||||
export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-test}"
|
||||
|
||||
# Helper function to run AWS CLI commands with endpoint
|
||||
aws_cmd() {
|
||||
aws --endpoint-url "$FLOCI_ENDPOINT" --region "$AWS_DEFAULT_REGION" --output json "$@" 2>&1
|
||||
}
|
||||
|
||||
# Generate unique name for test resources
|
||||
unique_name() {
|
||||
local prefix="${1:-test}"
|
||||
echo "${prefix}-$(date +%s)-$$"
|
||||
}
|
||||
|
||||
# Wait for DynamoDB table to exist
|
||||
ddb_wait_table() {
|
||||
local table_name="$1"
|
||||
aws_cmd dynamodb wait table-exists --table-name "$table_name" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# Extract JSON value using jq
|
||||
json_get() {
|
||||
local json="$1"
|
||||
local path="$2"
|
||||
echo "$json" | jq -r "$path" 2>/dev/null || echo ""
|
||||
}
|
||||
|
||||
# Check if operation is unsupported
|
||||
is_unsupported_operation() {
|
||||
local output="$1"
|
||||
[[ "$output" == *"(UnsupportedOperation)"* ]] || [[ "$output" == *" is not supported."* ]]
|
||||
}
|
||||
Reference in New Issue
Block a user