Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[libbeat] Add unit tests for libbeat's client proxy settings #12044

Merged
merged 9 commits into from
May 9, 2019
215 changes: 215 additions & 0 deletions libbeat/outputs/elasticsearch/client_proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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.

// This file contains tests to confirm that elasticsearch.Client uses proxy
// settings following the intended precedence.

package elasticsearch

import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/elastic/beats/libbeat/common/atomic"
"github.com/elastic/beats/libbeat/outputs/outil"
)

// These constants are inserted into client http request headers and confirmed
// by the server listeners.
const (
headerTestField = "X-Test-Value"
headerTestValue = "client_proxy_test test value"
)

// TestClientPing is a placeholder test that does nothing on a standard run,
// but starts up a client and sends a ping when the environment variable
// TEST_START_CLIENT is set to 1 as in execClient).
func TestClientPing(t *testing.T) {
// If this is the child process, start up the client, otherwise do nothing.
if os.Getenv("TEST_START_CLIENT") == "1" {
doClientPing(t)
return
}
}

// TestBaseline makes sure we can have a client process ping the server that
// we start, with no changes to the proxy settings. (This is really a
// meta-test for the helpers that create the servers / client.)
func TestBaseline(t *testing.T) {
servers, teardown := startServers(t)
defer teardown()

// Start a bare client with no proxy settings, pointed at the main server.
execClient(t, "TEST_SERVER_URL="+servers.serverURL)
// We expect one server request and 0 proxy requests
assert.Equal(t, 1, servers.serverRequestCount())
assert.Equal(t, 0, servers.proxyRequestCount())
}

// TestClientSettingsProxy confirms that we can control the proxy of a client
// by setting its ClientSettings.Proxy value on creation. (The child process
// uses the TEST_PROXY_URL environment variable to initialize the flag.)
func TestClientSettingsProxy(t *testing.T) {
servers, teardown := startServers(t)
defer teardown()

// Start a client with ClientSettings.Proxy set to the proxy listener.
execClient(t,
"TEST_SERVER_URL="+servers.serverURL,
"TEST_PROXY_URL="+servers.proxyURL)
// We expect one proxy request and 0 server requests
assert.Equal(t, 0, servers.serverRequestCount())
assert.Equal(t, 1, servers.proxyRequestCount())
}

// TestEnvironmentProxy confirms that we can control the proxy of a client by
// setting the HTTP_PROXY environment variable (see
// https://golang.org/pkg/net/http/#ProxyFromEnvironment).
func TestEnvironmentProxy(t *testing.T) {
servers, teardown := startServers(t)
defer teardown()

// Start a client with HTTP_PROXY set to the proxy listener.
// The server is set to a nonexistent URL because ProxyFromEnvironment
// always returns a nil proxy for local destination URLs.
execClient(t,
"TEST_SERVER_URL=http://fakeurl.fake.not-real",
"HTTP_PROXY="+servers.proxyURL)
// We expect one proxy request and 0 server requests
assert.Equal(t, 0, servers.serverRequestCount())
assert.Equal(t, 1, servers.proxyRequestCount())
}

// TestClientSettingsOverrideEnvironmentProxy confirms that when both
// ClientSettings.Proxy and HTTP_PROXY are set, ClientSettings takes precedence.
func TestClientSettingsOverrideEnvironmentProxy(t *testing.T) {
servers, teardown := startServers(t)
defer teardown()

// Start a client with ClientSettings.Proxy set to the proxy listener and
// HTTP_PROXY set to the server listener. We expect that the former will
// override the latter and thus we will only see a ping to the proxy.
// As above, the fake URL is needed to ensure ProxyFromEnvironment gives a
// non-nil result.
execClient(t,
"TEST_SERVER_URL=http://fakeurl.fake.not-real",
"TEST_PROXY_URL="+servers.proxyURL,
"HTTP_PROXY="+servers.serverURL)
// We expect one proxy request and 0 server requests
assert.Equal(t, 0, servers.serverRequestCount())
assert.Equal(t, 1, servers.proxyRequestCount())
}

// runClientTest executes the current test binary as a child process,
// running only the TestClientPing, and calling it with the environment variable
// TEST_START_CLIENT=1 (so the test can recognize that it is the child process),
// and any additional environment settings specified in env.
// This is helpful for testing proxy settings, since we need to have both a
// proxy / server-side listener and a client that communicates with the server
// using various proxy settings.
func execClient(t *testing.T, env ...string) {
// The child process always runs only the TestClientPing test, which pings
// the server at TEST_SERVER_URL and then terminates.
cmd := exec.Command(os.Args[0], "-test.run=TestClientPing")
cmd.Env = append(append(os.Environ(),
"TEST_START_CLIENT=1"),
env...)
cmdOutput := new(bytes.Buffer)
cmd.Stderr = cmdOutput
cmd.Stdout = cmdOutput

err := cmd.Run()
if err != nil {
t.Error("Error executing client:\n" + cmdOutput.String())
}
}

func doClientPing(t *testing.T) {
serverURL := os.Getenv("TEST_SERVER_URL")
faec marked this conversation as resolved.
Show resolved Hide resolved
require.NotEqual(t, serverURL, "")
proxy := os.Getenv("TEST_PROXY_URL")
clientSettings := ClientSettings{
URL: serverURL,
Index: outil.MakeSelector(outil.ConstSelectorExpr("test")),
Headers: map[string]string{headerTestField: headerTestValue},
}
if proxy != "" {
proxyURL, err := url.Parse(proxy)
require.NoError(t, err)
clientSettings.Proxy = proxyURL
}
client, err := NewClient(clientSettings, nil)
require.NoError(t, err)

// This ping won't succeed; we aren't testing end-to-end communication
// (which would require a lot more setup work), we just want to make sure
// the client is pointed at the right server or proxy.
client.Ping()
}

// serverState contains the state of the http listeners for proxy tests,
// including the endpoint URLs and the observed request count for each one.
type serverState struct {
serverURL string
proxyURL string

_serverRequestCount atomic.Int // Requests directly to the server
_proxyRequestCount atomic.Int // Requests via the proxy
}

// Convenience functions to unwrap the atomic primitives
func (s serverState) serverRequestCount() int {
return s._serverRequestCount.Load()
}

func (s serverState) proxyRequestCount() int {
return s._proxyRequestCount.Load()
}

// startServers starts endpoints representing a backend server and a proxy,
// and returns the corresponding serverState and a teardown function that
// should be called to shut them down at the end of the test.
func startServers(t *testing.T) (*serverState, func()) {
state := serverState{}
server := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, headerTestValue, r.Header.Get(headerTestField))
fmt.Fprintln(w, "Hello, client")
state._serverRequestCount.Inc()
}))
proxy := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, headerTestValue, r.Header.Get(headerTestField))
fmt.Fprintln(w, "Hello, client")
state._proxyRequestCount.Inc()
}))
state.serverURL = server.URL
state.proxyURL = proxy.URL
return &state, func() {
server.Close()
proxy.Close()
}
faec marked this conversation as resolved.
Show resolved Hide resolved
}