test(http): add dashboard HTTP client tests

pull/10616/head
Chris Goller 2018-09-16 21:39:46 -05:00
parent 31a2ed1fbb
commit a4982e4107
5 changed files with 558 additions and 21 deletions

View File

@ -6,6 +6,53 @@ import (
"net/url"
)
// Service connects to an InfluxDB via HTTP.
type Service struct {
Addr string
Token string
InsecureSkipVerify bool
*AuthorizationService
*OrganizationService
*UserService
*BucketService
*QueryService
*DashboardService
}
// NewService returns a service that is an HTTP
// client to a remote
func NewService(addr, token string) *Service {
return &Service{
Addr: addr,
Token: token,
AuthorizationService: &AuthorizationService{
Addr: addr,
Token: token,
},
OrganizationService: &OrganizationService{
Addr: addr,
Token: token,
},
UserService: &UserService{
Addr: addr,
Token: token,
},
BucketService: &BucketService{
Addr: addr,
Token: token,
},
QueryService: &QueryService{
Addr: addr,
Token: token,
},
DashboardService: &DashboardService{
Addr: addr,
Token: token,
},
}
}
// Shared transports for all clients to prevent leaking connections
var (
skipVerifyTransport = &http.Transport{

View File

@ -7,6 +7,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"path"
"github.com/influxdata/platform"
"github.com/influxdata/platform/kit/errors"
@ -20,22 +21,29 @@ type DashboardHandler struct {
DashboardService platform.DashboardService
}
const (
dashboardsPath = "/v2/dashboards"
dashboardsIDPath = "/v2/dashboards/:id"
dashboardsIDCellsPath = "/v2/dashboards/:id/cells"
dashboardsIDCellsIDPath = "/v2/dashboards/:id/cells/:cellID"
)
// NewDashboardHandler returns a new instance of DashboardHandler.
func NewDashboardHandler() *DashboardHandler {
h := &DashboardHandler{
Router: httprouter.New(),
}
h.HandlerFunc("POST", "/v2/dashboards", h.handlePostDashboard)
h.HandlerFunc("GET", "/v2/dashboards", h.handleGetDashboards)
h.HandlerFunc("GET", "/v2/dashboards/:id", h.handleGetDashboard)
h.HandlerFunc("DELETE", "/v2/dashboards/:id", h.handleDeleteDashboard)
h.HandlerFunc("PATCH", "/v2/dashboards/:id", h.handlePatchDashboard)
h.HandlerFunc("POST", dashboardsPath, h.handlePostDashboard)
h.HandlerFunc("GET", dashboardsPath, h.handleGetDashboards)
h.HandlerFunc("GET", dashboardsIDPath, h.handleGetDashboard)
h.HandlerFunc("DELETE", dashboardsIDPath, h.handleDeleteDashboard)
h.HandlerFunc("PATCH", dashboardsIDPath, h.handlePatchDashboard)
h.HandlerFunc("PUT", "/v2/dashboards/:id/cells", h.handlePutDashboardCells)
h.HandlerFunc("POST", "/v2/dashboards/:id/cells", h.handlePostDashboardCell)
h.HandlerFunc("DELETE", "/v2/dashboards/:id/cells/:cellID", h.handleDeleteDashboardCell)
h.HandlerFunc("PATCH", "/v2/dashboards/:id/cells/:cellID", h.handlePatchDashboardCell)
h.HandlerFunc("PUT", dashboardsIDCellsPath, h.handlePutDashboardCells)
h.HandlerFunc("POST", dashboardsIDCellsPath, h.handlePostDashboardCell)
h.HandlerFunc("DELETE", dashboardsIDCellsIDPath, h.handleDeleteDashboardCell)
h.HandlerFunc("PATCH", dashboardsIDCellsIDPath, h.handlePatchDashboardCell)
return h
}
@ -50,6 +58,25 @@ type dashboardResponse struct {
Links dashboardLinks `json:"links"`
}
func (d dashboardResponse) toPlatform() *platform.Dashboard {
if len(d.Cells) == 0 {
return &platform.Dashboard{
ID: d.ID,
Name: d.Name,
}
}
cells := make([]*platform.Cell, 0, len(d.Cells))
for i := range d.Cells {
cells = append(cells, d.Cells[i].toPlatform())
}
return &platform.Dashboard{
ID: d.ID,
Name: d.Name,
Cells: cells,
}
}
func newDashboardResponse(d *platform.Dashboard) dashboardResponse {
res := dashboardResponse{
Links: dashboardLinks{
@ -72,6 +99,10 @@ type dashboardCellResponse struct {
Links map[string]string `json:"links"`
}
func (c dashboardCellResponse) toPlatform() *platform.Cell {
return &c.Cell
}
func newDashboardCellResponse(dashboardID platform.ID, c *platform.Cell) dashboardCellResponse {
return dashboardCellResponse{
Cell: *c,
@ -105,8 +136,13 @@ func newDashboardCellsResponse(dashboardID platform.ID, cs []*platform.Cell) das
// handleGetDashboards returns all dashboards within the store.
func (h *DashboardHandler) handleGetDashboards(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// TODO(desa): support filtering via query params
dashboards, _, err := h.DashboardService.FindDashboards(ctx, platform.DashboardFilter{})
req, err := decodeGetDashboardsRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
dashboards, _, err := h.DashboardService.FindDashboards(ctx, req.filter)
if err != nil {
EncodeError(ctx, errors.InternalErrorf("Error loading dashboards: %v", err), w)
return
@ -118,6 +154,23 @@ func (h *DashboardHandler) handleGetDashboards(w http.ResponseWriter, r *http.Re
}
}
type getDashboardsRequest struct {
filter platform.DashboardFilter
}
func decodeGetDashboardsRequest(ctx context.Context, r *http.Request) (*getDashboardsRequest, error) {
qp := r.URL.Query()
req := &getDashboardsRequest{}
if id := qp.Get("id"); id != "" {
req.filter.ID = &platform.ID{}
if err := req.filter.ID.DecodeFromString(id); err != nil {
return nil, err
}
}
return req, nil
}
type getDashboardsLinks struct {
Self string `json:"self"`
}
@ -127,6 +180,14 @@ type getDashboardsResponse struct {
Dashboards []dashboardResponse `json:"dashboards"`
}
func (d getDashboardsResponse) toPlatform() []*platform.Dashboard {
res := make([]*platform.Dashboard, len(d.Dashboards))
for i := range d.Dashboards {
res[i] = d.Dashboards[i].toPlatform()
}
return res
}
func newGetDashboardsResponse(dashboards []*platform.Dashboard) getDashboardsResponse {
res := getDashboardsResponse{
Links: getDashboardsLinks{
@ -537,3 +598,331 @@ func (h *DashboardHandler) handlePatchDashboardCell(w http.ResponseWriter, r *ht
return
}
}
// DashboardService is a dashboard service over HTTP to the influxdb server.
type DashboardService struct {
Addr string
Token string
InsecureSkipVerify bool
}
// FindDashboardByID returns a single dashboard by ID.
func (s *DashboardService) FindDashboardByID(ctx context.Context, id platform.ID) (*platform.Dashboard, error) {
path := dashboardIDPath(id)
url, err := newURL(s.Addr, path)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, err
}
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
if err := CheckError(resp); err != nil {
return nil, err
}
var dr dashboardResponse
if err := json.NewDecoder(resp.Body).Decode(&dr); err != nil {
return nil, err
}
dashboard := dr.toPlatform()
return dashboard, nil
}
// FindDashboards returns a list of dashboards that match filter and the total count of matching dashboards.
// Additional options provide pagination & sorting.
func (s *DashboardService) FindDashboards(ctx context.Context, filter platform.DashboardFilter) ([]*platform.Dashboard, int, error) {
url, err := newURL(s.Addr, dashboardsPath)
if err != nil {
return nil, 0, err
}
qp := url.Query()
if filter.ID != nil {
qp.Add("id", filter.ID.String())
}
url.RawQuery = qp.Encode()
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, 0, err
}
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, 0, err
}
if err := CheckError(resp); err != nil {
return nil, 0, err
}
var dr getDashboardsResponse
if err := json.NewDecoder(resp.Body).Decode(&dr); err != nil {
return nil, 0, err
}
dashboards := dr.toPlatform()
return dashboards, len(dashboards), nil
}
// CreateDashboard creates a new dashboard and sets b.ID with the new identifier.
func (s *DashboardService) CreateDashboard(ctx context.Context, d *platform.Dashboard) error {
url, err := newURL(s.Addr, dashboardsPath)
if err != nil {
return err
}
b, err := json.Marshal(d)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url.String(), bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
if err := CheckError(resp); err != nil {
return err
}
return json.NewDecoder(resp.Body).Decode(d)
}
// UpdateDashboard updates a single dashboard with changeset.
// Returns the new dashboard state after update.
func (s *DashboardService) UpdateDashboard(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
u, err := newURL(s.Addr, dashboardIDPath(id))
if err != nil {
return nil, err
}
b, err := json.Marshal(upd)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PATCH", u.String(), bytes.NewReader(b))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
if err := CheckError(resp); err != nil {
return nil, err
}
var d platform.Dashboard
if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
return nil, err
}
defer resp.Body.Close()
if len(d.Cells) == 0 {
d.Cells = nil
}
return &d, nil
}
// DeleteDashboard removes a dashboard by ID.
func (s *DashboardService) DeleteDashboard(ctx context.Context, id platform.ID) error {
u, err := newURL(s.Addr, dashboardIDPath(id))
if err != nil {
return err
}
req, err := http.NewRequest("DELETE", u.String(), nil)
if err != nil {
return err
}
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
return CheckError(resp)
}
// AddDashboardCell adds a cell to a dashboard.
func (s *DashboardService) AddDashboardCell(ctx context.Context, id platform.ID, c *platform.Cell, opts platform.AddDashboardCellOptions) error {
url, err := newURL(s.Addr, cellPath(id))
if err != nil {
return err
}
b, err := json.Marshal(c)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url.String(), bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
if err := CheckError(resp); err != nil {
return err
}
// TODO (goller): deal with the dashboard cell options
return json.NewDecoder(resp.Body).Decode(c)
}
// RemoveDashboardCell removes a dashboard.
func (s *DashboardService) RemoveDashboardCell(ctx context.Context, dashboardID, cellID platform.ID) error {
u, err := newURL(s.Addr, dashboardCellIDPath(dashboardID, cellID))
if err != nil {
return err
}
req, err := http.NewRequest("DELETE", u.String(), nil)
if err != nil {
return err
}
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
return CheckError(resp)
}
// UpdateDashboardCell replaces the dashboard cell with the provided ID.
func (s *DashboardService) UpdateDashboardCell(ctx context.Context, dashboardID, cellID platform.ID, upd platform.CellUpdate) (*platform.Cell, error) {
u, err := newURL(s.Addr, dashboardCellIDPath(dashboardID, cellID))
if err != nil {
return nil, err
}
b, err := json.Marshal(upd)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PATCH", u.String(), bytes.NewReader(b))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
if err := CheckError(resp); err != nil {
return nil, err
}
var c platform.Cell
if err := json.NewDecoder(resp.Body).Decode(&c); err != nil {
return nil, err
}
defer resp.Body.Close()
return &c, nil
}
// ReplaceDashboardCells replaces all cells in a dashboard
func (s *DashboardService) ReplaceDashboardCells(ctx context.Context, id platform.ID, cs []*platform.Cell) error {
u, err := newURL(s.Addr, cellPath(id))
if err != nil {
return err
}
// TODO(goller): I think this should be {"cells":[]}
b, err := json.Marshal(cs)
if err != nil {
return err
}
req, err := http.NewRequest("PUT", u.String(), bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
if err := CheckError(resp); err != nil {
return err
}
cells := dashboardCellsResponse{}
if err := json.NewDecoder(resp.Body).Decode(&cells); err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func dashboardIDPath(id platform.ID) string {
return path.Join(dashboardsPath, id.String())
}
func cellPath(id platform.ID) string {
return path.Join(dashboardIDPath(id), "cells")
}
func dashboardCellIDPath(id platform.ID, cellID platform.ID) string {
return path.Join(cellPath(id), cellID.String())
}

View File

@ -11,7 +11,9 @@ import (
"testing"
"github.com/influxdata/platform"
"github.com/influxdata/platform/inmem"
"github.com/influxdata/platform/mock"
platformtesting "github.com/influxdata/platform/testing"
"github.com/julienschmidt/httprouter"
)
@ -989,3 +991,54 @@ func TestService_handlePatchDashboardCell(t *testing.T) {
})
}
}
func Test_dashboardCellIDPath(t *testing.T) {
t.Parallel()
dashboard, err := platform.IDFromString("deadbeef")
if err != nil {
t.Fatal(err)
}
cell, err := platform.IDFromString("cade9a7e")
if err != nil {
t.Fatal(err)
}
want := "/v2/dashboards/deadbeef/cells/cade9a7e"
if got := dashboardCellIDPath(*dashboard, *cell); got != want {
t.Errorf("dashboardCellIDPath() = got: %s want: %s", got, want)
}
}
func initDashboardService(f platformtesting.DashboardFields, t *testing.T) (platform.DashboardService, func()) {
t.Helper()
svc := inmem.NewService()
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
for _, o := range f.Dashboards {
if err := svc.PutDashboard(ctx, o); err != nil {
t.Fatalf("failed to populate organizations")
}
}
for _, b := range f.Views {
if err := svc.PutView(ctx, b); err != nil {
t.Fatalf("failed to populate views")
}
}
handler := NewDashboardHandler()
handler.DashboardService = svc
server := httptest.NewServer(handler)
client := DashboardService{
Addr: server.URL,
}
done := server.Close
return &client, done
}
func TestDashboardService(t *testing.T) {
t.Parallel()
platformtesting.DashboardService(initDashboardService, t)
}

View File

@ -88,7 +88,6 @@ func (s *Service) UpdateDashboard(ctx context.Context, id platform.ID, upd platf
}
s.dashboardKV.Store(o.ID.String(), o)
return o, nil
}

View File

@ -38,6 +38,55 @@ type DashboardFields struct {
Views []*platform.View
}
// DashboardService tests all the service functions.
func DashboardService(
init func(DashboardFields, *testing.T) (platform.DashboardService, func()), t *testing.T,
) {
tests := []struct {
name string
fn func(init func(DashboardFields, *testing.T) (platform.DashboardService, func()),
t *testing.T)
}{
{
name: "FindDashboardByID",
fn: FindDashboardByID,
},
{
name: "FindDashboards",
fn: FindDashboards,
},
{
name: "CreateDashboard",
fn: CreateDashboard,
},
{
name: "UpdateDashboard",
fn: UpdateDashboard,
},
{
name: "AddDashboardCell",
fn: AddDashboardCell,
},
{
name: "RemoveDashboardCell",
fn: RemoveDashboardCell,
},
{
name: "UpdateDashboardCell",
fn: UpdateDashboardCell,
},
{
name: "ReplaceDashboardCells",
fn: ReplaceDashboardCells,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.fn(init, t)
})
}
}
// CreateDashboard testing
func CreateDashboard(
init func(DashboardFields, *testing.T) (platform.DashboardService, func()),
@ -96,7 +145,7 @@ func CreateDashboard(
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
ctx := context.Background()
err := s.CreateDashboard(ctx, tt.args.dashboard)
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
@ -180,7 +229,7 @@ func AddDashboardCell(
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
ctx := context.Background()
err := s.AddDashboardCell(ctx, tt.args.dashboardID, tt.args.cell, platform.AddDashboardCellOptions{})
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
@ -253,7 +302,7 @@ func FindDashboardByID(
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
ctx := context.Background()
dashboard, err := s.FindDashboardByID(ctx, tt.args.id)
if (err != nil) != (tt.wants.err != nil) {
@ -353,7 +402,7 @@ func FindDashboards(
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
ctx := context.Background()
filter := platform.DashboardFilter{}
if tt.args.ID != nil {
@ -460,7 +509,7 @@ func DeleteDashboard(
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
ctx := context.Background()
err := s.DeleteDashboard(ctx, tt.args.ID)
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
@ -536,7 +585,7 @@ func UpdateDashboard(
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
ctx := context.Background()
upd := platform.DashboardUpdate{}
if tt.args.name != "" {
@ -636,7 +685,7 @@ func RemoveDashboardCell(
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
ctx := context.Background()
err := s.RemoveDashboardCell(ctx, tt.args.dashboardID, tt.args.cellID)
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
@ -736,7 +785,7 @@ func UpdateDashboardCell(
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
ctx := context.Background()
upd := platform.CellUpdate{}
if tt.args.x != 0 {
upd.X = &tt.args.x
@ -990,7 +1039,7 @@ func ReplaceDashboardCells(
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
ctx := context.Background()
err := s.ReplaceDashboardCells(ctx, tt.args.dashboardID, tt.args.cells)
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)