From c9ccdcf2caadc4ecbc89b9cdb1e2ad180a4ddcf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredy=20L=C3=B3pez?= <53862917+fredylo@users.noreply.github.com> Date: Thu, 15 Aug 2019 17:29:50 +0200 Subject: [PATCH] Added read, list, search methods for numbers API (#67) * Added read, list, search methods for numbers API * WIP-numbers api endpoints and tests * Removed hardcode type and added possible values for search pattern * WIP numbers api * Add tests - numbers API * add newline * Added @j-evs changes * Improved comments and fixed phone numbers type * eng-154 numbers change test * eng-154 change mbtest import * Merge @j-evs pull request --- number/number.go | 216 ++++++++++++++++++ number/number_test.go | 131 +++++++++++ number/testdata/numberCreateObject.json | 15 ++ .../testdata/numberCreateRequestObject.json | 1 + number/testdata/numberList.json | 21 ++ number/testdata/numberObject.json | 13 ++ number/testdata/numberRead.json | 13 ++ number/testdata/numberSearch.json | 14 ++ .../testdata/numberUpdateRequestObject.json | 1 + number/testdata/numberUpdatedObject.json | 13 ++ 10 files changed, 438 insertions(+) create mode 100644 number/number.go create mode 100644 number/number_test.go create mode 100644 number/testdata/numberCreateObject.json create mode 100644 number/testdata/numberCreateRequestObject.json create mode 100644 number/testdata/numberList.json create mode 100644 number/testdata/numberObject.json create mode 100644 number/testdata/numberRead.json create mode 100644 number/testdata/numberSearch.json create mode 100644 number/testdata/numberUpdateRequestObject.json create mode 100644 number/testdata/numberUpdatedObject.json diff --git a/number/number.go b/number/number.go new file mode 100644 index 0000000..f7d8f87 --- /dev/null +++ b/number/number.go @@ -0,0 +1,216 @@ +package number + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + + messagebird "github.com/messagebird/go-rest-api" +) + +const ( + // apiRoot is the absolute URL of the Numbers API. + apiRoot = "https://numbers.messagebird.com/v1" + + // pathNumbers is the path for the Numbers resource, relative to apiRoot. + // and path. + pathNumbers = "phone-numbers" + + // pathNumbersAvailable is the path for the Search Number resource, relative to apiRoot. + pathNumbersAvailable = "available-phone-numbers" +) + +// Number represents a specific phone number. +type Number struct { + Number string + Country string + Region string + Locality string + Features []string + Tags []string + Type string + Status string +} + +// NumberList provide a list of all purchased phone numbers. +type NumberList struct { + Offset int + Limit int + Count int + TotalCount int + Items []*Number +} + +// NumberSearchingList provide a list of all phone numbers. +// that are available for purchase. +type NumberSearchingList struct { + Items []*Number + Limit int + Count int +} + +// NumberListParams can be used to set query params in List(). +type NumberListParams struct { + Limit int + Offset int + Number string + Country string + Region string + Locality string + Features []string + Type string + Status string + SearchPattern NumberPattern +} + +// NumberUpdateRequest can be used to set tags update. +type NumberUpdateRequest struct { + Tags []string `json:"tags"` +} + +// NumberPurchaseRequest can be used to purchase a number. +type NumberPurchaseRequest struct { + Number string `json:"number"` + Country string `json:"countryCode"` + BillingIntervalMonths int `json:"billingIntervalMonths"` +} + +type NumberPattern string + +const ( + // NumberPatternStart force phone numbers to start with the provided fragment. + NumberPatternStart NumberPattern = "start" + + // NumberPatternEnd phone numbers can be somewhere within the provided fragment. + NumberPatternEnd NumberPattern = "end" + + // NumberPatternAnyWhere force phone numbers to end with the provided fragment. + NumberPatternAnyWhere NumberPattern = "anywhere" +) + +// request does the exact same thing as Client.Request. It does, however, +// prefix the path with the Numbers API's root. This ensures the client +// doesn't "handle" this for us: by default, it uses the REST API. +func request(c *messagebird.Client, v interface{}, method, path string, data interface{}) error { + return c.Request(v, method, fmt.Sprintf("%s/%s", apiRoot, path), data) +} + +// List get all purchased phone numbers +func List(c *messagebird.Client, listParams *NumberListParams) (*NumberList, error) { + uri := getpath(listParams, pathNumbers) + + numberList := &NumberList{} + if err := request(c, numberList, http.MethodGet, uri, nil); err != nil { + return nil, err + } + return numberList, nil +} + +// Search for phone numbers available for purchase, countryCode needs to be in Alpha-2 country code (example: NL) +func Search(c *messagebird.Client, countryCode string, listParams *NumberListParams) (*NumberSearchingList, error) { + uri := getpath(listParams, pathNumbersAvailable+"/"+countryCode) + + numberList := &NumberSearchingList{} + if err := request(c, numberList, http.MethodGet, uri, nil); err != nil { + return nil, err + } + + return numberList, nil +} + +// Read get a purchased phone number +func Read(c *messagebird.Client, phoneNumber string) (*Number, error) { + if len(phoneNumber) < 5 { + return nil, fmt.Errorf("a phoneNumber is too short") + } + + uri := fmt.Sprintf("%s/%s", pathNumbers, phoneNumber) + + number := &Number{} + if err := request(c, number, http.MethodGet, uri, nil); err != nil { + return nil, err + } + + return number, nil +} + +// Delete a purchased phone number +func Delete(c *messagebird.Client, phoneNumber string) error { + uri := fmt.Sprintf("%s/%s", pathNumbers, phoneNumber) + return request(c, nil, http.MethodDelete, uri, nil) +} + +// Update updates a purchased phone number. +// Only updating *tags* is supported at the moment. +func Update(c *messagebird.Client, phoneNumber string, numberUpdateRequest *NumberUpdateRequest) (*Number, error) { + uri := fmt.Sprintf("%s/%s", pathNumbers, phoneNumber) + + number := &Number{} + if err := request(c, number, http.MethodPatch, uri, numberUpdateRequest); err != nil { + return nil, err + } + + return number, nil +} + +// Purchases purchases a phone number. +func Purchase(c *messagebird.Client, numberPurchaseRequest *NumberPurchaseRequest) (*Number, error) { + + number := &Number{} + if err := request(c, number, http.MethodPost, pathNumbers, numberPurchaseRequest); err != nil { + return nil, err + } + + return number, nil +} + +// GetPath get the full path for the request +func getpath(listParams *NumberListParams, path string) string { + params := paramsForMessageList(listParams) + return fmt.Sprintf("%s?%s", path, params.Encode()) +} + +// paramsForMessageList build query params +func paramsForMessageList(params *NumberListParams) *url.Values { + urlParams := &url.Values{} + + if params == nil { + return urlParams + } + + if len(params.Features) > 0 { + paramsForArrays("features", params.Features, urlParams) + } + + if params.Type != "" { + urlParams.Set("type", params.Type) + } + + if params.Number != "" { + urlParams.Set("number", params.Number) + } + if params.Country != "" { + urlParams.Set("country", params.Country) + } + if params.Limit != 0 { + urlParams.Set("limit", strconv.Itoa(params.Limit)) + } + + if params.SearchPattern != "" { + urlParams.Set("search_pattern", string(params.SearchPattern)) + } + + if params.Offset != 0 { + urlParams.Set("offset", strconv.Itoa(params.Offset)) + } + + return urlParams +} + +// paramsForArrays build query for array params +func paramsForArrays(field string, values []string, urlParams *url.Values) { + for _, value := range values { + urlParams.Add(field, value) + } +} diff --git a/number/number_test.go b/number/number_test.go new file mode 100644 index 0000000..a76fa30 --- /dev/null +++ b/number/number_test.go @@ -0,0 +1,131 @@ +package number + +import ( + "net/http" + "reflect" + "testing" + + "github.com/messagebird/go-rest-api/internal/mbtest" +) + +func TestMain(m *testing.M) { + mbtest.EnableServer(m) +} + +func TestSearch(t *testing.T) { + mbtest.WillReturnTestdata(t, "numberSearch.json", http.StatusOK) + client := mbtest.Client(t) + + numLis, err := Search(client, "NL", &NumberListParams{ + Limit: 10, + Features: []string{"sms", "voice"}, + Type: "mobile", + SearchPattern: NumberPatternEnd, + }) + if err != nil { + t.Fatalf("unexpected error searching Numbers: %s", err) + } + + if numLis.Items[0].Country != "NL" { + t.Errorf("got %s, expected NL", numLis.Items[0].Country) + } + + mbtest.AssertEndpointCalled(t, http.MethodGet, "/v1/available-phone-numbers/NL") + + if query := mbtest.Request.URL.RawQuery; query != "features=sms&features=voice&limit=10&search_pattern=end&type=mobile" { + t.Fatalf("got %s, expected features=sms&features=voice&limit=10&search_pattern=end&type=mobile", query) + } +} + +func TestList(t *testing.T) { + mbtest.WillReturnTestdata(t, "numberList.json", http.StatusOK) + client := mbtest.Client(t) + + numLis, err := List(client, &NumberListParams{Limit: 10}) + if err != nil { + t.Fatalf("unexpected error searching Numbers: %s", err) + } + + if numLis.Items[0].Country != "NL" { + t.Errorf("got %s, expected NL", numLis.Items[0].Country) + } + + mbtest.AssertEndpointCalled(t, http.MethodGet, "/v1/phone-numbers") + + if query := mbtest.Request.URL.RawQuery; query != "limit=10" { + t.Fatalf("got %s, expected limit=10", query) + } +} + +func TestRead(t *testing.T) { + mbtest.WillReturnTestdata(t, "numberRead.json", http.StatusOK) + client := mbtest.Client(t) + + num, err := Read(client, "31612345670") + if err != nil { + t.Fatalf("unexpected error searching Numbers: %s", err) + } + + if num.Number != "31612345670" { + t.Fatalf("got %s, expected 31612345670", num.Number) + } + + mbtest.AssertEndpointCalled(t, http.MethodGet, "/v1/phone-numbers/31612345670") +} + +func TestDelete(t *testing.T) { + mbtest.WillReturn([]byte(""), http.StatusNoContent) + client := mbtest.Client(t) + + if err := Delete(client, "31612345670"); err != nil { + t.Errorf("unexpected error canceling Number: %s", err) + } + + mbtest.AssertEndpointCalled(t, http.MethodDelete, "/v1/phone-numbers/31612345670") +} + +func TestUpdate(t *testing.T) { + + mbtest.WillReturnTestdata(t, "numberUpdatedObject.json", http.StatusOK) + client := mbtest.Client(t) + + number, err := Update(client, "31612345670", &NumberUpdateRequest{ + Tags: []string{"tag1", "tag2", "tag3"}, + }) + + if err != nil { + t.Errorf("unexpected error updating Number: %s", err) + } + + mbtest.AssertEndpointCalled(t, http.MethodPatch, "/v1/phone-numbers/31612345670") + mbtest.AssertTestdata(t, "numberUpdateRequestObject.json", mbtest.Request.Body) + + if !reflect.DeepEqual(number.Tags, []string{"tag1", "tag2", "tag3"}) { + t.Errorf("Unexpected number tags: %s, expected: ['tag1', 'tag2', 'tag3']", number.Tags) + } +} + +func TestPurchase(t *testing.T) { + mbtest.WillReturnTestdata(t, "numberCreateObject.json", http.StatusCreated) + client := mbtest.Client(t) + + number, err := Purchase(client, &NumberPurchaseRequest{ + Number: "31971234567", + Country: "NL", + BillingIntervalMonths: 1, + }) + if err != nil { + t.Errorf("unexpected error creating Number: %s", err) + } + + mbtest.AssertEndpointCalled(t, http.MethodPost, "/v1/phone-numbers") + mbtest.AssertTestdata(t, "numberCreateRequestObject.json", mbtest.Request.Body) + + if number.Number != "31971234567" { + t.Errorf("Unexpected number message id: %s, expected: 31971234567", number.Number) + } + + if number.Country != "NL" { + t.Errorf("Unexpected number country: %s, expected: NL", number.Country) + } +} diff --git a/number/testdata/numberCreateObject.json b/number/testdata/numberCreateObject.json new file mode 100644 index 0000000..45c0483 --- /dev/null +++ b/number/testdata/numberCreateObject.json @@ -0,0 +1,15 @@ +{ + "number": "31971234567", + "country": "NL", + "region": "Haarlem", + "locality": "Haarlem", + "features": [ + "sms", + "voice" + ], + "tags": [], + "type": "landline_or_mobile", + "status": "active", + "createdAt": "2019-04-25T14:04:04Z", + "renewalAt": "2019-05-25T00:00:00Z" +} \ No newline at end of file diff --git a/number/testdata/numberCreateRequestObject.json b/number/testdata/numberCreateRequestObject.json new file mode 100644 index 0000000..571795a --- /dev/null +++ b/number/testdata/numberCreateRequestObject.json @@ -0,0 +1 @@ +{"number":"31971234567","countryCode":"NL","billingIntervalMonths":1} \ No newline at end of file diff --git a/number/testdata/numberList.json b/number/testdata/numberList.json new file mode 100644 index 0000000..d4edd94 --- /dev/null +++ b/number/testdata/numberList.json @@ -0,0 +1,21 @@ +{ + "offset": 0, + "limit": 20, + "count": 1, + "totalCount": 1, + "items": [ + { + "number": "31612345670", + "country": "NL", + "region": "Texel", + "locality": "Texel", + "features": [ + "sms", + "voice" + ], + "tags": [], + "type": "mobile", + "status": "active" + } + ] +} \ No newline at end of file diff --git a/number/testdata/numberObject.json b/number/testdata/numberObject.json new file mode 100644 index 0000000..baf7f0d --- /dev/null +++ b/number/testdata/numberObject.json @@ -0,0 +1,13 @@ +{ + "number": "31612345670", + "country": "NL", + "region": "Texel", + "locality": "Texel", + "features": [ + "sms", + "voice" + ], + "tags": ["tag1"], + "type": "mobile", + "status": "active" +} \ No newline at end of file diff --git a/number/testdata/numberRead.json b/number/testdata/numberRead.json new file mode 100644 index 0000000..e74cfc7 --- /dev/null +++ b/number/testdata/numberRead.json @@ -0,0 +1,13 @@ +{ + "number": "31612345670", + "country": "NL", + "region": "Texel", + "locality": "Texel", + "features": [ + "sms", + "voice" + ], + "tags": [], + "type": "mobile", + "status": "active" +} \ No newline at end of file diff --git a/number/testdata/numberSearch.json b/number/testdata/numberSearch.json new file mode 100644 index 0000000..735a2bf --- /dev/null +++ b/number/testdata/numberSearch.json @@ -0,0 +1,14 @@ +{ + "items": [ + { + "number": "3197010260188", + "country": "NL", + "region": "", + "locality": "", + "features": ["sms", "voice"], + "type": "mobile" + } + ], + "limit": 20, + "count": 1 +} diff --git a/number/testdata/numberUpdateRequestObject.json b/number/testdata/numberUpdateRequestObject.json new file mode 100644 index 0000000..d93ca42 --- /dev/null +++ b/number/testdata/numberUpdateRequestObject.json @@ -0,0 +1 @@ +{"tags":["tag1","tag2","tag3"]} \ No newline at end of file diff --git a/number/testdata/numberUpdatedObject.json b/number/testdata/numberUpdatedObject.json new file mode 100644 index 0000000..03416e9 --- /dev/null +++ b/number/testdata/numberUpdatedObject.json @@ -0,0 +1,13 @@ +{ + "number": "31612345670", + "country": "NL", + "region": "Texel", + "locality": "Texel", + "features": [ + "sms", + "voice" + ], + "tags": ["tag1", "tag2", "tag3"], + "type": "mobile", + "status": "active" +} \ No newline at end of file