Skip to content

Commit c9ccdcf

Browse files
Fredy Lópezepels
authored andcommitted
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
1 parent 266ee18 commit c9ccdcf

10 files changed

+438
-0
lines changed

number/number.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package number
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/url"
7+
"strconv"
8+
9+
messagebird "github.com/messagebird/go-rest-api"
10+
)
11+
12+
const (
13+
// apiRoot is the absolute URL of the Numbers API.
14+
apiRoot = "https://numbers.messagebird.com/v1"
15+
16+
// pathNumbers is the path for the Numbers resource, relative to apiRoot.
17+
// and path.
18+
pathNumbers = "phone-numbers"
19+
20+
// pathNumbersAvailable is the path for the Search Number resource, relative to apiRoot.
21+
pathNumbersAvailable = "available-phone-numbers"
22+
)
23+
24+
// Number represents a specific phone number.
25+
type Number struct {
26+
Number string
27+
Country string
28+
Region string
29+
Locality string
30+
Features []string
31+
Tags []string
32+
Type string
33+
Status string
34+
}
35+
36+
// NumberList provide a list of all purchased phone numbers.
37+
type NumberList struct {
38+
Offset int
39+
Limit int
40+
Count int
41+
TotalCount int
42+
Items []*Number
43+
}
44+
45+
// NumberSearchingList provide a list of all phone numbers.
46+
// that are available for purchase.
47+
type NumberSearchingList struct {
48+
Items []*Number
49+
Limit int
50+
Count int
51+
}
52+
53+
// NumberListParams can be used to set query params in List().
54+
type NumberListParams struct {
55+
Limit int
56+
Offset int
57+
Number string
58+
Country string
59+
Region string
60+
Locality string
61+
Features []string
62+
Type string
63+
Status string
64+
SearchPattern NumberPattern
65+
}
66+
67+
// NumberUpdateRequest can be used to set tags update.
68+
type NumberUpdateRequest struct {
69+
Tags []string `json:"tags"`
70+
}
71+
72+
// NumberPurchaseRequest can be used to purchase a number.
73+
type NumberPurchaseRequest struct {
74+
Number string `json:"number"`
75+
Country string `json:"countryCode"`
76+
BillingIntervalMonths int `json:"billingIntervalMonths"`
77+
}
78+
79+
type NumberPattern string
80+
81+
const (
82+
// NumberPatternStart force phone numbers to start with the provided fragment.
83+
NumberPatternStart NumberPattern = "start"
84+
85+
// NumberPatternEnd phone numbers can be somewhere within the provided fragment.
86+
NumberPatternEnd NumberPattern = "end"
87+
88+
// NumberPatternAnyWhere force phone numbers to end with the provided fragment.
89+
NumberPatternAnyWhere NumberPattern = "anywhere"
90+
)
91+
92+
// request does the exact same thing as Client.Request. It does, however,
93+
// prefix the path with the Numbers API's root. This ensures the client
94+
// doesn't "handle" this for us: by default, it uses the REST API.
95+
func request(c *messagebird.Client, v interface{}, method, path string, data interface{}) error {
96+
return c.Request(v, method, fmt.Sprintf("%s/%s", apiRoot, path), data)
97+
}
98+
99+
// List get all purchased phone numbers
100+
func List(c *messagebird.Client, listParams *NumberListParams) (*NumberList, error) {
101+
uri := getpath(listParams, pathNumbers)
102+
103+
numberList := &NumberList{}
104+
if err := request(c, numberList, http.MethodGet, uri, nil); err != nil {
105+
return nil, err
106+
}
107+
return numberList, nil
108+
}
109+
110+
// Search for phone numbers available for purchase, countryCode needs to be in Alpha-2 country code (example: NL)
111+
func Search(c *messagebird.Client, countryCode string, listParams *NumberListParams) (*NumberSearchingList, error) {
112+
uri := getpath(listParams, pathNumbersAvailable+"/"+countryCode)
113+
114+
numberList := &NumberSearchingList{}
115+
if err := request(c, numberList, http.MethodGet, uri, nil); err != nil {
116+
return nil, err
117+
}
118+
119+
return numberList, nil
120+
}
121+
122+
// Read get a purchased phone number
123+
func Read(c *messagebird.Client, phoneNumber string) (*Number, error) {
124+
if len(phoneNumber) < 5 {
125+
return nil, fmt.Errorf("a phoneNumber is too short")
126+
}
127+
128+
uri := fmt.Sprintf("%s/%s", pathNumbers, phoneNumber)
129+
130+
number := &Number{}
131+
if err := request(c, number, http.MethodGet, uri, nil); err != nil {
132+
return nil, err
133+
}
134+
135+
return number, nil
136+
}
137+
138+
// Delete a purchased phone number
139+
func Delete(c *messagebird.Client, phoneNumber string) error {
140+
uri := fmt.Sprintf("%s/%s", pathNumbers, phoneNumber)
141+
return request(c, nil, http.MethodDelete, uri, nil)
142+
}
143+
144+
// Update updates a purchased phone number.
145+
// Only updating *tags* is supported at the moment.
146+
func Update(c *messagebird.Client, phoneNumber string, numberUpdateRequest *NumberUpdateRequest) (*Number, error) {
147+
uri := fmt.Sprintf("%s/%s", pathNumbers, phoneNumber)
148+
149+
number := &Number{}
150+
if err := request(c, number, http.MethodPatch, uri, numberUpdateRequest); err != nil {
151+
return nil, err
152+
}
153+
154+
return number, nil
155+
}
156+
157+
// Purchases purchases a phone number.
158+
func Purchase(c *messagebird.Client, numberPurchaseRequest *NumberPurchaseRequest) (*Number, error) {
159+
160+
number := &Number{}
161+
if err := request(c, number, http.MethodPost, pathNumbers, numberPurchaseRequest); err != nil {
162+
return nil, err
163+
}
164+
165+
return number, nil
166+
}
167+
168+
// GetPath get the full path for the request
169+
func getpath(listParams *NumberListParams, path string) string {
170+
params := paramsForMessageList(listParams)
171+
return fmt.Sprintf("%s?%s", path, params.Encode())
172+
}
173+
174+
// paramsForMessageList build query params
175+
func paramsForMessageList(params *NumberListParams) *url.Values {
176+
urlParams := &url.Values{}
177+
178+
if params == nil {
179+
return urlParams
180+
}
181+
182+
if len(params.Features) > 0 {
183+
paramsForArrays("features", params.Features, urlParams)
184+
}
185+
186+
if params.Type != "" {
187+
urlParams.Set("type", params.Type)
188+
}
189+
190+
if params.Number != "" {
191+
urlParams.Set("number", params.Number)
192+
}
193+
if params.Country != "" {
194+
urlParams.Set("country", params.Country)
195+
}
196+
if params.Limit != 0 {
197+
urlParams.Set("limit", strconv.Itoa(params.Limit))
198+
}
199+
200+
if params.SearchPattern != "" {
201+
urlParams.Set("search_pattern", string(params.SearchPattern))
202+
}
203+
204+
if params.Offset != 0 {
205+
urlParams.Set("offset", strconv.Itoa(params.Offset))
206+
}
207+
208+
return urlParams
209+
}
210+
211+
// paramsForArrays build query for array params
212+
func paramsForArrays(field string, values []string, urlParams *url.Values) {
213+
for _, value := range values {
214+
urlParams.Add(field, value)
215+
}
216+
}

