-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
10 changed files
with
438 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"number":"31971234567","countryCode":"NL","billingIntervalMonths":1} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
] | ||
} |
Oops, something went wrong.