diff --git a/core/admin.go b/core/admin.go index cb283156d..93a7f3469 100644 --- a/core/admin.go +++ b/core/admin.go @@ -113,6 +113,7 @@ func getAllHandlers(hoverfly *Hoverfly) []handlers.AdminHandler { &v2.StateHandler{Hoverfly: hoverfly}, &v2.DiffHandler{Hoverfly: hoverfly}, &v2.HoverflyPostServeActionDetailsHandler{Hoverfly: hoverfly}, + &v2.HoverflyTemplateDataSourceHandler{Hoverfly: hoverfly}, } return list diff --git a/core/handlers/v2/hoverfly_templatedatasource_handler.go b/core/handlers/v2/hoverfly_templatedatasource_handler.go new file mode 100644 index 000000000..6d9306179 --- /dev/null +++ b/core/handlers/v2/hoverfly_templatedatasource_handler.go @@ -0,0 +1,65 @@ +package v2 + +import ( + "encoding/json" + "github.com/SpectoLabs/hoverfly/core/handlers" + "github.com/codegangsta/negroni" + "github.com/go-zoo/bone" + "net/http" +) + +type HoverflyTemplateDataSource interface { + SetCsvDataSource(string, string) error + DeleteDataSource(string) + GetAllDataSources() TemplateDataSourceView +} + +type HoverflyTemplateDataSourceHandler struct { + Hoverfly HoverflyTemplateDataSource +} + +func (templateDataSourceHandler HoverflyTemplateDataSourceHandler) RegisterRoutes(mux *bone.Mux, am *handlers.AuthHandler) { + + mux.Get("/api/v2/hoverfly/templating-data-source/csv", negroni.New( + negroni.HandlerFunc(am.RequireTokenAuthentication), + negroni.HandlerFunc(templateDataSourceHandler.Get), + )) + + mux.Put("/api/v2/hoverfly/templating-data-source/csv", negroni.New( + negroni.HandlerFunc(am.RequireTokenAuthentication), + negroni.HandlerFunc(templateDataSourceHandler.Put), + )) + + mux.Delete("/api/v2/hoverfly/templating-data-source/csv/:dataSourceName", negroni.New( + negroni.HandlerFunc(am.RequireTokenAuthentication), + negroni.HandlerFunc(templateDataSourceHandler.Delete), + )) +} + +func (templateDataSourceHandler HoverflyTemplateDataSourceHandler) Put(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) { + var templateDataSourceRequest CSVDataSourceView + err := handlers.ReadFromRequest(req, &templateDataSourceRequest) + if err != nil { + handlers.WriteErrorResponse(rw, err.Error(), 400) + return + } + if err := templateDataSourceHandler.Hoverfly.SetCsvDataSource(templateDataSourceRequest.Name, templateDataSourceRequest.Data); err != nil { + handlers.WriteErrorResponse(rw, err.Error(), 400) + return + } + templateDataSourceHandler.Get(rw, req, next) +} + +func (templateDataSourceHandler HoverflyTemplateDataSourceHandler) Delete(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) { + dataSourceName := bone.GetValue(req, "dataSourceName") + templateDataSourceHandler.Hoverfly.DeleteDataSource(dataSourceName) + templateDataSourceHandler.Get(rw, req, next) +} + +func (templateDataSourceHandler HoverflyTemplateDataSourceHandler) Get(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + + templateDataSourceView := templateDataSourceHandler.Hoverfly.GetAllDataSources() + bytes, _ := json.Marshal(templateDataSourceView) + + handlers.WriteResponse(rw, bytes) +} diff --git a/core/handlers/v2/hoverfly_templatedatasource_handler_test.go b/core/handlers/v2/hoverfly_templatedatasource_handler_test.go new file mode 100644 index 000000000..e9fbb916d --- /dev/null +++ b/core/handlers/v2/hoverfly_templatedatasource_handler_test.go @@ -0,0 +1,80 @@ +package v2 + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + + . "github.com/onsi/gomega" +) + +var csvDataSource = CSVDataSourceView{ + "Test-CSV", + "id,name,marks\n1,Test1,55\n2,Test2,56\n", +} + +const path = "/api/v2/hoverfly/templating-data-source/csv" + +type HoverflyTemplateDataSourceStub struct{} + +func (HoverflyTemplateDataSourceStub) GetAllDataSources() TemplateDataSourceView { + + csvDataSources := []CSVDataSourceView{csvDataSource} + return TemplateDataSourceView{DataSources: csvDataSources} +} + +func (HoverflyTemplateDataSourceStub) SetCsvDataSource(string, string) error { + return nil +} + +func (HoverflyTemplateDataSourceStub) DeleteDataSource(string) { +} + +func Test_TemplateDataSourceHandler_DeleteDataSource(t *testing.T) { + RegisterTestingT(t) + + stubHoverfly := &HoverflyTemplateDataSourceStub{} + unit := HoverflyTemplateDataSourceHandler{Hoverfly: stubHoverfly} + + request, err := http.NewRequest("DELETE", path+"/test-source", nil) + Expect(err).To(BeNil()) + + response := makeRequestOnHandler(unit.Delete, request) + Expect(response.Code).To(Equal(http.StatusOK)) +} + +func Test_TemplateDataSourceHandler_SetDataSource(t *testing.T) { + RegisterTestingT(t) + + stubHoverfly := &HoverflyTemplateDataSourceStub{} + unit := HoverflyTemplateDataSourceHandler{Hoverfly: stubHoverfly} + + bodyBytes, err := json.Marshal(csvDataSource) + Expect(err).To(BeNil()) + + request, err := http.NewRequest("PUT", path, ioutil.NopCloser(bytes.NewBuffer(bodyBytes))) + Expect(err).To(BeNil()) + + response := makeRequestOnHandler(unit.Put, request) + Expect(response.Code).To(Equal(http.StatusOK)) +} + +func Test_TemplateDataSourceHandler_GetAllDataSources(t *testing.T) { + RegisterTestingT(t) + stubHoverfly := &HoverflyTemplateDataSourceStub{} + unit := HoverflyTemplateDataSourceHandler{Hoverfly: stubHoverfly} + + request, err := http.NewRequest("GET", path, nil) + Expect(err).To(BeNil()) + + response := makeRequestOnHandler(unit.Get, request) + Expect(response.Code).To(Equal(http.StatusOK)) + + responseBody := response.Body.String() + expectedResponseBodyBytes, _ := json.Marshal(TemplateDataSourceView{DataSources: []CSVDataSourceView{csvDataSource}}) + + Expect(responseBody).To(Equal(string(expectedResponseBodyBytes))) + +} diff --git a/core/handlers/v2/template_datasource_views.go b/core/handlers/v2/template_datasource_views.go new file mode 100644 index 000000000..79a8bf68c --- /dev/null +++ b/core/handlers/v2/template_datasource_views.go @@ -0,0 +1,10 @@ +package v2 + +type TemplateDataSourceView struct { + DataSources []CSVDataSourceView `json:"csvDataSources,omitempty"` +} + +type CSVDataSourceView struct { + Name string `json:"name"` + Data string `json:"data"` +} diff --git a/core/hoverfly_service.go b/core/hoverfly_service.go index 47a3218cf..ceca85242 100644 --- a/core/hoverfly_service.go +++ b/core/hoverfly_service.go @@ -525,3 +525,18 @@ func (hf *Hoverfly) SetCsvDataSource(dataSourceName, dataSourceContent string) e hf.templator.TemplateDataSource.SetDataSource(dataSourceName, dataStore) return nil } + +func (hf *Hoverfly) DeleteDataSource(dataSourceName string) { + + hf.templator.TemplateDataSource.DeleteDataSource(dataSourceName) +} + +func (hf *Hoverfly) GetAllDataSources() v2.TemplateDataSourceView { + + var csvDataSourceViews []v2.CSVDataSourceView + for _, value := range hf.templator.TemplateDataSource.GetAllDataSources() { + csvDataSource, _ := value.GetDataSourceView() + csvDataSourceViews = append(csvDataSourceViews, csvDataSource) + } + return v2.TemplateDataSourceView{DataSources: csvDataSourceViews} +} diff --git a/core/hoverfly_service_test.go b/core/hoverfly_service_test.go index e8fd1b901..85fffee7a 100644 --- a/core/hoverfly_service_test.go +++ b/core/hoverfly_service_test.go @@ -1448,3 +1448,36 @@ func TestHoverfly_SetMultipleTemplateDataSource(t *testing.T) { Expect(unit.templator.TemplateDataSource.DataSources["test-csv1"].Data[2][1]).To(Equal("Test2")) Expect(unit.templator.TemplateDataSource.DataSources["test-csv2"].Data[3][0]).To(Equal("31")) } + +func TestHoverfly_DeleteTemplateDataSource(t *testing.T) { + + RegisterTestingT(t) + + unit := NewHoverflyWithConfiguration(&Configuration{}) + + err := unit.SetCsvDataSource("test-csv1", "id,name,marks\n1,Test1,55\n2,Test2,56\n") + + Expect(err).To(BeNil()) + + unit.DeleteDataSource("test-csv1") + + Expect(err).To(BeNil()) + Expect(unit.templator.TemplateDataSource.DataSources).To(HaveLen(0)) +} + +func TestHoverfly_GetTemplateDataSources(t *testing.T) { + + RegisterTestingT(t) + + unit := NewHoverflyWithConfiguration(&Configuration{}) + content := "id,name,marks\n1,Test1,55\n2,Test2,56\n" + err := unit.SetCsvDataSource("test-csv1", content) + Expect(err).To(BeNil()) + + templateDataSourceView := unit.GetAllDataSources() + + Expect(templateDataSourceView).NotTo(BeNil()) + Expect(templateDataSourceView.DataSources).To(HaveLen(1)) + Expect(templateDataSourceView.DataSources[0].Name).To(Equal("test-csv1")) + Expect(templateDataSourceView.DataSources[0].Data).To(Equal(content)) +} diff --git a/core/templating/datasource.go b/core/templating/datasource.go index 665da1141..b5ac59f60 100644 --- a/core/templating/datasource.go +++ b/core/templating/datasource.go @@ -2,6 +2,8 @@ package templating import ( "encoding/csv" + v2 "github.com/SpectoLabs/hoverfly/core/handlers/v2" + log "github.com/sirupsen/logrus" "strings" ) @@ -21,3 +23,30 @@ func NewCsvDataSource(fileName, fileContent string) (*DataSource, error) { return &DataSource{"csv", fileName, records}, nil } + +func (dataSource DataSource) GetDataSourceView() (v2.CSVDataSourceView, error) { + + content, err := getData(dataSource) + if err != nil { + return v2.CSVDataSourceView{}, err + } + return v2.CSVDataSourceView{Name: dataSource.Name, Data: content}, nil +} + +func getData(source DataSource) (string, error) { + + var csvData strings.Builder + csvWriter := csv.NewWriter(&csvData) + for _, row := range source.Data { + if err := csvWriter.Write(row); err != nil { + log.Error("error writing csv") + return "", err + } + } + csvWriter.Flush() + if err := csvWriter.Error(); err != nil { + log.Error(err) + return "", err + } + return csvData.String(), nil +} diff --git a/core/templating/datasource_test.go b/core/templating/datasource_test.go index d67c82b08..f34b3bddf 100644 --- a/core/templating/datasource_test.go +++ b/core/templating/datasource_test.go @@ -1,6 +1,7 @@ package templating import ( + v2 "github.com/SpectoLabs/hoverfly/core/handlers/v2" . "github.com/onsi/gomega" "testing" ) @@ -27,3 +28,12 @@ func Test_NewDataSourceMethod(t *testing.T) { Expect(dataSource.Data[2][1]).To(Equal("Test2")) Expect(dataSource.Data[2][2]).To(Equal("56")) } + +func Test_GetDataSourceView(t *testing.T) { + RegisterTestingT(t) + dataSource, err := NewCsvDataSource("test-csv", "id,name,marks\n1,Test1,55\n2,Test2,56\n") + + Expect(err).To(BeNil()) + Expect(dataSource.GetDataSourceView()).To(Equal(v2.CSVDataSourceView{Name: "test-csv", Data: "id,name,marks\n1,Test1,55\n2,Test2,56\n"})) + +} diff --git a/core/templating/template_datasource.go b/core/templating/template_datasource.go index c3492cb65..8ba629155 100644 --- a/core/templating/template_datasource.go +++ b/core/templating/template_datasource.go @@ -22,3 +22,17 @@ func (templateDataSource *TemplateDataSource) SetDataSource(dataSourceName strin templateDataSource.DataSources[dataSourceName] = dataSource templateDataSource.RWMutex.Unlock() } + +func (templateDataSource *TemplateDataSource) DeleteDataSource(dataSourceName string) { + + templateDataSource.RWMutex.Lock() + if _, ok := templateDataSource.DataSources[dataSourceName]; ok { + delete(templateDataSource.DataSources, dataSourceName) + } + templateDataSource.RWMutex.Unlock() +} + +func (templateDataSource *TemplateDataSource) GetAllDataSources() map[string]*DataSource { + + return templateDataSource.DataSources +} diff --git a/docs/pages/keyconcepts/templating/templating.rst b/docs/pages/keyconcepts/templating/templating.rst index 46f80e2aa..12a02c37f 100644 --- a/docs/pages/keyconcepts/templating/templating.rst +++ b/docs/pages/keyconcepts/templating/templating.rst @@ -92,6 +92,8 @@ Additional data can come from helper methods. These are the ones Hoverfly curren +-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ | Generate random data using go-fakeit | ``{{ faker 'Name' }}`` | John Smith | +-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Query CSV data source where ID = 3 and return its name | ``{{csv 'test-csv' 'id' '3' 'name'}}`` | John Smith | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ Time offset ~~~~~~~~~~~ @@ -171,6 +173,66 @@ For example, you can generate a random name using the following expression: Fakers that require arguments are currently not supported. +Templating Data Source +~~~~~~~~~~~~~~~~~~~~~~ + +User can upload CSV data file using hoverfly/hoverctl CLI or Admin API that can be queried via templating function. + +.. code:: json + + { + "body": "{\"name\": \"{{csv '(data-source-name)' '(column-name)' '(query-value)' '(selected-column)' }}\"}" + } + +.. note:: + + Data source name is case sensitive whereas other parameters in this function are case insensitive. + Secondly, you can refer hoverfly/hoverctl options or Admin API docs in order to upload CSV data source to running hoverfly instance. + + +Example: Start hoverfly with templating CSV datasource(student-marks.csv) provided below. + +.. code:: bash + + hoverfly -templating-data-source "student-marks " + + ++-----------+-------------------+ +| ID | Name | Marks | ++-----------+-------------------+ +| 1 | Test1 | 55 | ++-----------+-------------------+ +| 2 | Test2 | 65 | ++-----------+-------------------+ +| 3 | Test3 | 98 | ++-----------+-------------------+ +| 4 | Test4 | 23 | ++-----------+-------------------+ +| 5 | Test5 | 15 | ++-----------+-------------------+ +| * | NA | 0 | ++-----------+-------------------+ + ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Description | Example | Result | ++===========================================================+===========================================================+=========================================+ +| Search where ID = 3 and return name | csv 'student-marks' 'Id' '3' 'Name' | Test3 | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Search where ID = 4 and return its marks | csv 'student-marks' 'Id' '4' 'Marks' | Test23 | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Search where Name = Test1 and return marks | csv 'student-marks' 'Name' 'Test1' 'Marks' | 55 | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Search where Id is not match and return marks | csv 'student-marks' 'Id' 'Test100' 'Marks' | 0 | +| (in this scenario, it matches wildcard * and returns) | | | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ +| Search where Id = first path param and return marks | csv 'student-marks' 'Id' 'Request.Path.[0]' 'Marks' | 15 | +| URL looks like - http://test.com/students/5/marks | | | ++-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+ + + + + + Conditional Templating, Looping and More ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/pages/reference/api/api.rst b/docs/pages/reference/api/api.rst index b1241b863..38877613d 100644 --- a/docs/pages/reference/api/api.rst +++ b/docs/pages/reference/api/api.rst @@ -989,6 +989,53 @@ DELETE /api/v2/diff """"""""""""""""""" Deletes all reports containing differences from Hoverfly. +------------------------------------------------------------------------------------------------------------- + + +GET /api/v2/hoverfly/templating-data-source/csv +""""""""""""""""""""""""""""""""""""""""""""""" + +Get all the templating data source for the running instance of Hoverfly. +It will return list of data sources that has been uploaded by the user to be queried by the template function in the response. + +**Example response body** +:: + + { + "csvDataSources": [ + { + "name": "student-marks", + "data": "id,name,marks\n1,Test1,300\n2,Test2,600\n" + } + ] + } + + +PUT /api/v2/hoverfly/templating-data-source/csv +""""""""""""""""""""""""""""""""""""""""""""""" + +Sets new template data source, overwriting the existing template data source for the running instance of Hoverfly. +The template CSV data source being set is being queried via template function to generate the response. +This API call returns back all the templating data source that have been set. + +**Example request body** +:: + + { + "name":"student-marks", + "data":"id,name,marks\n1,Test1,55\n2,Test2,56\n*,Dummy,ABSENT" + } + +DELETE /api/v2/hoverfly/templating-data-source/csv/:data-source-name +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + +Delete a particular data source for the running instance of Hoverfly. It returns all the remaining template data sources. + +------------------------------------------------------------------------------------------------------------- + + + DELETE /api/v2/shutdown """"""""""""""""""""""" Shuts down the hoverfly instance. + diff --git a/docs/pages/reference/hoverctl/hoverctl.output b/docs/pages/reference/hoverctl/hoverctl.output index 7367121d0..053715808 100644 --- a/docs/pages/reference/hoverctl/hoverctl.output +++ b/docs/pages/reference/hoverctl/hoverctl.output @@ -23,6 +23,7 @@ Available Commands: status Get the current status of Hoverfly stop Stop Hoverfly targets Get the current targets registered with hoverctl + templating-data-source Manage templating CSV data source for Hoverfly version Get the version of hoverctl Flags: diff --git a/docs/pages/reference/hoverfly/hoverfly.output b/docs/pages/reference/hoverfly/hoverfly.output index cf35c8af5..6dca3d48d 100644 --- a/docs/pages/reference/hoverfly/hoverfly.output +++ b/docs/pages/reference/hoverfly/hoverfly.output @@ -93,6 +93,10 @@ Usage of hoverfly: Start Hoverfly in spy mode, similar to simulate but calls real server when cache miss -synthesize Start Hoverfly in synthesize mode (middleware is required) + -templating-data-source + Set templating CSV data source by passing data source name and CSV data file path separated by space. + (i.e. -templating-data-source " ") + We can set multiple template CSV data source and then we can query data by using templating csv helper method -tls-verification Turn on/off tls verification for outgoing requests (will not try to verify certificates) (default true) -upstream-proxy string diff --git a/functional-tests/core/ft_templatedatasource_test.go b/functional-tests/core/ft_templatedatasource_test.go new file mode 100644 index 000000000..fffc6b7eb --- /dev/null +++ b/functional-tests/core/ft_templatedatasource_test.go @@ -0,0 +1,40 @@ +package hoverfly_test + +import ( + functional_tests "github.com/SpectoLabs/hoverfly/functional-tests" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Manage template data source in hoverfly", func() { + + var ( + hoverfly *functional_tests.Hoverfly + ) + + BeforeEach(func() { + hoverfly = functional_tests.NewHoverfly() + }) + + AfterEach(func() { + hoverfly.Stop() + }) + + Context("get template data source", func() { + + Context("hoverfly with template data source", func() { + + BeforeEach(func() { + hoverfly.Start("-templating-data-source", "test-csv testdata/test-student-data.csv") + }) + + It("Should return template data sources", func() { + templateDataSourceView := hoverfly.GetAllDataSources() + Expect(templateDataSourceView).NotTo(BeNil()) + Expect(templateDataSourceView.DataSources).To(HaveLen(1)) + Expect(templateDataSourceView.DataSources[0].Name).To(Equal("test-csv")) + Expect(templateDataSourceView.DataSources[0].Data).To(Equal("Id,Name,Marks\n1,Test1,45\n2,Test2,55\n3,Test3,67\n4,Test4,89\n*,NA,ABSENT\n")) + }) + }) + }) +}) diff --git a/functional-tests/core/testdata/test-student-data.csv b/functional-tests/core/testdata/test-student-data.csv new file mode 100644 index 000000000..a0ae12fd5 --- /dev/null +++ b/functional-tests/core/testdata/test-student-data.csv @@ -0,0 +1,6 @@ +Id,Name,Marks +1,Test1,45 +2,Test2,55 +3,Test3,67 +4,Test4,89 +*,NA,ABSENT diff --git a/functional-tests/functional_tests.go b/functional-tests/functional_tests.go index 4abce1d8e..442b0a86c 100644 --- a/functional-tests/functional_tests.go +++ b/functional-tests/functional_tests.go @@ -585,3 +585,45 @@ func TableToSliceMapStringString(table string) map[string]map[string]string { return results } + +func (this Hoverfly) GetAllDataSources() *v2.TemplateDataSourceView { + templateDataSourceView := &v2.TemplateDataSourceView{} + resp := DoRequest(sling.New().Get(fmt.Sprintf("http://localhost:%v/api/v2/hoverfly/templating-data-source/csv", this.adminPort))) + + body, err := ioutil.ReadAll(resp.Body) + Expect(err).To(BeNil()) + + err = json.Unmarshal(body, templateDataSourceView) + Expect(err).To(BeNil()) + + return templateDataSourceView +} + +func (this Hoverfly) SetTemplateDataSource(dataSourceName, content string) *v2.TemplateDataSourceView { + csvDataSource := v2.CSVDataSourceView{Data: content, Name: dataSourceName} + + resp := DoRequest(sling.New().Put(fmt.Sprintf("http://localhost:%v/api/v2/hoverfly/templating-data-source/csv", this.adminPort)).BodyJSON(csvDataSource)) + + templateDataSourceView := &v2.TemplateDataSourceView{} + body, err := ioutil.ReadAll(resp.Body) + Expect(err).To(BeNil()) + + err = json.Unmarshal(body, templateDataSourceView) + Expect(err).To(BeNil()) + + return templateDataSourceView +} + +func (this Hoverfly) DeleteDataSource(name string) *v2.TemplateDataSourceView { + + resp := DoRequest(sling.New().Delete(fmt.Sprintf("http://localhost:%v/api/v2/hoverfly/templating-data-source/csv/%v", this.adminPort, name))) + + templateDataSourceView := &v2.TemplateDataSourceView{} + body, err := ioutil.ReadAll(resp.Body) + Expect(err).To(BeNil()) + + err = json.Unmarshal(body, templateDataSourceView) + Expect(err).To(BeNil()) + + return templateDataSourceView +} diff --git a/functional-tests/hoverctl/sandbox-1769533177/testdata/test-student-data.csv b/functional-tests/hoverctl/sandbox-1769533177/testdata/test-student-data.csv new file mode 100644 index 000000000..fcf0aa580 --- /dev/null +++ b/functional-tests/hoverctl/sandbox-1769533177/testdata/test-student-data.csv @@ -0,0 +1,6 @@ +Id,Name,Marks +1,Test1,45 +2,Test2,55 +3,Test3,67 +4,Test4,89 +*,NA,ABSENT \ No newline at end of file diff --git a/functional-tests/hoverctl/templatedatasource_test.go b/functional-tests/hoverctl/templatedatasource_test.go new file mode 100644 index 000000000..c10d0857c --- /dev/null +++ b/functional-tests/hoverctl/templatedatasource_test.go @@ -0,0 +1,94 @@ +package hoverctl_suite + +import ( + functional_tests "github.com/SpectoLabs/hoverfly/functional-tests" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("When I use hoverctl", func() { + + var ( + hoverfly *functional_tests.Hoverfly + ) + + Describe("set template-data-source", func() { + + BeforeEach(func() { + hoverfly = functional_tests.NewHoverfly() + hoverfly.Start() + functional_tests.Run(hoverctlBinary, "targets", "update", "local", "--admin-port", hoverfly.GetAdminPort()) + }) + + AfterEach(func() { + hoverfly.Stop() + }) + + It("should return success on setting template-data-source", func() { + output := functional_tests.Run(hoverctlBinary, "templating-data-source", "set", "--name", "test-csv", "--filePath", "testdata/test-student-data.csv") + + Expect(output).To(ContainSubstring("Success")) + }) + + It("should override existing template-data-source", func() { + output1 := functional_tests.Run(hoverctlBinary, "templating-data-source", "set", "--name", "test-csv", "--filePath", "testdata/test-student-data.csv") + + Expect(output1).To(ContainSubstring("Success")) + + output2 := functional_tests.Run(hoverctlBinary, "templating-data-source", "set", "--name", "test-csv", "--filePath", "testdata/test-student-data1.csv") + Expect(output2).To(ContainSubstring("Success")) + + output3 := functional_tests.Run(hoverctlBinary, "templating-data-source", "get-all") + Expect(output3).To(ContainSubstring("test-csv")) + Expect(output3).To(ContainSubstring("1,Test1,20")) + + }) + + }) + + Describe("delete template-data-source", func() { + + BeforeEach(func() { + hoverfly = functional_tests.NewHoverfly() + hoverfly.Start() + functional_tests.Run(hoverctlBinary, "targets", "update", "local", "--admin-port", hoverfly.GetAdminPort()) + }) + + AfterEach(func() { + hoverfly.Stop() + }) + + It("should return success on deleting template-data-source", func() { + output := functional_tests.Run(hoverctlBinary, "templating-data-source", "set", "--name", "test-csv", "--filePath", "testdata/test-student-data.csv") + Expect(output).To(ContainSubstring("Success")) + output = functional_tests.Run(hoverctlBinary, "templating-data-source", "delete", "--name", "test-csv") + Expect(output).To(ContainSubstring("Success")) + }) + + }) + + Describe("get templating-data-source", func() { + + BeforeEach(func() { + hoverfly = functional_tests.NewHoverfly() + hoverfly.Start() + functional_tests.Run(hoverctlBinary, "targets", "update", "local", "--admin-port", hoverfly.GetAdminPort()) + }) + + AfterEach(func() { + hoverfly.Stop() + }) + + It("should return template data source", func() { + output := functional_tests.Run(hoverctlBinary, "templating-data-source", "set", "--name", "test-csv", "--filePath", "testdata/test-student-data.csv") + + Expect(output).To(ContainSubstring("Success")) + + output = functional_tests.Run(hoverctlBinary, "templating-data-source", "get-all") + Expect(output).To(ContainSubstring("test-csv")) + Expect(output).To(ContainSubstring("test-csv")) + Expect(output).To(ContainSubstring("1,Test1,45")) + }) + + }) +}) diff --git a/functional-tests/hoverctl/testdata/test-student-data.csv b/functional-tests/hoverctl/testdata/test-student-data.csv new file mode 100644 index 000000000..fcf0aa580 --- /dev/null +++ b/functional-tests/hoverctl/testdata/test-student-data.csv @@ -0,0 +1,6 @@ +Id,Name,Marks +1,Test1,45 +2,Test2,55 +3,Test3,67 +4,Test4,89 +*,NA,ABSENT \ No newline at end of file diff --git a/functional-tests/hoverctl/testdata/test-student-data1.csv b/functional-tests/hoverctl/testdata/test-student-data1.csv new file mode 100644 index 000000000..9aebcc94a --- /dev/null +++ b/functional-tests/hoverctl/testdata/test-student-data1.csv @@ -0,0 +1,3 @@ +Id,Name,Marks +1,Test1,20 +*,NA,ABSENT \ No newline at end of file diff --git a/hoverctl/cmd/postserveaction.go b/hoverctl/cmd/postserveaction.go index 00d292ae8..d243bec71 100644 --- a/hoverctl/cmd/postserveaction.go +++ b/hoverctl/cmd/postserveaction.go @@ -96,7 +96,7 @@ func getPostServeActionsTabularData(postServeActions v2.PostServeActionDetailsVi postServeActionsData := [][]string{{"Action Name", "Binary", "Script", "Delay(Ms)"}} for _, action := range postServeActions.Actions { - actionData := []string{action.ActionName, action.Binary, getScriptShorthand(action.ScriptContent), fmt.Sprint(action.DelayInMs)} + actionData := []string{action.ActionName, action.Binary, getContentShorthand(action.ScriptContent), fmt.Sprint(action.DelayInMs)} postServeActionsData = append(postServeActionsData, actionData) } return postServeActionsData diff --git a/hoverctl/cmd/templatedatasource.go b/hoverctl/cmd/templatedatasource.go new file mode 100644 index 000000000..5f2ecd16c --- /dev/null +++ b/hoverctl/cmd/templatedatasource.go @@ -0,0 +1,98 @@ +package cmd + +import ( + "fmt" + + v2 "github.com/SpectoLabs/hoverfly/core/handlers/v2" + "github.com/SpectoLabs/hoverfly/hoverctl/configuration" + "github.com/SpectoLabs/hoverfly/hoverctl/wrapper" + "github.com/spf13/cobra" +) + +var name, filePath string + +var templateCsvDataSourceCommand = &cobra.Command{ + Use: "templating-data-source", + Short: "Manage the templating data source for Hoverfly", + Long: ` + This allows you to manage templating data source for Hoverfly. Only CSV datasource is supported as of now + `, +} + +var templateCsvDataSourceGetCommand = &cobra.Command{ + Use: "get-all", + Short: "Get all templating data source for Hoverfly", + Long: `Get all templating data source for Hoverfly`, + Run: func(cmd *cobra.Command, args []string) { + checkTargetAndExit(target) + + if len(args) == 0 { + templatingDataSourceView, err := wrapper.GetAllTemplateDataSources(*target) + handleIfError(err) + drawTable(getTemplatingDataSourceTabularData(templatingDataSourceView), true) + } + }, +} + +var templateCsvDataSourceSetCommand = &cobra.Command{ + Use: "set", + Short: "Set csv templating data source for Hoverfly", + Long: ` +Hoverfly Templating CSV DataSource can be set using the following flags: + --name --filePath +`, + Run: func(cmd *cobra.Command, args []string) { + checkTargetAndExit(target) + if name == "" || filePath == "" { + fmt.Println("data source name and file path are compulsory to set csv templating data source") + } else { + data, err := configuration.ReadFile(filePath) + handleIfError(err) + err = wrapper.SetCsvTemplateDataSource(name, string(data), *target) + handleIfError(err) + fmt.Println("Success") + } + }, +} + +var templateCsvDataSourceDeleteCommand = &cobra.Command{ + Use: "delete", + Short: "Delete csv templating data source for Hoverfly", + Long: ` +Hoverfly CSV templating datasource can be deleted using the following flags: + --name +`, + Run: func(cmd *cobra.Command, args []string) { + checkTargetAndExit(target) + if name == "" { + fmt.Println("csv datasource name to be deleted not provided") + } else { + err := wrapper.DeleteCsvDataSource(name, *target) + handleIfError(err) + fmt.Println("Success") + } + }, +} + +func init() { + RootCmd.AddCommand(templateCsvDataSourceCommand) + templateCsvDataSourceCommand.AddCommand(templateCsvDataSourceGetCommand) + templateCsvDataSourceCommand.AddCommand(templateCsvDataSourceSetCommand) + templateCsvDataSourceCommand.AddCommand(templateCsvDataSourceDeleteCommand) + + templateCsvDataSourceSetCommand.PersistentFlags().StringVar(&name, "name", "", "Datasource Name to be set") + templateCsvDataSourceSetCommand.PersistentFlags().StringVar(&filePath, "filePath", "", + "An absolute or relative path to a csv file that Hoverfly will use for templating") + templateCsvDataSourceDeleteCommand.PersistentFlags().StringVar(&name, "name", "", "Datasource Name to be set") + +} + +func getTemplatingDataSourceTabularData(templatingDataSourceView v2.TemplateDataSourceView) [][]string { + + templateDataSourceDetails := [][]string{{"CSV DataSource Name", "Content"}} + for _, csvDataSourceView := range templatingDataSourceView.DataSources { + csvDataSource := []string{csvDataSourceView.Name, getContentShorthand(csvDataSourceView.Data)} + templateDataSourceDetails = append(templateDataSourceDetails, csvDataSource) + } + return templateDataSourceDetails +} diff --git a/hoverctl/cmd/utils.go b/hoverctl/cmd/utils.go index a66dc9532..f41c2821a 100644 --- a/hoverctl/cmd/utils.go +++ b/hoverctl/cmd/utils.go @@ -87,11 +87,11 @@ func drawTable(data [][]string, header bool) { table.Render() } -func getScriptShorthand(script string) string { - if script != "" { - scriptArr := strings.Split(script, "\n") +func getContentShorthand(content string) string { + if content != "" { + scriptArr := strings.Split(content, "\n") if verbose || len(scriptArr) < 5 { - return script + return content } else { return fmt.Sprintln(scriptArr[0], scriptArr[1], scriptArr[2], scriptArr[3], scriptArr[4], "...") } diff --git a/hoverctl/wrapper/hoverfly.go b/hoverctl/wrapper/hoverfly.go index 631cdae19..60451bcf3 100644 --- a/hoverctl/wrapper/hoverfly.go +++ b/hoverctl/wrapper/hoverfly.go @@ -26,6 +26,7 @@ const ( v2ApiState = "/api/v2/state" v2ApiMiddleware = "/api/v2/hoverfly/middleware" v2ApiPostServeAction = "/api/v2/hoverfly/post-serve-action" + v2ApiTemplateDataSourceAction = "/api/v2/hoverfly/templating-data-source/csv" v2ApiPac = "/api/v2/hoverfly/pac" v2ApiCache = "/api/v2/cache" v2ApiLogs = "/api/v2/logs" diff --git a/hoverctl/wrapper/templatedatasource.go b/hoverctl/wrapper/templatedatasource.go new file mode 100644 index 000000000..c1007605b --- /dev/null +++ b/hoverctl/wrapper/templatedatasource.go @@ -0,0 +1,72 @@ +package wrapper + +import ( + "encoding/json" + v2 "github.com/SpectoLabs/hoverfly/core/handlers/v2" + "github.com/SpectoLabs/hoverfly/hoverctl/configuration" +) + +func GetAllTemplateDataSources(target configuration.Target) (v2.TemplateDataSourceView, error) { + + response, err := doRequest(target, "GET", v2ApiTemplateDataSourceAction, "", nil) + if err != nil { + return v2.TemplateDataSourceView{}, err + } + + defer response.Body.Close() + + err = handleResponseError(response, "Could not retrieve all template data sources") + if err != nil { + return v2.TemplateDataSourceView{}, err + } + + var templateDataSourceView v2.TemplateDataSourceView + + err = UnmarshalToInterface(response, &templateDataSourceView) + if err != nil { + return v2.TemplateDataSourceView{}, err + } + + return templateDataSourceView, nil +} + +func SetCsvTemplateDataSource(dataSourceName, scriptContent string, target configuration.Target) error { + + csvDataSource := v2.CSVDataSourceView{ + Data: scriptContent, + Name: dataSourceName, + } + marshalledCsvDataSource, err := json.Marshal(csvDataSource) + if err != nil { + return err + } + response, err := doRequest(target, "PUT", v2ApiTemplateDataSourceAction, string(marshalledCsvDataSource), nil) + if err != nil { + return err + } + + defer response.Body.Close() + + err = handleResponseError(response, "Could not set csv data source") + if err != nil { + return err + } + return nil +} + +func DeleteCsvDataSource(dataSourceName string, target configuration.Target) error { + + response, err := doRequest(target, "DELETE", v2ApiTemplateDataSourceAction+"/"+dataSourceName, "", nil) + if err != nil { + return err + } + + defer response.Body.Close() + + err = handleResponseError(response, "Could not delete data source") + if err != nil { + return err + } + + return nil +}