number/number_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package number
2+
3+
import (
4+
"net/http"
5+
"reflect"
6+
"testing"
7+
8+
"github.com/messagebird/go-rest-api/internal/mbtest"
9+
)
10+
11+
func TestMain(m *testing.M) {
12+
mbtest.EnableServer(m)
13+
}
14+
15+
func TestSearch(t *testing.T) {
16+
mbtest.WillReturnTestdata(t, "numberSearch.json", http.StatusOK)
17+
client := mbtest.Client(t)
18+
19+
numLis, err := Search(client, "NL", &NumberListParams{
20+
Limit: 10,
21+
Features: []string{"sms", "voice"},
22+
Type: "mobile",
23+
SearchPattern: NumberPatternEnd,
24+
})
25+
if err != nil {
26+
t.Fatalf("unexpected error searching Numbers: %s", err)
27+
}
28+
29+
if numLis.Items[0].Country != "NL" {
30+
t.Errorf("got %s, expected NL", numLis.Items[0].Country)
31+
}
32+
33+
mbtest.AssertEndpointCalled(t, http.MethodGet, "/v1/available-phone-numbers/NL")
34+
35+
if query := mbtest.Request.URL.RawQuery; query != "features=sms&features=voice&limit=10&search_pattern=end&type=mobile" {
36+
t.Fatalf("got %s, expected features=sms&features=voice&limit=10&search_pattern=end&type=mobile", query)
37+
}
38+
}
39+
40+
func TestList(t *testing.T) {
41+
mbtest.WillReturnTestdata(t, "numberList.json", http.StatusOK)
42+
client := mbtest.Client(t)
43+
44+
numLis, err := List(client, &NumberListParams{Limit: 10})
45+
if err != nil {
46+
t.Fatalf("unexpected error searching Numbers: %s", err)
47+
}
48+
49+
if numLis.Items[0].Country != "NL" {
50+
t.Errorf("got %s, expected NL", numLis.Items[0].Country)
51+
}
52+
53+
mbtest.AssertEndpointCalled(t, http.MethodGet, "/v1/phone-numbers")
54+
55+
if query := mbtest.Request.URL.RawQuery; query != "limit=10" {
56+
t.Fatalf("got %s, expected limit=10", query)
57+
}
58+
}
59+
60+
func TestRead(t *testing.T) {
61+
mbtest.WillReturnTestdata(t, "numberRead.json", http.StatusOK)
62+
client := mbtest.Client(t)
63+
64+
num, err := Read(client, "31612345670")
65+
if err != nil {
66+
t.Fatalf("unexpected error searching Numbers: %s", err)
67+
}
68+
69+
if num.Number != "31612345670" {
70+
t.Fatalf("got %s, expected 31612345670", num.Number)
71+
}
72+
73+
mbtest.AssertEndpointCalled(t, http.MethodGet, "/v1/phone-numbers/31612345670")
74+
}
75+
76+
func TestDelete(t *testing.T) {
77+
mbtest.WillReturn([]byte(""), http.StatusNoContent)
78+
client := mbtest.Client(t)
79+
80+
if err := Delete(client, "31612345670"); err != nil {
81+
t.Errorf("unexpected error canceling Number: %s", err)
82+
}
83+
84+
mbtest.AssertEndpointCalled(t, http.MethodDelete, "/v1/phone-numbers/31612345670")
85+
}
86+
87+
func TestUpdate(t *testing.T) {
88+
89+
mbtest.WillReturnTestdata(t, "numberUpdatedObject.json", http.StatusOK)
90+
client := mbtest.Client(t)
91+
92+
number, err := Update(client, "31612345670", &NumberUpdateRequest{
93+
Tags: []string{"tag1", "tag2", "tag3"},
94+
})
95+
96+
if err != nil {
97+
t.Errorf("unexpected error updating Number: %s", err)
98+
}
99+
100+
mbtest.AssertEndpointCalled(t, http.MethodPatch, "/v1/phone-numbers/31612345670")
101+
mbtest.AssertTestdata(t, "numberUpdateRequestObject.json", mbtest.Request.Body)
102+
103+
if !reflect.DeepEqual(number.Tags, []string{"tag1", "tag2", "tag3"}) {
104+
t.Errorf("Unexpected number tags: %s, expected: ['tag1', 'tag2', 'tag3']", number.Tags)
105+
}
106+
}
107+
108+
func TestPurchase(t *testing.T) {
109+
mbtest.WillReturnTestdata(t, "numberCreateObject.json", http.StatusCreated)
110+
client := mbtest.Client(t)
111+
112+
number, err := Purchase(client, &NumberPurchaseRequest{
113+
Number: "31971234567",
114+
Country: "NL",
115+
BillingIntervalMonths: 1,
116+
})
117+
if err != nil {
118+
t.Errorf("unexpected error creating Number: %s", err)
119+
}
120+
121+
mbtest.AssertEndpointCalled(t, http.MethodPost, "/v1/phone-numbers")
122+
mbtest.AssertTestdata(t, "numberCreateRequestObject.json", mbtest.Request.Body)
123+
124+
if number.Number != "31971234567" {
125+
t.Errorf("Unexpected number message id: %s, expected: 31971234567", number.Number)
126+
}
127+
128+
if number.Country != "NL" {
129+
t.Errorf("Unexpected number country: %s, expected: NL", number.Country)
130+
}
131+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"number": "31971234567",
3+
"country": "NL",
4+
"region": "Haarlem",
5+
"locality": "Haarlem",
6+
"features": [
7+
"sms",
8+
"voice"
9+
],
10+
"tags": [],
11+
"type": "landline_or_mobile",
12+
"status": "active",
13+
"createdAt": "2019-04-25T14:04:04Z",
14+
"renewalAt": "2019-05-25T00:00:00Z"
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"number":"31971234567","countryCode":"NL","billingIntervalMonths":1}

number/testdata/numberList.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"offset": 0,
3+
"limit": 20,
4+
"count": 1,
5+
"totalCount": 1,
6+
"items": [
7+
{
8+
"number": "31612345670",
9+
"country": "NL",
10+
"region": "Texel",
11+
"locality": "Texel",
12+
"features": [
13+
"sms",
14+
"voice"
15+
],
16+
"tags": [],
17+
"type": "mobile",
18+
"status": "active"
19+
}
20+
]
21+
}

0 commit comments

Comments
 (0)