mirror of
https://github.com/Oxalide/vsphere-influxdb-go.git
synced 2023-10-10 13:36:51 +02:00
707 lines
24 KiB
Go
707 lines
24 KiB
Go
|
package httpd_test
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"log"
|
||
|
"mime/multipart"
|
||
|
"net/http"
|
||
|
"net/http/httptest"
|
||
|
"net/url"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/influxdata/influxdb/internal"
|
||
|
|
||
|
"github.com/dgrijalva/jwt-go"
|
||
|
"github.com/influxdata/influxdb/influxql"
|
||
|
"github.com/influxdata/influxdb/models"
|
||
|
"github.com/influxdata/influxdb/services/httpd"
|
||
|
"github.com/influxdata/influxdb/services/meta"
|
||
|
)
|
||
|
|
||
|
// Ensure the handler returns results from a query (including nil results).
|
||
|
func TestHandler_Query(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
if stmt.String() != `SELECT * FROM bar` {
|
||
|
t.Fatalf("unexpected query: %s", stmt.String())
|
||
|
} else if ctx.Database != `foo` {
|
||
|
t.Fatalf("unexpected db: %s", ctx.Database)
|
||
|
}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series0"}})}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 2, Series: models.Rows([]*models.Row{{Name: "series1"}})}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar", nil))
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"results":[{"statement_id":1,"series":[{"name":"series0"}]},{"statement_id":2,"series":[{"name":"series1"}]}]}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler returns results from a query passed as a file.
|
||
|
func TestHandler_Query_File(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
if stmt.String() != `SELECT * FROM bar` {
|
||
|
t.Fatalf("unexpected query: %s", stmt.String())
|
||
|
} else if ctx.Database != `foo` {
|
||
|
t.Fatalf("unexpected db: %s", ctx.Database)
|
||
|
}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series0"}})}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 2, Series: models.Rows([]*models.Row{{Name: "series1"}})}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
var body bytes.Buffer
|
||
|
writer := multipart.NewWriter(&body)
|
||
|
part, err := writer.CreateFormFile("q", "")
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
io.WriteString(part, "SELECT * FROM bar")
|
||
|
|
||
|
if err := writer.Close(); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
r := MustNewJSONRequest("POST", "/query?db=foo", &body)
|
||
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
||
|
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, r)
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"results":[{"statement_id":1,"series":[{"name":"series0"}]},{"statement_id":2,"series":[{"name":"series1"}]}]}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Test query with user authentication.
|
||
|
func TestHandler_Query_Auth(t *testing.T) {
|
||
|
// Create the handler to be tested.
|
||
|
h := NewHandler(true)
|
||
|
|
||
|
// Set mock meta client functions for the handler to use.
|
||
|
h.MetaClient.AdminUserExistsFn = func() bool { return true }
|
||
|
|
||
|
h.MetaClient.UserFn = func(username string) (meta.User, error) {
|
||
|
if username != "user1" {
|
||
|
return nil, meta.ErrUserNotFound
|
||
|
}
|
||
|
return &meta.UserInfo{
|
||
|
Name: "user1",
|
||
|
Hash: "abcd",
|
||
|
Admin: true,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
h.MetaClient.AuthenticateFn = func(u, p string) (meta.User, error) {
|
||
|
if u != "user1" {
|
||
|
return nil, fmt.Errorf("unexpected user: exp: user1, got: %s", u)
|
||
|
} else if p != "abcd" {
|
||
|
return nil, fmt.Errorf("unexpected password: exp: abcd, got: %s", p)
|
||
|
}
|
||
|
return h.MetaClient.User(u)
|
||
|
}
|
||
|
|
||
|
// Set mock query authorizer for handler to use.
|
||
|
h.QueryAuthorizer.AuthorizeQueryFn = func(u meta.User, query *influxql.Query, database string) error {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Set mock statement executor for handler to use.
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
if stmt.String() != `SELECT * FROM bar` {
|
||
|
t.Fatalf("unexpected query: %s", stmt.String())
|
||
|
} else if ctx.Database != `foo` {
|
||
|
t.Fatalf("unexpected db: %s", ctx.Database)
|
||
|
}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series0"}})}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 2, Series: models.Rows([]*models.Row{{Name: "series1"}})}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Test the handler with valid user and password in the URL parameters.
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?u=user1&p=abcd&db=foo&q=SELECT+*+FROM+bar", nil))
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String())
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"results":[{"statement_id":1,"series":[{"name":"series0"}]},{"statement_id":2,"series":[{"name":"series1"}]}]}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
|
||
|
// Test the handler with valid user and password using basic auth.
|
||
|
w = httptest.NewRecorder()
|
||
|
r := MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar", nil)
|
||
|
r.SetBasicAuth("user1", "abcd")
|
||
|
h.ServeHTTP(w, r)
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String())
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"results":[{"statement_id":1,"series":[{"name":"series0"}]},{"statement_id":2,"series":[{"name":"series1"}]}]}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
|
||
|
// Test the handler with valid JWT bearer token.
|
||
|
req := MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar", nil)
|
||
|
// Create a signed JWT token string and add it to the request header.
|
||
|
_, signedToken := MustJWTToken("user1", h.Config.SharedSecret, false)
|
||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken))
|
||
|
|
||
|
w = httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, req)
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String())
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"results":[{"statement_id":1,"series":[{"name":"series0"}]},{"statement_id":2,"series":[{"name":"series1"}]}]}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
|
||
|
// Test the handler with JWT token signed with invalid key.
|
||
|
req = MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar", nil)
|
||
|
// Create a signed JWT token string and add it to the request header.
|
||
|
_, signedToken = MustJWTToken("user1", "invalid key", false)
|
||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken))
|
||
|
|
||
|
w = httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, req)
|
||
|
if w.Code != http.StatusUnauthorized {
|
||
|
t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String())
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"error":"signature is invalid"}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
|
||
|
// Test handler with valid JWT token carrying non-existant user.
|
||
|
_, signedToken = MustJWTToken("bad_user", h.Config.SharedSecret, false)
|
||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken))
|
||
|
|
||
|
w = httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, req)
|
||
|
if w.Code != http.StatusUnauthorized {
|
||
|
t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String())
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"error":"user not found"}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
|
||
|
// Test handler with expired JWT token.
|
||
|
_, signedToken = MustJWTToken("user1", h.Config.SharedSecret, true)
|
||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken))
|
||
|
|
||
|
w = httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, req)
|
||
|
if w.Code != http.StatusUnauthorized {
|
||
|
t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String())
|
||
|
} else if !strings.Contains(w.Body.String(), `{"error":"Token is expired`) {
|
||
|
t.Fatalf("unexpected body: %s", w.Body.String())
|
||
|
}
|
||
|
|
||
|
// Test handler with JWT token that has no expiration set.
|
||
|
token, _ := MustJWTToken("user1", h.Config.SharedSecret, false)
|
||
|
delete(token.Claims.(jwt.MapClaims), "exp")
|
||
|
signedToken, err := token.SignedString([]byte(h.Config.SharedSecret))
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken))
|
||
|
w = httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, req)
|
||
|
if w.Code != http.StatusUnauthorized {
|
||
|
t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String())
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"error":"token expiration required"}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
|
||
|
// Test the handler with valid user and password in the url and invalid in
|
||
|
// basic auth (prioritize url).
|
||
|
w = httptest.NewRecorder()
|
||
|
r = MustNewJSONRequest("GET", "/query?u=user1&p=abcd&db=foo&q=SELECT+*+FROM+bar", nil)
|
||
|
r.SetBasicAuth("user1", "efgh")
|
||
|
h.ServeHTTP(w, r)
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String())
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"results":[{"statement_id":1,"series":[{"name":"series0"}]},{"statement_id":2,"series":[{"name":"series1"}]}]}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler returns results from a query (including nil results).
|
||
|
func TestHandler_QueryRegex(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
if stmt.String() != `SELECT * FROM test WHERE url =~ /http\:\/\/www.akamai\.com/` {
|
||
|
t.Fatalf("unexpected query: %s", stmt.String())
|
||
|
} else if ctx.Database != `test` {
|
||
|
t.Fatalf("unexpected db: %s", ctx.Database)
|
||
|
}
|
||
|
ctx.Results <- nil
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewRequest("GET", "/query?db=test&q=SELECT%20%2A%20FROM%20test%20WHERE%20url%20%3D~%20%2Fhttp%5C%3A%5C%2F%5C%2Fwww.akamai%5C.com%2F", nil))
|
||
|
}
|
||
|
|
||
|
// Ensure the handler merges results from the same statement.
|
||
|
func TestHandler_Query_MergeResults(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series0"}})}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series1"}})}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar", nil))
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"results":[{"statement_id":1,"series":[{"name":"series0"},{"name":"series1"}]}]}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler merges results from the same statement.
|
||
|
func TestHandler_Query_MergeEmptyResults(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows{}}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series1"}})}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar", nil))
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"results":[{"statement_id":1,"series":[{"name":"series1"}]}]}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler can parse chunked and chunk size query parameters.
|
||
|
func TestHandler_Query_Chunked(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
if ctx.ChunkSize != 2 {
|
||
|
t.Fatalf("unexpected chunk size: %d", ctx.ChunkSize)
|
||
|
}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series0"}})}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series1"}})}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar&chunked=true&chunk_size=2", nil))
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
} else if w.Body.String() != `{"results":[{"statement_id":1,"series":[{"name":"series0"}]}]}
|
||
|
{"results":[{"statement_id":1,"series":[{"name":"series1"}]}]}
|
||
|
` {
|
||
|
t.Fatalf("unexpected body: %s", w.Body.String())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler can accept an async query.
|
||
|
func TestHandler_Query_Async(t *testing.T) {
|
||
|
done := make(chan struct{})
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
if stmt.String() != `SELECT * FROM bar` {
|
||
|
t.Fatalf("unexpected query: %s", stmt.String())
|
||
|
} else if ctx.Database != `foo` {
|
||
|
t.Fatalf("unexpected db: %s", ctx.Database)
|
||
|
}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series0"}})}
|
||
|
ctx.Results <- &influxql.Result{StatementID: 2, Series: models.Rows([]*models.Row{{Name: "series1"}})}
|
||
|
close(done)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar&async=true", nil))
|
||
|
if w.Code != http.StatusNoContent {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
|
||
|
// Wait to make sure the async query runs and completes.
|
||
|
timer := time.NewTimer(100 * time.Millisecond)
|
||
|
defer timer.Stop()
|
||
|
|
||
|
select {
|
||
|
case <-timer.C:
|
||
|
t.Fatal("timeout while waiting for async query to complete")
|
||
|
case <-done:
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler returns a status 400 if the query is not passed in.
|
||
|
func TestHandler_Query_ErrQueryRequired(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query", nil))
|
||
|
if w.Code != http.StatusBadRequest {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"error":"missing required parameter \"q\""}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler returns a status 400 if the query cannot be parsed.
|
||
|
func TestHandler_Query_ErrInvalidQuery(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?q=SELECT", nil))
|
||
|
if w.Code != http.StatusBadRequest {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"error":"error parsing query: found EOF, expected identifier, string, number, bool at line 1, char 8"}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler returns an appropriate 401 or 403 status when authentication or authorization fails.
|
||
|
func TestHandler_Query_ErrAuthorize(t *testing.T) {
|
||
|
h := NewHandler(true)
|
||
|
h.QueryAuthorizer.AuthorizeQueryFn = func(u meta.User, q *influxql.Query, db string) error {
|
||
|
return errors.New("marker")
|
||
|
}
|
||
|
h.MetaClient.AdminUserExistsFn = func() bool { return true }
|
||
|
h.MetaClient.AuthenticateFn = func(u, p string) (meta.User, error) {
|
||
|
|
||
|
users := []meta.UserInfo{
|
||
|
{
|
||
|
Name: "admin",
|
||
|
Hash: "admin",
|
||
|
Admin: true,
|
||
|
},
|
||
|
{
|
||
|
Name: "user1",
|
||
|
Hash: "abcd",
|
||
|
Privileges: map[string]influxql.Privilege{
|
||
|
"db0": influxql.ReadPrivilege,
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, user := range users {
|
||
|
if u == user.Name {
|
||
|
if p == user.Hash {
|
||
|
return &user, nil
|
||
|
}
|
||
|
return nil, meta.ErrAuthenticate
|
||
|
}
|
||
|
}
|
||
|
return nil, meta.ErrUserNotFound
|
||
|
}
|
||
|
|
||
|
for i, tt := range []struct {
|
||
|
user string
|
||
|
password string
|
||
|
query string
|
||
|
code int
|
||
|
}{
|
||
|
{
|
||
|
query: "/query?q=SHOW+DATABASES",
|
||
|
code: http.StatusUnauthorized,
|
||
|
},
|
||
|
{
|
||
|
user: "user1",
|
||
|
password: "abcd",
|
||
|
query: "/query?q=SHOW+DATABASES",
|
||
|
code: http.StatusForbidden,
|
||
|
},
|
||
|
{
|
||
|
user: "user2",
|
||
|
password: "abcd",
|
||
|
query: "/query?q=SHOW+DATABASES",
|
||
|
code: http.StatusUnauthorized,
|
||
|
},
|
||
|
} {
|
||
|
w := httptest.NewRecorder()
|
||
|
r := MustNewJSONRequest("GET", tt.query, nil)
|
||
|
params := r.URL.Query()
|
||
|
if tt.user != "" {
|
||
|
params.Set("u", tt.user)
|
||
|
}
|
||
|
if tt.password != "" {
|
||
|
params.Set("p", tt.password)
|
||
|
}
|
||
|
r.URL.RawQuery = params.Encode()
|
||
|
|
||
|
h.ServeHTTP(w, r)
|
||
|
if w.Code != tt.code {
|
||
|
t.Errorf("%d. unexpected status: got=%d exp=%d\noutput: %s", i, w.Code, tt.code, w.Body.String())
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler returns a status 200 if an error is returned in the result.
|
||
|
func TestHandler_Query_ErrResult(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
return errors.New("measurement not found")
|
||
|
}
|
||
|
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SHOW+SERIES+from+bin", nil))
|
||
|
if w.Code != http.StatusOK {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
} else if body := strings.TrimSpace(w.Body.String()); body != `{"results":[{"statement_id":0,"error":"measurement not found"}]}` {
|
||
|
t.Fatalf("unexpected body: %s", body)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure that closing the HTTP connection causes the query to be interrupted.
|
||
|
func TestHandler_Query_CloseNotify(t *testing.T) {
|
||
|
// Avoid leaking a goroutine when this fails.
|
||
|
done := make(chan struct{})
|
||
|
defer close(done)
|
||
|
|
||
|
interrupted := make(chan struct{})
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
select {
|
||
|
case <-ctx.InterruptCh:
|
||
|
case <-done:
|
||
|
}
|
||
|
close(interrupted)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
s := httptest.NewServer(h)
|
||
|
defer s.Close()
|
||
|
|
||
|
// Parse the URL and generate a query request.
|
||
|
u, err := url.Parse(s.URL)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
u.Path = "/query"
|
||
|
|
||
|
values := url.Values{}
|
||
|
values.Set("q", "SELECT * FROM cpu")
|
||
|
values.Set("db", "db0")
|
||
|
values.Set("rp", "rp0")
|
||
|
values.Set("chunked", "true")
|
||
|
u.RawQuery = values.Encode()
|
||
|
|
||
|
req, err := http.NewRequest("GET", u.String(), nil)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Perform the request and retrieve the response.
|
||
|
resp, err := http.DefaultClient.Do(req)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Validate that the interrupted channel has NOT been closed yet.
|
||
|
timer := time.NewTimer(100 * time.Millisecond)
|
||
|
select {
|
||
|
case <-interrupted:
|
||
|
timer.Stop()
|
||
|
t.Fatal("query interrupted unexpectedly")
|
||
|
case <-timer.C:
|
||
|
}
|
||
|
|
||
|
// Close the response body which should abort the query in the handler.
|
||
|
resp.Body.Close()
|
||
|
|
||
|
// The query should abort within 100 milliseconds.
|
||
|
timer.Reset(100 * time.Millisecond)
|
||
|
select {
|
||
|
case <-interrupted:
|
||
|
timer.Stop()
|
||
|
case <-timer.C:
|
||
|
t.Fatal("timeout while waiting for query to abort")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler handles ping requests correctly.
|
||
|
// TODO: This should be expanded to verify the MetaClient check in servePing is working correctly
|
||
|
func TestHandler_Ping(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewRequest("GET", "/ping", nil))
|
||
|
if w.Code != http.StatusNoContent {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
}
|
||
|
h.ServeHTTP(w, MustNewRequest("HEAD", "/ping", nil))
|
||
|
if w.Code != http.StatusNoContent {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler returns the version correctly from the different endpoints.
|
||
|
func TestHandler_Version(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
return nil
|
||
|
}
|
||
|
tests := []struct {
|
||
|
method string
|
||
|
endpoint string
|
||
|
body io.Reader
|
||
|
}{
|
||
|
{
|
||
|
method: "GET",
|
||
|
endpoint: "/ping",
|
||
|
body: nil,
|
||
|
},
|
||
|
{
|
||
|
method: "GET",
|
||
|
endpoint: "/query?db=foo&q=SELECT+*+FROM+bar",
|
||
|
body: nil,
|
||
|
},
|
||
|
{
|
||
|
method: "POST",
|
||
|
endpoint: "/write",
|
||
|
body: bytes.NewReader(make([]byte, 10)),
|
||
|
},
|
||
|
{
|
||
|
method: "GET",
|
||
|
endpoint: "/notfound",
|
||
|
body: nil,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, test := range tests {
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewRequest(test.method, test.endpoint, test.body))
|
||
|
if v, ok := w.HeaderMap["X-Influxdb-Version"]; ok {
|
||
|
if v[0] != "0.0.0" {
|
||
|
t.Fatalf("unexpected version: %s", v)
|
||
|
}
|
||
|
} else {
|
||
|
t.Fatalf("Header entry 'X-Influxdb-Version' not present")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure the handler handles status requests correctly.
|
||
|
func TestHandler_Status(t *testing.T) {
|
||
|
h := NewHandler(false)
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewRequest("GET", "/status", nil))
|
||
|
if w.Code != http.StatusNoContent {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
}
|
||
|
h.ServeHTTP(w, MustNewRequest("HEAD", "/status", nil))
|
||
|
if w.Code != http.StatusNoContent {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure write endpoint can handle bad requests
|
||
|
func TestHandler_HandleBadRequestBody(t *testing.T) {
|
||
|
b := bytes.NewReader(make([]byte, 10))
|
||
|
h := NewHandler(false)
|
||
|
w := httptest.NewRecorder()
|
||
|
h.ServeHTTP(w, MustNewRequest("POST", "/write", b))
|
||
|
if w.Code != http.StatusBadRequest {
|
||
|
t.Fatalf("unexpected status: %d", w.Code)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ensure X-Forwarded-For header writes the correct log message.
|
||
|
func TestHandler_XForwardedFor(t *testing.T) {
|
||
|
var buf bytes.Buffer
|
||
|
h := NewHandler(false)
|
||
|
h.CLFLogger = log.New(&buf, "", 0)
|
||
|
|
||
|
req := MustNewRequest("GET", "/query", nil)
|
||
|
req.Header.Set("X-Forwarded-For", "192.168.0.1")
|
||
|
req.RemoteAddr = "127.0.0.1"
|
||
|
h.ServeHTTP(httptest.NewRecorder(), req)
|
||
|
|
||
|
parts := strings.Split(buf.String(), " ")
|
||
|
if parts[0] != "192.168.0.1,127.0.0.1" {
|
||
|
t.Errorf("unexpected host ip address: %s", parts[0])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewHandler represents a test wrapper for httpd.Handler.
|
||
|
type Handler struct {
|
||
|
*httpd.Handler
|
||
|
MetaClient *internal.MetaClientMock
|
||
|
StatementExecutor HandlerStatementExecutor
|
||
|
QueryAuthorizer HandlerQueryAuthorizer
|
||
|
}
|
||
|
|
||
|
// NewHandler returns a new instance of Handler.
|
||
|
func NewHandler(requireAuthentication bool) *Handler {
|
||
|
config := httpd.NewConfig()
|
||
|
config.AuthEnabled = requireAuthentication
|
||
|
config.SharedSecret = "super secret key"
|
||
|
|
||
|
h := &Handler{
|
||
|
Handler: httpd.NewHandler(config),
|
||
|
}
|
||
|
|
||
|
h.MetaClient = &internal.MetaClientMock{}
|
||
|
|
||
|
h.Handler.MetaClient = h.MetaClient
|
||
|
h.Handler.QueryExecutor = influxql.NewQueryExecutor()
|
||
|
h.Handler.QueryExecutor.StatementExecutor = &h.StatementExecutor
|
||
|
h.Handler.QueryAuthorizer = &h.QueryAuthorizer
|
||
|
h.Handler.Version = "0.0.0"
|
||
|
return h
|
||
|
}
|
||
|
|
||
|
// HandlerStatementExecutor is a mock implementation of Handler.StatementExecutor.
|
||
|
type HandlerStatementExecutor struct {
|
||
|
ExecuteStatementFn func(stmt influxql.Statement, ctx influxql.ExecutionContext) error
|
||
|
}
|
||
|
|
||
|
func (e *HandlerStatementExecutor) ExecuteStatement(stmt influxql.Statement, ctx influxql.ExecutionContext) error {
|
||
|
return e.ExecuteStatementFn(stmt, ctx)
|
||
|
}
|
||
|
|
||
|
// HandlerQueryAuthorizer is a mock implementation of Handler.QueryAuthorizer.
|
||
|
type HandlerQueryAuthorizer struct {
|
||
|
AuthorizeQueryFn func(u meta.User, query *influxql.Query, database string) error
|
||
|
}
|
||
|
|
||
|
func (a *HandlerQueryAuthorizer) AuthorizeQuery(u meta.User, query *influxql.Query, database string) error {
|
||
|
return a.AuthorizeQueryFn(u, query, database)
|
||
|
}
|
||
|
|
||
|
// MustNewRequest returns a new HTTP request. Panic on error.
|
||
|
func MustNewRequest(method, urlStr string, body io.Reader) *http.Request {
|
||
|
r, err := http.NewRequest(method, urlStr, body)
|
||
|
if err != nil {
|
||
|
panic(err.Error())
|
||
|
}
|
||
|
return r
|
||
|
}
|
||
|
|
||
|
// MustNewRequest returns a new HTTP request with the content type set. Panic on error.
|
||
|
func MustNewJSONRequest(method, urlStr string, body io.Reader) *http.Request {
|
||
|
r := MustNewRequest(method, urlStr, body)
|
||
|
r.Header.Set("Accept", "application/json")
|
||
|
return r
|
||
|
}
|
||
|
|
||
|
// MustJWTToken returns a new JWT token and signed string or panics trying.
|
||
|
func MustJWTToken(username, secret string, expired bool) (*jwt.Token, string) {
|
||
|
token := jwt.New(jwt.GetSigningMethod("HS512"))
|
||
|
token.Claims.(jwt.MapClaims)["username"] = username
|
||
|
if expired {
|
||
|
token.Claims.(jwt.MapClaims)["exp"] = time.Now().Add(-time.Second).Unix()
|
||
|
} else {
|
||
|
token.Claims.(jwt.MapClaims)["exp"] = time.Now().Add(time.Minute * 10).Unix()
|
||
|
}
|
||
|
signed, err := token.SignedString([]byte(secret))
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
return token, signed
|
||
|
}
|