From 5d84aad6a86c44eec3c5a8c628c4941248747a0c Mon Sep 17 00:00:00 2001 From: Lizan Zhou Date: Wed, 4 Jan 2017 10:38:01 -0800 Subject: [PATCH] First t-test --- WORKSPACE | 10 + src/envoy/prototype/BUILD | 1 + test/BUILD | 0 test/nginx.bzl | 90 ++++ test/repositories.bzl | 49 +++ test/t/ApiManager.pm | 713 +++++++++++++++++++++++++++++++ test/t/BUILD | 39 ++ test/t/HttpServer.pm | 187 ++++++++ test/t/check.t | 145 +++++++ test/t/testdata/bookstore.pb.txt | 43 ++ 10 files changed, 1277 insertions(+) create mode 100644 test/BUILD create mode 100644 test/nginx.bzl create mode 100644 test/repositories.bzl create mode 100644 test/t/ApiManager.pm create mode 100644 test/t/BUILD create mode 100644 test/t/HttpServer.pm create mode 100644 test/t/check.t create mode 100644 test/t/testdata/bookstore.pb.txt diff --git a/WORKSPACE b/WORKSPACE index bbafd205918..3acf173d624 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -47,3 +47,13 @@ load( ) envoy_repositories() + +load( + "//test:repositories.bzl", + "perl_repositories", + "nginx_test_repositories", +) + +perl_repositories() + +nginx_test_repositories() diff --git a/src/envoy/prototype/BUILD b/src/envoy/prototype/BUILD index e5cbaeba10c..cefdc74539f 100644 --- a/src/envoy/prototype/BUILD +++ b/src/envoy/prototype/BUILD @@ -31,4 +31,5 @@ cc_binary( "@envoy_git//:envoy-main" ], linkstatic=1, + visibility=["//visibility:public"], ) diff --git a/test/BUILD b/test/BUILD new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/nginx.bzl b/test/nginx.bzl new file mode 100644 index 00000000000..bb110acf292 --- /dev/null +++ b/test/nginx.bzl @@ -0,0 +1,90 @@ +# Copyright (C) Extensible Service Proxy Authors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +################################################################################ +# +# Skylark macros for nginx tests. + +load("@io_bazel_rules_perl//perl:perl.bzl", "perl_test") + +def nginx_test(name, nginx, data=None, env=None, config=None, **kwargs): + if nginx == None or len(nginx) == 0: + fail("'nginx' parameter must be a non-empty string (target).") + + if data == None: + data = [] + data += [ nginx ] + + if env == None: + env = {} + + if config != None: + data += [ config ] + c = Label(config) + env["TEST_CONFIG"] = "server_config " + "${TEST_SRCDIR}/" + c.package + "/" + c.name + ";" + name = name + '_' + c.name.split("/")[-1].split(".")[0] + + # Count existing rules in the BUILD file and assign base port using + # Each rule can use 10 ports in its range + # Rules generated from config_list get separate ranges + port = 9000 + len(native.existing_rules().values()) * 10 + + l = Label(nginx) + env["TEST_PORT"] = "%s" % port + + env_files = { + "TEST_NGINX_BINARY": "../__main__/" + l.package + "/" + l.name + } + perl_test(name=name, data=data, env=env, env_files=env_files, **kwargs) + +def nginx_suite(tests, deps, nginx, size="small", config_list=[], data=None, tags=[], + timeout="short", env=None): + for test in tests: + if not config_list: + nginx_test( + name = test.split(".")[0], + size = size, + timeout = timeout, + srcs = [test], + deps = deps, + data = data, + nginx = nginx, + config = None, + tags = tags, + env = env, + ) + else: + for config in config_list: + nginx_test( + name = test.split(".")[0], + size = size, + timeout = timeout, + srcs = [test], + deps = deps, + data = data, + nginx = nginx, + config = config, + tags = tags, + env = env, + ) diff --git a/test/repositories.bzl b/test/repositories.bzl new file mode 100644 index 00000000000..b6ffa8812dd --- /dev/null +++ b/test/repositories.bzl @@ -0,0 +1,49 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +# + +def perl_repositories(bind=True): + native.git_repository( + name = "io_bazel_rules_perl", + remote = "https://github.com/bazelbuild/rules_perl", + commit = "f6211c2db1e54d0a30bc3c3a718f2b5d45f02a22", + ) + +def nginx_test_repositories(bind=True): + BUILD = """ +load("@io_bazel_rules_perl//perl:perl.bzl", "perl_library") + +perl_library( + name = "nginx_test", + srcs = glob([ + "lib/Test/**/*.pm", + ]), + visibility = ["//visibility:public"], +) +""" + + native.new_git_repository( + name = "nginx_tests_git", + remote = "https://nginx.googlesource.com/nginx-tests", + commit = "e740612281f4b64c168e99f2e6d04260d6e9ca28", + build_file_content = BUILD, + ) + + if bind: + native.bind( + name = "nginx_test", + actual = "@nginx_tests_git//:nginx_test", + ) diff --git a/test/t/ApiManager.pm b/test/t/ApiManager.pm new file mode 100644 index 00000000000..2ef77fc6758 --- /dev/null +++ b/test/t/ApiManager.pm @@ -0,0 +1,713 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +# +# A shared module for ESP end-to-end tests. +# Sets up a TEST_NGINX_BINARY environment variable for Nginx test framework +# to find ESP build of Nginx. +# Adds Nginx test library (nginx-tests/lib) to the module search path. + +use strict; +use warnings; + +package ApiManager; + +use FindBin; +use JSON::PP; +use Data::Dumper; +use MIME::Base64; +use Test::More; + +sub repo_root { + my $testdir = $FindBin::Bin; + my @path = split('/', $testdir); + return (join('/', @path[0 .. $#path - 3]), $testdir); +} + +BEGIN { + our ($Root, $TestDir) = repo_root(); + our $TestLib = $Root . "/nginx_tests_git/lib"; + + if (!defined $ENV{TEST_SRCDIR}) { + $ENV{TEST_SRCDIR} = $Root; + } +} + +use lib $ApiManager::TestLib; + +select STDERR; $| = 1; # flush stderr immediately +select STDOUT; $| = 1; # flush stdout immediately + +sub write_binary_file { + my ($name, $content) = @_; + open F, '>>', $name or die "Can't create $name: $!"; + binmode F; + print F $content; + close F; +} + +sub compare { + my ($x, $y, $path, $ignore_keys) = @_; + + my $refx = ref $x; + my $refy = ref $y; + if(!$refx && !$refy) { # both are scalars + unless ($x eq $y) { + print "$path value doesn't match $x != $y.\n"; + return 0; + } + } + elsif ($refx ne $refy) { # not the same type + print "$path type doesn't match $refx != $refy.\n"; + return 0; + } + elsif ($refx eq 'SCALAR' || $refx eq 'REF') { + return compare(${$x}, ${$y}, $path, $ignore_keys); + } + elsif ($refx eq 'ARRAY') { + if ($#{$x} == $#{$y}) { # same length + my $i = -1; + for (@$x) { + $i++; + return 0 unless compare( + $x->[$i], $y->[$i], "$path:[$i]", $ignore_keys); + } + } + else { + print "$path array size doesn't match: $#{$x} != $#{$y}.\n"; + return 0; + } + } + elsif ($refx eq 'HASH') { + my @diff = grep { !exists $ignore_keys->{$_} && !exists $y->{$_} } keys %$x; + if (@diff) { + print "$path has following extra keys:\n"; + for (@diff) { + print "$_: $x->{$_}\n"; + } + return 0; + } + for (keys %$y) { + unless(exists($x->{$_})) { + print "$path key $_ doesn't exist.\n"; + return 0; + } + return 0 unless compare($x->{$_}, $y->{$_}, "$path:$_", $ignore_keys); + } + } else { + print "$path: Not supported type: $refx\n"; + return 0; + } + return 1; +} + +sub compare_json { + my ($json, $expected, $random_metrics) = @_; + my $json_obj = decode_json($json); + + print Dumper $json_obj if $ENV{TEST_NGINX_VERBOSE}; + return compare($json_obj, $expected, "", {}); +} + +sub compare_json_with_random_metrics { + my ($json, $expected, @random_metrics) = @_; + my $json_obj = decode_json($json); + + # A list of metrics with non-deterministic values. + my %random_metric_map = map { $_ => 1 } @random_metrics; + + # Check and remove the above random metrics before making the comparison. + my $matched_random_metric_count = 0; + if (not exists $json_obj->{operations}) { + return 0; + } + + my $operation = $json_obj->{operations}->[0]; + if (not exists $operation->{metricValueSets}) { + return 0; + } + + my @metric_value_sets; + foreach my $metric (@{$operation->{metricValueSets}}) { + if (exists($random_metric_map{$metric->{metricName}})) { + $matched_random_metric_count += 1; + } else { + push @metric_value_sets, $metric; + } + } + + if ($matched_random_metric_count != scalar @random_metrics) { + return 0; + } + $operation->{metricValueSets} = \@metric_value_sets; + + print Dumper $json_obj if $ENV{TEST_NGINX_VERBOSE}; + my %ignore_keys = map { $_ => "1" } qw( + startTime endTime timestamp operationId); + return compare($json_obj, $expected, "", \%ignore_keys); +} + +sub compare_user_info { + my ($user_info, $expected) = @_; + my $json_obj = decode_json(decode_base64($user_info)); + print Dumper $json_obj if $ENV{TEST_NGINX_VERBOSE}; + return compare($json_obj, $expected, "", {}); +} + +sub read_file_using_full_path { + my ($full_path) = @_; + local $/; + open F, '<', $full_path or die "Can't open $full_path $!"; + my $content = ; + close F; + return $content; +} + +sub read_test_file { + my ($name) = @_; + return read_file_using_full_path($ApiManager::TestDir . '/' . $name); +} + +sub write_file_expand { + if (!defined $ENV{TEST_CONFIG}) { + $ENV{TEST_CONFIG} = ""; + } + my ($t, $name, $content) = @_; + $content =~ s/%%TEST_CONFIG%%/$ENV{TEST_CONFIG}/gmse; + $t->write_file_expand($name, $content); +} + +sub get_bookstore_service_config { + return read_test_file("testdata/bookstore.pb.txt"); +} + +sub get_bookstore_service_config_allow_all_http_requests { + return read_test_file('testdata/bookstore_allow_all_http_requests.pb.txt'); +} + +sub get_bookstore_service_config_allow_unregistered { + return get_bookstore_service_config . + read_test_file("testdata/usage_fragment.pb.txt"); +} + +sub get_bookstore_service_config_allow_some_unregistered { + return get_bookstore_service_config . + read_test_file("testdata/usage_frag.pb.txt"); +} + +sub get_echo_service_config { + return read_test_file("testdata/echo_service.pb.txt"); +} + +sub get_grpc_test_service_config { + my ($GrpcBackendPort) = @_; + return read_test_file("testdata/grpc_echo_service.pb.txt") . <.appspot.com/$host_name/; + # Replace the project id + $service_config =~ s//endpoints-transcoding-test/; + # Replace the service control address + $service_config =~ s/servicecontrol.googleapis.com/$service_control_address/; + return $service_config; +} + +sub get_grpc_echo_test_service_config { + my ($host_name, $service_control_address) = @_; + my $path = './test/grpc/local/service.json'; + my $service_config = read_file_using_full_path($path); + # Replace the host name + $service_config =~ s/echo-dot-esp-grpc-load-test.appspot.com/$host_name/; + # Replace the service control address + $service_config =~ s/servicecontrol.googleapis.com/$service_control_address/; + return $service_config; +} + +sub get_large_report_request { + my ($t, $size) = @_; + my $testdir = $t->testdir(); + my $cmd = './src/tools/service_control_json_gen'; + system "$cmd --report_request_size=$size --json > $testdir/large_data.json"; + return $t->read_file('large_data.json'); +} + +sub get_metadata_response_body { + return <testdir(); + + $t->write_file_expand('envoy.json', <<" EOF"); + { + "listeners": [ + { + "port": $Port, + "filters": [ + { + "type": "read", + "name": "http_connection_manager", + "config": { + "codec_type": "auto", + "stat_prefix": "ingress_http", + "route_config": { + "virtual_hosts": [ + { + "name": "backend", + "domains": ["*"], + "routes": [ + { + "timeout_ms": 0, + "prefix": "/", + "cluster": "service1" + } + ] + } + ] + }, + "access_log": [ + { + "path": "/tmp/access.envoy" + } + ], + "filters": [ + { + "type": "both", + "name": "esp", + "config": { + "service_config": "$ServiceConfig" + } + }, + { + "type": "decoder", + "name": "router", + "config": {} + } + ] + } + } + ] + } + ], + "admin": { + "access_log_path": "$testdir/access.envoy", + "port": 1080 + }, + "cluster_manager": { + "clusters": [ + { + "name": "service1", + "connect_timeout_ms": 5000, + "type": "strict_dns", + "lb_type": "round_robin", + "hosts": [ + { + "url": "tcp://localhost:$BackendPort" + } + ] + }, + { + "name": "api_manager", + "connect_timeout_ms": 5000, + "type": "strict_dns", + "lb_type": "round_robin", + "hosts": [ + { + "url": "tcp://localhost:$ServiceControlPort" + } + ] + } + ] + }, + "tracing_enabled": "true" + } + EOF + + chdir $testdir; + my $server = $ENV{TEST_NGINX_BINARY}; + exec $server, "-c", "envoy.json", "-l", "debug"; +} + +sub grpc_test_server { + my ($t, @args) = @_; + my $server = './test/grpc/grpc-test-server'; + exec $server, @args; +} + +sub grpc_interop_server { + my ($t, $port) = @_; + my $server = './test/grpc/interop-server'; + exec $server, "--port", $port; +} + +sub transcoding_test_server { + my ($t, @args) = @_; + my $server = './test/transcoding/bookstore-server'; + exec $server, @args; +} + +# Runs the gRPC server for testing transcoding and redirects the output to a +# file. +sub run_transcoding_test_server { + my ($t, $output_file, @args) = @_; + my $redirect_file = $t->{_testdir}.'/'.$output_file; + + # redirect, fork & run, restore + open ORIGINAL, ">&", \*STDOUT; + open STDOUT, ">", $redirect_file; + $t->run_daemon(\&transcoding_test_server, $t, @args); + open STDOUT, ">&", \*ORIGINAL; +} + +sub call_bookstore_client { + my ($t, @args) = @_; + my $client = './test/transcoding/bookstore-client'; + my $output_file = $t->{_testdir} . '/bookstore-client.log'; + + my $rc = system "$client " . join(' ', @args) . " > $output_file"; + + return ($rc, read_file_using_full_path($output_file)) +} + +sub run_grpc_test { + my ($t, $plans) = @_; + $t->write_file('test_plans.txt', $plans); + my $testdir = $t->testdir(); + my $client = './test/grpc/grpc-test-client'; + system "$client < $testdir/test_plans.txt > $testdir/test_results.txt"; + return $t->read_file('test_results.txt'); +} + +sub run_grpc_interop_test { + my ($t, $port, $test_case, @args) = @_; + my $testdir = $t->testdir(); + my $client = './test/grpc/interop-client'; + return system "$client --server_port $port --test_case $test_case " . join(' ', @args) +} + +sub run_nginx_with_stderr_redirect { + my $t = shift; + my $redirect_file = $t->{_testdir}.'/stderr.log'; + + # redirect, fork & run, restore + open ORIGINAL, ">&", \*STDERR; + open STDERR, ">", $redirect_file; + $t->run(); + open STDERR, ">&", \*ORIGINAL; +} + +# Runs an HTTP server that returns "404 Not Found" for every request. +sub not_found_server { + my ($t, $port) = @_; + + my $server = HttpServer->new($port, $t->testdir() . '/nop.log') + or die "Can't create test server socket: $!\n"; + + $server->run(); +} + +# Reads a file which contains a stream of HTTP requests, +# parses out individual requests and returns them in an array. +sub read_http_stream { + my ($t, $file) = @_; + + my $http = $t->read_file($file); + + # Parse out individual HTTP requests. + + my @requests; + + while ($http ne '') { + my ($request_headers, $rest) = split /\r\n\r\n/, $http, 2; + my @header_lines = split /\r\n/, $request_headers; + + my %headers; + my $verb = ''; + my $uri = ''; + my $path = ''; + my $body = ''; + + # Process request line. + my $request_line = $header_lines[0]; + if ($request_line =~ /^(\S+)\s+(([^? ]+)(\?[^ ]+)?)\s+HTTP/i) { + $verb = $1; + $uri = $2; + $path = $3; + } + + # Process headers + foreach my $header (@header_lines[1 .. $#header_lines]) { + my ($key, $value) = split /\s*:\s*/, $header, 2; + $headers{lc $key} = $value; + } + + my $content_length = $headers{'content-length'} || 0; + if ($content_length > 0) { + $body = substr $rest, 0, $content_length; + $rest = substr $rest, $content_length; + } + + push @requests, { + 'verb' => $verb, + 'path' => $path, + 'uri' => $uri, + 'headers' => \%headers, + 'body' => $body + }; + + $http = $rest; + } + + return @requests; +} + +# Checks that response Content-Type header is application/json and matches the +# response body with the expected JSON. +sub verify_http_json_response { + my ($response, $expected_body) = @_; + + # Parse out the body + my ($headers, $actual_body) = split /\r\n\r\n/, $response, 2; + + if ($headers !~ qr/HTTP\/1.1 200 OK/i) { + Test::More::diag("Status code doesn't match\n"); + Test::More::diag("Expected: 200 OK\n"); + Test::More::diag("Actual headers: ${headers}\n"); + return 0; + } + + if ($headers !~ qr/content-type:(\s)*application\/json/i) { + Test::More::diag("Content-Type doesn't match\n"); + Test::More::diag("Expected: application/json\n"); + Test::More::diag("Actual headers: ${headers}\n"); + return 0; + } + + if (!compare_json($actual_body, $expected_body)) { + Test::More::diag("Response body doesn't match\n"); + Test::More::diag("Expected: " . encode_json(${expected_body}) . "\n"); + Test::More::diag("Actual: ${actual_body}\n"); + return 0; + } + + return 1; +} + +# Initial port is 8080 or $TEST_PORT env variable. A test is allowed to use 10 +# subsequent ports. +sub available_port_range { + my %port_range; + if (!defined $ENV{TEST_PORT}) { + %port_range = (8080, 8090); + } else { + %port_range = ($ENV{TEST_PORT}, $ENV{TEST_PORT} + 10); + } + + printf("Available port range: [%d, %d)\n", %port_range); + return %port_range; +} + +my ($next_port, $max_port) = available_port_range(); + +# Select an open port +sub pick_port { + for (my $port = $next_port; $port < $max_port; $port++) { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => $port, + ) + or next; + close $server; + $next_port = $port + 1; + print "Pick port: $port\n"; + return $port; + } + die "Could not find an available port for testing\n" +} + +# +# These routines are copied from Nginx.pm to support custom ports +# + +sub log_core { + my ($prefix, $msg) = @_; + ($prefix, $msg) = ('', $prefix) unless defined $msg; + $prefix .= ' ' if length($prefix) > 0; + + if (length($msg) > 2048) { + $msg = substr($msg, 0, 2048) + . "(...logged only 2048 of " . length($msg) + . " bytes)"; + } + + $msg =~ s/^/# $prefix/gm; + $msg =~ s/([^\x20-\x7e])/sprintf('\\x%02x', ord($1)) . (($1 eq "\n") ? "\n" : '')/gmxe; + $msg .= "\n" unless $msg =~ /\n\Z/; + print $msg; +} + +sub log_out { + log_core('>>', @_); +} + +sub log_in { + log_core('<<', @_); +} + +sub http_get($;$;%) { + my ($port, $url, %extra) = @_; + return http($port, <new( + Proto => 'tcp', + PeerAddr => "127.0.0.1:$port" + ) + or die "Can't connect to nginx: $!\n"; + + log_out($request); + $s->print($request); + + select undef, undef, undef, $extra{sleep} if $extra{sleep}; + return '' if $extra{aborted}; + + if ($extra{body}) { + log_out($extra{body}); + $s->print($extra{body}); + } + + alarm(0); + }; + alarm(0); + if ($@) { + log_in("died: $@"); + return undef; + } + + return $s; +} + +sub http_end($;%) { + my ($s) = @_; + my $reply; + + eval { + local $SIG{ALRM} = sub { die "timeout\n" }; + local $SIG{PIPE} = sub { die "sigpipe\n" }; + alarm(8); + + local $/; + $reply = $s->getline(); + + alarm(0); + }; + alarm(0); + if ($@) { + log_in("died: $@"); + return undef; + } + + log_in($reply); + return $reply; +} + +1; diff --git a/test/t/BUILD b/test/t/BUILD new file mode 100644 index 00000000000..90c99ddd08a --- /dev/null +++ b/test/t/BUILD @@ -0,0 +1,39 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +# +load("//test:nginx.bzl", "nginx_test") +load("@io_bazel_rules_perl//perl:perl.bzl", "perl_library") + +perl_library( + name = "perl_library", + srcs = glob(["*.pm"]), + data = glob(["testdata/*"]), + deps = [ + "//external:nginx_test", + ], +) + +nginx_test( + name = "check", + srcs = [ + "check.t", + ], + nginx = "//src/envoy/prototype:envoy_esp", + deps = [ + ":perl_library", + ], + tags = ["local"], +) diff --git a/test/t/HttpServer.pm b/test/t/HttpServer.pm new file mode 100644 index 00000000000..9cc75a50779 --- /dev/null +++ b/test/t/HttpServer.pm @@ -0,0 +1,187 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +# +# A shared module for ESP end-to-end tests. +# A simple Http Server. + +package HttpServer; + +use strict; +use warnings; + +use Data::Dumper; +use File::Basename; +use IO::Select; +use IO::Socket::SSL; +use Socket qw/ CRLF /; + +sub new { + my (undef, $port, $file, $ssl) = @_; + + my $self = { + _port => $port, + _file => $file, + _http => [], + _http_cb => {}, + _ssl => $ssl, + }; + + bless $self; + return $self; +} + +sub port { + my ($self) = @_; + return $self->{_port}; +} + +sub on { + my ($self, $method, $url, $response) = @_; + push @{$self->{_http}}, { + method => $method, + url => $url, + response => $response, + }; +} + +sub on_sub { + my ($self, $method, $url, $handler) = @_; + my $method_url = join($method, $url); + $self->{_http_cb}{$method_url} = $handler; +} + +sub handle_client { + my ($self, $client, $rh) = @_; + + my $request = ''; + + # Read headers. + while (<$client>) { + $request .= $_; + last if (/^\x0d?\x0a?$/); + } + + # Read the request. + if ($request =~ /^(\S+)\s+(([^? ]+)(\?[^ ]+)?)\s+HTTP/i) { + my $verb = $1; + my $uri = $2; + my $path = $3; + my $body = ''; + my %headers; + + print $rh $request; + + my @request_parts = split /\s*\r\n/, $request; + foreach my $header (@request_parts[1 .. $#request_parts]) { + my ($key, $value) = split /\s*:\s*/, $header, 2; + $headers{lc $key} = $value; + } + + my $content_length = $headers{'content-length'} || 0; + + while ($content_length > 0) { + my $chunk = ""; + $client->read($chunk, $content_length); + $content_length -= length $chunk; + $body .= $chunk; + print $rh $chunk; + } + + my $method_uri = join($verb, $uri); + my $method_path = join($verb, $path); + if (exists($self->{_http_cb}{$method_uri})) { + my $handler = $self->{_http_cb}{$method_uri}; + &$handler($request, $body, $client); + } elsif (exists($self->{_http_cb}{$method_path})) { + my $handler = $self->{_http_cb}{$method_path}; + &$handler($request, $body, $client); + } elsif ($uri ne '') { + if (@{$self->{_http}} && + $verb eq $self->{_http}->[0]->{method} && + ($uri eq $self->{_http}->[0]->{url} || + $path eq $self->{_http}->[0]->{url})) { + my $http = shift @{$self->{_http}}; + print $client $http->{response}; + } else { + #print $rh "URL ", $uri, " not found.\n"; + print $client <new(); + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => $self->{_port}, + Listen => 5, + Reuse => 1 + ) + or die "Can't create test server socket: $!\n"; + $server->blocking(0); + $listeners->add($server); + + # Please avoid taking ports that are not allocated + if ($self->{_ssl}) { + my $ssl_server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => $self->{_port} + 443, + Listen => 5, + Reuse => 1 + ) + or die "Can't create test SSL server socket: $!\n"; + $ssl_server->blocking(0); + $listeners->add($ssl_server); + } + + local $SIG{PIPE} = 'IGNORE'; + + open my $rh, '>', $self->{_file} or die "cannot open > " . $self->{_file}; + select $rh; $| = 1; # Enable auto-flush. + + while (1) { + my @ready = $listeners->can_read; + foreach(@ready) { + next unless my $client = $_->accept(); + $client->blocking(1); + $client->autoflush(1); + if ($client->sockport() != $self->{_port}) { + # SSL upgrade client + my $dir = dirname($self->{_file}); + IO::Socket::SSL->start_SSL($client, + SSL_server => 1, + SSL_cert_file => "$dir/test.crt", + SSL_key_file => "$dir/test.key", + ) or die "failed to ssl handshake: $SSL_ERROR"; + } + $self->handle_client($client, $rh); + close $client; + } + } + close $rh; +} + +1; diff --git a/test/t/check.t b/test/t/check.t new file mode 100644 index 00000000000..da3ec54d069 --- /dev/null +++ b/test/t/check.t @@ -0,0 +1,145 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +# +use strict; +use warnings; + +################################################################################ + +use test::t::ApiManager; # Must be first (sets up import path to the Nginx test module) +use test::t::HttpServer; +use Test::Nginx; # Imports Nginx's test module +use Test::More; # And the test framework + +################################################################################ + +# Port assignments +my $NginxPort = ApiManager::pick_port(); +my $BackendPort = ApiManager::pick_port(); +my $ServiceControlPort = ApiManager::pick_port(); + +my $t = Test::Nginx->new()->has(qw/http proxy/)->plan(10); + +# Save service name in the service configuration protocol buffer file. + +$t->write_file('service.pb.txt', ApiManager::get_bookstore_service_config . <<"EOF"); +control { + environment: "http://127.0.0.1:${ServiceControlPort}" +} +EOF + +ApiManager::write_file_expand($t, 'nginx.conf', <<"EOF"); +%%TEST_GLOBALS%% +daemon off; +events { + worker_connections 32; +} +http { + %%TEST_GLOBALS_HTTP%% + server_tokens off; + server { + listen 127.0.0.1:${NginxPort}; + server_name localhost; + location / { + endpoints { + api service.pb.txt; + %%TEST_CONFIG%% + on; + } + proxy_pass http://127.0.0.1:${BackendPort}; + } + } +} +EOF + +$t->run_daemon(\&bookstore, $t, $BackendPort, 'bookstore.log'); +$t->run_daemon(\&servicecontrol, $t, $ServiceControlPort, 'servicecontrol.log'); +is($t->waitforsocket("127.0.0.1:${BackendPort}"), 1, 'Bookstore socket ready.'); +is($t->waitforsocket("127.0.0.1:${ServiceControlPort}"), 1, 'Service control socket ready.'); +$t->run_daemon(\&ApiManager::envoy, $t, 'service.pb.txt', $NginxPort, $BackendPort, $ServiceControlPort); +is($t->waitforsocket("127.0.0.1:${NginxPort}"), 1, 'Envoy socket ready.'); + +<>; +################################################################################ + +my $response = ApiManager::http_get($NginxPort,'/shelves?key=this-is-an-api-key'); + +$t->stop_daemons(); + +my ($response_headers, $response_body) = split /\r\n\r\n/, $response, 2; + +like($response_headers, qr/HTTP\/1\.1 200 OK/, 'Returned HTTP 200.'); +is($response_body, <<'EOF', 'Shelves returned in the response body.'); +{ "shelves": [ + { "name": "shelves/1", "theme": "Fiction" }, + { "name": "shelves/2", "theme": "Fantasy" } + ] +} +EOF + +my @requests = ApiManager::read_http_stream($t, 'bookstore.log'); +is(scalar @requests, 1, 'Backend received one request'); + +my $r = shift @requests; +is($r->{verb}, 'GET', 'Backend request was a get'); +is($r->{uri}, '/shelves?key=this-is-an-api-key', 'Backend uri was /shelves'); +#is($r->{headers}->{host}, "127.0.0.1:${BackendPort}", 'Host header was set'); + +@requests = ApiManager::read_http_stream($t, 'servicecontrol.log'); +is(scalar @requests, 1, 'Service control received one request'); + +$r = shift @requests; +is($r->{verb}, 'POST', ':check verb was post'); +is($r->{uri}, '/v1/services/endpoints-test.cloudendpointsapis.com:check', ':check was called'); +#is($r->{headers}->{host}, "127.0.0.1:${ServiceControlPort}", 'Host header was set'); +is($r->{headers}->{'content-type'}, 'application/x-protobuf', ':check Content-Type was protocol buffer'); + +################################################################################ + +sub bookstore { + my ($t, $port, $file) = @_; + my $server = HttpServer->new($port, $t->testdir() . '/' . $file) + or die "Can't create test server socket: $!\n"; + local $SIG{PIPE} = 'IGNORE'; + + $server->on('GET', '/shelves?key=this-is-an-api-key', <<'EOF'); +HTTP/1.1 200 OK +Content-Length: 118 +Connection: close + +{ "shelves": [ + { "name": "shelves/1", "theme": "Fiction" }, + { "name": "shelves/2", "theme": "Fantasy" } + ] +} +EOF + $server->run(); +} + +sub servicecontrol { + my ($t, $port, $file) = @_; + my $server = HttpServer->new($port, $t->testdir() . '/' . $file) + or die "Can't create test server socket: $!\n"; + local $SIG{PIPE} = 'IGNORE'; + $server->on('POST', '/v1/services/endpoints-test.cloudendpointsapis.com:check', <<'EOF'); +HTTP/1.1 200 OK +Connection: close + +EOF + $server->run(); +} + +################################################################################ diff --git a/test/t/testdata/bookstore.pb.txt b/test/t/testdata/bookstore.pb.txt new file mode 100644 index 00000000000..88ec4b979ff --- /dev/null +++ b/test/t/testdata/bookstore.pb.txt @@ -0,0 +1,43 @@ +name: "endpoints-test.cloudendpointsapis.com" +id: "2016-08-25r1" +http { + rules { + selector: "ListShelves" + get: "/shelves" + } + rules { + selector: "CorsShelves" + custom: { + kind: "OPTIONS" + path: "/shelves" + } + } + rules { + selector: "CreateShelf" + post: "/shelves" + } + rules { + selector: "GetShelf" + get: "/shelves/{shelf}" + } + rules { + selector: "DeleteShelf" + delete: "/shelves/{shelf}" + } + rules { + selector: "ListBooks" + get: "/shelves/{shelf}/books" + } + rules { + selector: "CreateBook" + post: "/shelves/{shelf}/books" + } + rules { + selector: "GetBook" + get: "/shelves/{shelf}/books/{book}" + } + rules { + selector: "DeleteBook" + delete: "/shelves/{shelf}/books/{book}" + } +}