diff --git a/mockns1/datasets.go b/mockns1/datasets.go new file mode 100644 index 0000000..260d9a4 --- /dev/null +++ b/mockns1/datasets.go @@ -0,0 +1,61 @@ +package mockns1 + +import ( + "net/http" + + "gopkg.in/ns1/ns1-go.v2/rest/model/dataset" +) + +// AddDatasetListTestCase sets up a test case for the api.Client.Datasets.List() function +func (s *Service) AddDatasetListTestCase( + requestHeaders, responseHeaders http.Header, + response []*dataset.Dataset, +) error { + return s.AddTestCase( + http.MethodGet, "/datasets", http.StatusOK, requestHeaders, + responseHeaders, "", response, + ) +} + +// AddDatasetGetTestCase sets up a test case for the api.Client.Datasets.Get() function +func (s *Service) AddDatasetGetTestCase( + id string, + requestHeaders, responseHeaders http.Header, + response *dataset.Dataset, +) error { + return s.AddTestCase( + http.MethodGet, "/datasets/"+id, http.StatusOK, requestHeaders, + responseHeaders, "", response, + ) +} + +// AddDatasetCreateTestCase sets up a test case for the api.Client.Datasets.Create() function +func (s *Service) AddDatasetCreateTestCase( + requestHeaders, responseHeaders http.Header, + request, response *dataset.Dataset, +) error { + return s.AddTestCase( + http.MethodPut, "/datasets", http.StatusCreated, requestHeaders, + responseHeaders, request, response, + ) +} + +// AddDatasetDeleteTestCase sets up a test case for the api.Client.Datasets.Delete() function +func (s *Service) AddDatasetDeleteTestCase( + id string, requestHeaders, responseHeaders http.Header, +) error { + return s.AddTestCase( + http.MethodDelete, "/datasets/"+id, http.StatusNoContent, requestHeaders, + responseHeaders, "", "", + ) +} + +// AddDatasetGetReportTestCase sets up a test case for the api.Client.Datasets.GetReport() function +func (s *Service) AddDatasetGetReportTestCase( + id string, reportId string, requestHeaders, responseHeaders http.Header, fileContents []byte, +) error { + return s.AddTestCase( + http.MethodGet, "/datasets/"+id+"/reports/"+reportId, http.StatusOK, requestHeaders, + responseHeaders, "", fileContents, + ) +} diff --git a/rest/_examples/datasets.go b/rest/_examples/datasets.go new file mode 100644 index 0000000..6e20280 --- /dev/null +++ b/rest/_examples/datasets.go @@ -0,0 +1,117 @@ +package main + +import ( + "encoding/json" + "fmt" + "gopkg.in/ns1/ns1-go.v2/rest/model/dataset" + "io" + "log" + "net/http" + "os" + "strings" + "time" + + api "gopkg.in/ns1/ns1-go.v2/rest" +) + +var client *api.Client + +// Helper that initializes rest api client from environment variable. +func init() { + k := os.Getenv("NS1_APIKEY") + if k == "" { + fmt.Println("NS1_APIKEY environment variable is not set, giving up") + } + + httpClient := &http.Client{Timeout: time.Second * 10} + // Adds logging to each http request. + doer := api.Decorate(httpClient, api.Logging(log.New(os.Stdout, "", log.LstdFlags))) + client = api.NewClient(doer, api.SetAPIKey(k)) +} + +func main() { + dt, _, err := client.Datasets.Create(&dataset.Dataset{ + Name: "My dataset", + Datatype: &dataset.Datatype{ + Type: dataset.DatatypeTypeNumQueries, + Scope: dataset.DatatypeScopeAccount, + Data: nil, + }, + Repeat: nil, + Timeframe: &dataset.Timeframe{ + Aggregation: dataset.TimeframeAggregationMontly, + Cycles: func() *int32 { i := int32(1); return &i }(), + }, + ExportType: dataset.ExportTypeCSV, + RecipientEmails: nil, + }) + if err != nil { + log.Fatal(err) + } + + b, _ := json.MarshalIndent(dt, "", " ") + fmt.Println(string(b)) + + for { + fmt.Println("waiting for report to be generated...") + + dt, _, err = client.Datasets.Get(dt.ID) + if err != nil { + log.Fatal(err) + } + if len(dt.Reports) == 0 { + time.Sleep(time.Second * 5) + continue + } + + dtStatus := dt.Reports[0].Status + + if dtStatus == dataset.ReportStatusGenerating || dtStatus == dataset.ReportStatusQueued { + time.Sleep(time.Second * 5) + continue + } + + if dtStatus == dataset.ReportStatusFailed { + log.Fatal(fmt.Errorf("dataset[%s] report[%s] failed to generate", dt.ID, dt.Reports[0].ID)) + } + + // report generated + break + } + + reportBuf, reportResp, err := client.Datasets.GetReport(dt.ID, dt.Reports[0].ID) + if err != nil { + log.Fatal(err) + } + + var fileName string + contentDisposition := reportResp.Header.Get("Content-Disposition") + params := strings.Split(contentDisposition, ";") + for _, param := range params { + param = strings.TrimSpace(param) + if strings.HasPrefix(param, "filename=") { + fileName = strings.TrimPrefix(param, "filename=") + fileName = strings.TrimSpace(fileName) + fileName = strings.Trim(fileName, "\"") + break + } + } + + cwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + file, err := os.Create(fmt.Sprintf("%s/%s", cwd, fileName)) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + _, err = io.Copy(file, reportBuf) + if err != nil { + log.Fatal(err) + } + + fmt.Println("dataset report saved to", fileName) +} diff --git a/rest/client.go b/rest/client.go index 005a2af..c3db840 100644 --- a/rest/client.go +++ b/rest/client.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "net/url" @@ -88,6 +89,7 @@ type Client struct { View *DNSViewService Network *NetworkService GlobalIPWhitelist *GlobalIPWhitelistService + Datasets *DatasetsService } // NewClient constructs and returns a reference to an instantiated Client. @@ -134,6 +136,7 @@ func NewClient(httpClient Doer, options ...func(*Client)) *Client { c.View = (*DNSViewService)(&c.common) c.Network = (*NetworkService)(&c.common) c.GlobalIPWhitelist = (*GlobalIPWhitelistService)(&c.common) + c.Datasets = (*DatasetsService)(&c.common) for _, option := range options { option(c) @@ -199,6 +202,14 @@ func (c Client) Do(req *http.Request, v interface{}) (*http.Response, error) { } if v != nil { + // For non-JSON responses, the desired destination might be a bytes buffer + if buf, ok := v.(*bytes.Buffer); ok { + if _, err := io.Copy(buf, resp.Body); err != nil { + return nil, err + } + return resp, err + } + // Try to unmarshal body into given type using streaming decoder. if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { return nil, err diff --git a/rest/dataset.go b/rest/dataset.go new file mode 100644 index 0000000..4479230 --- /dev/null +++ b/rest/dataset.go @@ -0,0 +1,129 @@ +package rest + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "strings" + + "gopkg.in/ns1/ns1-go.v2/rest/model/dataset" +) + +var ( + // ErrDatasetNotFound bundles GET/POST/DELETE not found errors. + ErrDatasetNotFound = errors.New("dataset not found") +) + +// DatasetsService handles 'datasets' endpoint. +type DatasetsService service + +// List returns the configured datasets. +// +// NS1 API docs: https://developer.ibm.com/apis/catalog/ns1--ibm-ns1-connect-api/api/API--ns1--ibm-ns1-connect-api#listDataset +func (s *DatasetsService) List() ([]*dataset.Dataset, *http.Response, error) { + req, err := s.client.NewRequest("GET", "datasets", nil) + if err != nil { + return nil, nil, err + } + + dts := make([]*dataset.Dataset, 0) + resp, err := s.client.Do(req, &dts) + + return dts, resp, err +} + +// Get takes a dataset id and returns all its data. +// +// NS1 API docs: https://developer.ibm.com/apis/catalog/ns1--ibm-ns1-connect-api/api/API--ns1--ibm-ns1-connect-api#getDataset +func (s *DatasetsService) Get(dtID string) (*dataset.Dataset, *http.Response, error) { + path := fmt.Sprintf("datasets/%s", dtID) + + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + var dt dataset.Dataset + resp, err := s.client.Do(req, &dt) + if err != nil { + var clientErr *Error + switch { + case errors.As(err, &clientErr): + if strings.HasSuffix(clientErr.Message, " not found") { + return nil, resp, ErrDatasetNotFound + } + } + return nil, resp, err + } + + return &dt, resp, nil +} + +// Create takes a *Dataset and creates a new dataset. +// +// NS1 API docs: https://developer.ibm.com/apis/catalog/ns1--ibm-ns1-connect-api/api/API--ns1--ibm-ns1-connect-api#createDataset +func (s *DatasetsService) Create(dt *dataset.Dataset) (*dataset.Dataset, *http.Response, error) { + req, err := s.client.NewRequest("PUT", "datasets", dt) + if err != nil { + return nil, nil, err + } + + resp, err := s.client.Do(req, dt) + return dt, resp, err +} + +// Delete takes a dataset id and deletes it. +// +// NS1 API docs: https://developer.ibm.com/apis/catalog/ns1--ibm-ns1-connect-api/api/API--ns1--ibm-ns1-connect-api#deleteDataset +func (s *DatasetsService) Delete(dtID string) (*http.Response, error) { + path := fmt.Sprintf("datasets/%s", dtID) + + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + var clientErr *Error + switch { + case errors.As(err, &clientErr): + if strings.HasSuffix(clientErr.Message, " not found") { + return resp, ErrDatasetNotFound + } + } + return resp, err + } + + return resp, nil +} + +// GetReport takes a dataset id and a report id and returns bytes.Buffer which contains the file contents +// Additionally, file name can be grabbed from the 'Content-Disposition' header in the http.Response +// +// NS1 API docs: https://developer.ibm.com/apis/catalog/ns1--ibm-ns1-connect-api/api/API--ns1--ibm-ns1-connect-api#getDatasetReport +func (s *DatasetsService) GetReport(dtID string, reportID string) (*bytes.Buffer, *http.Response, error) { + path := fmt.Sprintf("datasets/%s/reports/%s", dtID, reportID) + + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + var buf bytes.Buffer + + resp, err := s.client.Do(req, &buf) + if err != nil { + var clientErr *Error + switch { + case errors.As(err, &clientErr): + if strings.HasSuffix(clientErr.Message, " not found") { + return nil, resp, ErrDatasetNotFound + } + } + return nil, resp, err + } + + return &buf, resp, nil +} diff --git a/rest/dataset_test.go b/rest/dataset_test.go new file mode 100644 index 0000000..520c8c2 --- /dev/null +++ b/rest/dataset_test.go @@ -0,0 +1,224 @@ +package rest_test + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/ns1/ns1-go.v2/mockns1" + api "gopkg.in/ns1/ns1-go.v2/rest" + "gopkg.in/ns1/ns1-go.v2/rest/model/dataset" + "net/http" + "testing" +) + +func TestDatasetsService(t *testing.T) { + mock, doer, err := mockns1.New(t) + require.Nil(t, err) + defer mock.Shutdown() + + client := api.NewClient(doer, api.SetEndpoint("https://"+mock.Address+"/v1/")) + + id := "87461586-c681-43b7-b283-f840be95e13c" + + t.Run("List", func(t *testing.T) { + t.Run("Success", func(t *testing.T) { + defer mock.ClearTestCases() + + dtList := []*dataset.Dataset{ + { + ID: "dt-1", + Name: "My dataset 1", + Datatype: &dataset.Datatype{ + Type: dataset.DatatypeTypeNumQueries, + Scope: dataset.DatatypeScopeAccount, + Data: nil, + }, + }, + { + ID: "dt-2", + Name: "My dataset 2", + Datatype: &dataset.Datatype{ + Type: dataset.DatatypeTypeNumQueries, + Scope: dataset.DatatypeScopeAccount, + Data: nil, + }, + }, + } + + require.Nil(t, mock.AddDatasetListTestCase(nil, nil, dtList)) + + respDts, _, err := client.Datasets.List() + require.Nil(t, err) + require.NotNil(t, respDts) + require.Equal(t, len(dtList), len(respDts)) + + for i := range dtList { + require.Equal(t, dtList[i].ID, respDts[i].ID, i) + } + }) + + t.Run("Error", func(t *testing.T) { + t.Run("HTTP", func(t *testing.T) { + defer mock.ClearTestCases() + + require.Nil(t, mock.AddTestCase( + http.MethodGet, "/datasets", http.StatusNotFound, + nil, nil, "", `{"message": "test error"}`, + )) + + _, resp, err := client.Datasets.List() + require.NotNil(t, err) + require.Contains(t, err.Error(), "test error") + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("Other", func(t *testing.T) { + c := api.NewClient(errorClient{}, api.SetEndpoint("")) + _, resp, err := c.Datasets.List() + require.Nil(t, resp) + require.Error(t, err) + }) + }) + }) + + t.Run("Get", func(t *testing.T) { + t.Run("Success", func(t *testing.T) { + defer mock.ClearTestCases() + + client.FollowPagination = true + dt := &dataset.Dataset{ + Name: "My dataset", + Datatype: &dataset.Datatype{ + Type: dataset.DatatypeTypeNumQueries, + Scope: dataset.DatatypeScopeAccount, + Data: nil, + }, + Repeat: nil, + Timeframe: &dataset.Timeframe{ + Aggregation: dataset.TimeframeAggregationMontly, + Cycles: func() *int32 { i := int32(1); return &i }(), + }, + ExportType: dataset.ExportTypeCSV, + RecipientEmails: nil, + } + require.Nil(t, mock.AddDatasetGetTestCase(id, nil, nil, dt)) + + respDt, _, err := client.Datasets.Get(id) + require.Nil(t, err) + require.NotNil(t, respDt) + require.Equal(t, dt.ID, respDt.ID) + }) + + t.Run("Error", func(t *testing.T) { + t.Run("HTTP", func(t *testing.T) { + defer mock.ClearTestCases() + + require.Nil(t, mock.AddTestCase( + http.MethodGet, "/datasets/"+id, http.StatusNotFound, + nil, nil, "", `{"message": "test error"}`, + )) + + dt, resp, err := client.Datasets.Get(id) + require.Nil(t, dt) + require.NotNil(t, err) + require.Contains(t, err.Error(), "test error") + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("Other", func(t *testing.T) { + c := api.NewClient(errorClient{}, api.SetEndpoint("")) + dt, resp, err := c.Datasets.Get(id) + require.Nil(t, resp) + require.Error(t, err) + require.Nil(t, dt) + }) + }) + }) + + t.Run("Create", func(t *testing.T) { + dt := &dataset.Dataset{ + Name: "My dataset", + Datatype: &dataset.Datatype{ + Type: dataset.DatatypeTypeNumQueries, + Scope: dataset.DatatypeScopeAccount, + Data: nil, + }, + Repeat: nil, + Timeframe: &dataset.Timeframe{ + Aggregation: dataset.TimeframeAggregationMontly, + Cycles: func() *int32 { i := int32(1); return &i }(), + }, + ExportType: dataset.ExportTypeCSV, + RecipientEmails: nil, + } + + t.Run("Success", func(t *testing.T) { + defer mock.ClearTestCases() + + require.Nil(t, mock.AddDatasetCreateTestCase(nil, nil, dt, dt)) + + _, _, err := client.Datasets.Create(dt) + require.Nil(t, err) + }) + + t.Run("Error", func(t *testing.T) { + defer mock.ClearTestCases() + + require.Nil(t, mock.AddTestCase( + http.MethodPut, "/datasets", http.StatusConflict, + nil, nil, dt, `{"message": "invalid parameters"}`, + )) + + _, _, err := client.Datasets.Create(dt) + require.Contains(t, err.Error(), "invalid parameters") + }) + }) + + t.Run("Delete", func(t *testing.T) { + t.Run("Success", func(t *testing.T) { + defer mock.ClearTestCases() + + require.Nil(t, mock.AddDatasetDeleteTestCase(id, nil, nil)) + + _, err := client.Datasets.Delete(id) + require.Nil(t, err) + }) + + t.Run("Error", func(t *testing.T) { + defer mock.ClearTestCases() + + require.Nil(t, mock.AddTestCase( + http.MethodDelete, "/datasets/"+id, http.StatusNotFound, + nil, nil, "", `{"message": "dataset not found"}`, + )) + + _, err := client.Datasets.Delete(id) + require.Equal(t, api.ErrDatasetNotFound, err) + }) + }) + + t.Run("Get Report", func(t *testing.T) { + reportId := "f840be95e13c" + + t.Run("Success", func(t *testing.T) { + defer mock.ClearTestCases() + fileContents := []byte(`foo,bar`) + require.Nil(t, mock.AddDatasetGetReportTestCase(id, reportId, nil, nil, fileContents)) + + buf, _, err := client.Datasets.GetReport(id, reportId) + require.Nil(t, err) + assert.Equal(t, fileContents, buf.Bytes()) + }) + + t.Run("Error", func(t *testing.T) { + defer mock.ClearTestCases() + + require.Nil(t, mock.AddTestCase( + http.MethodGet, "/datasets/"+id+"/reports/"+reportId, http.StatusNotFound, + nil, nil, "", `{"message": "dataset not found"}`, + )) + + _, _, err := client.Datasets.GetReport(id, reportId) + require.Equal(t, api.ErrDatasetNotFound, err) + }) + }) +} diff --git a/rest/model/dataset/dataset.go b/rest/model/dataset/dataset.go new file mode 100644 index 0000000..5178291 --- /dev/null +++ b/rest/model/dataset/dataset.go @@ -0,0 +1,214 @@ +package dataset + +import ( + "bytes" + "encoding/json" + "time" +) + +// ExportType is a string enum +type ExportType string + +const ( + ExportTypeCSV = ExportType("csv") + ExportTypeJSON = ExportType("json") + ExportTypeXLSX = ExportType("xlsx") +) + +// Dataset wraps an NS1 /datasets resource +type Dataset struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Datatype *Datatype `json:"datatype,omitempty"` + Repeat *Repeat `json:"repeat,omitempty"` + Timeframe *Timeframe `json:"timeframe,omitempty"` + ExportType ExportType `json:"export_type,omitempty"` + Reports []*Report `json:"reports,omitempty"` + RecipientEmails []string `json:"recipient_emails,omitempty"` + CreatedAt UnixTimestamp `json:"created_at,omitempty"` + UpdatedAt UnixTimestamp `json:"updated_at,omitempty"` +} + +type DatatypeType string + +const ( + DatatypeTypeNumQueries = DatatypeType("num_queries") + DatatypeTypeEBOTResponse = DatatypeType("num_ebot_response") + DatatypeTypeNXDResponse = DatatypeType("num_nxd_response") + DatatypeTypeZeroQueries = DatatypeType("zero_queries") +) + +type DatatypeScope string + +const ( + DatatypeScopeAccount = DatatypeScope("account") + DatatypeScopeNetworkSingle = DatatypeScope("network_single") + DatatypeScopeRecordSingle = DatatypeScope("record_single") + DatatypeScopeZoneSingle = DatatypeScope("zone_single") + DatatypeScopeNetworkEach = DatatypeScope("network_each") + DatatypeScopeRecordEach = DatatypeScope("record_each") + DatatypeScopeZoneEach = DatatypeScope("zone_each") + DatatypeScopeTopNZones = DatatypeScope("top_n_zones") + DatatypeScopeTopNRecords = DatatypeScope("top_n_records") +) + +// Datatype wraps Dataset's "Datatype" attribute +type Datatype struct { + Type DatatypeType `json:"type,omitempty"` + Scope DatatypeScope `json:"scope,omitempty"` + Data map[string]string `json:"data,omitempty"` +} + +// RepeatsEvery is a string enum +type RepeatsEvery string + +const ( + RepeatsEveryWeek = RepeatsEvery("week") + RepeatsEveryMonth = RepeatsEvery("month") + RepeatsEveryYear = RepeatsEvery("year") +) + +// Repeat wraps Dataset's "Repeat" attribute +type Repeat struct { + Start UnixTimestamp `json:"start,omitempty"` + RepeatsEvery RepeatsEvery `json:"repeats_every,omitempty"` + EndAfterN int32 `json:"end_after_n,omitempty"` +} + +// TimeframeAggregation is a string enum +type TimeframeAggregation string + +const ( + TimeframeAggregationDaily = TimeframeAggregation("daily") + TimeframeAggregationMontly = TimeframeAggregation("monthly") + TimeframeAggregationBillingPeriod = TimeframeAggregation("billing_period") +) + +// Timeframe wraps Dataset's "Timeframe" attribute +type Timeframe struct { + Aggregation TimeframeAggregation `json:"aggregation,omitempty"` + Cycles *int32 `json:"cycles,omitempty"` + From *UnixTimestamp `json:"from,omitempty"` + To *UnixTimestamp `json:"to,omitempty"` +} + +// ReportStatus is a string enum +type ReportStatus string + +const ( + ReportStatusQueued = ReportStatus("queued") + ReportStatusGenerating = ReportStatus("generating") + ReportStatusAvailable = ReportStatus("available") + ReportStatusFailed = ReportStatus("failed") +) + +// Report wraps Dataset's "Report" attribute +type Report struct { + ID string `json:"id,omitempty"` + Status ReportStatus `json:"status,omitempty"` + Start UnixTimestamp `json:"start,omitempty"` + End UnixTimestamp `json:"end,omitempty"` + CreatedAt UnixTimestamp `json:"created_at,omitempty"` +} + +// NewDataset takes the properties for a Dataset and creates a new instance +func NewDataset( + id string, + name string, + datatype *Datatype, + repeat *Repeat, + timeframe *Timeframe, + exportType ExportType, + reports []*Report, + recipientEmails []string, + createdAt UnixTimestamp, + updatedAt UnixTimestamp, +) *Dataset { + return &Dataset{ + ID: id, + Name: name, + Datatype: datatype, + Repeat: repeat, + Timeframe: timeframe, + ExportType: exportType, + Reports: reports, + RecipientEmails: recipientEmails, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } +} + +// NewDatatype takes the properties for a Datatype and creates a new instance +func NewDatatype( + dtype DatatypeType, + scope DatatypeScope, + data map[string]string, +) *Datatype { + return &Datatype{ + Type: dtype, + Scope: scope, + Data: data, + } +} + +// NewRepeat takes the properties for a Repeat and creates a new instance +func NewRepeat( + start UnixTimestamp, + repeatsEvery RepeatsEvery, + endAfterN int32, +) *Repeat { + return &Repeat{ + Start: start, + RepeatsEvery: repeatsEvery, + EndAfterN: endAfterN, + } +} + +// NewTimeframe takes the properties for a Timeframe and creates a new instance +func NewTimeframe( + aggregation TimeframeAggregation, + cycles *int32, + from *UnixTimestamp, + to *UnixTimestamp, +) *Timeframe { + return &Timeframe{ + Aggregation: aggregation, + Cycles: cycles, + From: from, + To: to, + } +} + +// NewReport takes the properties for a Report and creates a new instance +func NewReport( + id string, + status ReportStatus, + start UnixTimestamp, + end UnixTimestamp, + createdAt UnixTimestamp, +) *Report { + return &Report{ + ID: id, + Status: status, + Start: start, + End: end, + CreatedAt: createdAt, + } +} + +// UnixTimestamp represents a timestamp field that comes as a string-based unix timestamp +type UnixTimestamp time.Time + +func (ut *UnixTimestamp) UnmarshalJSON(data []byte) error { + var unix int64 + data = bytes.Replace(data, []byte(`"`), []byte(""), -1) + if err := json.Unmarshal(data, &unix); err != nil { + return err + } + *ut = UnixTimestamp(time.Unix(unix, 0)) + return nil +} + +func (ut *UnixTimestamp) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Time(*ut).Unix()) +} diff --git a/rest/model/dataset/dataset_test.go b/rest/model/dataset/dataset_test.go new file mode 100644 index 0000000..6e49c40 --- /dev/null +++ b/rest/model/dataset/dataset_test.go @@ -0,0 +1,103 @@ +package dataset + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +var now = time.Unix(time.Now().Unix(), 0) + +var mockedDataset = &Dataset{ + ID: "dt-id", + Name: "dt-name", + Datatype: &Datatype{ + Type: DatatypeTypeNumQueries, + Scope: DatatypeScopeAccount, + Data: map[string]string{"foo": "bar"}, + }, + Repeat: &Repeat{ + Start: UnixTimestamp(now), + RepeatsEvery: RepeatsEveryMonth, + EndAfterN: 1, + }, + Timeframe: &Timeframe{ + Aggregation: TimeframeAggregationBillingPeriod, + Cycles: getInt32Ptr(3), + From: getTimestampPtr(UnixTimestamp(now)), + To: getTimestampPtr(UnixTimestamp(now)), + }, + ExportType: ExportTypeCSV, + Reports: []*Report{ + { + ID: "dt-report-id", + Status: ReportStatusAvailable, + Start: UnixTimestamp(now), + End: UnixTimestamp(now), + CreatedAt: UnixTimestamp(now), + }, + }, + RecipientEmails: []string{"datasets@ns1.com"}, + CreatedAt: UnixTimestamp(now), + UpdatedAt: UnixTimestamp(now), +} + +func getTimestampPtr(v UnixTimestamp) *UnixTimestamp { + return &v +} + +func getInt32Ptr(v int32) *int32 { + return &v +} + +func TestNewDataset(t *testing.T) { + dt := NewDataset( + mockedDataset.ID, + mockedDataset.Name, + NewDatatype( + mockedDataset.Datatype.Type, + mockedDataset.Datatype.Scope, + mockedDataset.Datatype.Data, + ), + NewRepeat( + mockedDataset.Repeat.Start, + mockedDataset.Repeat.RepeatsEvery, + mockedDataset.Repeat.EndAfterN, + ), + NewTimeframe( + mockedDataset.Timeframe.Aggregation, + mockedDataset.Timeframe.Cycles, + mockedDataset.Timeframe.From, + mockedDataset.Timeframe.To, + ), + mockedDataset.ExportType, + []*Report{ + NewReport( + mockedDataset.Reports[0].ID, + mockedDataset.Reports[0].Status, + mockedDataset.Reports[0].Start, + mockedDataset.Reports[0].End, + mockedDataset.Reports[0].CreatedAt, + ), + }, + mockedDataset.RecipientEmails, + mockedDataset.CreatedAt, + mockedDataset.UpdatedAt, + ) + + assert.Equal(t, mockedDataset, dt) +} + +func TestDatasetUnmarshall(t *testing.T) { + dt := &Dataset{} + + dtJSON, err := json.Marshal(mockedDataset) + require.NoError(t, err) + + err = json.Unmarshal(dtJSON, dt) + require.NoError(t, err) + + assert.Equal(t, mockedDataset, dt) +} diff --git a/rest/model/dns/example_test.go b/rest/model/dns/example_test.go index 13dfdf7..ff692e1 100644 --- a/rest/model/dns/example_test.go +++ b/rest/model/dns/example_test.go @@ -54,7 +54,7 @@ func ExampleZone_MakePrimary() { func ExampleRecord() { // Construct the A record - record := dns.NewRecord("test.com", "a", "A") + record := dns.NewRecord("test.com", "a", "A", nil, nil) record.TTL = 300 // Construct primary answer(higher priority) @@ -125,11 +125,11 @@ func ExampleRecord() { func ExampleRecord_LinkTo() { // Construct the src record - srcRecord := dns.NewRecord("test.com", "a", "A") + srcRecord := dns.NewRecord("test.com", "a", "A", nil, nil) srcRecord.TTL = 300 srcRecord.Meta.Priority = 2 - linkedRecord := dns.NewRecord("test.com", "l", "A") + linkedRecord := dns.NewRecord("test.com", "l", "A", nil, nil) linkedRecord.LinkTo(srcRecord.Domain) fmt.Println(linkedRecord) fmt.Println(linkedRecord.Meta) diff --git a/rest/model/dns/record_test.go b/rest/model/dns/record_test.go index 2a2a3b0..63a3d2e 100644 --- a/rest/model/dns/record_test.go +++ b/rest/model/dns/record_test.go @@ -18,7 +18,7 @@ var marshalRecordCases = []struct { "marshalCAARecord", NewRecord("example.com", "caa.example.com", "CAA", nil, nil), []*Answer{NewCAAAnswer(0, "issue", "letsencrypt.org")}, - []byte(`{"meta":{},"zone":"example.com","domain":"caa.example.com","type":"CAA","answers":[{"meta":{},"answer":["0","issue","letsencrypt.org"]}],"filters":[]}`), + []byte(`{"meta":{},"zone":"example.com","domain":"caa.example.com","type":"CAA","answers":[{"meta":{},"answer":["0","issue","letsencrypt.org"]}],"filters":[],"tags":null,"blocked_tags":null}`), }, { "marshalURLFWDRecord", @@ -27,7 +27,7 @@ var marshalRecordCases = []struct { NewURLFWDAnswer("/net", "https://example.net", 301, 1, 1), NewURLFWDAnswer("/org", "https://example.org", 302, 2, 0), }, - []byte(`{"answers":[{"answer":["/net","https://example.net",301,1,1],"meta":{}},{"answer":["/org","https://example.org",302,2,0],"meta":{}}],"meta":{},"zone":"example.com","domain":"fwd.example.com","type":"URLFWD","filters":[]}`), + []byte(`{"answers":[{"answer":["/net","https://example.net",301,1,1],"meta":{}},{"answer":["/org","https://example.org",302,2,0],"meta":{}}],"meta":{},"zone":"example.com","domain":"fwd.example.com","type":"URLFWD","filters":[],"tags":null,"blocked_tags":null}`), }, } @@ -60,19 +60,19 @@ func TestMarshalRecordsOverrideTTL(t *testing.T) { "marshalOverrideTTLNil", NewRecord("example.com", "example.com", "ALIAS", nil, nil), nil, - []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","answers":[],"filters":[]}`), + []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","answers":[],"filters":[],"tags":null,"blocked_tags":null}`), }, { "marshalOverrideTTLTrue", NewRecord("example.com", "example.com", "ALIAS", nil, nil), &trueb, - []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","override_ttl":true,"answers":[],"filters":[]}`), + []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","override_ttl":true,"answers":[],"filters":[],"tags":null,"blocked_tags":null}`), }, { "marshalOverrideTTLFalse", NewRecord("example.com", "example.com", "ALIAS", nil, nil), &falseb, - []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","override_ttl":false,"answers":[],"filters":[]}`), + []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","override_ttl":false,"answers":[],"filters":[],"tags":null,"blocked_tags":null}`), }, } for _, tt := range marshalALIASRecordCases { @@ -104,21 +104,21 @@ func TestMarshalRecordsOverrideAddressRecords(t *testing.T) { NewRecord("example.com", "example.com", "ALIAS", nil, nil), nil, nil, - []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","answers":[],"filters":[]}`), + []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","answers":[],"filters":[],"tags":null,"blocked_tags":null}`), }, { "marshalOverrideAddressRecordsTrue", NewRecord("example.com", "example.com", "ALIAS", nil, nil), &trueb, &trueb, - []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","override_ttl":true,"override_address_records":true,"answers":[],"filters":[]}`), + []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","override_ttl":true,"override_address_records":true,"answers":[],"filters":[],"tags":null,"blocked_tags":null}`), }, { "marshalOverrideAddressRecordsFalse", NewRecord("example.com", "example.com", "ALIAS", nil, nil), &falseb, &falseb, - []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","override_ttl":false,"override_address_records":false,"answers":[],"filters":[]}`), + []byte(`{"meta":{},"zone":"example.com","domain":"example.com","type":"ALIAS","override_ttl":false,"override_address_records":false,"answers":[],"filters":[],"tags":null,"blocked_tags":null}`), }, } for _, tt := range marshalALIASRecordCases { @@ -129,7 +129,7 @@ func TestMarshalRecordsOverrideAddressRecords(t *testing.T) { if err != nil { t.Error(err) } - if bytes.Compare(result, tt.out) != 0 { + if !bytes.Equal(result, tt.out) { t.Errorf("got %q, want %q", result, tt.out) } })