Skip to content

Commit

Permalink
[libbeat] Add unit tests for libbeat's client proxy settings (elastic…
Browse files Browse the repository at this point in the history
…#12044)

These tests set up server listeners and create libbeat clients with varying proxy settings, and verify that the clients ping the correct target URL.

This is a preparation for elastic#11713, since most of the logic (and work) is in testing the proxy settings; the much simpler PR adding the proxy-disable flag will be a followup to this one, to keep the functional changes isolated in case of rollbacks etc.
  • Loading branch information
faec authored and ph committed May 21, 2019
1 parent 38c5487 commit 983e5a8
Showing 1 changed file with 215 additions and 0 deletions.
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")
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()
}
}

0 comments on commit 983e5a8

Please sign in to comment.