essential 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +35 -0
- data/LICENSE +21 -0
- data/README.md +107 -0
- data/Rakefile +9 -0
- data/essential.gemspec +26 -0
- data/lib/essential.rb +32 -0
- data/lib/essential/account.rb +56 -0
- data/lib/essential/client.rb +65 -0
- data/lib/essential/errors/api_error.rb +54 -0
- data/lib/essential/messaging/channel.rb +36 -0
- data/lib/essential/messaging/message.rb +68 -0
- data/lib/essential/messaging/property.rb +27 -0
- data/lib/essential/messaging/subscriber.rb +64 -0
- data/lib/essential/messaging/transport.rb +29 -0
- data/lib/essential/resource.rb +127 -0
- data/lib/essential/resource/attr_methods.rb +65 -0
- data/lib/essential/resource/attr_relations.rb +81 -0
- data/lib/essential/resource/create.rb +21 -0
- data/lib/essential/resource/delete.rb +20 -0
- data/lib/essential/resource/list.rb +17 -0
- data/lib/essential/resource/paginator_proxy.rb +115 -0
- data/lib/essential/resource/update.rb +26 -0
- data/lib/essential/version.rb +3 -0
- data/test/essential/account_test.rb +22 -0
- data/test/essential/messaging/channel_test.rb +62 -0
- data/test/essential/messaging/message_test.rb +119 -0
- data/test/essential/messaging/property_test.rb +27 -0
- data/test/essential/messaging/subscriber_test.rb +106 -0
- data/test/essential/messaging/transport_test.rb +37 -0
- data/test/integration/alternate_authentication_test.rb +128 -0
- data/test/test_helper.rb +34 -0
- metadata +125 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
module Essential
|
2
|
+
class Resource
|
3
|
+
module Create
|
4
|
+
|
5
|
+
def create(params, headers: @headers)
|
6
|
+
params = (params || {}).clone
|
7
|
+
params = @params.merge(params) if @params
|
8
|
+
params = filter_attrs(params)
|
9
|
+
response = request(:post,
|
10
|
+
url: url,
|
11
|
+
params: params,
|
12
|
+
headers: headers)
|
13
|
+
json = JSON.parse(response)
|
14
|
+
json = @params.merge(json) if @params
|
15
|
+
json = @attrs.merge(json) if @attrs
|
16
|
+
from_attributes(json)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Essential
|
2
|
+
class Resource
|
3
|
+
module Delete
|
4
|
+
|
5
|
+
def delete(headers: @headers)
|
6
|
+
response = self.class.request(:delete, url: self.url, headers: headers)
|
7
|
+
|
8
|
+
if response.nil? || response.empty?
|
9
|
+
nil
|
10
|
+
else
|
11
|
+
json = JSON.parse(response)
|
12
|
+
json = @params.merge(json) if @params
|
13
|
+
json = @attrs.merge(json) if @attrs
|
14
|
+
init_from(json)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Essential
|
2
|
+
class Resource
|
3
|
+
module List
|
4
|
+
|
5
|
+
def retrieve(sid, fetch: true, headers: @headers)
|
6
|
+
me = self.new(sid: sid, headers: headers)
|
7
|
+
me.fetch() if fetch
|
8
|
+
me
|
9
|
+
end
|
10
|
+
|
11
|
+
def list(params: {}, url: nil, headers: @headers)
|
12
|
+
return PaginatorProxy.new(self, url: url, params: params, headers: headers)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Essential
|
2
|
+
class Resource
|
3
|
+
class PaginatorProxy
|
4
|
+
include Create
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :proxied_class, :url
|
8
|
+
attr_reader :headers, :params
|
9
|
+
attr_reader :total, :pages, :per_page
|
10
|
+
alias :size :total
|
11
|
+
alias :count :total
|
12
|
+
|
13
|
+
def initialize(proxied_class, url: nil, params: {}, headers: {}, attrs: {})
|
14
|
+
@fetched_pages = {}
|
15
|
+
@proxied_class = proxied_class
|
16
|
+
@url = url || @proxied_class.url
|
17
|
+
@params = params || {}
|
18
|
+
@headers = headers || {}
|
19
|
+
@attrs = attrs || {}
|
20
|
+
|
21
|
+
@total = 0
|
22
|
+
@pages = 0
|
23
|
+
@per_page = 0
|
24
|
+
|
25
|
+
self.refresh
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](i)
|
29
|
+
i = i.to_i
|
30
|
+
|
31
|
+
# support reverse wrap
|
32
|
+
i += @total if i < 0
|
33
|
+
|
34
|
+
return nil if i >= @total || i < 0
|
35
|
+
|
36
|
+
page = (i / @per_page) + 1
|
37
|
+
index = i % @per_page
|
38
|
+
|
39
|
+
fetch_page(page) unless @fetched_pages.key?(page)
|
40
|
+
@fetched_pages[page][index]
|
41
|
+
end
|
42
|
+
|
43
|
+
def last
|
44
|
+
self[-1]
|
45
|
+
end
|
46
|
+
|
47
|
+
def each
|
48
|
+
return enum_for(:each) unless block_given?
|
49
|
+
|
50
|
+
(0...self.total).each do |idx|
|
51
|
+
yield self[idx]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def refresh
|
56
|
+
@fetched_pages.clear
|
57
|
+
fetch_page(nil)
|
58
|
+
end
|
59
|
+
|
60
|
+
def as_json
|
61
|
+
self.map(&:as_json)
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
format(
|
66
|
+
'#<%s:0x%s %s>',
|
67
|
+
self.class.name,
|
68
|
+
(self.object_id << 1).to_s(16),
|
69
|
+
[:total, :per_page, :headers, :params].map do |m|
|
70
|
+
attr = self.send(m)
|
71
|
+
if attr
|
72
|
+
format('@%s=%s', m, attr.inspect)
|
73
|
+
else
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
end.compact.join(', ')
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def fetch_page(page)
|
83
|
+
params = (@params || {}).clone
|
84
|
+
params = {page: page}.merge(params) if page
|
85
|
+
|
86
|
+
# Actually touch the network
|
87
|
+
response = @proxied_class.request(
|
88
|
+
:get,
|
89
|
+
url: @url,
|
90
|
+
params: params,
|
91
|
+
headers: @headers
|
92
|
+
)
|
93
|
+
|
94
|
+
# update our paging information
|
95
|
+
@total = response.headers[:total].to_i
|
96
|
+
@per_page = response.headers[:per_page].to_i
|
97
|
+
@pages = (@total.to_f / @per_page).ceil
|
98
|
+
|
99
|
+
json = JSON.parse(response)
|
100
|
+
@fetched_pages[page || 1] = json.map do |json|
|
101
|
+
# persist @attrs across returned objects, as when listing a Subscriber Property.
|
102
|
+
attrs = @attrs.merge(json)
|
103
|
+
@proxied_class.from_attributes(attrs, headers: @headers)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def method_missing(method, *args, &block)
|
110
|
+
@proxied_class.send(method, *args, &block)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Essential
|
2
|
+
class Resource
|
3
|
+
module Update
|
4
|
+
|
5
|
+
def update(params, headers: @headers)
|
6
|
+
params = filter_attrs(params || {})
|
7
|
+
params = @params.merge(params) if @params
|
8
|
+
params = self.as_json.merge(params)
|
9
|
+
params = filter_attrs(params)
|
10
|
+
|
11
|
+
response = self.class.request(
|
12
|
+
:put,
|
13
|
+
url: self.url,
|
14
|
+
params: params,
|
15
|
+
headers: headers
|
16
|
+
)
|
17
|
+
json = JSON.parse(response)
|
18
|
+
json = @params.merge(json) if @params
|
19
|
+
json = @attrs.merge(json) if @attrs
|
20
|
+
init_from(json)
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class AccountTest < EssentialTest
|
4
|
+
|
5
|
+
def test_invalid_credentials
|
6
|
+
error = assert_raises Essential::APIError do
|
7
|
+
Essential::Account.retrieve(headers: {sid: 'sid_nope'})
|
8
|
+
end
|
9
|
+
assert_equal 401, error.http_status
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_valid_credentials
|
13
|
+
account = Essential::Account.retrieve
|
14
|
+
|
15
|
+
prefixed_sid = Essential.sid
|
16
|
+
prefixed_sid = 'ac_' << prefixed_sid unless 0 == prefixed_sid.index('ac_')
|
17
|
+
|
18
|
+
assert_equal prefixed_sid, account.sid
|
19
|
+
assert_equal Essential.token, account.token
|
20
|
+
refute_nil account.name
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ChannelTest < EssentialTest
|
4
|
+
include Essential::Messaging
|
5
|
+
|
6
|
+
def test_retrieve
|
7
|
+
assert_raises ArgumentError do
|
8
|
+
channel = Channel.retrieve('')
|
9
|
+
end
|
10
|
+
|
11
|
+
assert_raises ArgumentError do
|
12
|
+
Channel.retrieve(nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
channel = Channel.retrieve('ch_p03Gjl8Uzn0RkZSpHXHnrw')
|
16
|
+
assert_equal 'ch_p03Gjl8Uzn0RkZSpHXHnrw', channel.sid
|
17
|
+
refute_nil channel.name
|
18
|
+
refute_nil channel.created_at
|
19
|
+
refute_nil channel.updated_at
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_list
|
23
|
+
channels = Channel.list
|
24
|
+
|
25
|
+
assert channels.size > 1, 'must have more than one channel'
|
26
|
+
|
27
|
+
first = channels[0]
|
28
|
+
last = channels[-1]
|
29
|
+
|
30
|
+
refute_equal first.sid, last.sid
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_update
|
34
|
+
secondary = Channel.list.find do |ch|
|
35
|
+
ch.name != 'default' && ch.name != ch.name.reverse
|
36
|
+
end
|
37
|
+
|
38
|
+
refute_nil secondary
|
39
|
+
sid = secondary.sid
|
40
|
+
original_name = secondary.name
|
41
|
+
|
42
|
+
refute_equal 'default', secondary.name
|
43
|
+
|
44
|
+
secondary.update(name: secondary.name.reverse)
|
45
|
+
assert_equal sid, secondary.sid
|
46
|
+
refute_equal original_name, secondary.name
|
47
|
+
assert_equal original_name, secondary.name.reverse
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_relations
|
51
|
+
channel = Channel.retrieve('p03Gjl8Uzn0RkZSpHXHnrw')
|
52
|
+
assert_equal Essential::Account.retrieve, channel.account
|
53
|
+
|
54
|
+
subs = channel.subscribers
|
55
|
+
refute_equal 0, subs.count, 'test requires at least one Subscriber'
|
56
|
+
assert subs.all? {|s| Subscriber === s}
|
57
|
+
|
58
|
+
mess = channel.messages
|
59
|
+
refute_equal 0, mess.count, 'test requires at least one Message'
|
60
|
+
assert mess.all? {|m| Message === m}
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class MessageTest < EssentialTest
|
4
|
+
include Essential::Messaging
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@channel = Channel.list.first
|
8
|
+
assert_equal 'default', @channel.name, 'default channel must exist'
|
9
|
+
@subscriber = @channel.subscribers.first
|
10
|
+
@transport = Transport.retrieve(@subscriber.transport_sid)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_create
|
14
|
+
required = [
|
15
|
+
{body: 'a body', subscriber: @subscriber.sid},
|
16
|
+
{body: 'a body', subscriber: @subscriber.phone_number}
|
17
|
+
]
|
18
|
+
|
19
|
+
optional = [
|
20
|
+
{},
|
21
|
+
{transport: @transport.sid},
|
22
|
+
{transport: @transport.endpoint},
|
23
|
+
{channel: @channel.sid},
|
24
|
+
{channel: @channel.name},
|
25
|
+
]
|
26
|
+
|
27
|
+
required.each do |req|
|
28
|
+
optional.each do |opt|
|
29
|
+
assert_valid_message Message.create(req.merge(opt))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_create_errors
|
36
|
+
map_of_brokenness = {
|
37
|
+
nil => [:body, :subscriber],
|
38
|
+
|
39
|
+
{} => [:body, :subscriber],
|
40
|
+
|
41
|
+
{ body: 'haha'} => [:subscriber],
|
42
|
+
|
43
|
+
{ body: 'haha', subscriber: 'not_a_sid'} => [:subscriber],
|
44
|
+
|
45
|
+
{ body: 'haha',
|
46
|
+
subscriber: 'not_a_phone_number' } => [:subscriber],
|
47
|
+
|
48
|
+
{ body: 'haha',
|
49
|
+
subscriber: @subscriber.sid,
|
50
|
+
transport: 'not_a_sid' } => [:transport],
|
51
|
+
|
52
|
+
{ body: 'haha',
|
53
|
+
subscriber: @subscriber.sid,
|
54
|
+
transport: 'not_a_transport' } => [:transport],
|
55
|
+
|
56
|
+
{ body: 'haha',
|
57
|
+
subscriber: @subscriber.sid,
|
58
|
+
channel: 'not_a_sid' } => [:channel],
|
59
|
+
|
60
|
+
{ body: 'haha',
|
61
|
+
subscriber_sid: @subscriber.sid,
|
62
|
+
channel: 'olives' } => [:channel],
|
63
|
+
}
|
64
|
+
|
65
|
+
map_of_brokenness.each do |params, errors|
|
66
|
+
assert_error_with_params *errors do
|
67
|
+
Message.create(params)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_create_via_aliases
|
73
|
+
assert_valid_message Message.create(
|
74
|
+
subscriber: @subscriber.phone_number,
|
75
|
+
body: 'addressed to @subscriber.phone_number'
|
76
|
+
)
|
77
|
+
|
78
|
+
assert_valid_message Message.create(
|
79
|
+
subscriber: @subscriber.sid,
|
80
|
+
transport: @transport.endpoint,
|
81
|
+
body: 'sent via @transport.endpoint'
|
82
|
+
)
|
83
|
+
|
84
|
+
assert_valid_message Message.create(
|
85
|
+
subscriber: @subscriber.phone_number,
|
86
|
+
channel: @channel.name,
|
87
|
+
body: 'sent via @channel.name'
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_create_new_subscriber
|
92
|
+
next_phone = @channel.subscribers.map(&:phone_number).sort.last.to_i + 1
|
93
|
+
|
94
|
+
m = Message.create(
|
95
|
+
subscriber: format('%d', next_phone),
|
96
|
+
body: format('a message for %d', next_phone)
|
97
|
+
)
|
98
|
+
|
99
|
+
refute_equal m.subscriber_sid, @subscriber.sid, 'subscriber_sid must match'
|
100
|
+
assert_equal m.transport_sid, @transport.sid, 'transport_sid must match'
|
101
|
+
assert_equal m.channel_sid, @channel.sid, 'channel_sid must match'
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_relations
|
105
|
+
message = @subscriber.messages.last
|
106
|
+
|
107
|
+
assert_equal @channel, message.channel
|
108
|
+
assert_equal @subscriber, message.subscriber
|
109
|
+
assert_equal @transport, message.transport
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def assert_valid_message(message)
|
115
|
+
assert_equal message.subscriber_sid, @subscriber.sid, 'subscriber_sid must match'
|
116
|
+
assert_equal message.transport_sid, @transport.sid, 'transport_sid must match'
|
117
|
+
assert_equal message.channel_sid, @channel.sid, 'channel_sid must match'
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class PropertyTest < EssentialTest
|
4
|
+
def setup
|
5
|
+
channel = Essential::Messaging::Channel.list.first
|
6
|
+
@sub_a = channel.subscribers.create(phone_number: '2221110000')
|
7
|
+
@sub_b = channel.subscribers.create(phone_number: '2221110001')
|
8
|
+
refute_equal @sub_a, @sub_b, 'test requires at least two subscribers'
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_set_props
|
12
|
+
props_a = @sub_a.properties
|
13
|
+
existing_names = Set.new(props_a.map(&:name))
|
14
|
+
|
15
|
+
five_new = (5.times).map do
|
16
|
+
props_a.create(name: SecureRandom.uuid, value: SecureRandom.uuid)
|
17
|
+
end
|
18
|
+
|
19
|
+
new_names = Set.new(five_new.map(&:name))
|
20
|
+
|
21
|
+
assert (existing_names & new_names).empty?, 'new_names must be new'
|
22
|
+
|
23
|
+
props_a.refresh
|
24
|
+
existing_names = Set.new(props_a.map(&:name))
|
25
|
+
assert_equal (existing_names & new_names), new_names, 'must contain all new_names'
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class SubscriberTest < EssentialTest
|
4
|
+
include Essential::Messaging
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@channel = Channel.list.first
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_create
|
11
|
+
sub = Subscriber.create(
|
12
|
+
phone_number: '1002003000',
|
13
|
+
channel: @channel.name
|
14
|
+
)
|
15
|
+
|
16
|
+
assert_equal '1002003000', sub.phone_number
|
17
|
+
assert_equal @channel.sid, sub.channel_sid
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_list
|
21
|
+
(1..4).each do |n|
|
22
|
+
Subscriber.create(phone_number: format('100200300%d', n))
|
23
|
+
end
|
24
|
+
|
25
|
+
subs = Subscriber.list
|
26
|
+
assert subs.size > 4, 'should have more than 4 subscribers'
|
27
|
+
|
28
|
+
sub_sids = Set.new(subs.map(&:sid))
|
29
|
+
assert_equal subs.size, sub_sids.size, 'must contain N unique sids'
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_list_bad_channel_sid
|
33
|
+
error = assert_raises Essential::APIError do
|
34
|
+
Subscriber.list(params: {channel: :not_a_real_sid})
|
35
|
+
end
|
36
|
+
|
37
|
+
assert_equal 400, error.http_status
|
38
|
+
refute_nil error.params
|
39
|
+
assert error.params.key?('channel'), 'must complain about channel_sid'
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_list_bad_channel_name
|
43
|
+
error = assert_raises Essential::APIError do
|
44
|
+
Subscriber.list(params: {channel: :not_a_real_name})
|
45
|
+
end
|
46
|
+
|
47
|
+
assert_equal 400, error.http_status
|
48
|
+
refute_nil error.params
|
49
|
+
assert error.params.key?('channel'), 'must complain about channel_name'
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_update
|
53
|
+
sub = Subscriber.create(
|
54
|
+
phone_number: '9998887777',
|
55
|
+
channel: @channel.name
|
56
|
+
)
|
57
|
+
sid = sub.sid.dup
|
58
|
+
|
59
|
+
sub.update(phone_number: '7778889999')
|
60
|
+
assert_equal sid, sub.sid
|
61
|
+
assert_equal '7778889999', sub.phone_number
|
62
|
+
|
63
|
+
sub = Subscriber.retrieve(sid)
|
64
|
+
assert_equal sid, sub.sid
|
65
|
+
assert_equal '7778889999', sub.phone_number
|
66
|
+
|
67
|
+
error = assert_raises Essential::APIError do
|
68
|
+
duplicate = Subscriber.create(
|
69
|
+
phone_number: '1012023333',
|
70
|
+
channel: @channel.sid
|
71
|
+
)
|
72
|
+
duplicate.update(phone_number: sub.phone_number)
|
73
|
+
end
|
74
|
+
assert_equal 400, error.http_status
|
75
|
+
refute_nil error.params
|
76
|
+
assert error.params.key?('phone_number')
|
77
|
+
|
78
|
+
# change it back: if we're running a smoke test, these are
|
79
|
+
# unique and we need to make sure that '7778889999' doesn't
|
80
|
+
# exist at the start of the next run!
|
81
|
+
sub.update(phone_number: '9998887777')
|
82
|
+
assert_equal '9998887777', sub.phone_number
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_unsubscribe
|
86
|
+
sub = Subscriber.create(
|
87
|
+
phone_number: '1002003000',
|
88
|
+
channel: @channel.name
|
89
|
+
)
|
90
|
+
|
91
|
+
response = sub.unsubscribe
|
92
|
+
|
93
|
+
Subscriber.retrieve(sub.sid)
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_relations
|
97
|
+
sub = @channel.subscribers.first
|
98
|
+
|
99
|
+
assert_equal @channel, sub.channel
|
100
|
+
assert_equal sub.transport_sid, sub.transport.sid
|
101
|
+
|
102
|
+
mess = sub.messages
|
103
|
+
refute_equal 0, mess.count
|
104
|
+
assert mess.all? {|m| Message === m}
|
105
|
+
end
|
106
|
+
end
|