txter 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/gateway.rb ADDED
@@ -0,0 +1,45 @@
1
+ module Txter
2
+ class Gateway
3
+ class Request
4
+ end
5
+
6
+ class Response
7
+ def initialize(*args)
8
+ @options = args.last
9
+ end
10
+
11
+ def success?
12
+ :success == @options[:status]
13
+ end
14
+ end
15
+ Success = Txter::Gateway::Response.new(:status => :success)
16
+ Error = Txter::Gateway::Response.new(:status => :error)
17
+
18
+ def self.deliver(*args)
19
+ # subclasses should actually do something here
20
+ Success
21
+ end
22
+
23
+ def self.unblock(*args)
24
+ # subclasses should actually do something here
25
+ Success
26
+ end
27
+
28
+ def self.current
29
+ case Txter.configuration.gateway
30
+ when 'twilio'
31
+ gem 'twiliolib'
32
+ require 'twiliolib'
33
+ GatewayTwilio
34
+ when '4info'
35
+ Gateway4info
36
+ when 'test'
37
+ Txter::Gateway
38
+ else
39
+ raise "You need to specify your Txter gateway!"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ require File.join File.dirname(__FILE__), 'gateway_4info'
45
+ require File.join File.dirname(__FILE__), 'gateway_twilio'
@@ -0,0 +1,114 @@
1
+ module Txter
2
+ class Gateway4info < Txter::Gateway
3
+
4
+ API = 'http://gateway.4info.net/msg'
5
+
6
+ def self.deliver(message, number)
7
+ Gateway4info.perform Request.new.deliver_message(message, number)
8
+ end
9
+
10
+ def self.perform(body)
11
+ if :live == Txter.mode
12
+ require 'net/http'
13
+ uri = URI.parse API
14
+ body = start do |http|
15
+ http.post(
16
+ uri.path,
17
+ body,
18
+ {'Content-Type' => 'text/xml'}
19
+ ).read_body
20
+ end
21
+ Response.new(body)
22
+ else
23
+ Txter.log "Would have sent to 4info.net: #{body}"
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ def self.start
30
+ c = Txter.configuration
31
+ net = c.proxy_address ?
32
+ Net::HTTP::Proxy(
33
+ c.proxy_address,
34
+ c.proxy_port,
35
+ c.proxy_username,
36
+ c.proxy_password) :
37
+ Net::HTTP
38
+ uri = URI.parse API
39
+ net.start(uri.host, uri.port) do |http|
40
+ yield http
41
+ end
42
+ end
43
+
44
+ class Response < Txter::Gateway::Response
45
+ def initialize(xml)
46
+ gem 'hpricot'
47
+ require 'hpricot'
48
+ @xml = xml
49
+ @body = Hpricot.parse(xml)
50
+ end
51
+
52
+ def inspect
53
+ @xml.to_s
54
+ end
55
+
56
+ def [](name)
57
+ nodes = (@body/name)
58
+ 1 == nodes.size ? nodes.first : nodes
59
+ end
60
+
61
+ def success?
62
+ 'Success' == self['message'].inner_text
63
+ end
64
+
65
+ def confirmation_code
66
+ self[:confcode].inner_text
67
+ end
68
+ end
69
+
70
+ class Request < Txter::Gateway::Request
71
+
72
+ attr_accessor :number
73
+ attr_accessor :message
74
+ attr_accessor :config
75
+
76
+ def initialize
77
+ unless Txter.configured?
78
+ raise "You need to configure Txter before using it!"
79
+ end
80
+ self.config = Txter.configuration
81
+ end
82
+
83
+ def deliver_message(message, number)
84
+ self.number = Txter.internationalize(number)
85
+ self.message = message
86
+
87
+ template(:deliver).render(self)
88
+ end
89
+
90
+ def confirm(number)
91
+ self.number = Txter.internationalize(number)
92
+
93
+ template(:confirm).render(self)
94
+ end
95
+
96
+ def unblock(number)
97
+ self.number = Txter.internationalize(number)
98
+
99
+ template(:unblock).render(self)
100
+ end
101
+
102
+ protected
103
+
104
+ def template(name)
105
+ # Haml templates for XML
106
+ require 'cgi'
107
+ templates = Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), '4info_templates', '*.haml')))
108
+ file = templates.detect {|t| File.basename(t).chomp('.haml').to_sym == name.to_sym }
109
+ raise ArgumentError, "Missing 4Info template: #{name}" unless file
110
+ Haml::Engine.new(File.read(file))
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,45 @@
1
+ module Txter
2
+ class GatewayTwilio < Txter::Gateway
3
+
4
+ API_VERSION = '2008-08-01'
5
+
6
+ class << self
7
+
8
+ def deliver(message, to, from = nil)
9
+
10
+ from ||= Txter.configuration.default_from_phone_number
11
+ raise "'From' number required for Twilio" unless from
12
+
13
+ response = post 'To' => to,
14
+ 'From' => from,
15
+ 'Body' => message
16
+
17
+ Net::HTTPCreated == response.code_type ?
18
+ Txter::Gateway::Success :
19
+ Txter::Gateway::Error
20
+ end
21
+
22
+ def account
23
+ @account ||= begin
24
+ if Txter.configuration.client_id.blank? ||
25
+ Txter.configuration.client_key.blank?
26
+ raise "Add your Twilio account id (as client_id) and token (as client_key) to the Txter.configure block"
27
+ end
28
+
29
+ Twilio::RestAccount.new(
30
+ Txter.configuration.client_id,
31
+ Txter.configuration.client_key
32
+ )
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def post(data = {})
39
+ account.request "/#{API_VERSION}/Accounts/#{Txter.configuration.client_id}/SMS/Messages",
40
+ "POST",
41
+ data
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/txter.rb ADDED
@@ -0,0 +1,50 @@
1
+ module Txter
2
+ class << self
3
+ def gateway
4
+ Txter::Gateway.current
5
+ end
6
+
7
+ delegate :deliver, :to => :gateway
8
+ delegate :unblock, :to => :gateway
9
+
10
+ def log(msg)
11
+ if defined?(Rails)
12
+ Rails.logger.info msg
13
+ else
14
+ STDOUT.puts msg
15
+ end
16
+ end
17
+
18
+ def numerize(numberish)
19
+ numberish.to_s.scan(/\d+/).join
20
+ end
21
+
22
+ def internationalize(given_number)
23
+ number = numerize(given_number)
24
+ case number.size
25
+ when 10
26
+ "+1#{number}"
27
+ when 11
28
+ "+#{number}"
29
+ when 12
30
+ number =~ /\+\d(11)/ ? number : nil
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
36
+ def confirmation_message(confirmation_code)
37
+ "4INFO alert confirm. code: #{confirmation_code} Enter code on web to verify phone. Msg&data rates may apply. Freq set by u. T&C & support at www.4info.com. Txt HELP for help"
38
+ end
39
+
40
+ def generate_confirmation_code
41
+ chars = (0..9).to_a + ('A'..'Z').to_a
42
+ (0...6).collect { chars[Kernel.rand(chars.length)] }.join
43
+ end
44
+ end
45
+ end
46
+
47
+ require File.join(File.dirname(__FILE__), 'configuration')
48
+ require File.join(File.dirname(__FILE__), 'gateway')
49
+ require File.join(File.dirname(__FILE__), 'contactable')
50
+ require File.join(File.dirname(__FILE__), 'controller')
data/test/.gitignore ADDED
@@ -0,0 +1 @@
1
+ debug.log
data/test/database.yml ADDED
@@ -0,0 +1,18 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :database: plugin.sqlite.db
4
+ sqlite3:
5
+ :adapter: sqlite3
6
+ :database: ":memory:"
7
+ postgresql:
8
+ :adapter: postgresql
9
+ :username: postgres
10
+ :password: postgres
11
+ :database: plugin_test
12
+ :min_messages: ERROR
13
+ mysql:
14
+ :adapter: mysql
15
+ :host: localhost
16
+ :username: rails
17
+ :password:
18
+ :database: plugin_test
@@ -0,0 +1,75 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class Gateway4infoTest < ActiveSupport::TestCase
4
+
5
+ Error = Txter::Gateway4info::Response.new('<?xml version="1.0" encoding="UTF-8"?>
6
+ <response>
7
+ <status>
8
+ <id>4</id>
9
+ <message>Error</message>
10
+ </status>
11
+ </response>')
12
+ Success = Txter::Gateway4info::Response.new('<?xml version="1.0" ?>
13
+ <response>
14
+ <requestId>F81D4FAE-7DEC-11D0-A765-00A0C91E6BF6</requestId>
15
+ <status>
16
+ <id>1</id>
17
+ <message>Success</message>
18
+ </status>
19
+ </response>')
20
+
21
+ context "with 4info gateway" do
22
+ setup {
23
+ Txter.configure do |config|
24
+ config.client_id = 12345
25
+ config.client_key = 'ABC123'
26
+ config.gateway = '4info'
27
+ end
28
+ }
29
+ context "with stubbed success" do
30
+ setup {
31
+ Txter::Gateway4info.stubs(:perform).returns(Success)
32
+ }
33
+ should "generate a success response object" do
34
+ assert Txter.deliver("msg", "1-555-867-5309").success?
35
+ end
36
+ end
37
+ context "with stubbed error" do
38
+ setup {
39
+ Txter::Gateway4info.stubs(:perform).returns(Error)
40
+ }
41
+ should "generate a success response object" do
42
+ assert !Txter.deliver("msg", "1-555-867-5309").success?
43
+ end
44
+ end
45
+ should "create proper xml for delivery" do
46
+ expected = <<-EOXML
47
+ <?xml version='1.0' encoding='utf-8' ?>
48
+ <request clientId='12345' clientKey='ABC123' type='MESSAGE'>
49
+ <message>
50
+ <recipient>
51
+ <type>5</type>
52
+ <id>+15558675309</id>
53
+ </recipient>
54
+ <text>msg</text>
55
+ </message>
56
+ </request>
57
+ EOXML
58
+ assert_equal expected, Txter::Gateway4info::Request.new.deliver_message("msg", "1-555-867-5309")
59
+ end
60
+ should "create proper xml for unblock" do
61
+ expected = <<-EOXML
62
+ <?xml version='1.0' encoding='utf-8' ?>
63
+ <request clientId='12345' clientKey='ABC123' type='UNBLOCK'>
64
+ <unblock>
65
+ <recipient>
66
+ <type>5</type>
67
+ <id>+15558675309</id>
68
+ </recipient>
69
+ </unblock>
70
+ </request>
71
+ EOXML
72
+ assert_equal expected, Txter::Gateway4info::Request.new.unblock("1-555-867-5309")
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,15 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class Gateway4infoTest < ActiveSupport::TestCase
4
+
5
+ context "with twilio gateway" do
6
+ setup {
7
+ Txter.configure do |config|
8
+ config.client_id = 12345
9
+ config.client_key = 'ABC123'
10
+ config.gateway = 'twilio'
11
+ end
12
+ }
13
+ # TODO: figure out what's left to test
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ gem 'test-unit'
3
+ require 'test/unit'
4
+ require 'mocha'
5
+ require 'active_support'
6
+ require 'active_record'
7
+ require 'active_support/test_case'
8
+ require 'shoulda'
9
+ require File.join(File.dirname(__FILE__), "..", 'lib', 'txter')
10
+
11
+ # default test configuration
12
+ Txter.configure do |config|
13
+ config.client_id = '1'
14
+ config.client_key = 'ABCDEF'
15
+ config.gateway = 'test'
16
+ end
17
+
18
+ Txter.mode = :test
19
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
20
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
21
+ ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
22
+
23
+ ActiveRecord::Schema.define(:version => 1) do
24
+ create_table :users do |t|
25
+ t.column :sms_phone_number, :string
26
+ t.column :sms_confirmed_phone_number, :string
27
+ t.column :sms_blocked, :boolean
28
+ t.column :sms_confirmation_attempted, :datetime
29
+ t.column :sms_confirmation_code, :string
30
+
31
+ t.column :type, :string
32
+ end
33
+ end
34
+
35
+ class User < ActiveRecord::Base
36
+ include Txter::Contactable
37
+ end
38
+
39
+ # kill all network access
40
+ module Txter
41
+ class Request
42
+ def start
43
+ raise "You forgot to stub out your net requests!"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,270 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class TxterContactableTest < ActiveSupport::TestCase
4
+
5
+ Success = Txter::Gateway::Response.new(:status => :success)
6
+ Error = Txter::Gateway::Response.new(:status => :error)
7
+
8
+ context "contactable class" do
9
+ setup {
10
+ @klass = Class.new
11
+ @klass.send :include, Txter::Contactable
12
+ Txter.configure do |config|
13
+ config.client_id = 12345
14
+ config.client_key = 'ABC123'
15
+ config.gateway = 'test'
16
+ end
17
+ }
18
+ Txter::Contactable::Attributes.each do |attribute|
19
+ should "begin with appropriate default for #{attribute}_column" do
20
+ assert_equal attribute, @klass.send("#{attribute}_column")
21
+ end
22
+ should "allow setting #{attribute}_column" do
23
+ new_column_name = :custom_column
24
+ @klass.send "#{attribute}_column", new_column_name
25
+ assert_equal new_column_name, @klass.send("#{attribute}_column")
26
+ end
27
+ end
28
+ end
29
+
30
+ context "contactable instance" do
31
+ setup {
32
+ User.delete_all
33
+ @user = User.create! User.sms_phone_number_column => '(555) 123-4567'
34
+ }
35
+
36
+ should "normalize phone number" do
37
+ assert_equal '5551234567', @user.txter_sms_phone_number
38
+ end
39
+ context "when phone number is blank" do
40
+ setup { @user.txter_sms_phone_number = nil}
41
+ context "confirming phone number" do
42
+ setup { @user.send_sms_confirmation! }
43
+ should_not_change "any attributes" do
44
+ @user.attributes.inspect
45
+ end
46
+ end
47
+ context "sending message" do
48
+ setup {
49
+ Txter.gateway.stubs(:perform).returns(Success)
50
+ @worked = @user.send_sms!('message')
51
+ }
52
+ should "not work" do assert !@worked end
53
+ should_not_change "any attributes" do
54
+ @user.attributes.inspect
55
+ end
56
+ end
57
+ end
58
+
59
+ context "when phone number exists" do
60
+ setup { @user.txter_sms_phone_number = "206-555-5555"}
61
+ context "confirming phone number" do
62
+ setup {
63
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
64
+ @worked = @user.send_sms_confirmation!
65
+ }
66
+ should "work" do assert @worked end
67
+ should "save confirmation number in proper attribute" do
68
+ assert @user.txter_sms_confirmation_code
69
+ end
70
+ should "set confirmation attempted time" do
71
+ assert @user.txter_sms_confirmation_attempted > 3.minutes.ago
72
+ end
73
+ should_change "stored code" do
74
+ @user.txter_sms_confirmation_code
75
+ end
76
+ should "not have number confirmed yet" do
77
+ assert !@user.sms_confirmed?
78
+ end
79
+ context "calling sms_confirm_with(right_code)" do
80
+ setup { @user.sms_confirm_with(@user.txter_sms_confirmation_code) }
81
+ should "work" do
82
+ assert @worked
83
+ end
84
+ should "save the phone number into the confirmed attribute" do
85
+ assert_equal @user.txter_sms_confirmed_phone_number,
86
+ @user.txter_sms_phone_number
87
+ end
88
+ should_change "confirmed phone number attribute" do
89
+ @user.txter_sms_confirmed_phone_number
90
+ end
91
+ context "and then attempting to confirm another number" do
92
+ setup {
93
+ @user.txter_sms_phone_number = "206-555-5555"
94
+ Txter.stubs(:deliver).returns(Success).once
95
+ @user.send_sms_confirmation!
96
+ }
97
+ should "eliminate the previous confirmed phone number" do
98
+ assert @user.txter_sms_confirmed_phone_number.blank?
99
+ end
100
+ should "un-confirm the record" do
101
+ assert !@user.sms_confirmed?
102
+ end
103
+ end
104
+ end
105
+ context "calling sms_confirm_with(right code, wrong case)" do
106
+ setup {
107
+ @downcased_code = @user.txter_sms_confirmation_code.downcase
108
+ @worked = @user.sms_confirm_with(@downcased_code)
109
+ }
110
+ should "have good test data" do
111
+ assert_not_equal @downcased_code,
112
+ @user.txter_sms_confirmation_code
113
+ end
114
+ should "work" do
115
+ assert @worked
116
+ end
117
+ should "save the phone number into the confirmed attribute" do
118
+ assert_equal @user.txter_sms_confirmed_phone_number,
119
+ @user.txter_sms_phone_number
120
+ end
121
+ end
122
+ context "calling sms_confirm_with(wrong_code)" do
123
+ setup { @worked = @user.sms_confirm_with('wrong_code') }
124
+ should "not work" do
125
+ assert !@worked
126
+ end
127
+ should "not save the phone number into the confirmed attribute" do
128
+ assert_not_equal @user.txter_sms_confirmed_phone_number,
129
+ @user.txter_sms_phone_number
130
+ end
131
+ should_not_change "confirmed phone number attribute" do
132
+ @user.reload.txter_sms_confirmed_phone_number
133
+ end
134
+ end
135
+ end
136
+ context "confirming phone number with a custom short code" do
137
+ context "with expectations" do
138
+ setup {
139
+ Txter.configure do |config|
140
+ config.short_code = '0005'
141
+ config.gateway = 'test'
142
+ config.client_id = 1
143
+ config.client_key = 'ABC123'
144
+ end
145
+ message = "long message blah blah MYCODE blah"
146
+ Txter.expects(:generate_confirmation_code).returns('MYCODE').once
147
+ Txter.expects(:confirmation_message).returns(message).once
148
+ Txter::Request.any_instance.expects(:deliver_message).with(message, @user.txter_sms_phone_number).once
149
+ @user.send_sms_confirmation!
150
+ }
151
+ end
152
+ context "(normal)" do
153
+ setup {
154
+ Txter.configure do |config|
155
+ config.short_code = '0005'
156
+ config.gateway = 'test'
157
+ config.client_id = 1
158
+ config.client_key = 'ABC123'
159
+ end
160
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
161
+ @worked = @user.send_sms_confirmation!
162
+ }
163
+ should "work" do
164
+ assert @worked
165
+ end
166
+ end
167
+ end
168
+ context "confirming phone number when the confirmation fails for some reason" do
169
+ setup {
170
+ Txter.stubs(:deliver).returns(Error)
171
+ @worked = @user.send_sms_confirmation!
172
+ }
173
+ should "not work" do assert !@worked end
174
+ should "not save confirmation number" do
175
+ assert @user.txter_sms_confirmation_code.blank?
176
+ end
177
+ should_not_change "stored code" do
178
+ @user.txter_sms_confirmation_code
179
+ end
180
+ end
181
+ end
182
+
183
+ context "when the number is not confirmed" do
184
+ context "sending a message" do
185
+ setup {
186
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
187
+ @result = @user.send_sms!('message')
188
+ }
189
+ should "send send no messages" do
190
+ assert_equal false, @result
191
+ end
192
+ end
193
+ end
194
+ context "when the number is blocked" do
195
+ setup {
196
+ @user.txter_sms_blocked = true
197
+ @user.save!
198
+ }
199
+ context "sending a message" do
200
+ setup { @result = @user.send_sms!('message') }
201
+ should "send nothing" do
202
+ assert_equal false, @result
203
+ end
204
+ end
205
+ end
206
+ context "when the number is confirmed" do
207
+ setup {
208
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
209
+ @user.stubs(:sms_confirmed?).returns(true)
210
+ }
211
+ context "sending a message" do
212
+ setup { @result = @user.send_sms!('message') }
213
+ should "send send exactly one message" do
214
+ assert_equal [7], @result
215
+ end
216
+ end
217
+ context "sending a blank message" do
218
+ setup { @result = @user.send_sms!('') }
219
+ should "send send zero messages" do
220
+ assert_equal false, @result
221
+ end
222
+ end
223
+ context "sending a huge message" do
224
+ context "without the allow_multiple flag" do
225
+ should "raise an error" do
226
+ assert_raises ArgumentError do
227
+ @user.send_sms!("A"*200)
228
+ end
229
+ end
230
+ end
231
+ context "with the allow_multiple flag" do
232
+ setup { @result = @user.send_sms!("A"*200, true) }
233
+ should "send multiple messages" do
234
+ assert_equal [160, 40], @result
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ context "when the number is not blocked" do
241
+ setup {
242
+ Txter::Request.any_instance.expects(:perform).never
243
+ }
244
+ context "unblocking" do
245
+ setup { @worked = @user.unblock_sms! }
246
+ should "not do anything" do
247
+ assert !@worked
248
+ end
249
+ should_not_change "any attributes" do
250
+ @user.attributes.inspect
251
+ end
252
+ end
253
+ end
254
+ context "when the number is blocked" do
255
+ setup {
256
+ Txter::Request.any_instance.stubs(:perform).returns(Success)
257
+ @user.update_attributes!(:sms_blocked => true)
258
+ }
259
+ context "unblocking" do
260
+ setup { @worked = @user.unblock_sms! }
261
+ should "work" do
262
+ assert @worked
263
+ end
264
+ should_change "blocked attribute" do
265
+ @user.reload.sms_blocked
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end