terraform/svchost/disco/disco_test.go

285 lines
8.1 KiB
Go

package disco
import (
"crypto/tls"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"testing"
"github.com/hashicorp/terraform/svchost"
"github.com/hashicorp/terraform/svchost/auth"
)
func TestMain(m *testing.M) {
// During all tests we override the HTTP transport we use for discovery
// so it'll tolerate the locally-generated TLS certificates we use
// for test URLs.
httpTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
os.Exit(m.Run())
}
func TestDiscover(t *testing.T) {
t.Run("happy path", func(t *testing.T) {
portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) {
resp := []byte(`
{
"thingy.v1": "http://example.com/foo",
"wotsit.v2": "http://example.net/bar"
}
`)
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Content-Length", strconv.Itoa(len(resp)))
w.Write(resp)
})
defer close()
givenHost := "localhost" + portStr
host, err := svchost.ForComparison(givenHost)
if err != nil {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
discovered := d.Discover(host)
gotURL := discovered.ServiceURL("thingy.v1")
if gotURL == nil {
t.Fatalf("found no URL for thingy.v1")
}
if got, want := gotURL.String(), "http://example.com/foo"; got != want {
t.Fatalf("wrong result %q; want %q", got, want)
}
})
t.Run("chunked encoding", func(t *testing.T) {
portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) {
resp := []byte(`
{
"thingy.v1": "http://example.com/foo",
"wotsit.v2": "http://example.net/bar"
}
`)
w.Header().Add("Content-Type", "application/json")
// We're going to force chunked encoding here -- and thus prevent
// the server from predicting the length -- so we can make sure
// our client is tolerant of servers using this encoding.
w.Write(resp[:5])
w.(http.Flusher).Flush()
w.Write(resp[5:])
w.(http.Flusher).Flush()
})
defer close()
givenHost := "localhost" + portStr
host, err := svchost.ForComparison(givenHost)
if err != nil {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
discovered := d.Discover(host)
gotURL := discovered.ServiceURL("wotsit.v2")
if gotURL == nil {
t.Fatalf("found no URL for wotsit.v2")
}
if got, want := gotURL.String(), "http://example.net/bar"; got != want {
t.Fatalf("wrong result %q; want %q", got, want)
}
})
t.Run("with credentials", func(t *testing.T) {
var authHeaderText string
portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) {
resp := []byte(`{}`)
authHeaderText = r.Header.Get("Authorization")
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Content-Length", strconv.Itoa(len(resp)))
w.Write(resp)
})
defer close()
givenHost := "localhost" + portStr
host, err := svchost.ForComparison(givenHost)
if err != nil {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
d.SetCredentialsSource(auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
host: map[string]interface{}{
"token": "abc123",
},
}))
d.Discover(host)
if got, want := authHeaderText, "Bearer abc123"; got != want {
t.Fatalf("wrong Authorization header\ngot: %s\nwant: %s", got, want)
}
})
t.Run("not JSON", func(t *testing.T) {
portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) {
resp := []byte(`{"thingy.v1": "http://example.com/foo"}`)
w.Header().Add("Content-Type", "application/octet-stream")
w.Write(resp)
})
defer close()
givenHost := "localhost" + portStr
host, err := svchost.ForComparison(givenHost)
if err != nil {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
discovered := d.Discover(host)
// result should be empty, which we can verify only by reaching into
// its internals.
if discovered.services != nil {
t.Errorf("response not empty; should be")
}
})
t.Run("malformed JSON", func(t *testing.T) {
portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) {
resp := []byte(`{"thingy.v1": "htt`) // truncated, for example...
w.Header().Add("Content-Type", "application/json")
w.Write(resp)
})
defer close()
givenHost := "localhost" + portStr
host, err := svchost.ForComparison(givenHost)
if err != nil {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
discovered := d.Discover(host)
// result should be empty, which we can verify only by reaching into
// its internals.
if discovered.services != nil {
t.Errorf("response not empty; should be")
}
})
t.Run("JSON with redundant charset", func(t *testing.T) {
// The JSON RFC defines no parameters for the application/json
// MIME type, but some servers have a weird tendency to just add
// "charset" to everything, so we'll make sure we ignore it successfully.
// (JSON uses content sniffing for encoding detection, not media type params.)
portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) {
resp := []byte(`{"thingy.v1": "http://example.com/foo"}`)
w.Header().Add("Content-Type", "application/json; charset=latin-1")
w.Write(resp)
})
defer close()
givenHost := "localhost" + portStr
host, err := svchost.ForComparison(givenHost)
if err != nil {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
discovered := d.Discover(host)
if discovered.services == nil {
t.Errorf("response is empty; shouldn't be")
}
})
t.Run("no discovery doc", func(t *testing.T) {
portStr, close := testServer(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
})
defer close()
givenHost := "localhost" + portStr
host, err := svchost.ForComparison(givenHost)
if err != nil {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
discovered := d.Discover(host)
// result should be empty, which we can verify only by reaching into
// its internals.
if discovered.services != nil {
t.Errorf("response not empty; should be")
}
})
t.Run("redirect", func(t *testing.T) {
// For this test, we have two servers and one redirects to the other
portStr1, close1 := testServer(func(w http.ResponseWriter, r *http.Request) {
// This server is the one that returns a real response.
resp := []byte(`{"thingy.v1": "http://example.com/foo"}`)
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Content-Length", strconv.Itoa(len(resp)))
w.Write(resp)
})
portStr2, close2 := testServer(func(w http.ResponseWriter, r *http.Request) {
// This server is the one that redirects.
http.Redirect(w, r, "https://127.0.0.1"+portStr1+"/.well-known/terraform.json", 302)
})
defer close1()
defer close2()
givenHost := "localhost" + portStr2
host, err := svchost.ForComparison(givenHost)
if err != nil {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
discovered := d.Discover(host)
gotURL := discovered.ServiceURL("thingy.v1")
if gotURL == nil {
t.Fatalf("found no URL for thingy.v1")
}
if got, want := gotURL.String(), "http://example.com/foo"; got != want {
t.Fatalf("wrong result %q; want %q", got, want)
}
// The base URL for the host object should be the URL we redirected to,
// rather than the we redirected _from_.
gotBaseURL := discovered.discoURL.String()
wantBaseURL := "https://127.0.0.1" + portStr1 + "/.well-known/terraform.json"
if gotBaseURL != wantBaseURL {
t.Errorf("incorrect base url %s; want %s", gotBaseURL, wantBaseURL)
}
})
}
func testServer(h func(w http.ResponseWriter, r *http.Request)) (portStr string, close func()) {
server := httptest.NewTLSServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// Test server always returns 404 if the URL isn't what we expect
if r.URL.Path != "/.well-known/terraform.json" {
w.WriteHeader(404)
w.Write([]byte("not found"))
return
}
// If the URL is correct then the given hander decides the response
h(w, r)
},
))
serverURL, _ := url.Parse(server.URL)
portStr = serverURL.Port()
if portStr != "" {
portStr = ":" + portStr
}
close = func() {
server.Close()
}
return
}