mote_sms 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .ruby-version
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - jruby-19mode
4
+ - rbx-19mode
5
+ - 1.9.2
6
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mobiletechnics_client.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 at-point ag, Lukas Westermann, http://at-point.ch/
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # MobileTechnics SMS API Client
2
+
3
+ Unofficial ruby adapter for MobileTechnics HTTP Bulk SMS API. Tries to mimick
4
+ mail API, so users can switch e.g. ActionMailer with this SMS provider. Requires
5
+ Ruby 1.9.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'mote_sms'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install mote_sms
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ # Quick and dirty
25
+ MoteSMS.transport = MoteSMS::MobileTechnicsTransport.new 'https://endpoint.com:1234', 'username', 'password'
26
+ MoteSMS.deliver do
27
+ body 'Quick hello world'
28
+ to '+41 79 111 22 33'
29
+ from 'ARUBYGEM'
30
+ end
31
+ ```
32
+
33
+ ```ruby
34
+ # Using global transport
35
+ MoteSMS.transport = MoteSMS::MobileTechnicsTransport.new 'https://endpoint.com:1234', 'username', 'password'
36
+ sms = MoteSMS.Message.new do
37
+ to '+41 79 111 22 33'
38
+ from 'ARUBYGEM'
39
+ body 'Hello world, you know.'
40
+ end
41
+ sms.deliver
42
+ ```
43
+
44
+ ```ruby
45
+ # Using client instance
46
+ transport = MoteSMS::MobileTechnicsTransport.new 'https://endpoint.com:1234', 'username', 'password'
47
+ sms = MoteSMS.Message.new do
48
+ # create message
49
+ end
50
+ sms.deliver(transport: transport)
51
+ ```
52
+
53
+ ## Contributing
54
+
55
+ 1. Fork it
56
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
57
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
58
+ 4. Push to the branch (`git push origin my-new-feature`)
59
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ task :default => :spec
6
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,102 @@
1
+ require 'mote_sms/number'
2
+ require 'mote_sms/number_list'
3
+
4
+ module MoteSMS
5
+
6
+ # Represents an SMS message, currently only provides the
7
+ # tools to build new messages, not parse incoming messages or
8
+ # similar stuff.
9
+ #
10
+ class Message
11
+
12
+ # The transport instance to use, if not defined
13
+ # falls back to use global MoteSMS.transport instance.
14
+ attr_accessor :transport
15
+
16
+ # Public: Create a new SMS message instance.
17
+ #
18
+ # Examples:
19
+ #
20
+ # sms = MoteSMS::Message.new do
21
+ # from '41791112233'
22
+ # to '41797776655'
23
+ # body 'Hi there.'
24
+ # end
25
+ # sms.from # => '41791112233'
26
+ # sms.to # => ['41797776655']
27
+ # sms.body # => 'Hi there.'
28
+ #
29
+ # Returns a new instance.
30
+ def initialize(transport = nil, &block)
31
+ @transport = transport
32
+ @to = MoteSMS::NumberList.new
33
+ instance_eval(&block) if block_given?
34
+ end
35
+
36
+ # Public: Returns current SMS message body, which should
37
+ # be something stringish.
38
+ #
39
+ # Returns value of body.
40
+ attr_writer :body
41
+ def body(val = nil)
42
+ @body = val if val
43
+ @body
44
+ end
45
+
46
+ # Public: Returns string of sender, the sender should
47
+ # either be 11 alphanumeric characters or 20 numbers.
48
+ #
49
+ # Examples:
50
+ #
51
+ # sms.from = '41791231234'
52
+ # sms.from # => '41791231234'
53
+ #
54
+ # Returns value of sender.
55
+ def from(val = nil)
56
+ self.from = val if val
57
+ @from
58
+ end
59
+
60
+ # Public: Asign an instance of Number instead of the direct
61
+ # string, so only vanity numbers are allowed.
62
+ def from=(val)
63
+ @from = val ? Number.new(val, :vanity => true) : nil
64
+ end
65
+
66
+ # Public: Set to multiple arguments or array, or whatever.
67
+ #
68
+ # Examples:
69
+ #
70
+ # sms.to = '41791231212'
71
+ # sms.to # => ['41791231212']
72
+ #
73
+ # sms.to = ['41791231212', '41791231212']
74
+ # sms.to # => ['41791231212', '41791231212']
75
+ #
76
+ # Returns nothing.
77
+ def to=(*args)
78
+ @to = MoteSMS::NumberList.new.push(*args)
79
+ end
80
+
81
+ # Public: Returns NumberList for this message.
82
+ #
83
+ # Returns NumberList instance.
84
+ def to(*numbers)
85
+ @to.push(*numbers) unless numbers.empty?
86
+ @to
87
+ end
88
+
89
+ # Public: Deliver message using defined transport, to select the correct
90
+ # transport method uses any of these values:
91
+ #
92
+ # 1. if options[:transport] is defined
93
+ # 2. falls back to self.transport
94
+ # 3. falls back to use MoteSMS.transport (global transport)
95
+ #
96
+ # Returns result of transport#deliver.
97
+ def deliver(options = {})
98
+ transport = options.delete(:transport) || self.transport || MoteSMS.transport
99
+ transport.deliver(self, options)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,53 @@
1
+ require 'phony'
2
+
3
+ module MoteSMS
4
+
5
+ # MoteSMS::Number handles all the number parsing and formatting
6
+ # issues, also a number is immutable.
7
+ class Number
8
+
9
+ # Access the E164 normalized value of the number.
10
+ attr_reader :number
11
+ alias :to_number :number
12
+
13
+ def initialize(value, options = {})
14
+ @options = options || {}
15
+ @raw_number = value.to_s
16
+ parse_raw_number
17
+ end
18
+
19
+ # Public: Returns true if this **could** represent a vanity number.
20
+ #
21
+ # Returns Boolean, true if this is a vanity number, else false.
22
+ def vanity?
23
+ !!@options[:vanity]
24
+ end
25
+
26
+ # Public: Prints formatted number, i.e. the human readable
27
+ # variant.
28
+ #
29
+ # Returns formatted number.
30
+ def to_s
31
+ @formatted_number ||= vanity? ? number : Phony.formatted(number)
32
+ end
33
+
34
+ protected
35
+
36
+ # Internal: Parse raw number with the help of Phony. Automatically
37
+ # adds the country code if missing.
38
+ #
39
+ def parse_raw_number
40
+ unless vanity?
41
+ raise ArgumentError, "Unable to parse #{@raw_number} as number" unless Phony.plausible?(@raw_number)
42
+ normalized = Phony.normalize(@raw_number)
43
+ normalized = "#{@options[:cc]}#{normalized}" unless @options[:cc] && normalized.start_with?(@options[:cc])
44
+ raise ArgumentError, "Wrong national destination code #{@raw_number}" unless Phony.plausible?(normalized, @options)
45
+
46
+ @number = Phony.normalize normalized
47
+ else
48
+ @number = @raw_number.gsub(/[^A-Z0-9]/i, '').upcase.strip
49
+ raise ArgumentError, "Invalid vanity number #{@raw_number}" if @number.length == 0 || @number.length > 11
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,84 @@
1
+ require 'mote_sms/number'
2
+
3
+ module MoteSMS
4
+
5
+ # List of Number instances, which transparantly is able to add
6
+ # new Number instances from strings, or whatever.
7
+ #
8
+ # Implements Enumerable, thus can be used like any regular array.
9
+ #
10
+ # Examples:
11
+ #
12
+ # list << '+41 79 123 12 12'
13
+ # list.push '044 123 12 12', cc: '41'
14
+ # list.push Number.new('0800 123 12 12')
15
+ # list.normalized_numbers
16
+ # # => ['41791231212', '41441231212', '08001231212']
17
+ #
18
+ class NumberList
19
+
20
+ # Load ruby enumerable support.
21
+ include ::Enumerable
22
+
23
+ # Internal numbers array.
24
+ attr_reader :numbers
25
+
26
+ # Public: Create a new number list instance.
27
+ def initialize
28
+ @numbers = ::Array.new
29
+ end
30
+
31
+ # Public: Count of numbers in the list.
32
+ def length
33
+ numbers.length
34
+ end
35
+ alias :size :length
36
+
37
+ # Public: Conform to arrayish behavior.
38
+ def empty?
39
+ numbers.empty?
40
+ end
41
+ alias :blank? :empty?
42
+
43
+ # Public: Add number to internal list, use duck typing to detect if
44
+ # it appears to be a number instance or not. So everything which does
45
+ # not respond to `to_number` is converted into a Number instance.
46
+ #
47
+ # number - The Number or String to add.
48
+ #
49
+ # Returns nothing.
50
+ def <<(number)
51
+ self.push(number)
52
+ end
53
+
54
+ # Public: Add multiple numbers, with optional options hash which can
55
+ # be used to set country options etc.
56
+ #
57
+ # Returns self.
58
+ def push(*numbers)
59
+ options = numbers.last.is_a?(Hash) ? numbers.pop : {}
60
+ numbers.flatten.each do |number|
61
+ number = Number.new(number, options) unless number.respond_to?(:to_number)
62
+ self.numbers << number
63
+ end
64
+ self
65
+ end
66
+
67
+ # Public: Yields each Number instance from this number list
68
+ # to the provided block. This interface is also required to be
69
+ # implemeneted for Enumerable support.
70
+ #
71
+ # Returns self.
72
+ def each(&block)
73
+ numbers.each(&block)
74
+ self
75
+ end
76
+
77
+ # Public: Fetch numbers using to_number.
78
+ #
79
+ # Returns Array of E164 normalized numbers.
80
+ def normalized_numbers
81
+ numbers.map(&:to_number)
82
+ end
83
+ end
84
+ end
File without changes
@@ -0,0 +1,185 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'phony'
4
+ require 'logger'
5
+
6
+ module MoteSMS
7
+
8
+ # MoteSMS::MobileTechnicsTransport provides the implementation to
9
+ # send messages using nth.ch bulk SMS HTTP/S API. Each customer has
10
+ # custom endpoint (with port) and username/password.
11
+ #
12
+ # Examples:
13
+ #
14
+ # transport = MoteSMS::MobileTechnicsTransport.new 'https://mygateway.nth.ch', 'username', 'password'
15
+ # transport.deliver message
16
+ # # => ['000-791234', '001-7987324']
17
+ #
18
+ class MobileTechnicsTransport
19
+
20
+ # Maximum recipients allowed by API
21
+ MAX_RECIPIENT = 100
22
+
23
+ # Custom exception subclass.
24
+ ServiceError = Class.new(::Exception)
25
+
26
+ # Accessible attributes
27
+ attr_accessor :endpoint, :username, :password, :logger
28
+
29
+ # Options are readable as hash
30
+ attr_reader :options
31
+
32
+ # Public: Global default parameters for sending messages, Procs/lambdas
33
+ # are evaluated on #deliver. Ensure to use only symbols as keys. Contains
34
+ # `allow_adaption: true` as default.
35
+ #
36
+ # Examples:
37
+ #
38
+ # MoteSMS::MobileTechnicsTransports.defaults[:messageid] = ->(msg) { "#{msg.from}-#{SecureRandom.hex}" }
39
+ #
40
+ # Returns Hash with options.
41
+ def self.defaults
42
+ @@options ||= {
43
+ allow_adaption: true
44
+ }
45
+ end
46
+
47
+ # Public: Logger used to log HTTP requests to mobile
48
+ # technics API endpoint.
49
+ #
50
+ # Returns Logger instance.
51
+ def self.logger
52
+ @@logger ||= ::Logger.new($stdout)
53
+ end
54
+
55
+ # Public: Change the logger used to log all HTTP requests to
56
+ # the endpoint.
57
+ #
58
+ # logger - The Logger instance, should at least respond to #debug, #error.
59
+ #
60
+ # Returns nothing.
61
+ def self.logger=(logger)
62
+ @@logger = logger
63
+ end
64
+
65
+ # Public: Create a new instance using specified endpoint, username
66
+ # and password.
67
+ #
68
+ # endpoint - The String with the URL (with protocol et all) to nth gateway.
69
+ # username - The String with username.
70
+ # password - The String with password.
71
+ # options - The Hash with additional options.
72
+ #
73
+ # Returns a new instance.
74
+ def initialize(endpoint, username, password, options = nil)
75
+ self.endpoint = endpoint
76
+ self.username = username
77
+ self.password = password
78
+ @options = options || {}
79
+ end
80
+
81
+ # Public: Delivers message using mobile technics HTTP/S API.
82
+ #
83
+ # message - The MoteSMS::Message instance to send.
84
+ # options - The Hash with service specific options.
85
+ #
86
+ # Returns Array with sender ids.
87
+ def deliver(message, options = {})
88
+ raise ArgumentError, "Too many recipients, max. is #{MAX_RECIPIENT} (current: #{message.to.length})" if message.to.length > MAX_RECIPIENT
89
+
90
+ # Prepare request
91
+ uri = URI.parse endpoint
92
+ http = http_client uri
93
+ request = http_request uri, post_params(message, options)
94
+
95
+ # Log
96
+ self.class.logger.debug "curl -X#{request.method} #{http.use_ssl? ? '-k ' : ''}'#{endpoint}' -d '#{request.body}'"
97
+
98
+ # Perform request
99
+ resp = http.request request
100
+
101
+ # Handle errors
102
+ raise ServiceError, "Endpoint did respond with #{resp.code}" unless resp.code.to_i == 200
103
+ raise ServiceError, "Endpoint was unable to deliver message to all recipients" unless resp.body.split("\n").all? { |l| l =~ /Result_code: 00/ }
104
+
105
+ # extract Nth-SmsIds
106
+ resp['X-Nth-SmsId'].split(',')
107
+ end
108
+
109
+ protected
110
+
111
+ # Internal: Prepare request including body, headers etc.
112
+ #
113
+ # uri - The URI from the endpoint.
114
+ # params - The Array with the attributes.
115
+ #
116
+ # Returns Net::HTTP::Post instance.
117
+ def http_request(uri, params)
118
+ Net::HTTP::Post.new(uri.request_uri).tap do |request|
119
+ request.body = URI.encode_www_form params
120
+ request.content_type = 'application/x-www-form-urlencoded; charset=utf-8'
121
+ end
122
+ end
123
+
124
+ # Internal: Build new Net::HTTP instance, enables SSL if requested.
125
+ # FIXME: Add ability to change verify_mode, so e.g. certificates can be
126
+ # verified!
127
+ #
128
+ # uri - The URI from the endpoint.
129
+ #
130
+ # Returns Net::HTTP client instance.
131
+ def http_client(uri)
132
+ Net::HTTP.new(uri.host, uri.port).tap do |http|
133
+ # SSL support
134
+ if uri.instance_of?(URI::HTTPS)
135
+ http.use_ssl = true
136
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
137
+ end
138
+ end
139
+ end
140
+
141
+ # Internal: Merge defaults from class and instance with options
142
+ # supplied to #deliver.
143
+ #
144
+ # options - The Hash to merge with #defaults and #options.
145
+ #
146
+ # Returns Hash.
147
+ def prepare_options(options)
148
+ self.class.defaults.merge(self.options).merge(options)
149
+ end
150
+
151
+ # Internal: Convert NumberList instance to ; separated string with international
152
+ # relative formatted numbers. Formatting is done using phony.
153
+ #
154
+ # number_list - The NumberList instance.
155
+ #
156
+ # Returns String with numbers separated by ;.
157
+ def prepare_numbers(number_list)
158
+ number_list.normalized_numbers.map { |n| Phony.formatted(n, format: :international_relative, spaces: '') }.join(';')
159
+ end
160
+
161
+ # Internal: Prepare parameters for sending POST to endpoint, merges defaults,
162
+ # local and per call options, adds message related informations etc etc.
163
+ #
164
+ # message - The MoteSMS::Message to create the POST body for.
165
+ # options - The Hash with additional, per delivery options.
166
+ #
167
+ # Returns Array with params.
168
+ def post_params(message, options)
169
+ params = prepare_options options
170
+ params.merge! username: self.username,
171
+ password: self.password,
172
+ origin: message.from ? message.from.to_number : params[:origin],
173
+ text: message.body,
174
+ :'call-number' => prepare_numbers(message.to)
175
+
176
+ # Post process params (Procs & allow_adaption)
177
+ params.map do |param, value|
178
+ value = value.call(message) if value.respond_to?(:call)
179
+ value = value ? 1 : 0 if param == :allow_adaption
180
+
181
+ [param.to_s, value.to_s]
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,40 @@
1
+ module MoteSMS
2
+
3
+ # Public: Provide access to global array of delivered
4
+ # messages, this can be used in testing to assert sent
5
+ # SMS messages, test their contents, recipients etc.
6
+ #
7
+ # Must be cleared manually (!)
8
+ @@deliveries = []
9
+ def self.deliveries
10
+ @@deliveries
11
+ end
12
+
13
+ # MoteSMS::TestTransport provides a transport implementation which
14
+ # can be used in test cases. This allows to test sending SMSes
15
+ # et all without depending on an API or accidentally sending out
16
+ # messages to real people.
17
+ #
18
+ # It works similar to testing ActionMailers, all delivered messages
19
+ # will be appended to `MoteSMS.deliveries`. This array must be
20
+ # cleared manually, so it makes sense to add a before hook to
21
+ # your favorite testing framework:
22
+ #
23
+ # before do
24
+ # MoteSMS.transport = MoteSMS::TestTransport
25
+ # MoteSMS.deliveries.clear
26
+ # end
27
+ #
28
+ module TestTransport
29
+
30
+ # Public: Appends supplied message to global deliveries array.
31
+ #
32
+ # message - The MoteSMS::Message instance to deliver.
33
+ # options - The Hash with additional, transport specific options.
34
+ #
35
+ # Returns nothing.
36
+ def self.deliver(message, options = {})
37
+ MoteSMS.deliveries << message
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,7 @@
1
+ module MoteSMS
2
+
3
+ # All transports live within mote_sms/transports, though should be
4
+ # available in ruby as `MoteSMS::<Some>Transport`.
5
+ autoload :TestTransport, 'mote_sms/transports/test_transport'
6
+ autoload :MobileTechnicsTransport, 'mote_sms/transports/mobile_technics_transport'
7
+ end
@@ -0,0 +1,3 @@
1
+ module MoteSMS
2
+ VERSION = "1.0.0"
3
+ end
data/lib/mote_sms.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'mote_sms/transports'
2
+
3
+ module MoteSMS
4
+ autoload :Number, 'mote_sms/number'
5
+ autoload :NumberList, 'mote_sms/number_list'
6
+ autoload :Message, 'mote_sms/message'
7
+
8
+ autoload :VERSION, 'mote_sms/version'
9
+
10
+ # No default transport.
11
+ @@transport = nil
12
+
13
+ # Public: Get globally defined transport method, if any.
14
+ # Defaults to `nil`.
15
+ #
16
+ # Returns global SMS transport method.
17
+ def self.transport
18
+ @@transport
19
+ end
20
+
21
+ # Public: Set global transport method to use.
22
+ #
23
+ # transport - Any object which implements `#deliver(message, options)`.
24
+ #
25
+ # Returns nothing.
26
+ def self.transport=(transport)
27
+ @@transport = transport
28
+ end
29
+
30
+ # Public: Directly deliver a message using global transport.
31
+ #
32
+ # Examples:
33
+ #
34
+ # MoteSMS.deliver do
35
+ # to '0041 79 123 12 12'
36
+ # from 'SENDER'
37
+ # body 'Hello world'
38
+ # end
39
+ #
40
+ # Returns result of #deliver.
41
+ def self.deliver(&block)
42
+ raise ArgumentError, 'Block missing' unless block_given?
43
+ Message.new(&block).deliver
44
+ end
45
+ end
data/mote_sms.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/mote_sms/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'mote_sms'
6
+ gem.authors = ['Lukas Westermann']
7
+ gem.email = ['lukas.westermann@at-point.ch']
8
+ gem.summary = %q{Deliver SMS using MobileTechnics HTTP API.}
9
+ gem.description = %q{Unofficial ruby adapter for MobileTechnics HTTP Bulk SMS API.
10
+ Tries to mimick mail API, so users can switch e.g. ActionMailer
11
+ with this SMS provider.}
12
+ gem.homepage = 'https://at-point.ch/opensource'
13
+
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = %w{lib}
18
+ gem.version = MoteSMS::VERSION
19
+
20
+ gem.required_ruby_version = '>= 1.9'
21
+
22
+ gem.add_dependency 'phony', ['~> 1.7.0']
23
+
24
+ gem.add_development_dependency 'rake'
25
+ gem.add_development_dependency 'rspec', ['~> 2.10']
26
+ gem.add_development_dependency 'webmock', ['~> 1.8.0']
27
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+ require 'mote_sms/message'
3
+
4
+ describe MoteSMS::Message do
5
+ it 'can be constructed using a block' do
6
+ msg = described_class.new do
7
+ from 'SENDER'
8
+ to '+41 79 123 12 12'
9
+ body 'This is the SMS content'
10
+ end
11
+ msg.from.number.should == 'SENDER'
12
+ msg.to.normalized_numbers.should == %w{41791231212}
13
+ msg.body.should == 'This is the SMS content'
14
+ end
15
+
16
+ context '#to' do
17
+ it 'behaves as accessor' do
18
+ subject.to = '41791231212'
19
+ subject.to.normalized_numbers.should == %w{41791231212}
20
+ end
21
+
22
+ it 'behaves as array' do
23
+ subject.to << '41791231212'
24
+ subject.to << '41797775544'
25
+ subject.to.normalized_numbers.should == %w{41791231212 41797775544}
26
+ end
27
+
28
+ it 'normalizes numbers' do
29
+ subject.to = '+41 79 123 12 12'
30
+ subject.to.normalized_numbers.should == %w{41791231212}
31
+ end
32
+ end
33
+
34
+ context "#deliver" do
35
+ let(:transport) { double("Some Transport") }
36
+ subject { described_class.new(transport) }
37
+
38
+ it "sends messages to transport" do
39
+ transport.should_receive(:deliver).with(subject, {})
40
+ subject.deliver
41
+ end
42
+
43
+ it "can pass additional attributes to transport" do
44
+ transport.should_receive(:deliver).with(subject, serviceid: "myapplication")
45
+ subject.deliver serviceid: "myapplication"
46
+ end
47
+
48
+ it "can override per message transport using :transport option" do
49
+ transport.should_not_receive(:deliver)
50
+ subject.deliver transport: double(deliver: true)
51
+ end
52
+
53
+ it "uses global MoteSMS.transport if no per message transport defined" do
54
+ message = described_class.new
55
+ transport.should_receive(:deliver).with(message, {})
56
+ MoteSMS.should_receive(:transport) { transport }
57
+ message.deliver
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'mote_sms/number_list'
3
+
4
+ describe MoteSMS::NumberList do
5
+ it 'has length' do
6
+ subject.length.should == 0
7
+ subject << '+41 79 111 22 33'
8
+ subject.length.should == 1
9
+ 5.times { subject << '+41 79 222 33 44' }
10
+ subject.length.should == 6
11
+ end
12
+
13
+ it 'has empty?' do
14
+ subject.empty?.should be_true
15
+ subject << '+41 79 111 22 33'
16
+ subject.empty?.should be_false
17
+ end
18
+
19
+ it 'can add numbers by string' do
20
+ subject << '+41 79 111 22 33'
21
+ subject.normalized_numbers.should == %w{41791112233}
22
+ end
23
+
24
+ it 'can multiple numbers using push' do
25
+ subject.push '+41 79 111 22 33', '+41 44 111 22 33'
26
+ subject.normalized_numbers.should == %w{41791112233 41441112233}
27
+ end
28
+
29
+ it 'can push multiple numbers with adding country codes' do
30
+ subject.push '079 111 22 33', '0041 44 111 22 33', cc: '41', ndc: /(44|79)/
31
+ subject.normalized_numbers.should == %w{41791112233 41441112233}
32
+ end
33
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+ require 'mote_sms/number'
3
+
4
+ describe MoteSMS::Number do
5
+ context 'normalized number' do
6
+ subject { described_class.new('41443643533') }
7
+
8
+ its(:to_s) { should == '+41 44 364 35 33' }
9
+ its(:number) { should == '41443643533' }
10
+ its(:to_number) { should == '41443643533' }
11
+ end
12
+
13
+ context 'E164 conforming number' do
14
+ subject { described_class.new('+41 44 3643533') }
15
+
16
+ its(:to_s) { should == '+41 44 364 35 33' }
17
+ its(:number) { should == '41443643533' }
18
+ its(:to_number) { should == '41443643533' }
19
+ end
20
+
21
+ context 'E164 conforming number with name' do
22
+ subject { described_class.new('Frank: +41 44 3643533', cc: '41') }
23
+
24
+ its(:to_s) { should == '+41 44 364 35 33' }
25
+ its(:number) { should == '41443643533' }
26
+ its(:to_number) { should == '41443643533' }
27
+ end
28
+
29
+ context 'handles local numbers' do
30
+ subject { described_class.new('079 700 50 90', cc: '41') }
31
+
32
+ its(:to_s) { should == '+41 79 700 50 90' }
33
+ its(:number) { should == '41797005090'}
34
+ its(:to_number) { should == '41797005090' }
35
+ end
36
+
37
+ context 'non conforming number' do
38
+ it 'raises error when creating' do
39
+ Proc.new { described_class.new('what ever?') }.should raise_error(ArgumentError, /unable to parse/i)
40
+ Proc.new { described_class.new('0000') }.should raise_error(ArgumentError, /unable to parse/i)
41
+ Proc.new { described_class.new('123456789012345678901') }.should raise_error(ArgumentError, /unable to parse/i)
42
+ end
43
+ end
44
+
45
+ context 'wrong cc/ndc' do
46
+ it 'raises error when creating instance with wrong ndc' do
47
+ Proc.new { described_class.new('+41 44 364 35 33', cc: '41', ndc: '43') }.should raise_error(ArgumentError, /national destination/i)
48
+ end
49
+ end
50
+
51
+ context 'vanity numbers' do
52
+ subject { described_class.new('0800-vanity', vanity: true) }
53
+
54
+ its(:to_s) { should == '0800VANITY' }
55
+ its(:number) { should == '0800VANITY' }
56
+ its(:to_number) { should == '0800VANITY' }
57
+ its(:vanity?) { should be_true }
58
+
59
+ it 'raises error if more than 11 alpha numeric chars' do
60
+ Proc.new { described_class.new('1234567890AB', vanity: true) }.should raise_error(ArgumentError, /invalid vanity/i)
61
+ end
62
+
63
+ it 'can also be normal phone number' do
64
+ num = described_class.new('0800 123 12 12', vanity: true)
65
+ num.to_s.should == '08001231212'
66
+ num.to_number.should == '08001231212'
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,124 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+ require 'cgi'
4
+ require 'mote_sms/message'
5
+ require 'mote_sms/transports/mobile_technics_transport'
6
+
7
+ describe MoteSMS::MobileTechnicsTransport do
8
+ before do
9
+ @logger = described_class.logger
10
+ described_class.logger = stub(debug: true, info: true, error: true)
11
+ end
12
+
13
+ after do
14
+ described_class.logger = @logger
15
+ end
16
+
17
+ subject { described_class.new(endpoint, "example", "123456") }
18
+
19
+ let(:endpoint) { "http://test.nth.ch" }
20
+ let(:message) {
21
+ MoteSMS::Message.new do
22
+ to '0041 079 123 12 12'
23
+ from 'SENDER'
24
+ body 'Hello World, with äöü.'
25
+ end
26
+ }
27
+
28
+ let(:success) {
29
+ { body: "Result_code: 00, Message OK", status: 200, headers: { 'X-Nth-SmsId' => '43797917' } }
30
+ }
31
+
32
+ context "#deliver" do
33
+ it "sends POST to endpoint with URL encoded body" do
34
+ stub_request(:post, endpoint).with do |req|
35
+ params = CGI.parse(req.body)
36
+ params['username'].should == %w{example}
37
+ params['password'].should == %w{123456}
38
+ params['text'].should == ['Hello World, with äöü.']
39
+ params['call-number'].should == ['0041791231212']
40
+ end.to_return(success)
41
+ subject.deliver message
42
+ end
43
+
44
+ it 'sends message in single request to multiple recipients' do
45
+ message.to << '+41 79 333 44 55'
46
+ message.to << '+41 78 111 22 33'
47
+
48
+ stub_request(:post, endpoint).with(body: hash_including('call-number' => '0041791231212;0041793334455;0041781112233')).to_return(success)
49
+ subject.deliver message
50
+ end
51
+
52
+ it 'raises exception if required parameter is missing' do
53
+ stub_request(:post, endpoint).to_return(body: 'Result_code: 02, call-number')
54
+ Proc.new { subject.deliver message }.should raise_error(MoteSMS::MobileTechnicsTransport::ServiceError)
55
+ end
56
+
57
+ it 'raises exception if status code is not 200' do
58
+ stub_request(:post, endpoint).to_return(status: 500)
59
+ Proc.new { subject.deliver message }.should raise_error(MoteSMS::MobileTechnicsTransport::ServiceError)
60
+ end
61
+
62
+ it 'returns message id' do
63
+ stub_request(:post, endpoint).to_return(success)
64
+ subject.deliver(message).should == %w{43797917}
65
+ end
66
+
67
+ it 'logs curl compatible output' do
68
+ io = StringIO.new
69
+ described_class.logger = Logger.new(io)
70
+ stub_request(:post, endpoint).to_return(success)
71
+ subject.deliver message
72
+ io.rewind
73
+ io.read.should =~ %r{curl -XPOST 'http://test.nth.ch' -d 'allow_adaption=1&}
74
+ end
75
+ end
76
+
77
+ context "#options" do
78
+ it 'can be passed in as last item in the constructor' do
79
+ transport = described_class.new endpoint, 'user', 'pass', allow_adaption: false, validity: 30
80
+ transport.options[:allow_adaption].should be_false
81
+ transport.options[:validity].should == 30
82
+ transport.options[:something].should be_nil
83
+ end
84
+
85
+ it 'should be exposed as hash' do
86
+ subject.options[:messageid] = "test"
87
+ subject.options[:messageid].should == "test"
88
+ end
89
+
90
+ it 'overrides settings from #defaults' do
91
+ described_class.defaults[:something] = 'global'
92
+ subject.options[:something] = 'local'
93
+
94
+ stub_request(:post, endpoint).with(body: hash_including('something' => 'local')).to_return(success)
95
+ subject.deliver message
96
+ end
97
+
98
+ it 'is overriden by options passed to #deliver' do
99
+ subject.options[:something] = 'local'
100
+
101
+ stub_request(:post, endpoint).with(body: hash_including('something' => 'deliver')).to_return(success)
102
+ subject.deliver message, something: 'deliver'
103
+ end
104
+
105
+ it 'evaluates Procs & lambdas' do
106
+ subject.options[:messageid] = Proc.new { "test" }
107
+
108
+ stub_request(:post, endpoint).with(body: hash_including('messageid' => 'test')).to_return(success)
109
+ subject.deliver message
110
+ end
111
+
112
+ it 'converts allow_adaption to 1 when true' do
113
+ subject.options[:allow_adaption] = true
114
+ stub_request(:post, endpoint).with(body: hash_including('allow_adaption' => '1')).to_return(success)
115
+ subject.deliver message
116
+ end
117
+
118
+ it 'converts allow_adaption to 0 when false' do
119
+ subject.options[:allow_adaption] = nil
120
+ stub_request(:post, endpoint).with(body: hash_including('allow_adaption' => '0')).to_return(success)
121
+ subject.deliver message
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'mote_sms/transports/test_transport'
3
+
4
+ describe MoteSMS::TestTransport do
5
+ subject { described_class }
6
+ before { MoteSMS.deliveries.clear }
7
+
8
+ it 'defines global #deliveries' do
9
+ MoteSMS.should respond_to(:deliveries)
10
+ end
11
+
12
+ it 'appends deliveries' do
13
+ subject.deliver "firstMessage"
14
+ subject.deliver "secondMessage"
15
+ MoteSMS.deliveries.should == %w{firstMessage secondMessage}
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'mote_sms'
3
+
4
+ describe MoteSMS do
5
+ subject { described_class }
6
+
7
+ it 'has a version' do
8
+ subject::VERSION.should =~ /\d/
9
+ end
10
+
11
+ context "transport" do
12
+ before { @current_transport = subject.transport }
13
+ after { subject.transport = @current_transport }
14
+ let(:transport) { double("transport") }
15
+
16
+ it "has no default transport" do
17
+ subject.transport.should be_nil
18
+ end
19
+
20
+ it "can change global transport" do
21
+ subject.transport = transport
22
+ subject.transport.should == transport
23
+ end
24
+
25
+ context "#deliver" do
26
+ it 'delivers quick and dirty using global transport' do
27
+ transport.should_receive(:deliver).with(kind_of(MoteSMS::Message), {})
28
+ subject.transport = transport
29
+ subject.deliver { body 'Hello World' }
30
+ end
31
+
32
+ it 'raises error if block is missing' do
33
+ Proc.new { subject.deliver }.should raise_error(ArgumentError, /block missing/i)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rspec'
5
+ require 'webmock/rspec'
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mote_sms
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Lukas Westermann
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: phony
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.7.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.7.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '2.10'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.10'
62
+ - !ruby/object:Gem::Dependency
63
+ name: webmock
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 1.8.0
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 1.8.0
78
+ description: ! "Unofficial ruby adapter for MobileTechnics HTTP Bulk SMS API.\n Tries
79
+ to mimick mail API, so users can switch e.g. ActionMailer\n with
80
+ this SMS provider."
81
+ email:
82
+ - lukas.westermann@at-point.ch
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - .gitignore
88
+ - .travis.yml
89
+ - Gemfile
90
+ - LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - lib/mote_sms.rb
94
+ - lib/mote_sms/message.rb
95
+ - lib/mote_sms/number.rb
96
+ - lib/mote_sms/number_list.rb
97
+ - lib/mote_sms/transports.rb
98
+ - lib/mote_sms/transports/.gitkeep
99
+ - lib/mote_sms/transports/mobile_technics_transport.rb
100
+ - lib/mote_sms/transports/test_transport.rb
101
+ - lib/mote_sms/version.rb
102
+ - mote_sms.gemspec
103
+ - spec/mote_sms/message_spec.rb
104
+ - spec/mote_sms/number_list_spec.rb
105
+ - spec/mote_sms/number_spec.rb
106
+ - spec/mote_sms/transports/mobile_technics_transport_spec.rb
107
+ - spec/mote_sms/transports/test_transport_spec.rb
108
+ - spec/mote_sms_spec.rb
109
+ - spec/spec_helper.rb
110
+ homepage: https://at-point.ch/opensource
111
+ licenses: []
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '1.9'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ none: false
124
+ requirements:
125
+ - - ! '>='
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ segments:
129
+ - 0
130
+ hash: 3261538551323079383
131
+ requirements: []
132
+ rubyforge_project:
133
+ rubygems_version: 1.8.23
134
+ signing_key:
135
+ specification_version: 3
136
+ summary: Deliver SMS using MobileTechnics HTTP API.
137
+ test_files:
138
+ - spec/mote_sms/message_spec.rb
139
+ - spec/mote_sms/number_list_spec.rb
140
+ - spec/mote_sms/number_spec.rb
141
+ - spec/mote_sms/transports/mobile_technics_transport_spec.rb
142
+ - spec/mote_sms/transports/test_transport_spec.rb
143
+ - spec/mote_sms_spec.rb
144
+ - spec/spec_helper.rb