Skip to content

Commit

Permalink
feature - csv templating function - milestone 2
Browse files Browse the repository at this point in the history
  • Loading branch information
kapishmalik authored and tommysitu committed Feb 26, 2024
1 parent 2b2fdc9 commit 3a86e43
Show file tree
Hide file tree
Showing 25 changed files with 744 additions and 5 deletions.
1 change: 1 addition & 0 deletions core/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions core/handlers/v2/hoverfly_templatedatasource_handler.go
Original file line number Diff line number Diff line change
@@ -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)
}
80 changes: 80 additions & 0 deletions core/handlers/v2/hoverfly_templatedatasource_handler_test.go
Original file line number Diff line number Diff line change
@@ -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)))

}
10 changes: 10 additions & 0 deletions core/handlers/v2/template_datasource_views.go
Original file line number Diff line number Diff line change
@@ -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"`
}
15 changes: 15 additions & 0 deletions core/hoverfly_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
33 changes: 33 additions & 0 deletions core/hoverfly_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
29 changes: 29 additions & 0 deletions core/templating/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package templating

import (
"encoding/csv"
v2 "github.com/SpectoLabs/hoverfly/core/handlers/v2"
log "github.com/sirupsen/logrus"
"strings"
)

Expand All @@ -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
}
10 changes: 10 additions & 0 deletions core/templating/datasource_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package templating

import (
v2 "github.com/SpectoLabs/hoverfly/core/handlers/v2"
. "github.com/onsi/gomega"
"testing"
)
Expand All @@ -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"}))

}
14 changes: 14 additions & 0 deletions core/templating/template_datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
62 changes: 62 additions & 0 deletions docs/pages/keyconcepts/templating/templating.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~
Expand Down Expand Up @@ -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 <path to below CSV file>"
+-----------+-------------------+
| 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Loading

0 comments on commit 3a86e43

Please sign in to comment.