package http import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/google/go-cmp/cmp" "github.com/influxdata/influxdb" "github.com/influxdata/influxdb/mock" influxdbtesting "github.com/influxdata/influxdb/testing" "github.com/julienschmidt/httprouter" "github.com/yudai/gojsondiff" "github.com/yudai/gojsondiff/formatter" "go.uber.org/zap" ) // NewMockViewBackend returns a ViewBackend with mock services. func NewMockViewBackend() *ViewBackend { return &ViewBackend{ Logger: zap.NewNop().With(zap.String("handler", "view")), ViewService: &mock.ViewService{}, UserService: mock.NewUserService(), UserResourceMappingService: &mock.UserResourceMappingService{}, LabelService: mock.NewLabelService(), } } func TestService_handleGetViews(t *testing.T) { type fields struct { ViewService influxdb.ViewService } type args struct { queryParams map[string][]string } type wants struct { statusCode int contentType string body string } tests := []struct { name string fields fields args args wants wants }{ { name: "get all views", fields: fields{ &mock.ViewService{ FindViewsF: func(ctx context.Context, filter influxdb.ViewFilter) ([]*influxdb.View, int, error) { return []*influxdb.View{ { ViewContents: influxdb.ViewContents{ ID: influxdbtesting.MustIDBase16("7365637465747572"), Name: "hello", }, Properties: influxdb.XYViewProperties{ Type: "xy", }, }, { ViewContents: influxdb.ViewContents{ ID: influxdbtesting.MustIDBase16("6167697474697320"), Name: "example", }, }, }, 2, nil }, }, }, args: args{}, wants: wants{ statusCode: http.StatusOK, contentType: "application/json; charset=utf-8", body: ` { "links": { "self": "/api/v2/views" }, "views": [ { "id": "7365637465747572", "name": "hello", "links": { "labels": "/api/v2/views/7365637465747572/labels", "self": "/api/v2/views/7365637465747572" }, "properties": { "shape": "chronograf-v2", "queries": null, "axes": null, "type": "xy", "colors": null, "legend": {}, "geom": "", "note": "", "showNoteWhenEmpty": false } }, { "id": "6167697474697320", "name": "example", "links": { "labels": "/api/v2/views/6167697474697320/labels", "self": "/api/v2/views/6167697474697320" }, "properties": { "shape": "empty" } } ] }`, }, }, { name: "get all views when there are none", fields: fields{ &mock.ViewService{ FindViewsF: func(ctx context.Context, filter influxdb.ViewFilter) ([]*influxdb.View, int, error) { return []*influxdb.View{}, 0, nil }, }, }, args: args{}, wants: wants{ statusCode: http.StatusOK, contentType: "application/json; charset=utf-8", body: ` { "links": { "self": "/api/v2/views" }, "views": [] }`, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { viewBackend := NewMockViewBackend() viewBackend.ViewService = tt.fields.ViewService h := NewViewHandler(viewBackend) r := httptest.NewRequest("GET", "http://any.url", nil) qp := r.URL.Query() for k, vs := range tt.args.queryParams { for _, v := range vs { qp.Add(k, v) } } r.URL.RawQuery = qp.Encode() w := httptest.NewRecorder() h.handleGetViews(w, r) res := w.Result() content := res.Header.Get("Content-Type") body, _ := ioutil.ReadAll(res.Body) if res.StatusCode != tt.wants.statusCode { t.Errorf("%q. handleGetViews() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) } if tt.wants.contentType != "" && content != tt.wants.contentType { t.Errorf("%q. handleGetViews() = %v, want %v", tt.name, content, tt.wants.contentType) } if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { t.Errorf("%q. handleGetViews() = ***%s***", tt.name, diff) } }) } } func TestService_handleGetView(t *testing.T) { type fields struct { ViewService influxdb.ViewService } type args struct { id string } type wants struct { statusCode int contentType string body string } tests := []struct { name string fields fields args args wants wants }{ { name: "get a view by id", fields: fields{ &mock.ViewService{ FindViewByIDF: func(ctx context.Context, id influxdb.ID) (*influxdb.View, error) { return &influxdb.View{ ViewContents: influxdb.ViewContents{ ID: influxdbtesting.MustIDBase16("020f755c3c082000"), Name: "example", }, }, nil }, }, }, args: args{ id: "020f755c3c082000", }, wants: wants{ statusCode: http.StatusOK, contentType: "application/json; charset=utf-8", body: ` { "id": "020f755c3c082000", "name": "example", "links": { "labels": "/api/v2/views/020f755c3c082000/labels", "self": "/api/v2/views/020f755c3c082000" }, "properties": { "shape": "empty" } } `, }, }, { name: "not found", fields: fields{ &mock.ViewService{ FindViewByIDF: func(ctx context.Context, id influxdb.ID) (*influxdb.View, error) { return nil, &influxdb.Error{ Code: influxdb.ENotFound, Msg: influxdb.ErrViewNotFound, } }, }, }, args: args{ id: "020f755c3c082000", }, wants: wants{ statusCode: http.StatusNotFound, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { viewBackend := NewMockViewBackend() viewBackend.ViewService = tt.fields.ViewService h := NewViewHandler(viewBackend) r := httptest.NewRequest("GET", "http://any.url", nil) r = r.WithContext(context.WithValue( context.TODO(), httprouter.ParamsKey, httprouter.Params{ { Key: "id", Value: tt.args.id, }, })) w := httptest.NewRecorder() h.handleGetView(w, r) res := w.Result() content := res.Header.Get("Content-Type") body, _ := ioutil.ReadAll(res.Body) if res.StatusCode != tt.wants.statusCode { t.Errorf("%q. handleGetView() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) } if tt.wants.contentType != "" && content != tt.wants.contentType { t.Errorf("%q. handleGetView() = %v, want %v", tt.name, content, tt.wants.contentType) } if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { t.Errorf("%q. handleGetView() = ***%s***", tt.name, diff) } }) } } func TestService_handlePostViews(t *testing.T) { type fields struct { ViewService influxdb.ViewService } type args struct { view *influxdb.View } type wants struct { statusCode int contentType string body string } tests := []struct { name string fields fields args args wants wants }{ { name: "create a new view", fields: fields{ &mock.ViewService{ CreateViewF: func(ctx context.Context, c *influxdb.View) error { c.ID = influxdbtesting.MustIDBase16("020f755c3c082000") return nil }, }, }, args: args{ view: &influxdb.View{ ViewContents: influxdb.ViewContents{ ID: influxdbtesting.MustIDBase16("020f755c3c082000"), Name: "hello", }, Properties: influxdb.XYViewProperties{ Type: "xy", }, }, }, wants: wants{ statusCode: http.StatusCreated, contentType: "application/json; charset=utf-8", body: ` { "id": "020f755c3c082000", "name": "hello", "links": { "labels": "/api/v2/views/020f755c3c082000/labels", "self": "/api/v2/views/020f755c3c082000" }, "properties": { "shape": "chronograf-v2", "queries": null, "axes": null, "type": "xy", "colors": null, "legend": {}, "geom": "", "note": "", "showNoteWhenEmpty": false } } `, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { viewBackend := NewMockViewBackend() viewBackend.ViewService = tt.fields.ViewService h := NewViewHandler(viewBackend) b, err := json.Marshal(tt.args.view) if err != nil { t.Fatalf("failed to unmarshal view: %v", err) } r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b)) w := httptest.NewRecorder() h.handlePostViews(w, r) res := w.Result() content := res.Header.Get("Content-Type") body, _ := ioutil.ReadAll(res.Body) if res.StatusCode != tt.wants.statusCode { t.Errorf("%q. handlePostViews() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) } if tt.wants.contentType != "" && content != tt.wants.contentType { t.Errorf("%q. handlePostViews() = %v, want %v", tt.name, content, tt.wants.contentType) } if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { t.Errorf("%q. handlePostViews() = ***%s***", tt.name, diff) } }) } } func TestService_handleDeleteView(t *testing.T) { type fields struct { ViewService influxdb.ViewService } type args struct { id string } type wants struct { statusCode int contentType string body string } tests := []struct { name string fields fields args args wants wants }{ { name: "remove a view by id", fields: fields{ &mock.ViewService{ DeleteViewF: func(ctx context.Context, id influxdb.ID) error { if id == influxdbtesting.MustIDBase16("020f755c3c082000") { return nil } return fmt.Errorf("wrong id") }, }, }, args: args{ id: "020f755c3c082000", }, wants: wants{ statusCode: http.StatusNoContent, }, }, { name: "view not found", fields: fields{ &mock.ViewService{ DeleteViewF: func(ctx context.Context, id influxdb.ID) error { return &influxdb.Error{ Code: influxdb.ENotFound, Msg: influxdb.ErrViewNotFound, } }, }, }, args: args{ id: "020f755c3c082000", }, wants: wants{ statusCode: http.StatusNotFound, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { viewBackend := NewMockViewBackend() viewBackend.ViewService = tt.fields.ViewService h := NewViewHandler(viewBackend) r := httptest.NewRequest("GET", "http://any.url", nil) r = r.WithContext(context.WithValue( context.TODO(), httprouter.ParamsKey, httprouter.Params{ { Key: "id", Value: tt.args.id, }, })) w := httptest.NewRecorder() h.handleDeleteView(w, r) res := w.Result() content := res.Header.Get("Content-Type") body, _ := ioutil.ReadAll(res.Body) if res.StatusCode != tt.wants.statusCode { t.Errorf("%q. handleDeleteView() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) } if tt.wants.contentType != "" && content != tt.wants.contentType { t.Errorf("%q. handleDeleteView() = %v, want %v", tt.name, content, tt.wants.contentType) } if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { t.Errorf("%q. handleDeleteView() = ***%s***", tt.name, diff) } }) } } func TestService_handlePatchView(t *testing.T) { type fields struct { ViewService influxdb.ViewService } type args struct { id string name string properties influxdb.ViewProperties } type wants struct { statusCode int contentType string body string } tests := []struct { name string fields fields args args wants wants }{ { name: "update a view", fields: fields{ &mock.ViewService{ UpdateViewF: func(ctx context.Context, id influxdb.ID, upd influxdb.ViewUpdate) (*influxdb.View, error) { if id == influxdbtesting.MustIDBase16("020f755c3c082000") { return &influxdb.View{ ViewContents: influxdb.ViewContents{ ID: influxdbtesting.MustIDBase16("020f755c3c082000"), Name: "example", }, Properties: influxdb.XYViewProperties{ Type: "xy", }, }, nil } return nil, fmt.Errorf("not found") }, }, }, args: args{ id: "020f755c3c082000", name: "example", }, wants: wants{ statusCode: http.StatusOK, contentType: "application/json; charset=utf-8", body: ` { "id": "020f755c3c082000", "name": "example", "links": { "labels": "/api/v2/views/020f755c3c082000/labels", "self": "/api/v2/views/020f755c3c082000" }, "properties": { "shape": "chronograf-v2", "queries": null, "axes": null, "type": "xy", "colors": null, "legend": {}, "geom": "", "note": "", "showNoteWhenEmpty": false } } `, }, }, { name: "update a view with empty request body", fields: fields{ &mock.ViewService{ UpdateViewF: func(ctx context.Context, id influxdb.ID, upd influxdb.ViewUpdate) (*influxdb.View, error) { if id == influxdbtesting.MustIDBase16("020f755c3c082000") { return &influxdb.View{ ViewContents: influxdb.ViewContents{ ID: influxdbtesting.MustIDBase16("020f755c3c082000"), Name: "example", }, Properties: influxdb.XYViewProperties{ Type: "xy", }, }, nil } return nil, fmt.Errorf("not found") }, }, }, args: args{ id: "020f755c3c082000", }, wants: wants{ statusCode: http.StatusBadRequest, }, }, { name: "view not found", fields: fields{ &mock.ViewService{ UpdateViewF: func(ctx context.Context, id influxdb.ID, upd influxdb.ViewUpdate) (*influxdb.View, error) { return nil, &influxdb.Error{ Code: influxdb.ENotFound, Msg: influxdb.ErrViewNotFound, } }, }, }, args: args{ id: "020f755c3c082000", name: "hello", }, wants: wants{ statusCode: http.StatusNotFound, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { viewBackend := NewMockViewBackend() viewBackend.ViewService = tt.fields.ViewService h := NewViewHandler(viewBackend) upd := influxdb.ViewUpdate{} if tt.args.name != "" { upd.Name = &tt.args.name } if tt.args.properties != nil { upd.Properties = tt.args.properties } b, err := json.Marshal(upd) if err != nil { t.Fatalf("failed to unmarshal view update: %v", err) } r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b)) r = r.WithContext(context.WithValue( context.TODO(), httprouter.ParamsKey, httprouter.Params{ { Key: "id", Value: tt.args.id, }, })) w := httptest.NewRecorder() h.handlePatchView(w, r) res := w.Result() content := res.Header.Get("Content-Type") body, _ := ioutil.ReadAll(res.Body) if res.StatusCode != tt.wants.statusCode { t.Errorf("%q. handlePatchView() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) } if tt.wants.contentType != "" && content != tt.wants.contentType { t.Errorf("%q. handlePatchView() = %v, want %v", tt.name, content, tt.wants.contentType) } if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { t.Errorf("%q. handlePatchView() = ***%s***", tt.name, diff) } }) } } func jsonEqual(s1, s2 string) (eq bool, diff string, err error) { var o1, o2 interface{} if s1 == s2 { return true, "", nil } if s1 == "" { return false, s2, fmt.Errorf("s1 is empty") } if s2 == "" { return false, s1, fmt.Errorf("s2 is empty") } if err = json.Unmarshal([]byte(s1), &o1); err != nil { return } if err = json.Unmarshal([]byte(s2), &o2); err != nil { return } differ := gojsondiff.New() d, err := differ.Compare([]byte(s1), []byte(s2)) if err != nil { return } config := formatter.AsciiFormatterConfig{} formatter := formatter.NewAsciiFormatter(o1, config) diff, err = formatter.Format(d) return cmp.Equal(o1, o2), diff, err } /* todo func initViewService(f influxdbtesting.ViewFields, t *testing.T) (influxdb.ViewService, func()) { t.Helper() svc := inmem.NewService() svc.IDGenerator = f.IDGenerator ctx := context.Background() for _, b := range f.Views { if err := s.PutView(ctx, b); err != nil { t.Fatalf("failed to populate Views") } } handler := NewViewHandler() handler.ViewService = svc server := httptest.NewServer(handler) client := ViewService{ Addr: server.URL, } done := server.Close return &client, done } func TestViewService(t *testing.T) { influxdbtesting.ViewService(initViewService, t) } */