Skip to content

Commit

Permalink
Added read, list, search methods for numbers API (#67)
Browse files Browse the repository at this point in the history
* 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
Fredy López authored and epels committed Aug 15, 2019
1 parent 266ee18 commit c9ccdcf
Show file tree
Hide file tree
Showing 10 changed files with 438 additions and 0 deletions.
216 changes: 216 additions & 0 deletions number/number.go
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)
}
}
131 changes: 131 additions & 0 deletions number/number_test.go
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)
}
}
15 changes: 15 additions & 0 deletions number/testdata/numberCreateObject.json
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"
}
1 change: 1 addition & 0 deletions number/testdata/numberCreateRequestObject.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"number":"31971234567","countryCode":"NL","billingIntervalMonths":1}
21 changes: 21 additions & 0 deletions number/testdata/numberList.json
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"
}
]
}
Loading

0 comments on commit c9ccdcf

Please sign in to comment.