diff --git a/mocks/sources.go b/mocks/sources.go new file mode 100644 index 000000000..16fac4ffa --- /dev/null +++ b/mocks/sources.go @@ -0,0 +1,43 @@ +package mocks + +import ( + "context" + + "github.com/influxdata/chronograf" +) + +var _ chronograf.SourcesStore = &SourcesStore{} + +// SourcesStore mock allows all functions to be set for testing +type SourcesStore struct { + AllF func(context.Context) ([]chronograf.Source, error) + AddF func(context.Context, chronograf.Source) (chronograf.Source, error) + DeleteF func(context.Context, chronograf.Source) error + GetF func(ctx context.Context, ID int) (chronograf.Source, error) + UpdateF func(context.Context, chronograf.Source) error +} + +// All returns all sources in the store +func (s *SourcesStore) All(ctx context.Context) ([]chronograf.Source, error) { + return s.AllF(ctx) +} + +// Add creates a new source in the SourcesStore and returns Source with ID +func (s *SourcesStore) Add(ctx context.Context, src chronograf.Source) (chronograf.Source, error) { + return s.AddF(ctx, src) +} + +// Delete the Source from the store +func (s *SourcesStore) Delete(ctx context.Context, src chronograf.Source) error { + return s.DeleteF(ctx, src) +} + +// Get retrieves Source if `ID` exists +func (s *SourcesStore) Get(ctx context.Context, ID int) (chronograf.Source, error) { + return s.GetF(ctx, ID) +} + +// Update the Source in the store. +func (s *SourcesStore) Update(ctx context.Context, src chronograf.Source) error { + return s.UpdateF(ctx, src) +} diff --git a/mocks/timeseries.go b/mocks/timeseries.go new file mode 100644 index 000000000..5d6aaea5b --- /dev/null +++ b/mocks/timeseries.go @@ -0,0 +1,41 @@ +package mocks + +import ( + "context" + + "github.com/influxdata/chronograf" +) + +var _ chronograf.TimeSeries = &TimeSeries{} + +// TimeSeries is a mockable chronograf time series by overriding the functions. +type TimeSeries struct { + // Query retrieves time series data from the database. + QueryF func(context.Context, chronograf.Query) (chronograf.Response, error) + // Connect will connect to the time series using the information in `Source`. + ConnectF func(context.Context, *chronograf.Source) error + // UsersStore represents the user accounts within the TimeSeries database + UsersF func(context.Context) chronograf.UsersStore + // Allowances returns all valid names permissions in this database + AllowancesF func(context.Context) chronograf.Allowances +} + +// Query retrieves time series data from the database. +func (t *TimeSeries) Query(ctx context.Context, query chronograf.Query) (chronograf.Response, error) { + return t.QueryF(ctx, query) +} + +// Connect will connect to the time series using the information in `Source`. +func (t *TimeSeries) Connect(ctx context.Context, src *chronograf.Source) error { + return t.ConnectF(ctx, src) +} + +// Users represents the user accounts within the TimeSeries database +func (t *TimeSeries) Users(ctx context.Context) chronograf.UsersStore { + return t.UsersF(ctx) +} + +// Allowances returns all valid names permissions in this database +func (t *TimeSeries) Allowances(ctx context.Context) chronograf.Allowances { + return t.AllowancesF(ctx) +} diff --git a/mocks/users.go b/mocks/users.go index e95317e15..78071307f 100644 --- a/mocks/users.go +++ b/mocks/users.go @@ -6,6 +6,8 @@ import ( "github.com/influxdata/chronograf" ) +var _ chronograf.UsersStore = &UsersStore{} + // UsersStore mock allows all functions to be set for testing type UsersStore struct { AllF func(context.Context) ([]chronograf.User, error) diff --git a/server/admin.go b/server/admin.go index e55d8f699..5f19a214d 100644 --- a/server/admin.go +++ b/server/admin.go @@ -40,8 +40,8 @@ type sourceUser struct { Links sourceUserLinks `json:"links"` // Links are URI locations related to user } -// NewSourceUser creates a new user in the InfluxDB data source -func NewSourceUser(srcID int, name string, perms chronograf.Permissions) sourceUser { +// newSourceUser creates a new user in the InfluxDB data source +func newSourceUser(srcID int, name string, perms chronograf.Permissions) sourceUser { u := &url.URL{Path: name} encodedUser := u.String() httpAPISrcs := "/chronograf/v1/sources" @@ -88,7 +88,7 @@ func (h *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) { return } - su := NewSourceUser(srcID, res.Name, req.Permissions) + su := newSourceUser(srcID, res.Name, req.Permissions) w.Header().Add("Location", su.Links.Self) encodeJSON(w, http.StatusCreated, su, h.Logger) } @@ -114,7 +114,7 @@ func (h *Service) SourceUsers(w http.ResponseWriter, r *http.Request) { su := []sourceUser{} for _, u := range users { - su = append(su, NewSourceUser(srcID, u.Name, u.Permissions)) + su = append(su, newSourceUser(srcID, u.Name, u.Permissions)) } res := sourceUsers{ @@ -140,7 +140,7 @@ func (h *Service) SourceUserID(w http.ResponseWriter, r *http.Request) { return } - res := NewSourceUser(srcID, u.Name, u.Permissions) + res := newSourceUser(srcID, u.Name, u.Permissions) encodeJSON(w, http.StatusOK, res, h.Logger) } @@ -192,9 +192,9 @@ func (h *Service) UpdateSourceUser(w http.ResponseWriter, r *http.Request) { return } - su := NewSourceUser(srcID, user.Name, user.Permissions) + su := newSourceUser(srcID, user.Name, user.Permissions) w.Header().Add("Location", su.Links.Self) - encodeJSON(w, http.StatusCreated, su, h.Logger) + encodeJSON(w, http.StatusOK, su, h.Logger) } func (h *Service) sourceUsersStore(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.UsersStore, error) { @@ -246,11 +246,16 @@ func (h *Service) Permissions(w http.ResponseWriter, r *http.Request) { Error(w, http.StatusBadRequest, err.Error(), h.Logger) return } - + httpAPISrcs := "/chronograf/v1/sources" res := struct { Permissions chronograf.Allowances `json:"permissions"` + Links map[string]string `json:"links"` // Links are URI locations related to user }{ Permissions: perms, + Links: map[string]string{ + "self": fmt.Sprintf("%s/%d/permissions", httpAPISrcs, srcID), + "source": fmt.Sprintf("%s/%d", httpAPISrcs, srcID), + }, } encodeJSON(w, http.StatusOK, res, h.Logger) } diff --git a/server/admin_test.go b/server/admin_test.go new file mode 100644 index 000000000..a0d56a0fe --- /dev/null +++ b/server/admin_test.go @@ -0,0 +1,797 @@ +package server_test + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bouk/httprouter" + + "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/log" + "github.com/influxdata/chronograf/mocks" + "github.com/influxdata/chronograf/server" +) + +func TestService_NewSourceUser(t *testing.T) { + type fields struct { + SourcesStore chronograf.SourcesStore + TimeSeries chronograf.TimeSeries + Logger chronograf.Logger + UseAuth bool + } + type args struct { + w *httptest.ResponseRecorder + r *http.Request + } + tests := []struct { + name string + fields fields + args args + ID string + wantStatus int + wantContentType string + wantBody string + }{ + { + name: "New user for data source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 1, + Name: "muh source", + Username: "username", + Password: "hunter2", + URL: "http://localhost:8086", + }, nil + }, + }, + TimeSeries: &mocks.TimeSeries{ + ConnectF: func(ctx context.Context, src *chronograf.Source) error { + return nil + }, + UsersF: func(ctx context.Context) chronograf.UsersStore { + return &mocks.UsersStore{ + AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { + return u, nil + }, + } + }, + }, + }, + ID: "1", + wantStatus: http.StatusCreated, + wantContentType: "application/json", + wantBody: `{"username":"marty","links":{"self":"/chronograf/v1/sources/1/users/marty"}} +`, + }, + { + name: "Error adding user", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 1, + Name: "muh source", + Username: "username", + Password: "hunter2", + URL: "http://localhost:8086", + }, nil + }, + }, + TimeSeries: &mocks.TimeSeries{ + ConnectF: func(ctx context.Context, src *chronograf.Source) error { + return nil + }, + UsersF: func(ctx context.Context) chronograf.UsersStore { + return &mocks.UsersStore{ + AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { + return nil, fmt.Errorf("Weight Has Nothing to Do With It") + }, + } + }, + }, + }, + ID: "1", + wantStatus: http.StatusBadRequest, + wantContentType: "application/json", + wantBody: `{"code":400,"message":"Weight Has Nothing to Do With It"}`, + }, + { + name: "Failure connecting to user store", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 1, + Name: "muh source", + Username: "username", + Password: "hunter2", + URL: "http://localhost:8086", + }, nil + }, + }, + TimeSeries: &mocks.TimeSeries{ + ConnectF: func(ctx context.Context, src *chronograf.Source) error { + return fmt.Errorf("Biff just happens to be my supervisor") + }, + }, + }, + ID: "1", + wantStatus: http.StatusBadRequest, + wantContentType: "application/json", + wantBody: `{"code":400,"message":"Unable to connect to source 1"}`, + }, + { + name: "Failure getting source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{}, fmt.Errorf("No McFly ever amounted to anything in the history of Hill Valley") + }, + }, + }, + ID: "1", + wantStatus: http.StatusNotFound, + wantContentType: "application/json", + wantBody: `{"code":404,"message":"ID 1 not found"}`, + }, + { + name: "Bad ID", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + }, + ID: "BAD", + wantStatus: http.StatusUnprocessableEntity, + wantContentType: "application/json", + wantBody: `{"code":422,"message":"Error converting ID BAD"}`, + }, + { + name: "Bad username", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{"password": "the_lake"}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + }, + ID: "BAD", + wantStatus: http.StatusUnprocessableEntity, + wantContentType: "application/json", + wantBody: `{"code":422,"message":"Username required"}`, + }, + { + name: "Bad JSON", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{password}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + }, + ID: "BAD", + wantStatus: http.StatusBadRequest, + wantContentType: "application/json", + wantBody: `{"code":400,"message":"Unparsable JSON"}`, + }, + } + for _, tt := range tests { + tt.args.r = tt.args.r.WithContext(httprouter.WithParams( + context.Background(), + httprouter.Params{ + { + Key: "id", + Value: tt.ID, + }, + })) + + h := &server.Service{ + SourcesStore: tt.fields.SourcesStore, + TimeSeries: tt.fields.TimeSeries, + Logger: tt.fields.Logger, + UseAuth: tt.fields.UseAuth, + } + + h.NewSourceUser(tt.args.w, tt.args.r) + + resp := tt.args.w.Result() + content := resp.Header.Get("Content-Type") + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != tt.wantStatus { + t.Errorf("%q. NewSourceUser() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) + } + if tt.wantContentType != "" && content != tt.wantContentType { + t.Errorf("%q. NewSourceUser() = %v, want %v", tt.name, content, tt.wantContentType) + } + if tt.wantBody != "" && string(body) != tt.wantBody { + t.Errorf("%q. NewSourceUser() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) + } + } +} + +func TestService_SourceUsers(t *testing.T) { + type fields struct { + SourcesStore chronograf.SourcesStore + TimeSeries chronograf.TimeSeries + Logger chronograf.Logger + UseAuth bool + } + type args struct { + w *httptest.ResponseRecorder + r *http.Request + } + tests := []struct { + name string + fields fields + args args + ID string + wantStatus int + wantContentType string + wantBody string + }{ + { + name: "All users for data source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "GET", + "http://server.local/chronograf/v1/sources/1", + nil), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 1, + Name: "muh source", + Username: "username", + Password: "hunter2", + URL: "http://localhost:8086", + }, nil + }, + }, + TimeSeries: &mocks.TimeSeries{ + ConnectF: func(ctx context.Context, src *chronograf.Source) error { + return nil + }, + UsersF: func(ctx context.Context) chronograf.UsersStore { + return &mocks.UsersStore{ + AllF: func(ctx context.Context) ([]chronograf.User, error) { + return []chronograf.User{ + { + Name: "strickland", + Passwd: "discipline", + Permissions: chronograf.Permissions{ + { + Scope: chronograf.AllScope, + Allowed: chronograf.Allowances{"READ"}, + }, + }, + }, + }, nil + }, + } + }, + }, + }, + ID: "1", + wantStatus: http.StatusOK, + wantContentType: "application/json", + wantBody: `{"users":[{"username":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"links":{"self":"/chronograf/v1/sources/1/users/strickland"}}]} +`, + }, + } + for _, tt := range tests { + tt.args.r = tt.args.r.WithContext(httprouter.WithParams( + context.Background(), + httprouter.Params{ + { + Key: "id", + Value: tt.ID, + }, + })) + h := &server.Service{ + SourcesStore: tt.fields.SourcesStore, + TimeSeries: tt.fields.TimeSeries, + Logger: tt.fields.Logger, + UseAuth: tt.fields.UseAuth, + } + + h.SourceUsers(tt.args.w, tt.args.r) + resp := tt.args.w.Result() + content := resp.Header.Get("Content-Type") + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != tt.wantStatus { + t.Errorf("%q. SourceUsers() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) + } + if tt.wantContentType != "" && content != tt.wantContentType { + t.Errorf("%q. SourceUsers() = %v, want %v", tt.name, content, tt.wantContentType) + } + if tt.wantBody != "" && string(body) != tt.wantBody { + t.Errorf("%q. SourceUsers() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) + } + } +} + +func TestService_SourceUserID(t *testing.T) { + type fields struct { + SourcesStore chronograf.SourcesStore + TimeSeries chronograf.TimeSeries + Logger chronograf.Logger + UseAuth bool + } + type args struct { + w *httptest.ResponseRecorder + r *http.Request + } + tests := []struct { + name string + fields fields + args args + ID string + UID string + wantStatus int + wantContentType string + wantBody string + }{ + { + name: "Single user for data source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "GET", + "http://server.local/chronograf/v1/sources/1", + nil), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 1, + Name: "muh source", + Username: "username", + Password: "hunter2", + URL: "http://localhost:8086", + }, nil + }, + }, + TimeSeries: &mocks.TimeSeries{ + ConnectF: func(ctx context.Context, src *chronograf.Source) error { + return nil + }, + UsersF: func(ctx context.Context) chronograf.UsersStore { + return &mocks.UsersStore{ + GetF: func(ctx context.Context, uid string) (*chronograf.User, error) { + return &chronograf.User{ + Name: "strickland", + Passwd: "discipline", + Permissions: chronograf.Permissions{ + { + Scope: chronograf.AllScope, + Allowed: chronograf.Allowances{"READ"}, + }, + }, + }, nil + }, + } + }, + }, + }, + ID: "1", + UID: "strickland", + wantStatus: http.StatusOK, + wantContentType: "application/json", + wantBody: `{"username":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"links":{"self":"/chronograf/v1/sources/1/users/strickland"}} +`, + }, + } + for _, tt := range tests { + tt.args.r = tt.args.r.WithContext(httprouter.WithParams( + context.Background(), + httprouter.Params{ + { + Key: "id", + Value: tt.ID, + }, + })) + h := &server.Service{ + SourcesStore: tt.fields.SourcesStore, + TimeSeries: tt.fields.TimeSeries, + Logger: tt.fields.Logger, + UseAuth: tt.fields.UseAuth, + } + + h.SourceUserID(tt.args.w, tt.args.r) + resp := tt.args.w.Result() + content := resp.Header.Get("Content-Type") + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != tt.wantStatus { + t.Errorf("%q. SourceUserID() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) + } + if tt.wantContentType != "" && content != tt.wantContentType { + t.Errorf("%q. SourceUserID() = %v, want %v", tt.name, content, tt.wantContentType) + } + if tt.wantBody != "" && string(body) != tt.wantBody { + t.Errorf("%q. SourceUserID() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) + } + } +} + +func TestService_RemoveSourceUser(t *testing.T) { + type fields struct { + SourcesStore chronograf.SourcesStore + TimeSeries chronograf.TimeSeries + Logger chronograf.Logger + UseAuth bool + } + type args struct { + w *httptest.ResponseRecorder + r *http.Request + } + tests := []struct { + name string + fields fields + args args + ID string + UID string + wantStatus int + wantContentType string + wantBody string + }{ + { + name: "Delete user for data source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "GET", + "http://server.local/chronograf/v1/sources/1", + nil), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 1, + Name: "muh source", + Username: "username", + Password: "hunter2", + URL: "http://localhost:8086", + }, nil + }, + }, + TimeSeries: &mocks.TimeSeries{ + ConnectF: func(ctx context.Context, src *chronograf.Source) error { + return nil + }, + UsersF: func(ctx context.Context) chronograf.UsersStore { + return &mocks.UsersStore{ + DeleteF: func(ctx context.Context, u *chronograf.User) error { + return nil + }, + } + }, + }, + }, + ID: "1", + UID: "strickland", + wantStatus: http.StatusNoContent, + }, + } + for _, tt := range tests { + tt.args.r = tt.args.r.WithContext(httprouter.WithParams( + context.Background(), + httprouter.Params{ + { + Key: "id", + Value: tt.ID, + }, + })) + h := &server.Service{ + SourcesStore: tt.fields.SourcesStore, + TimeSeries: tt.fields.TimeSeries, + Logger: tt.fields.Logger, + UseAuth: tt.fields.UseAuth, + } + h.RemoveSourceUser(tt.args.w, tt.args.r) + resp := tt.args.w.Result() + content := resp.Header.Get("Content-Type") + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != tt.wantStatus { + t.Errorf("%q. RemoveSourceUser() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) + } + if tt.wantContentType != "" && content != tt.wantContentType { + t.Errorf("%q. RemoveSourceUser() = %v, want %v", tt.name, content, tt.wantContentType) + } + if tt.wantBody != "" && string(body) != tt.wantBody { + t.Errorf("%q. RemoveSourceUser() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) + } + } +} + +func TestService_UpdateSourceUser(t *testing.T) { + type fields struct { + SourcesStore chronograf.SourcesStore + TimeSeries chronograf.TimeSeries + Logger chronograf.Logger + UseAuth bool + } + type args struct { + w *httptest.ResponseRecorder + r *http.Request + } + tests := []struct { + name string + fields fields + args args + ID string + UID string + wantStatus int + wantContentType string + wantBody string + }{ + { + name: "Update user password for data source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 1, + Name: "muh source", + Username: "username", + Password: "hunter2", + URL: "http://localhost:8086", + }, nil + }, + }, + TimeSeries: &mocks.TimeSeries{ + ConnectF: func(ctx context.Context, src *chronograf.Source) error { + return nil + }, + UsersF: func(ctx context.Context) chronograf.UsersStore { + return &mocks.UsersStore{ + UpdateF: func(ctx context.Context, u *chronograf.User) error { + return nil + }, + } + }, + }, + }, + ID: "1", + UID: "marty", + wantStatus: http.StatusOK, + wantContentType: "application/json", + wantBody: `{"username":"marty","links":{"self":"/chronograf/v1/sources/1/users/marty"}} +`, + }, + { + name: "Invalid update JSON", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{"username": "marty"}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + }, + ID: "1", + UID: "marty", + wantStatus: http.StatusUnprocessableEntity, + wantContentType: "application/json", + wantBody: `{"code":422,"message":"No fields to update"}`, + }, + } + for _, tt := range tests { + tt.args.r = tt.args.r.WithContext(httprouter.WithParams( + context.Background(), + httprouter.Params{ + { + Key: "id", + Value: tt.ID, + }, + { + Key: "uid", + Value: tt.UID, + }, + })) + h := &server.Service{ + SourcesStore: tt.fields.SourcesStore, + TimeSeries: tt.fields.TimeSeries, + Logger: tt.fields.Logger, + UseAuth: tt.fields.UseAuth, + } + h.UpdateSourceUser(tt.args.w, tt.args.r) + resp := tt.args.w.Result() + content := resp.Header.Get("Content-Type") + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != tt.wantStatus { + t.Errorf("%q. UpdateSourceUser() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) + } + if tt.wantContentType != "" && content != tt.wantContentType { + t.Errorf("%q. UpdateSourceUser() = %v, want %v", tt.name, content, tt.wantContentType) + } + if tt.wantBody != "" && string(body) != tt.wantBody { + t.Errorf("%q. UpdateSourceUser() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) + } + } +} + +func TestService_Permissions(t *testing.T) { + type fields struct { + SourcesStore chronograf.SourcesStore + TimeSeries chronograf.TimeSeries + Logger chronograf.Logger + UseAuth bool + } + type args struct { + w *httptest.ResponseRecorder + r *http.Request + } + tests := []struct { + name string + fields fields + args args + ID string + wantStatus int + wantContentType string + wantBody string + }{ + { + name: "New user for data source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "POST", + "http://server.local/chronograf/v1/sources/1", + ioutil.NopCloser( + bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + }, + fields: fields{ + UseAuth: true, + Logger: log.New(log.DebugLevel), + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 1, + Name: "muh source", + Username: "username", + Password: "hunter2", + URL: "http://localhost:8086", + }, nil + }, + }, + TimeSeries: &mocks.TimeSeries{ + ConnectF: func(ctx context.Context, src *chronograf.Source) error { + return nil + }, + AllowancesF: func(ctx context.Context) chronograf.Allowances { + return chronograf.Allowances{"READ", "WRITE"} + }, + }, + }, + ID: "1", + wantStatus: http.StatusOK, + wantContentType: "application/json", + wantBody: `{"permissions":["READ","WRITE"],"links":{"self":"/chronograf/v1/sources/1/permissions","source":"/chronograf/v1/sources/1"}} +`, + }, + } + for _, tt := range tests { + tt.args.r = tt.args.r.WithContext(httprouter.WithParams( + context.Background(), + httprouter.Params{ + { + Key: "id", + Value: tt.ID, + }, + })) + h := &server.Service{ + SourcesStore: tt.fields.SourcesStore, + TimeSeries: tt.fields.TimeSeries, + Logger: tt.fields.Logger, + UseAuth: tt.fields.UseAuth, + } + h.Permissions(tt.args.w, tt.args.r) + resp := tt.args.w.Result() + content := resp.Header.Get("Content-Type") + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != tt.wantStatus { + t.Errorf("%q. Permissions() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) + } + if tt.wantContentType != "" && content != tt.wantContentType { + t.Errorf("%q. Permissions() = %v, want %v", tt.name, content, tt.wantContentType) + } + if tt.wantBody != "" && string(body) != tt.wantBody { + t.Errorf("%q. Permissions() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) + } + } +}