diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..8deabde --- /dev/null +++ b/circle.yml @@ -0,0 +1,21 @@ +machine: + pre: + # install docker 1.7.1 + - sudo curl -L -o /usr/bin/docker 'https://s3-external-1.amazonaws.com/circle-downloads/docker-1.7.1-circleci'; sudo chmod 0755 /usr/bin/docker; true + services: + - docker + +dependencies: + override: + - sudo add-apt-repository ppa:duggan/bats --yes + - sudo apt-get update -qq + - sudo apt-get install -qq bats + - docker pull jwilder/docker-gen + - docker pull nginx + - docker pull python:3 + - docker pull rancher/socat-docker + +test: + override: + - docker build -t jwilder/nginx-proxy:bats . + - bats test \ No newline at end of file diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..721d436 --- /dev/null +++ b/test/README.md @@ -0,0 +1,14 @@ +Test suite +========== + +This test suite is implemented on top of the [Bats](https://github.com/sstephenson/bats/blob/master/README.md) test framework. + +It is intended to verify the correct behavior of the Docker image `jwilder/nginx-proxy:bats`. + +Running the test suite +---------------------- + +Make sure you have Bats installed, then run: + + docker build -t jwilder/nginx-proxy:bats . + bats test/ \ No newline at end of file diff --git a/test/default-host.bats b/test/default-host.bats new file mode 100644 index 0000000..0adf686 --- /dev/null +++ b/test/default-host.bats @@ -0,0 +1,38 @@ +#!/usr/bin/env bats +load test_helpers + +function setup { + # make sure to stop any web container before each test so we don't + # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set + docker ps -q --filter "label=bats-type=web" | xargs -r docker stop >&2 +} + + +@test "[$TEST_FILE] DEFAULT_HOST=web1.bats" { + SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1 + + # GIVEN a webserver with VIRTUAL_HOST set to web.bats + docker_clean bats-web + run docker run -d \ + --label bats-type="web" \ + --name bats-web \ + -e VIRTUAL_HOST=web.bats \ + --expose 80 \ + -w /var/www \ + python:3 \ + python -m http.server 80 + assert_success + + # WHEN nginx-proxy runs with DEFAULT_HOST set to web.bats + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro -e DEFAULT_HOST=web.bats + assert_success + docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" + + # THEN querying the proxy without Host header → 200 + run curl_container $SUT_CONTAINER / --head + assert_output -l 0 $'HTTP/1.1 200 OK\r' + + # THEN querying the proxy with any other Host header → 200 + run curl_container $SUT_CONTAINER / --head --header "Host: something.I.just.made.up" + assert_output -l 0 $'HTTP/1.1 200 OK\r' +} diff --git a/test/docker.bats b/test/docker.bats new file mode 100644 index 0000000..13d84a2 --- /dev/null +++ b/test/docker.bats @@ -0,0 +1,117 @@ +#!/usr/bin/env bats +load test_helpers + + +@test "[$TEST_FILE] start 2 web containers" { + prepare_web_container bats-web1 81 -e VIRTUAL_HOST=web1.bats + prepare_web_container bats-web2 82 -e VIRTUAL_HOST=web2.bats +} + + +@test "[$TEST_FILE] -v /var/run/docker.sock:/tmp/docker.sock:ro" { + SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1 + + # WHEN nginx-proxy runs on our docker host using the default unix socket + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro + assert_success + docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" + + # THEN + assert_nginxproxy_behaves $SUT_CONTAINER +} + + +@test "[$TEST_FILE] -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock" { + SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-2 + + # WHEN nginx-proxy runs on our docker host using a custom unix socket + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock + assert_success + docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" + + # THEN + assert_nginxproxy_behaves $SUT_CONTAINER +} + + +@test "[$TEST_FILE] -e DOCKER_HOST=tcp://..." { + SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-3 + # GIVEN a container exposing our docker host over TCP + run docker_tcp bats-docker-tcp + assert_success + sleep 1s + + # WHEN nginx-proxy runs on our docker host using tcp to connect to our docker host + run nginxproxy $SUT_CONTAINER -e DOCKER_HOST="tcp://bats-docker-tcp:2375" --link bats-docker-tcp:bats-docker-tcp + assert_success + docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" + + # THEN + assert_nginxproxy_behaves $SUT_CONTAINER +} + + +@test "[$TEST_FILE] separated containers (nginx + docker-gen + nginx.tmpl)" { + docker_clean bats-nginx + docker_clean bats-docker-gen + + # GIVEN a simple nginx container + run docker run -d \ + --name bats-nginx \ + -v /etc/nginx/conf.d/ \ + -v /etc/nginx/certs/ \ + nginx:latest + assert_success + run retry 5 1s curl --silent --fail -A "before-docker-gen" --head http://$(docker_ip bats-nginx)/ + assert_output -l 0 $'HTTP/1.1 200 OK\r' + + # WHEN docker-gen runs on our docker host + run docker run -d \ + --name bats-docker-gen \ + -v /var/run/docker.sock:/tmp/docker.sock:ro \ + -v $BATS_TEST_DIRNAME/../nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro \ + --volumes-from bats-nginx \ + jwilder/docker-gen:latest \ + -notify-sighup bats-nginx \ + -watch \ + -only-exposed \ + /etc/docker-gen/templates/nginx.tmpl \ + /etc/nginx/conf.d/default.conf + assert_success + docker_wait_for_log bats-docker-gen 6 "Watching docker events" + + # Give some time to the docker-gen container to notify bats-nginx so it + # reloads its config + sleep 2s + + run docker_running_state bats-nginx + assert_output "true" || { + docker logs bats-docker-gen + false + } >&2 + + # THEN + assert_nginxproxy_behaves bats-nginx +} + + +# $1 nginx-proxy container +function assert_nginxproxy_behaves { + local -r container=$1 + + # Querying the proxy without Host header → 503 + run curl_container $container / --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' + + # Querying the proxy with Host header → 200 + run curl_container $container /data --header "Host: web1.bats" + assert_output "answer from port 81" + + run curl_container $container /data --header "Host: web2.bats" + assert_output "answer from port 82" + + # Querying the proxy with unknown Host header → 503 + run curl_container $container /data --header "Host: webFOO.bats" --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' +} + diff --git a/test/lib/README.md b/test/lib/README.md new file mode 100644 index 0000000..bd444fd --- /dev/null +++ b/test/lib/README.md @@ -0,0 +1,6 @@ +bats lib +======== + +found on https://github.com/sstephenson/bats/pull/110 + +When that pull request will be merged, that lib won't be necessary anymore. \ No newline at end of file diff --git a/test/lib/bats/batslib.bash b/test/lib/bats/batslib.bash new file mode 100644 index 0000000..003ada6 --- /dev/null +++ b/test/lib/bats/batslib.bash @@ -0,0 +1,596 @@ +# +# batslib.bash +# ------------ +# +# The Standard Library is a collection of test helpers intended to +# simplify testing. It contains the following types of test helpers. +# +# - Assertions are functions that perform a test and output relevant +# information on failure to help debugging. They return 1 on failure +# and 0 otherwise. +# +# All output is formatted for readability using the functions of +# `output.bash' and sent to the standard error. +# + +source "${BATS_LIB}/batslib/output.bash" + + +######################################################################## +# ASSERTIONS +######################################################################## + +# Fail and display a message. When no parameters are specified, the +# message is read from the standard input. Other functions use this to +# report failure. +# +# Globals: +# none +# Arguments: +# $@ - [=STDIN] message +# Returns: +# 1 - always +# Inputs: +# STDIN - [=$@] message +# Outputs: +# STDERR - message +fail() { + (( $# == 0 )) && batslib_err || batslib_err "$@" + return 1 +} + +# Fail and display details if the expression evaluates to false. Details +# include the expression, `$status' and `$output'. +# +# NOTE: The expression must be a simple command. Compound commands, such +# as `[[', can be used only when executed with `bash -c'. +# +# Globals: +# status +# output +# Arguments: +# $1 - expression +# Returns: +# 0 - expression evaluates to TRUE +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert() { + if ! "$@"; then + { local -ar single=( + 'expression' "$*" + 'status' "$status" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'assertion failed' \ + | fail + fi +} + +# Fail and display details if the expected and actual values do not +# equal. Details include both values. +# +# Globals: +# none +# Arguments: +# $1 - actual value +# $2 - expected value +# Returns: +# 0 - values equal +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert_equal() { + if [[ $1 != "$2" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$2" \ + 'actual' "$1" \ + | batslib_decorate 'values do not equal' \ + | fail + fi +} + +# Fail and display details if `$status' is not 0. Details include +# `$status' and `$output'. +# +# Globals: +# status +# output +# Arguments: +# none +# Returns: +# 0 - `$status' is 0 +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert_success() { + if (( status != 0 )); then + { local -ir width=6 + batslib_print_kv_single "$width" 'status' "$status" + batslib_print_kv_single_or_multi "$width" 'output' "$output" + } | batslib_decorate 'command failed' \ + | fail + fi +} + +# Fail and display details if `$status' is 0. Details include `$output'. +# +# Optionally, when the expected status is specified, fail when it does +# not equal `$status'. In this case, details include the expected and +# actual status, and `$output'. +# +# Globals: +# status +# output +# Arguments: +# $1 - [opt] expected status +# Returns: +# 0 - `$status' is not 0, or +# `$status' equals the expected status +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert_failure() { + (( $# > 0 )) && local -r expected="$1" + if (( status == 0 )); then + batslib_print_kv_single_or_multi 6 'output' "$output" \ + | batslib_decorate 'command succeeded, but it was expected to fail' \ + | fail + elif (( $# > 0 )) && (( status != expected )); then + { local -ir width=8 + batslib_print_kv_single "$width" \ + 'expected' "$expected" \ + 'actual' "$status" + batslib_print_kv_single_or_multi "$width" \ + 'output' "$output" + } | batslib_decorate 'command failed as expected, but status differs' \ + | fail + fi +} + +# Fail and display details if the expected does not match the actual +# output or a fragment of it. +# +# By default, the entire output is matched. The assertion fails if the +# expected output does not equal `$output'. Details include both values. +# +# When `-l ' is used, only the -th line is matched. The +# assertion fails if the expected line does not equal +# `${lines[}'. Details include the compared lines and . +# +# When `-l' is used without the argument, the output is searched +# for the expected line. The expected line is matched against each line +# in `${lines[@]}'. If no match is found the assertion fails. Details +# include the expected line and `$output'. +# +# By default, literal matching is performed. Options `-p' and `-r' +# enable partial (i.e. substring) and extended regular expression +# matching, respectively. Specifying an invalid extended regular +# expression with `-r' displays an error. +# +# Options `-p' and `-r' are mutually exclusive. When used +# simultaneously, an error is displayed. +# +# Globals: +# output +# lines +# Options: +# -l - match against the -th element of `${lines[@]}' +# -l - search `${lines[@]}' for the expected line +# -p - partial matching +# -r - extended regular expression matching +# Arguments: +# $1 - expected output +# Returns: +# 0 - expected matches the actual output +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +# error message, on error +assert_output() { + local -i is_match_line=0 + local -i is_match_contained=0 + local -i is_mode_partial=0 + local -i is_mode_regex=0 + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -l) + if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + is_match_line=1 + local -ri idx="$2" + shift + else + is_match_contained=1; + fi + shift + ;; + -p) is_mode_partial=1; shift ;; + -r) is_mode_regex=1; shift ;; + --) break ;; + *) break ;; + esac + done + + if (( is_match_line )) && (( is_match_contained )); then + echo "\`-l' and \`-l ' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + if (( is_mode_partial )) && (( is_mode_regex )); then + echo "\`-p' and \`-r' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + # Arguments. + local -r expected="$1" + + if (( is_mode_regex == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + # Matching. + if (( is_match_contained )); then + # Line contained in output. + if (( is_mode_regex )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} =~ $expected ]] && return 0 + done + { local -ar single=( + 'regex' "$expected" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'no output line matches regular expression' \ + | fail + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == *"$expected"* ]] && return 0 + done + { local -ar single=( + 'substring' "$expected" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'no output line contains substring' \ + | fail + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == "$expected" ]] && return 0 + done + { local -ar single=( + 'line' "$expected" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'output does not contain line' \ + | fail + fi + elif (( is_match_line )); then + # Specific line. + if (( is_mode_regex )); then + if ! [[ ${lines[$idx]} =~ $expected ]]; then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'regex' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression does not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} != *"$expected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line does not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} != "$expected" ]]; then + batslib_print_kv_single 8 \ + 'index' "$idx" \ + 'expected' "$expected" \ + 'actual' "${lines[$idx]}" \ + | batslib_decorate 'line differs' \ + | fail + fi + fi + else + # Entire output. + if (( is_mode_regex )); then + if ! [[ $output =~ $expected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regex' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression does not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output != *"$expected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'output does not contain substring' \ + | fail + fi + else + if [[ $output != "$expected" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$expected" \ + 'actual' "$output" \ + | batslib_decorate 'output differs' \ + | fail + fi + fi + fi +} + +# Fail and display details if the unexpected matches the actual output +# or a fragment of it. +# +# By default, the entire output is matched. The assertion fails if the +# unexpected output equals `$output'. Details include `$output'. +# +# When `-l ' is used, only the -th line is matched. The +# assertion fails if the unexpected line equals `${lines[}'. +# Details include the compared line and . +# +# When `-l' is used without the argument, the output is searched +# for the unexpected line. The unexpected line is matched against each +# line in `${lines[]}'. If a match is found the assertion fails. +# Details include the unexpected line, the index where it was found and +# `$output' (with the unexpected line highlighted in it if `$output` is +# longer than one line). +# +# By default, literal matching is performed. Options `-p' and `-r' +# enable partial (i.e. substring) and extended regular expression +# matching, respectively. On failure, the substring or the regular +# expression is added to the details (if not already displayed). +# Specifying an invalid extended regular expression with `-r' displays +# an error. +# +# Options `-p' and `-r' are mutually exclusive. When used +# simultaneously, an error is displayed. +# +# Globals: +# output +# lines +# Options: +# -l - match against the -th element of `${lines[@]}' +# -l - search `${lines[@]}' for the unexpected line +# -p - partial matching +# -r - extended regular expression matching +# Arguments: +# $1 - unexpected output +# Returns: +# 0 - unexpected matches the actual output +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +# error message, on error +refute_output() { + local -i is_match_line=0 + local -i is_match_contained=0 + local -i is_mode_partial=0 + local -i is_mode_regex=0 + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -l) + if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + is_match_line=1 + local -ri idx="$2" + shift + else + is_match_contained=1; + fi + shift + ;; + -L) is_match_contained=1; shift ;; + -p) is_mode_partial=1; shift ;; + -r) is_mode_regex=1; shift ;; + --) break ;; + *) break ;; + esac + done + + if (( is_match_line )) && (( is_match_contained )); then + echo "\`-l' and \`-l ' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + if (( is_mode_partial )) && (( is_mode_regex )); then + echo "\`-p' and \`-r' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Arguments. + local -r unexpected="$1" + + if (( is_mode_regex == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Matching. + if (( is_match_contained )); then + # Line contained in output. + if (( is_mode_regex )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} =~ $unexpected ]]; then + { local -ar single=( + 'regex' "$unexpected" + 'index' "$idx" + ) + local -a may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \ + | batslib_prefix \ + | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } | batslib_decorate 'no line should match the regular expression' \ + | fail + return $? + fi + done + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + { local -ar single=( + 'substring' "$unexpected" + 'index' "$idx" + ) + local -a may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \ + | batslib_prefix \ + | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } | batslib_decorate 'no line should contain substring' \ + | fail + return $? + fi + done + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == "$unexpected" ]]; then + { local -ar single=( + 'line' "$unexpected" + 'index' "$idx" + ) + local -a may_be_multi=( + 'output' "$output" + ) + local -ir width="$( batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \ + | batslib_prefix \ + | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } | batslib_decorate 'line should not be in output' \ + | fail + return $? + fi + done + fi + elif (( is_match_line )); then + # Specific line. + if (( is_mode_regex )); then + if [[ ${lines[$idx]} =~ $unexpected ]] || (( $? == 0 )); then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'regex' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression should not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} == "$unexpected" ]]; then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should differ' \ + | fail + fi + fi + else + # Entire output. + if (( is_mode_regex )); then + if [[ $output =~ $unexpected ]] || (( $? == 0 )); then + batslib_print_kv_single_or_multi 6 \ + 'regex' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression should not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output == *"$unexpected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'output should not contain substring' \ + | fail + fi + else + if [[ $output == "$unexpected" ]]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output equals, but it was expected to differ' \ + | fail + fi + fi + fi +} \ No newline at end of file diff --git a/test/lib/bats/batslib/output.bash b/test/lib/bats/batslib/output.bash new file mode 100644 index 0000000..aa9cb87 --- /dev/null +++ b/test/lib/bats/batslib/output.bash @@ -0,0 +1,264 @@ +# +# output.bash +# ----------- +# +# Private functions implementing output formatting. Used by public +# helper functions. +# + +# Print a message to the standard error. When no parameters are +# specified, the message is read from the standard input. +# +# Globals: +# none +# Arguments: +# $@ - [=STDIN] message +# Returns: +# none +# Inputs: +# STDIN - [=$@] message +# Outputs: +# STDERR - message +batslib_err() { + { if (( $# > 0 )); then + echo "$@" + else + cat - + fi + } >&2 +} + +# Count the number of lines in the given string. +# +# TODO(ztombol): Fix tests and remove this note after #93 is resolved! +# NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not +# give the same result as `${#lines[@]}' when the output contains +# empty lines. +# See PR #93 (https://github.com/sstephenson/bats/pull/93). +# +# Globals: +# none +# Arguments: +# $1 - string +# Returns: +# none +# Outputs: +# STDOUT - number of lines +batslib_count_lines() { + local -i n_lines=0 + local line + while IFS='' read -r line || [[ -n $line ]]; do + (( ++n_lines )) + done < <(printf '%s' "$1") + echo "$n_lines" +} + +# Determine whether all strings are single-line. +# +# Globals: +# none +# Arguments: +# $@ - strings +# Returns: +# 0 - all strings are single-line +# 1 - otherwise +batslib_is_single_line() { + for string in "$@"; do + (( $(batslib_count_lines "$string") > 1 )) && return 1 + done + return 0 +} + +# Determine the length of the longest key that has a single-line value. +# +# This function is useful in determining the correct width of the key +# column in two-column format when some keys may have multi-line values +# and thus should be excluded. +# +# Globals: +# none +# Arguments: +# $odd - key +# $even - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - length of longest key +batslib_get_max_single_line_key_width() { + local -i max_len=-1 + while (( $# != 0 )); do + local -i key_len="${#1}" + batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len" + shift 2 + done + echo "$max_len" +} + +# Print key-value pairs in two-column format. +# +# Keys are displayed in the first column, and their corresponding values +# in the second. To evenly line up values, the key column is fixed-width +# and its width is specified with the first parameter (possibly computed +# using `batslib_get_max_single_line_key_width'). +# +# Globals: +# none +# Arguments: +# $1 - width of key column +# $even - key +# $odd - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - formatted key-value pairs +batslib_print_kv_single() { + local -ir col_width="$1"; shift + while (( $# != 0 )); do + printf '%-*s : %s\n' "$col_width" "$1" "$2" + shift 2 + done +} + +# Print key-value pairs in multi-line format. +# +# The key is displayed first with the number of lines of its +# corresponding value in parenthesis. Next, starting on the next line, +# the value is displayed. For better readability, it is recommended to +# indent values using `batslib_prefix'. +# +# Globals: +# none +# Arguments: +# $odd - key +# $even - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - formatted key-value pairs +batslib_print_kv_multi() { + while (( $# != 0 )); do + printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )" + printf '%s\n' "$2" + shift 2 + done +} + +# Print all key-value pairs in either two-column or multi-line format +# depending on whether all values are single-line. +# +# If all values are single-line, print all pairs in two-column format +# with the specified key column width (identical to using +# `batslib_print_kv_single'). +# +# Otherwise, print all pairs in multi-line format after indenting values +# with two spaces for readability (identical to using `batslib_prefix' +# and `batslib_print_kv_multi') +# +# Globals: +# none +# Arguments: +# $1 - width of key column (for two-column format) +# $even - key +# $odd - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - formatted key-value pairs +batslib_print_kv_single_or_multi() { + local -ir width="$1"; shift + local -a pairs=( "$@" ) + + local -a values=() + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + values+=( "${pairs[$i]}" ) + done + + if batslib_is_single_line "${values[@]}"; then + batslib_print_kv_single "$width" "${pairs[@]}" + else + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )" + done + batslib_print_kv_multi "${pairs[@]}" + fi +} + +# Prefix each line read from the standard input with the given string. +# +# Globals: +# none +# Arguments: +# $1 - [= ] prefix string +# Returns: +# none +# Inputs: +# STDIN - lines +# Outputs: +# STDOUT - prefixed lines +batslib_prefix() { + local -r prefix="${1:- }" + local line + while IFS='' read -r line || [[ -n $line ]]; do + printf '%s%s\n' "$prefix" "$line" + done +} + +# Mark select lines of the text read from the standard input by +# overwriting their beginning with the given string. +# +# Usually the input is indented by a few spaces using `batslib_prefix' +# first. +# +# Globals: +# none +# Arguments: +# $1 - marking string +# $@ - indices (zero-based) of lines to mark +# Returns: +# none +# Inputs: +# STDIN - lines +# Outputs: +# STDOUT - lines after marking +batslib_mark() { + local -r symbol="$1"; shift + # Sort line numbers. + set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" ) + + local line + local -i idx=0 + while IFS='' read -r line || [[ -n $line ]]; do + if (( ${1:--1} == idx )); then + printf '%s\n' "${symbol}${line:${#symbol}}" + shift + else + printf '%s\n' "$line" + fi + (( ++idx )) + done +} + +# Enclose the input text in header and footer lines. +# +# The header contains the given string as title. The output is preceded +# and followed by an additional newline to make it stand out more. +# +# Globals: +# none +# Arguments: +# $1 - title +# Returns: +# none +# Inputs: +# STDIN - text +# Outputs: +# STDOUT - decorated text +batslib_decorate() { + echo + echo "-- $1 --" + cat - + echo '--' + echo +} \ No newline at end of file diff --git a/test/lib/docker_helpers.bash b/test/lib/docker_helpers.bash new file mode 100644 index 0000000..05eb0be --- /dev/null +++ b/test/lib/docker_helpers.bash @@ -0,0 +1,61 @@ +## functions to help deal with docker + +# Removes container $1 +function docker_clean { + docker kill $1 &>/dev/null ||: + sleep .25s + docker rm -vf $1 &>/dev/null ||: + sleep .25s +} + +# get the ip of docker container $1 +function docker_ip { + docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1 +} + +# get the running state of container $1 +# → true/false +# fails if the container does not exist +function docker_running_state { + docker inspect -f {{.State.Running}} $1 +} + +# get the docker container $1 PID +function docker_pid { + docker inspect --format {{.State.Pid}} $1 +} + +# asserts logs from container $1 contains $2 +function docker_assert_log { + local -r container=$1 + shift + run docker logs $container + assert_output -p "$*" +} + +# wait for container $2 to contain a given text in its log +# $1 container +# $2 timeout in second +# $* text to wait for +function docker_wait_for_log { + local -r container=$1 + shift + local -ir timeout_sec=$1 + shift + retry $(( $timeout_sec * 2 )) .5s docker_assert_log $container "$*" +} + +# Create a docker container named $1 which exposes the docker host unix +# socket over tcp on port 2375. +# +# $1 container name +function docker_tcp { + local container_name="$1" + docker_clean $container_name + docker run -d \ + --name $container_name \ + --expose 2375 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + rancher/socat-docker + docker -H tcp://$(docker_ip $container_name):2375 version +} diff --git a/test/lib/helpers.bash b/test/lib/helpers.bash new file mode 100644 index 0000000..dffcd66 --- /dev/null +++ b/test/lib/helpers.bash @@ -0,0 +1,22 @@ +## add the retry function to bats + +# Retry a command $1 times until it succeeds. Wait $2 seconds between retries. +function retry { + local attempts=$1 + shift + local delay=$1 + shift + local i + + for ((i=0; i < attempts; i++)); do + run "$@" + if [ "$status" -eq 0 ]; then + echo "$output" + return 0 + fi + sleep $delay + done + + echo "Command \"$@\" failed $attempts times. Status: $status. Output: $output" >&2 + false +} diff --git a/test/multiple-hosts.bats b/test/multiple-hosts.bats new file mode 100644 index 0000000..0380509 --- /dev/null +++ b/test/multiple-hosts.bats @@ -0,0 +1,47 @@ +#!/usr/bin/env bats +load test_helpers +SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} + +function setup { + # make sure to stop any web container before each test so we don't + # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set + docker ps -q --filter "label=bats-type=web" | xargs -r docker stop >&2 +} + + +@test "[$TEST_FILE] start a nginx-proxy container" { + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro + assert_success + docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" +} + +@test "[$TEST_FILE] nginx-proxy forwards requests for 2 hosts" { + # WHEN a container runs a web server with VIRTUAL_HOST set for multiple hosts + docker_clean bats-multiple-hosts-1 + run docker run -d \ + --label bats-type="web" \ + --name bats-multiple-hosts-1 \ + -e VIRTUAL_HOST=multiple-hosts-1-A.bats,multiple-hosts-1-B.bats \ + --expose 80 \ + -w /data \ + python:3 python -m http.server 80 + assert_success + run retry 5 1s curl_container bats-multiple-hosts-1 / --head + assert_output -l 0 $'HTTP/1.0 200 OK\r' + + # THEN querying the proxy without Host header → 503 + run curl_container $SUT_CONTAINER / --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' + + # THEN querying the proxy with unknown Host header → 503 + run curl_container $SUT_CONTAINER /data --header "Host: webFOO.bats" --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' + + # THEN + run curl_container $SUT_CONTAINER / --head --header 'Host: multiple-hosts-1-A.bats' + assert_output -l 0 $'HTTP/1.1 200 OK\r' || (echo $output; echo $status; false) + + # THEN + run curl_container $SUT_CONTAINER / --head --header 'Host: multiple-hosts-1-B.bats' + assert_output -l 0 $'HTTP/1.1 200 OK\r' +} diff --git a/test/multiple-ports.bats b/test/multiple-ports.bats new file mode 100644 index 0000000..a711056 --- /dev/null +++ b/test/multiple-ports.bats @@ -0,0 +1,54 @@ +#!/usr/bin/env bats +load test_helpers +SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} + +function setup { + # make sure to stop any web container before each test so we don't + # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set + docker ps -q --filter "label=bats-type=web" | xargs -r docker stop >&2 +} + + +@test "[$TEST_FILE] start a nginx-proxy container" { + # GIVEN nginx-proxy + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro + assert_success + docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" +} + + +@test "[$TEST_FILE] nginx-proxy defaults to the service running on port 80" { + # WHEN + prepare_web_container bats-web-${TEST_FILE}-1 "80 90" -e VIRTUAL_HOST=web.bats + + # THEN + assert_response_is_from_port 80 +} + + +@test "[$TEST_FILE] VIRTUAL_PORT=90 while port 80 is also exposed" { + # GIVEN + prepare_web_container bats-web-${TEST_FILE}-2 "80 90" -e VIRTUAL_HOST=web.bats -e VIRTUAL_PORT=90 + + # THEN + assert_response_is_from_port 90 +} + + +@test "[$TEST_FILE] single exposed port != 80" { + # GIVEN + prepare_web_container bats-web-${TEST_FILE}-3 1234 -e VIRTUAL_HOST=web.bats + + # THEN + assert_response_is_from_port 1234 +} + + +# assert querying nginx-proxy provides a response from the expected port of the web container +# $1 port we are expecting an response from +function assert_response_is_from_port { + local -r port=$1 + run curl_container $SUT_CONTAINER /data --header "Host: web.bats" + assert_output "answer from port $port" +} + diff --git a/test/test_helpers.bash b/test/test_helpers.bash new file mode 100644 index 0000000..9063736 --- /dev/null +++ b/test/test_helpers.bash @@ -0,0 +1,128 @@ +# Test if requirements are met +( + type docker &>/dev/null || ( echo "docker is not available"; exit 1 ) + type curl &>/dev/null || ( echo "curl is not available"; exit 1 ) +)>&2 + + +# set a few global variables +SUT_IMAGE=jwilder/nginx-proxy:bats +TEST_FILE=$(basename $BATS_TEST_FILENAME .bats) + + +# load the Bats stdlib (see https://github.com/sstephenson/bats/pull/110) +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +export BATS_LIB="${DIR}/lib/bats" +load "${BATS_LIB}/batslib.bash" + + +# load additional bats helpers +load ${DIR}/lib/helpers.bash +load ${DIR}/lib/docker_helpers.bash + + +# Define functions specific to our test suite + +# run the SUT docker container +# and makes sure it remains started +# and displays the nginx-proxy start logs +# +# $1 container name +# $@ other options for the `docker run` command +function nginxproxy { + local -r container_name=$1 + shift + docker_clean $container_name \ + && docker run -d \ + --name $container_name \ + "$@" \ + $SUT_IMAGE \ + && wait_for_nginxproxy_container_to_start $container_name \ + && docker logs $container_name +} + + +# wait until the nginx-proxy container is ready to operate +# +# $1 container name +function wait_for_nginxproxy_container_to_start { + local -r container_name=$1 + sleep .5s # give time to eventually fail to initialize + + function is_running { + run docker_running_state $container_name + assert_output "true" + } + retry 3 1 is_running +} + + +# Send a HTTP request to container $1 for path $2 and +# Additional curl options can be passed as $@ +# +# $1 container name +# $2 HTTP path to query +# $@ additional options to pass to the curl command +function curl_container { + local -r container=$1 + local -r path=$2 + shift 2 + curl --silent \ + --connect-timeout 5 \ + --max-time 20 \ + "$@" \ + http://$(docker_ip $container)${path} +} + + +# start a container running (one or multiple) webservers listening on given ports +# +# $1 container name +# $2 container port(s). If multiple ports, provide them as a string: "80 90" with a space as a separator +# $@ `docker run` additional options +function prepare_web_container { + local -r container_name=$1 + local -r ports=$2 + shift 2 + local -r options="$@" + + local expose_option="" + for port in $ports; do + expose_option="${expose_option}--expose=$port " + done + + ( # used for debugging purpose. Will be display if test fails + echo "container_name: $container_name" + echo "ports: $ports" + echo "options: $options" + echo "expose_option: $expose_option" + )>&2 + + docker_clean $container_name + + # GIVEN a container exposing 1 webserver on ports 1234 + run docker run -d \ + --label bats-type="web" \ + --name $container_name \ + $expose_option \ + -w /var/www/ \ + $options \ + -e PYTHON_PORTS="$ports" \ + python:3 sh -c " + for port in \$PYTHON_PORTS; do + echo starting a web server listening on port \$port; + mkdir /var/www/\$port + cd /var/www/\$port + echo \"answer from port \$port\" > data + python -m http.server \$port & + done + wait + " + assert_success + + # THEN querying directly port works + for port in $ports; do + run retry 5 1s curl --silent --fail http://$(docker_ip $container_name):$port/data + assert_output "answer from port $port" + done +} \ No newline at end of file diff --git a/test/wildcard-hosts.bats b/test/wildcard-hosts.bats new file mode 100644 index 0000000..8242fc1 --- /dev/null +++ b/test/wildcard-hosts.bats @@ -0,0 +1,68 @@ +#!/usr/bin/env bats +load test_helpers +SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} + +function setup { + # make sure to stop any web container before each test so we don't + # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set + docker ps -q --filter "label=bats-type=web" | xargs -r docker stop >&2 +} + + +@test "[$TEST_FILE] start a nginx-proxy container" { + # GIVEN + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro + assert_success + docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" +} + + +@test "[$TEST_FILE] VIRTUAL_HOST=*.wildcard.bats" { + # WHEN + prepare_web_container bats-wildcard-hosts-1 80 -e VIRTUAL_HOST=*.wildcard.bats + + # THEN + assert_200 f00.wildcard.bats + assert_200 bar.wildcard.bats + assert_503 unexpected.host.bats +} + +@test "[$TEST_FILE] VIRTUAL_HOST=wildcard.bats.*" { + # WHEN + prepare_web_container bats-wildcard-hosts-2 80 -e VIRTUAL_HOST=wildcard.bats.* + + # THEN + assert_200 wildcard.bats.f00 + assert_200 wildcard.bats.bar + assert_503 unexpected.host.bats +} + +@test "[$TEST_FILE] VIRTUAL_HOST=~^foo\.bar\..*\.bats" { + # WHEN + prepare_web_container bats-wildcard-hosts-2 80 -e VIRTUAL_HOST=~^foo\.bar\..*\.bats + + # THEN + assert_200 foo.bar.whatever.bats + assert_200 foo.bar.why.not.bats + assert_503 unexpected.host.bats + +} + + +# assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response +# $1 Host HTTP header to use when querying nginx-proxy +function assert_200 { + local -r host=$1 + + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + assert_output -l 0 $'HTTP/1.1 200 OK\r' +} + +# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response +# $1 Host HTTP header to use when querying nginx-proxy +function assert_503 { + local -r host=$1 + + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' +} \ No newline at end of file