mote_sms 1.0.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.
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