clickatell-ruby19 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ require "cgi"
2
+ module Clickatell
3
+ class API
4
+
5
+ # Represents a Clickatell HTTP gateway command in the form
6
+ # of a complete URL (the raw, low-level request).
7
+ class Command
8
+ API_SERVICE_HOST = 'api.clickatell.com'
9
+
10
+ def initialize(command_name, service = 'http', opts={})
11
+ @command_name = command_name
12
+ @service = service
13
+ @options = { :secure => false }.merge(opts)
14
+ end
15
+
16
+ # Returns a URL for the given parameters (a hash).
17
+ def with_params(param_hash)
18
+ param_string = '?' + param_hash.map { |key, value| "#{::CGI.escape(key.to_s)}=#{::CGI.escape(value.to_s)}" }.sort.join('&')
19
+ return URI.parse(File.join(api_service_uri, @command_name + param_string))
20
+ end
21
+
22
+ protected
23
+ def api_service_uri
24
+ protocol = @options[:secure] ? 'https' : 'http'
25
+ api_service_host = ((Clickatell::API.api_service_host.nil? || Clickatell::API.api_service_host.empty?) ? API_SERVICE_HOST : Clickatell::API.api_service_host)
26
+ return "#{protocol}://#{api_service_host}/#{@service}/"
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,69 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+
4
+ module Clickatell
5
+ class API
6
+
7
+ class FakeHttpResponse
8
+ def body
9
+ "test"
10
+ end
11
+ end
12
+
13
+ # Used to run commands agains the Clickatell gateway.
14
+ class CommandExecutor
15
+ def initialize(authentication_hash, secure=false, debug=false, test_mode=false)
16
+ @authentication_hash = authentication_hash
17
+ @debug = debug
18
+ @secure = secure
19
+ @test_mode = test_mode
20
+
21
+ allow_request_recording if @test_mode
22
+ end
23
+
24
+ def in_test_mode?
25
+ @test_mode
26
+ end
27
+
28
+ # Builds a command object and sends it using HTTP GET.
29
+ # Will output URLs as they are requested to stdout when
30
+ # debugging is enabled.
31
+ def execute(command_name, service, parameters={})
32
+ request_uri = command(command_name, service, parameters)
33
+ puts "[debug] Sending request to #{request_uri}" if @debug
34
+ get_response(request_uri)
35
+ end
36
+
37
+ protected
38
+ def command(command_name, service, parameters) #:nodoc:
39
+ Command.new(command_name, service, :secure => @secure).with_params(
40
+ parameters.merge(@authentication_hash)
41
+ )
42
+ end
43
+
44
+ def get_response(uri)
45
+ if in_test_mode?
46
+ sms_requests << uri
47
+ [FakeHttpResponse.new]
48
+ else
49
+ http = Net::HTTP.new(uri.host, uri.port)
50
+ http.use_ssl = (uri.scheme == 'https')
51
+ http.start do |http|
52
+ resp, body = http.get([uri.path, uri.query].join('?'))
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def allow_request_recording
60
+ class << self
61
+ define_method :sms_requests do
62
+ @sms_requests ||= []
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,25 @@
1
+ module Clickatell
2
+ class API
3
+
4
+ # Clickatell API Error exception.
5
+ class Error < StandardError
6
+ attr_reader :code, :message
7
+
8
+ def initialize(code, message)
9
+ @code, @message = code, message
10
+ end
11
+
12
+ # Creates a new Error from a Clickatell HTTP response string
13
+ # e.g.:
14
+ #
15
+ # Error.parse("ERR: 001, Authentication error")
16
+ # # => #<Clickatell::API::Error code='001' message='Authentication error'>
17
+ def self.parse(error_string)
18
+ error_details = error_string.split(':').last.strip
19
+ code, message = error_details.split(',').map { |s| s.strip }
20
+ self.new(code, message)
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ module Clickatell
2
+ class API
3
+
4
+ class MessageStatus
5
+ STATUS_MAP = {
6
+ 1 => 'Message unknown',
7
+ 2 => 'Message queued',
8
+ 3 => 'Delivered to gateway',
9
+ 4 => 'Received by recipient',
10
+ 5 => 'Error with message',
11
+ 6 => 'User cancelled messaged delivery',
12
+ 7 => 'Error delivering message',
13
+ 8 => 'OK',
14
+ 9 => 'Routing error',
15
+ 10 => 'Message expired',
16
+ 11 => 'Message queued for later delivery',
17
+ 12 => 'Out of credit'
18
+ }
19
+
20
+ def self.[](code)
21
+ STATUS_MAP[code]
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ require 'yaml'
2
+
3
+ module Clickatell
4
+
5
+ # Used to parse HTTP responses returned from Clickatell API calls.
6
+ class Response
7
+
8
+ class << self
9
+ PARSE_REGEX = /[A-Za-z0-9]+:.*?(?:(?=[A-Za-z0-9]+:)|$)/
10
+
11
+ # Returns the HTTP response body data as a hash.
12
+ def parse(http_response)
13
+ return { 'OK' => 'session_id' } if API.test_mode
14
+
15
+ if http_response.body.scan(/ERR/).any?
16
+ raise Clickatell::API::Error.parse(http_response.body)
17
+ end
18
+ results = http_response.body.split("\n").map do |line|
19
+ # YAML.load converts integer strings that have leading zeroes into integers
20
+ # using octal rather than decimal. This isn't what we want, so we'll strip out any
21
+ # leading zeroes in numbers here.
22
+ response_fields = line.scan(PARSE_REGEX)
23
+ response_fields = response_fields.collect { |field| field.gsub(/\b0+(\d+)\b/, '\1') }
24
+ YAML.load(response_fields.join("\n"))
25
+ end
26
+ results.length == 1 ? results.first : results
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ module Clickatell
2
+ module Utility
3
+ end
4
+ end
5
+
6
+ require File.join(File.dirname(__FILE__), *%w[utility/options])
@@ -0,0 +1,109 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ module Clickatell
5
+ module Utility
6
+ class Options #:nodoc:
7
+ class << self
8
+
9
+ def parse(args)
10
+ @options = self.default_options
11
+ parser = OptionParser.new do |opts|
12
+ opts.banner = "Usage: sms [options] recipient(s) message"
13
+ opts.separator " Recipients can be a comma-separated list, up to 100 max."
14
+ opts.separator ""
15
+ opts.separator "Specific options:"
16
+
17
+ opts.on('-u', '--username USERNAME',
18
+ "Specify the clickatell username (overrides ~/.clickatell setting)") do |username|
19
+ @options.username = username
20
+ end
21
+
22
+ opts.on('-p', '--password PASSWORD',
23
+ "Specify the clickatell password (overrides ~/.clickatell setting)") do |password|
24
+ @options.password = password
25
+ end
26
+
27
+ opts.on('-k', '--apikey API_KEY',
28
+ "Specify the clickatell API key (overrides ~/.clickatell setting)") do |key|
29
+ @options.api_key = key
30
+ end
31
+
32
+ opts.on('-f', '--from NAME_OR_NUMBER',
33
+ "Specify the name or number that the SMS will appear from") do |from|
34
+ @options.from = from
35
+ end
36
+
37
+ opts.on('-b', '--show-balance',
38
+ "Shows the total number of credits remaining on your account") do
39
+ @options.show_balance = true
40
+ end
41
+
42
+ opts.on('-s', '--status MESSAGE_ID',
43
+ "Displays the status of the specified message.") do |message_id|
44
+ @options.message_id = message_id
45
+ @options.show_status = true
46
+ end
47
+
48
+ opts.on('-S', '--secure',
49
+ "Sends request using HTTPS") do
50
+ Clickatell::API.secure_mode = true
51
+ end
52
+
53
+ opts.on('-d', '--debug') do
54
+ Clickatell::API.debug_mode = true
55
+ end
56
+
57
+ opts.on_tail('-h', '--help', "Show this message") do
58
+ puts opts
59
+ exit
60
+ end
61
+
62
+ opts.on_tail('-v', '--version') do
63
+ puts "Ruby Clickatell SMS Utility #{Clickatell::VERSION}"
64
+ exit
65
+ end
66
+ end
67
+
68
+ @options.recipient = args[-2].split(',') rescue nil
69
+ @options.message = args[-1]
70
+
71
+ parser.parse!(args)
72
+
73
+ if (@options.message.nil? || @options.recipient.nil?) && send_message?
74
+ puts "You must specify a recipient and message!"
75
+ puts parser
76
+ exit
77
+ end
78
+
79
+ return @options
80
+
81
+ rescue OptionParser::MissingArgument => e
82
+ switch_given = e.message.split(':').last.strip
83
+ puts "The #{switch_given} option requires an argument."
84
+ puts parser
85
+ exit
86
+ end
87
+
88
+ def default_options
89
+ options = OpenStruct.new
90
+ config_file = File.open(File.join(ENV['HOME'], '.clickatell'))
91
+ config = YAML.load(config_file)
92
+ options.username = config['username']
93
+ options.password = config['password']
94
+ options.api_key = config['api_key']
95
+ options.from = config['from']
96
+ return options
97
+ rescue Errno::ENOENT
98
+ return options
99
+ end
100
+
101
+ def send_message?
102
+ (@options.show_status.nil? &&
103
+ @options.show_balance.nil?)
104
+ end
105
+
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,13 @@
1
+ module Clickatell #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 7
5
+ TINY = 1
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+
9
+ def self.to_s
10
+ STRING
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class Hash
2
+ # Returns a new hash containing only the keys specified
3
+ # that exist in the current hash.
4
+ #
5
+ # {:a => '1', :b => '2', :c => '3'}.only(:a, :c)
6
+ # # => {:a => '1', :c => '3'}
7
+ #
8
+ # Keys that do not exist in the original hash are ignored.
9
+ def only(*keys)
10
+ inject( {} ) do |new_hash, (key, value)|
11
+ new_hash[key] = value if keys.include?(key)
12
+ new_hash
13
+ end
14
+ end
15
+ end
data/scripts/txt2html ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'redcloth'
5
+ require 'syntax/convertors/html'
6
+ require 'erb'
7
+ require File.dirname(__FILE__) + '/../lib/clickatell/version.rb'
8
+
9
+ version = Clickatell::VERSION::STRING
10
+ download = 'http://rubyforge.org/projects/clickatell'
11
+
12
+ class Fixnum
13
+ def ordinal
14
+ # teens
15
+ return 'th' if (10..19).include?(self % 100)
16
+ # others
17
+ case self % 10
18
+ when 1: return 'st'
19
+ when 2: return 'nd'
20
+ when 3: return 'rd'
21
+ else return 'th'
22
+ end
23
+ end
24
+ end
25
+
26
+ class Time
27
+ def pretty
28
+ return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
29
+ end
30
+ end
31
+
32
+ def convert_syntax(syntax, source)
33
+ return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
34
+ end
35
+
36
+ if ARGV.length >= 1
37
+ src, template = ARGV
38
+ template ||= File.dirname(__FILE__) + '/../website/template.rhtml'
39
+
40
+ else
41
+ puts("Usage: #{File.split($0).last} source.txt [template.rhtml] > output.html")
42
+ exit!
43
+ end
44
+
45
+ template = ERB.new(File.open(template).read)
46
+
47
+ title = nil
48
+ body = nil
49
+ File.open(src) do |fsrc|
50
+ title_text = fsrc.readline
51
+ body_text = fsrc.read
52
+ syntax_items = []
53
+ body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</>!m){
54
+ ident = syntax_items.length
55
+ element, syntax, source = $1, $2, $3
56
+ syntax_items << "<#{element} class='syntax'>#{convert_syntax(syntax, source)}</#{element}>"
57
+ "syntax-temp-#{ident}"
58
+ }
59
+ title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
60
+ body = RedCloth.new(body_text).to_html
61
+ body.gsub!(%r!(?:<pre><code>)?syntax-temp-(d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
62
+ end
63
+ stat = File.stat(src)
64
+ created = stat.ctime
65
+ modified = stat.mtime
66
+
67
+ $stdout << template.result(binding)
data/spec/api_spec.rb ADDED
@@ -0,0 +1,239 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.dirname(__FILE__) + '/../lib/clickatell'
3
+
4
+ class FakeHttp
5
+ def start(&block)
6
+ yield self
7
+ end
8
+ end
9
+
10
+ module Clickatell
11
+
12
+ describe "API Command" do
13
+ before do
14
+ @command = API::Command.new('cmdname')
15
+ end
16
+
17
+ after do
18
+ Clickatell::API.api_service_host = nil
19
+ end
20
+
21
+ it "should return encoded URL for the specified command and parameters" do
22
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
23
+ url.should == URI.parse("http://api.clickatell.com/http/cmdname?param_one=abc&param_two=123")
24
+ end
25
+
26
+ it "should URL encode any special characters in parameters" do
27
+ url = @command.with_params(:param_one => 'abc', :param_two => 'hello world & goodbye cruel world <grin>')
28
+ url.should == URI.parse("http://api.clickatell.com/http/cmdname?param_one=abc&param_two=hello+world+%26+goodbye+cruel+world+%3Cgrin%3E")
29
+ end
30
+
31
+ it "should use a custom host when constructing command URLs if specified" do
32
+ Clickatell::API.api_service_host = 'api.clickatell-custom.co.uk'
33
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
34
+ url.should == URI.parse("http://api.clickatell-custom.co.uk/http/cmdname?param_one=abc&param_two=123")
35
+ end
36
+
37
+ it "should use the default host if specified custom host is nil" do
38
+ Clickatell::API.api_service_host = nil
39
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
40
+ url.should == URI.parse("http://api.clickatell.com/http/cmdname?param_one=abc&param_two=123")
41
+ end
42
+
43
+ it "should use the default host if specified custom host is an empty string" do
44
+ Clickatell::API.api_service_host = ''
45
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
46
+ url.should == URI.parse("http://api.clickatell.com/http/cmdname?param_one=abc&param_two=123")
47
+ end
48
+ end
49
+
50
+ describe "Secure API Command" do
51
+ before do
52
+ @command = API::Command.new('cmdname', 'http', :secure => true)
53
+ end
54
+
55
+ it "should use HTTPS" do
56
+ url = @command.with_params(:param_one => 'abc', :param_two => '123')
57
+ url.should == URI.parse("https://api.clickatell.com/http/cmdname?param_one=abc&param_two=123")
58
+ end
59
+ end
60
+
61
+ describe "Command executor" do
62
+ it "should create an API command with the given params" do
63
+ executor = API::CommandExecutor.new(:session_id => '12345')
64
+ executor.stubs(:get_response).returns([])
65
+ API::Command.expects(:new).with('cmdname', 'http', :secure => false).returns(command = stub('Command'))
66
+ command.expects(:with_params).with(:param_one => 'foo', :session_id => '12345').returns(stub_everything('URI'))
67
+ executor.execute('cmdname', 'http', :param_one => 'foo')
68
+ end
69
+
70
+ it "should send the URI generated by the created command via HTTP get and return the response" do
71
+ executor = API::CommandExecutor.new(:session_id => '12345')
72
+ command_uri = URI.parse('http://clickatell.com:8080/path?foo=bar')
73
+ API::Command.stubs(:new).returns(command = stub('Command', :with_params => command_uri))
74
+ Net::HTTP.stubs(:new).with(command_uri.host, command_uri.port).returns(http = FakeHttp.new)
75
+ http.stubs(:use_ssl=)
76
+ http.stubs(:get).with('/path?foo=bar').returns([response = stub('HTTP Response'), 'response body'])
77
+ executor.execute('cmdname', 'http').should == response
78
+ end
79
+
80
+ it "should send the command over a secure HTTPS connection if :secure option is set to true" do
81
+ executor = API::CommandExecutor.new({:session_id => '12345'}, secure = true)
82
+ Net::HTTP.stubs(:new).returns(http = mock('HTTP'))
83
+ http.expects(:use_ssl=).with(true)
84
+ http.stubs(:start).returns([])
85
+ executor.execute('cmdname', 'http')
86
+ end
87
+ end
88
+
89
+ describe "API" do
90
+ before do
91
+ API.debug_mode = false
92
+ API.secure_mode = false
93
+ API.test_mode = false
94
+
95
+ @executor = mock('command executor')
96
+ @api = API.new(:session_id => '1234')
97
+ API::CommandExecutor.stubs(:new).with({:session_id => '1234'}, false, false, false).returns(@executor)
98
+ end
99
+
100
+ it "should use the api_id, username and password to authenticate and return the new session id" do
101
+ @executor.expects(:execute).with('auth', 'http',
102
+ :api_id => '1234',
103
+ :user => 'joebloggs',
104
+ :password => 'superpass'
105
+ ).returns(response = stub('response'))
106
+ Response.stubs(:parse).with(response).returns('OK' => 'new_session_id')
107
+ @api.authenticate('1234', 'joebloggs', 'superpass').should == 'new_session_id'
108
+ end
109
+
110
+ it "should support ping, using the current session_id" do
111
+ @executor.expects(:execute).with('ping', 'http', :session_id => 'abcdefg').returns(response = stub('response'))
112
+ @api.ping('abcdefg').should == response
113
+ end
114
+
115
+ it "should support sending messages to a specified number, returning the message id" do
116
+ @executor.expects(:execute).with('sendmsg', 'http',
117
+ :to => '4477791234567',
118
+ :text => 'hello world & goodbye'
119
+ ).returns(response = stub('response'))
120
+ Response.stubs(:parse).with(response).returns('ID' => 'message_id')
121
+ @api.send_message('4477791234567', 'hello world & goodbye').should == 'message_id'
122
+ end
123
+
124
+ it "should support sending messages to a multiple numbers, returning the message ids" do
125
+ @executor.expects(:execute).with('sendmsg', 'http',
126
+ :to => '4477791234567,447779999999',
127
+ :text => 'hello world & goodbye'
128
+ ).returns(response = stub('response'))
129
+ Response.stubs(:parse).with(response).returns([{'ID' => 'message_1_id'}, {'ID' => 'message_2_id'}])
130
+ @api.send_message(['4477791234567', '447779999999'], 'hello world & goodbye').should == ['message_1_id', 'message_2_id']
131
+ end
132
+
133
+ it "should set the :from parameter and set the :req_feat to 48 when using a custom from string when sending a message" do
134
+ @executor.expects(:execute).with('sendmsg', 'http', has_entries(:from => 'LUKE', :req_feat => '48')).returns(response = stub('response'))
135
+ Response.stubs(:parse).with(response).returns('ID' => 'message_id')
136
+ @api.send_message('4477791234567', 'hello world', :from => 'LUKE')
137
+ end
138
+
139
+ it "should set the :mo flag to 1 when :set_mobile_originated is true when sending a message" do
140
+ @executor.expects(:execute).with('sendmsg', 'http', has_entry(:mo => '1')).returns(response=mock('response'))
141
+ Response.stubs(:parse).with(response).returns('ID' => 'message_id')
142
+ @api.send_message('4477791234567', 'hello world', :set_mobile_originated => true)
143
+ end
144
+
145
+ it "should set the callback flag to the number passed in the options hash" do
146
+ @executor.expects(:execute).with('sendmsg', 'http', has_entry(:callback => 1)).returns(response=mock('response'))
147
+ Response.stubs(:parse).with(response).returns('ID' => 'message_id')
148
+ @api.send_message('4477791234567', 'hello world', :callback => 1)
149
+ end
150
+
151
+ it "should ignore any invalid parameters when sending a message" do
152
+ @executor.expects(:execute).with('sendmsg', 'http', Not(has_key(:any_old_param))).returns(response = stub('response'))
153
+ Response.stubs(:parse).returns('ID' => 'foo')
154
+ @api.send_message('4477791234567', 'hello world', :from => 'LUKE', :any_old_param => 'test')
155
+ end
156
+
157
+ it "should support message status query for a given message id, returning the message status" do
158
+ @executor.expects(:execute).with('querymsg', 'http', :apimsgid => 'messageid').returns(response = stub('response'))
159
+ Response.expects(:parse).with(response).returns('ID' => 'message_id', 'Status' => 'message_status')
160
+ @api.message_status('messageid').should == 'message_status'
161
+ end
162
+
163
+ it "should support balance query, returning number of credits as a float" do
164
+ @executor.expects(:execute).with('getbalance', 'http', {}).returns(response=mock('response'))
165
+ Response.stubs(:parse).with(response).returns('Credit' => '10.0')
166
+ @api.account_balance.should == 10.0
167
+ end
168
+
169
+ it "should raise an API::Error if the response parser raises" do
170
+ @executor.stubs(:execute)
171
+ Response.stubs(:parse).raises(Clickatell::API::Error.new('', ''))
172
+ proc { @api.account_balance }.should raise_error(Clickatell::API::Error)
173
+ end
174
+ end
175
+
176
+ describe API, ' when authenticating' do
177
+ it "should authenticate to retrieve a session_id and return a new API instance using that session id" do
178
+ API.stubs(:new).returns(api = mock('api'))
179
+ api.stubs(:authenticate).with('my_api_key', 'joebloggs', 'mypassword').returns('new_session_id')
180
+ api.expects(:auth_options=).with(:session_id => 'new_session_id')
181
+ API.authenticate('my_api_key', 'joebloggs', 'mypassword')
182
+ end
183
+ end
184
+
185
+ describe API, ' with no authentication options set' do
186
+ it "should build commands with no authentication options" do
187
+ api = API.new
188
+ API::CommandExecutor.stubs(:new).with({}, false, false, false).returns(executor = stub('command executor'))
189
+ executor.stubs(:execute)
190
+ api.ping('1234')
191
+ end
192
+ end
193
+
194
+ describe API, ' in secure mode' do
195
+ it "should execute commands securely" do
196
+ API.secure_mode = true
197
+ api = API.new
198
+ API::CommandExecutor.expects(:new).with({}, true, false, false).returns(executor = stub('command executor'))
199
+ executor.stubs(:execute)
200
+ api.ping('1234')
201
+ end
202
+ end
203
+
204
+ describe "API Error" do
205
+ it "should parse http response string to create error" do
206
+ response_string = "ERR: 001, Authentication error"
207
+ error = Clickatell::API::Error.parse(response_string)
208
+ error.code.should == '001'
209
+ error.message.should == 'Authentication error'
210
+ end
211
+ end
212
+
213
+ describe API, "#test_mode" do
214
+ before(:each) do
215
+ API.secure_mode = false
216
+ API.test_mode = true
217
+ @api = API.new
218
+ end
219
+
220
+ it "should create a new CommandExecutor with test_mode parameter set to true" do
221
+ API::CommandExecutor.expects(:new).with({}, false, false, true).once.returns(executor = mock('command executor'))
222
+ executor.stubs(:execute)
223
+ executor.stubs(:sms_requests).returns([])
224
+ @api.ping('1234')
225
+ end
226
+
227
+ it "should record all commands" do
228
+ @api.ping('1234')
229
+ @api.sms_requests.should_not be_empty
230
+ end
231
+
232
+ it "should return the recorded commands in a flattened array" do
233
+ @api.ping('1234')
234
+ @api.sms_requests.size.should == 1
235
+ @api.sms_requests.first.should_not be_instance_of(Array)
236
+ end
237
+ end
238
+
239
+ end