diff --git a/.github/workflows/system.yml b/.github/workflows/system.yml new file mode 100644 index 00000000..1cc9ba42 --- /dev/null +++ b/.github/workflows/system.yml @@ -0,0 +1,42 @@ +name: system tests +on: pull_request +#on: +# workflow_dispatch: +# inputs: +# run_workflow: +# description: 'Run command-line tests' +jobs: + build: + name: RunTests + runs-on: ubuntu-latest + steps: + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 'stable' + id: go + + - name: Get Dependencies + run: | + go get -v -t -d ./... + + - name: Install BATS + run: | + git clone https://github.com/bats-core/bats-core.git + cd bats-core && ./install.sh $HOME + + - name: Build and Run + run: | + make testbin + export AWS_ACCESS_KEY_ID=user + export AWS_SECRET_ACCESS_KEY=pass + aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID --profile versity + aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY --profile versity + export VERSITY_EXE=./versitygw + mkdir /tmp/gw + VERSITYGW_TEST_ENV=$GITHUB_WORKSPACE/tests/.env.versitygw $HOME/bin/bats ./tests/s3_bucket_tests.sh + VERSITYGW_TEST_ENV=$GITHUB_WORKSPACE/tests/.env.versitygw $HOME/bin/bats ./tests/posix_tests.sh diff --git a/.gitignore b/.gitignore index c07c17f3..9270630e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,10 @@ VERSION /profile.txt dist/ + +# secrets file for local github-actions testing +.secrets + +# env files for testing +.env* +!.env.default diff --git a/tests/.env.default b/tests/.env.default new file mode 100644 index 00000000..cdf12fb0 --- /dev/null +++ b/tests/.env.default @@ -0,0 +1,6 @@ +AWS_REGION=us-west-2 +AWS_PROFILE=versity +VERSITY_EXE=./versitygw +BACKEND=posix +LOCAL_FOLDER=/tmp/gw +AWS_ENDPOINT_URL=http://127.0.0.1:7070 \ No newline at end of file diff --git a/tests/.env.versitygw b/tests/.env.versitygw new file mode 100644 index 00000000..a11be239 --- /dev/null +++ b/tests/.env.versitygw @@ -0,0 +1,6 @@ +AWS_REGION=us-east-1 +AWS_PROFILE=versity +VERSITY_EXE=./versitygw +BACKEND=posix +LOCAL_FOLDER=/tmp/gw +AWS_ENDPOINT_URL=http://127.0.0.1:7070 \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..351b393b --- /dev/null +++ b/tests/README.md @@ -0,0 +1,13 @@ +# Command-Line Tests + +Instructions: +1. Build the `versitygw` binary. +2. Create a local AWS profile for connection to S3, and add the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` values above to the profile. +3. Create an environment file (`.env`) similar to the ones in this folder, setting the `AWS_PROFILE` parameter to the name of the profile you created. +4. In the root repo folder, run with `VERSITYGW_TEST_ENV= tests/s3_bucket_tests.sh`. +5. If running/testing the GitHub workflow locally, create a `.secrets` file, and set the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` parameters here to the values of your AWS S3 IAM account. +``` +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +``` +6. To run the workflow locally, install **act** and run with `act -W .github/workflows/system.yml`. \ No newline at end of file diff --git a/tests/posix_tests.sh b/tests/posix_tests.sh new file mode 100644 index 00000000..56b5c878 --- /dev/null +++ b/tests/posix_tests.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bats + +source ./tests/tests.sh + +# check if object exists both on S3 and locally +# param: object path +# 0 for yes, 1 for no, 2 for error +object_exists_remote_and_local() { + if [ $# -ne 1 ]; then + echo "object existence check requires single name parameter" + return 2 + fi + object_exists "$1" || local exist_result=$? + if [[ $exist_result -eq 2 ]]; then + echo "Error checking if object exists" + return 2 + fi + if [[ $exist_result -eq 1 ]]; then + echo "Error: object doesn't exist remotely" + return 1 + fi + if [[ ! -e "$LOCAL_FOLDER"/"$1" ]]; then + echo "Error: object doesn't exist locally" + return 1 + fi + return 0 +} + +# check if object doesn't exist both on S3 and locally +# param: object path +# return 0 for doesn't exist, 1 for still exists, 2 for error +object_not_exists_remote_and_local() { + if [ $# -ne 1 ]; then + echo "object non-existence check requires single name parameter" + return 2 + fi + object_exists "$1" || local exist_result=$? + if [[ $exist_result -eq 2 ]]; then + echo "Error checking if object doesn't exist" + return 2 + fi + if [[ $exist_result -eq 0 ]]; then + echo "Error: object exists remotely" + return 1 + fi + if [[ -e "$LOCAL_FOLDER"/"$1" ]]; then + echo "Error: object exists locally" + return 1 + fi + return 0 +} + +# check if a bucket doesn't exist both on S3 and on gateway +# param: bucket name +# return: 0 for doesn't exist, 1 for does, 2 for error +bucket_not_exists_remote_and_local() { + if [ $# -ne 1 ]; then + echo "bucket existence check requires single name parameter" + return 2 + fi + bucket_exists "$1" || local exist_result=$? + if [[ $exist_result -eq 2 ]]; then + echo "Error checking if bucket exists" + return 2 + fi + if [[ $exist_result -eq 0 ]]; then + echo "Error: bucket exists remotely" + return 1 + fi + if [[ -e "$LOCAL_FOLDER"/"$1" ]]; then + echo "Error: bucket exists locally" + return 1 + fi + return 0 +} + +# check if a bucket exists both on S3 and on gateway +# param: bucket name +# return: 0 for yes, 1 for no, 2 for error +bucket_exists_remote_and_local() { + if [ $# -ne 1 ]; then + echo "bucket existence check requires single name parameter" + return 2 + fi + bucket_exists "$1" || local exist_result=$? + if [[ $exist_result -eq 2 ]]; then + echo "Error checking if bucket exists" + return 2 + fi + if [[ $exist_result -eq 1 ]]; then + echo "Error: bucket doesn't exist remotely" + return 1 + fi + if [[ ! -e "$LOCAL_FOLDER"/"$1" ]]; then + echo "Error: bucket doesn't exist locally" + return 1 + fi + return 0 +} + +# test that changes to local folders and files are reflected on S3 +@test test_local_creation_deletion { + + local bucket_name="versity-gwtest-put-object-test" + local object_name="test-object" + + bucket_exists_remote_and_local $bucket_name || local bucket_exists=$? + if [[ $bucket_exists -eq 2 ]]; then + fail "Bucket existence check error" + fi + local object="$bucket_name"/"$object_name" + if [[ $bucket_exists -eq 0 ]]; then + object_exists_remote_and_local "$object" || local object_exists=$? + if [[ $object_exists -eq 2 ]]; then + fail "Object existence check error" + fi + if [[ $object_exists -eq 0 ]]; then + delete_object "$object" || local delete_object=$? + [[ $delete_object -eq 0 ]] || fail "Failed to delete object" + fi + delete_bucket $bucket_name || local delete_bucket=$? + [[ $delete_bucket -eq 0 ]] || fail "Failed to delete bucket" + fi + mkdir "$LOCAL_FOLDER"/$bucket_name + touch "$LOCAL_FOLDER"/$object + bucket_exists_remote_and_local $bucket_name || local bucket_exists_two=$? + [[ $bucket_exists_two -eq 0 ]] || fail "Failed bucket existence check" + object_exists_remote_and_local $object || local object_exists_two=$? + [[ $object_exists_two -eq 0 ]] || fail "Failed object existence check" + rm "$LOCAL_FOLDER"/$object + sleep 1 + object_not_exists_remote_and_local $object || local object_deleted=$? + [[ $object_deleted -eq 0 ]] || fail "Failed object deletion check" + rmdir "$LOCAL_FOLDER"/$bucket_name + sleep 1 + bucket_not_exists_remote_and_local $bucket_name || local bucket_deleted=$? + [[ $bucket_deleted -eq 0 ]] || fail "Failed bucket deletion check" +} + diff --git a/tests/s3_bucket_tests.sh b/tests/s3_bucket_tests.sh new file mode 100755 index 00000000..39b4cf6e --- /dev/null +++ b/tests/s3_bucket_tests.sh @@ -0,0 +1,353 @@ +#!/usr/bin/env bats + +source ./tests/tests.sh + +# create an AWS bucket +# param: bucket name +# return 0 for success, 1 for failure +create_bucket() { + if [ $# -ne 1 ]; then + echo "create bucket missing bucket name" + return 1 + fi + local exit_code=0 + local error + error=$(aws s3 mb s3://"$1" 2>&1) || exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "error creating bucket: $error" + return 1 + fi + return 0 +} + +# delete an AWS bucket +# param: bucket name +# return 0 for success, 1 for failure +delete_bucket() { + if [ $# -ne 1 ]; then + echo "delete bucket missing bucket name" + return 1 + fi + local exit_code=0 + local error + error=$(aws s3 rb s3://"$1" 2>&1) || exit_code="$?" + if [ $exit_code -ne 0 ]; then + if [[ "$error" == *"The specified bucket does not exist"* ]]; then + return 0 + else + echo "error deleting bucket: $error" + return 1 + fi + fi + return 0 +} + +# check if bucket exists +# param: bucket name +# return 0 for true, 1 for false, 2 for error +bucket_exists() { + if [ $# -ne 1 ]; then + echo "bucket exists check missing bucket name" + return 2 + fi + local exit_code=0 + local error + error=$(aws s3 ls s3://"$1" 2>&1) || exit_code="$?" + echo "Exit code: $exit_code, error: $error" + if [ $exit_code -ne 0 ]; then + if [[ "$error" == *"The specified bucket does not exist"* ]] || [[ "$error" == *"Access Denied"* ]]; then + return 1 + else + echo "error checking if bucket exists: $error" + return 2 + fi + fi + return 0 +} + +# create bucket if it doesn't exist +# param: bucket name +# return 0 for success, 1 for failure +check_and_create_bucket() { + if [ $# -ne 1 ]; then + echo "bucket creation function requires bucket name" + return 1 + fi + local exists_result + bucket_exists "$1" || exists_result=$? + if [[ $exists_result -eq 2 ]]; then + echo "Bucket existence check error" + return 1 + fi + local create_result + if [[ $exists_result -eq 1 ]]; then + create_bucket "$1" || create_result=$? + if [[ $create_result -ne 0 ]]; then + echo "Error creating bucket" + return 1 + fi + fi + return 0 +} + +# check if object exists on S3 via gateway +# param: object path +# return 0 for true, 1 for false, 2 for error +object_exists() { + if [ $# -ne 1 ]; then + echo "object exists check missing object name" + return 2 + fi + local exit_code=0 + local error + error=$(aws s3 ls s3://"$1" 2>&1) || exit_code="$?" + if [ $exit_code -ne 0 ]; then + if [[ "$error" == "" ]]; then + return 1 + else + echo "error checking if object exists: $error" + return 2 + fi + fi + return 0 +} + +# add object to versitygw +# params: source file, destination copy location +# return 0 for success, 1 for failure +put_object() { + if [ $# -ne 2 ]; then + echo "put object command requires source, destination" + return 1 + fi + local exit_code=0 + local error + error=$(aws s3 cp "$1" s3://"$2" 2>&1) || exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "error copying object to bucket: $error" + return 1 + fi + return 0 +} + +# add object to versitygw if it doesn't exist +# params: source file, destination copy location +# return 0 for success or already exists, 1 for failure +check_and_put_object() { + if [ $# -ne 2 ]; then + echo "check and put object function requires source, destination" + return 1 + fi + object_exists "$2" || local exists_result=$? + if [ $exists_result -eq 2 ]; then + echo "error checking if object exists" + return 1 + fi + if [ $exists_result -eq 1 ]; then + put_object "$1" "$2" || local put_result=$? + if [ $put_result -ne 0 ]; then + echo "error adding object" + return 1 + fi + fi + return 0 +} + +# delete object from versitygw +# param: object location +# return 0 for success, 1 for failure +delete_object() { + if [ $# -ne 1 ]; then + echo "delete object command requires object parameter" + return 1 + fi + local exit_code=0 + local error + error=$(aws s3 rm s3://"$1" 2>&1) || exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "error deleting object: $error" + return 1 + fi + return 0 +} + +# list buckets on versitygw +# no params +# export bucket_array (bucket names) on success, return 1 for failure +list_buckets() { + local exit_code=0 + local output + output=$(aws s3 ls 2>&1) || exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "error listing buckets: $output" + return 1 + fi + + bucket_array=() + while IFS= read -r line; do + bucket_name=$(echo "$line" | awk '{print $NF}') + bucket_array+=("$bucket_name") + done <<< "$output" + + export bucket_array +} + +# list objects on versitygw, in bucket or folder +# param: path of bucket or folder +# export object_array (object names) on success, return 1 for failure +list_objects() { + if [ $# -ne 1 ]; then + echo "list objects command requires bucket or folder" + return 1 + fi + local exit_code=0 + local output + output=$(aws s3 ls s3://"$1" 2>&1) || exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "error listing objects: $output" + return 1 + fi + + object_array=() + while IFS= read -r line; do + object_name=$(echo "$line" | awk '{print $NF}') + object_array+=("$object_name") + done <<< "$output" + + export object_array +} + +# test creation and deletion of bucket on versitygw +@test "create_delete_bucket_test" { + + local bucket_name="versity-gwtest-create-delete-bucket-test" + + bucket_exists $bucket_name || local exists=$? + if [[ $exists -eq 2 ]]; then + fail "Bucket existence check error" + fi + if [[ $exists -eq 0 ]]; then + delete_bucket $bucket_name || local delete_result=$? + [[ $delete_result -eq 0 ]] || fail "Failed to delete bucket" + bucket_exists $bucket_name || local exists_two=$? + [[ $exists_two -eq 1 ]] || fail "Failed bucket deletion" + fi + create_bucket $bucket_name || local create_result=$? + [[ $create_result -eq 0 ]] || fail "Failed to create bucket" + bucket_exists $bucket_name || local exists_three=$? + [[ $exists_three -eq 0 ]] || fail "Failed bucket existence check" + delete_bucket $bucket_name || local delete_result_two=$? + [[ $delete_result_two -eq 0 ]] || fail "Failed to delete bucket" +} + +# test adding and removing an object on versitygw +@test "put_object_test" { + + local bucket_name="versity-gwtest-put-object-test" + local object_name="test-object" + + bucket_exists $bucket_name || local bucket_exists=$? + if [[ $bucket_exists -eq 2 ]]; then + fail "Bucket existence check error" + fi + local object="$bucket_name"/"$object_name" + if [[ $bucket_exists -eq 0 ]]; then + object_exists "$object" || local object_exists=$? + if [[ $object_exists -eq 2 ]]; then + fail "Object existence check error" + fi + if [[ $object_exists -eq 0 ]]; then + delete_object "$object" || local delete_object=$? + [[ $delete_object -eq 0 ]] || fail "Failed to delete object" + fi + delete_bucket $bucket_name || local delete_bucket=$? + [[ $delete_bucket -eq 0 ]] || fail "Failed to delete bucket" + fi + touch "$object_name" + create_bucket $bucket_name || local create_bucket=$? + [[ $create_bucket -eq 0 ]] || fail "Failed to create bucket" + put_object "$object_name" "$object" || local put_object=$? + [[ $put_object -eq 0 ]] || fail "Failed to add object to bucket" + object_exists "$object" || local object_exists_two=$? + [[ $object_exists_two -eq 0 ]] || fail "Object not added to bucket" + delete_object "$object" || local delete_object_two=$? + [[ $delete_object_two -eq 0 ]] || fail "Failed to delete object" + delete_bucket $bucket_name || local delete_bucket=$? + [[ $delete_bucket -eq 0 ]] || fail "Failed to delete bucket" + rm "$object_name" +} + +# test listing buckets on versitygw +@test "test_list_buckets" { + + bucket_name_one="versity-gwtest-list-one" + bucket_name_two="versity-gwtest-list-two" + + bucket_exists $bucket_name_one || local exists=$? + if [[ $exists -eq 2 ]]; then + fail "Bucket existence check error" + fi + if [[ $exists -eq 1 ]]; then + create_bucket $bucket_name_one || local bucket_create_one=$? + [[ $bucket_create_one -eq 0 ]] || fail "Failed to create bucket" + fi + bucket_exists $bucket_name_two || local exists_two=$? + if [[ $exists_two -eq 2 ]]; then + fail "Bucket existence check error" + fi + if [[ $exists_two -eq 1 ]]; then + create_bucket $bucket_name_two || local bucket_create_two=$? + [[ $bucket_create_two -eq 0 ]] || fail "Failed to create bucket" + fi + list_buckets + local bucket_one_found=false + local bucket_two_found=false + for bucket in "${bucket_array[@]}"; do + if [ "$bucket" == $bucket_name_one ]; then + bucket_one_found=true + elif [ "$bucket" == $bucket_name_two ]; then + bucket_two_found=true + fi + if [ $bucket_one_found == true ] && [ $bucket_two_found == true ]; then + return + fi + done + fail "$bucket_name_one and/or $bucket_name_two not listed (all buckets: ${bucket_array[*]})" + delete_bucket $bucket_name_one || local deleted_one=$? + [[ $deleted_one -eq 0 ]] || fail "Failed to delete bucket one" + delete_bucket $bucket_name_two || local deleted_two=$? + [[ $deleted_two -eq 0 ]] || fail "Failed to delete bucket one" +} + +# test listing a bucket's objects on versitygw +@test test_list_objects { + + bucket_name="versity-gwtest-list-object" + object_one="test-file-one" + object_two="test-file-two" + + touch $object_one $object_two + check_and_create_bucket $bucket_name || local result_one=$? + [[ result_one -eq 0 ]] || fail "Error creating bucket" + put_object $object_one "$bucket_name"/"$object_one" || local result_two=$? + [[ result_two -eq 0 ]] || fail "Error adding object one" + put_object $object_two "$bucket_name"/"$object_two" || local result_three=$? + [[ result_three -eq 0 ]] || fail "Error adding object two" + list_objects $bucket_name + local object_one_found=false + local object_two_found=false + for object in "${object_array[@]}"; do + if [ "$object" == $object_one ]; then + object_one_found=true + elif [ "$object" == $object_two ]; then + object_two_found=true + fi + done + if [ $object_one_found != true ] || [ $object_two_found != true ]; then + fail "$object_one and/or $object_two not listed (all objects: ${object_array[*]})" + fi + delete_object "$bucket_name"/"$object_one" + delete_object "$bucket_name"/"$object_two" + delete_bucket $bucket_name + rm $object_one $object_two +} diff --git a/tests/tests.sh b/tests/tests.sh new file mode 100644 index 00000000..291219cf --- /dev/null +++ b/tests/tests.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bats + +setup() { + + if [ "$GITHUB_ACTIONS" != "true" ] && [ -r .secrets ]; then + source .secrets + else + echo "Warning: no secrets file found" + fi + if [ -z "$VERSITYGW_TEST_ENV" ]; then + if [ -r tests/.env ]; then + source tests/.env + else + echo "Warning: no .env file found in tests folder" + fi + else + echo "$VERSITYGW_TEST_ENV" + source $VERSITYGW_TEST_ENV + fi + + if [ -z "$AWS_ACCESS_KEY_ID" ]; then + echo "No AWS access key set" + return 1 + elif [ -z "$AWS_SECRET_ACCESS_KEY" ]; then + echo "No AWS secret access key set" + return 1 + elif [ -z "$VERSITY_EXE" ]; then + echo "No versity executable location set" + return 1 + elif [ -z "$BACKEND" ]; then + echo "No backend parameter set (options: 'posix')" + return 1 + elif [ -z "$AWS_REGION" ]; then + echo "No AWS region set" + return 1 + elif [ -z "$AWS_PROFILE" ]; then + echo "No AWS profile set" + return 1 + elif [ -z "$LOCAL_FOLDER" ]; then + echo "No local storage folder set" + return 1 + elif [ -z "$AWS_ENDPOINT_URL" ]; then + echo "No AWS endpoint URL set" + return 1 + fi + + ROOT_ACCESS_KEY="$AWS_ACCESS_KEY_ID" ROOT_SECRET_KEY="$AWS_SECRET_ACCESS_KEY" "$VERSITY_EXE" "$BACKEND" "$LOCAL_FOLDER" & + + export AWS_REGION + export AWS_PROFILE + export AWS_ENDPOINT_URL + export LOCAL_FOLDER + + versitygw_pid=$! + export versitygw_pid +} + +fail() { + echo "$1" + return 1 +} + +teardown() { + if [ -n "$versitygw_pid" ]; then + if ps -p "$versitygw_pid" > /dev/null; then + kill "$versitygw_pid" + wait "$versitygw_pid" || true + else + echo "Process with PID $versitygw_pid does not exist." + fi + else + echo "versitygw_pid is not set or empty." + fi +}