sms_aero 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +7 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +12 -0
  6. data/.travis.yml +17 -0
  7. data/Gemfile +12 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +126 -0
  10. data/Rakefile +6 -0
  11. data/lib/sms_aero.rb +55 -0
  12. data/lib/sms_aero/models/answer.rb +8 -0
  13. data/lib/sms_aero/models/sms.rb +15 -0
  14. data/lib/sms_aero/models/tariff.rb +8 -0
  15. data/lib/sms_aero/operations/add_blacklist.rb +11 -0
  16. data/lib/sms_aero/operations/add_group.rb +11 -0
  17. data/lib/sms_aero/operations/add_phone.rb +17 -0
  18. data/lib/sms_aero/operations/check_balance.rb +11 -0
  19. data/lib/sms_aero/operations/check_groups.rb +12 -0
  20. data/lib/sms_aero/operations/check_senders.rb +15 -0
  21. data/lib/sms_aero/operations/check_sending.rb +11 -0
  22. data/lib/sms_aero/operations/check_sign.rb +15 -0
  23. data/lib/sms_aero/operations/check_status.rb +11 -0
  24. data/lib/sms_aero/operations/check_tariff.rb +12 -0
  25. data/lib/sms_aero/operations/delete_group.rb +11 -0
  26. data/lib/sms_aero/operations/delete_phone.rb +12 -0
  27. data/lib/sms_aero/operations/send_sms.rb +17 -0
  28. data/lib/sms_aero/operations/send_to_group.rb +15 -0
  29. data/lib/sms_aero/types/birthday.rb +20 -0
  30. data/lib/sms_aero/types/channel.rb +4 -0
  31. data/lib/sms_aero/types/digital.rb +8 -0
  32. data/lib/sms_aero/types/filled_string.rb +3 -0
  33. data/lib/sms_aero/types/future.rb +16 -0
  34. data/lib/sms_aero/types/phone.rb +12 -0
  35. data/lib/sms_aero/types/sign_status.rb +9 -0
  36. data/sms_aero.gemspec +23 -0
  37. data/spec/sms_aero/operations/add_blacklist_spec.rb +88 -0
  38. data/spec/sms_aero/operations/add_group_spec.rb +88 -0
  39. data/spec/sms_aero/operations/add_phone_spec.rb +221 -0
  40. data/spec/sms_aero/operations/check_balance_spec.rb +85 -0
  41. data/spec/sms_aero/operations/check_groups_spec.rb +72 -0
  42. data/spec/sms_aero/operations/check_senders_spec.rb +98 -0
  43. data/spec/sms_aero/operations/check_sending_spec.rb +88 -0
  44. data/spec/sms_aero/operations/check_sign_spec.rb +87 -0
  45. data/spec/sms_aero/operations/check_status_spec.rb +88 -0
  46. data/spec/sms_aero/operations/check_tariff_spec.rb +72 -0
  47. data/spec/sms_aero/operations/delete_group_spec.rb +88 -0
  48. data/spec/sms_aero/operations/delete_phone_spec.rb +118 -0
  49. data/spec/sms_aero/operations/send_sms_spec.rb +179 -0
  50. data/spec/sms_aero/operations/send_to_group_spec.rb +171 -0
  51. data/spec/sms_aero/types/birthday_spec.rb +33 -0
  52. data/spec/sms_aero/types/channel_spec.rb +19 -0
  53. data/spec/sms_aero/types/digital_spec.rb +15 -0
  54. data/spec/sms_aero/types/future_spec.rb +38 -0
  55. data/spec/sms_aero/types/phone_spec.rb +21 -0
  56. data/spec/sms_aero/types/sign_status_spec.rb +19 -0
  57. data/spec/spec_helper.rb +20 -0
  58. metadata +205 -0
@@ -0,0 +1,11 @@
1
+ class SmsAero
2
+ operation :check_status do
3
+ documentation "https://smsaero.ru/api/description/#check-status"
4
+
5
+ path { "status" }
6
+
7
+ query do
8
+ attribute :id, Types::Coercible::String.constrained(filled: true)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ class SmsAero
2
+ operation :check_tariff do
3
+ documentation "https://smsaero.ru/api/description/#get-balance"
4
+
5
+ path { "checktarif" }
6
+
7
+ response :success, 200, format: :json do
8
+ attribute :result, Types::FilledString
9
+ attribute :reason, Tariff, as: :tariff
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ class SmsAero
2
+ operation :delete_group do
3
+ documentation "https://smsaero.ru/api/description/#groups"
4
+
5
+ path { "delgroup" }
6
+
7
+ query do
8
+ attribute :group, Types::FilledString
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ class SmsAero
2
+ operation :delete_phone do
3
+ documentation "https://smsaero.ru/api/description/#contacts"
4
+
5
+ path { "delphone" }
6
+
7
+ query do
8
+ attribute :phone, Types::Phone
9
+ attribute :group, Types::FilledString, optional: true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ class SmsAero
2
+ operation :send_sms do
3
+ documentation "https://smsaero.ru/api/description/#send-sms"
4
+
5
+ path do |test: false, **|
6
+ test ? "testsend" : "send"
7
+ end
8
+
9
+ query model: Sms do
10
+ attribute :to, Types::Phone
11
+ end
12
+
13
+ response :success, 200, format: :json, model: Answer do
14
+ attribute :id, Types::Coercible::String
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ class SmsAero
2
+ operation :send_to_group do
3
+ documentation "https://smsaero.ru/api/description/#send-sms"
4
+
5
+ path { "sendtogroup" }
6
+
7
+ query model: Sms do
8
+ attribute :group, Types::FilledString, default: proc { "all" }
9
+ end
10
+
11
+ response :success, 200, format: :json, model: Answer do
12
+ attribute :id, Types::Coercible::String, optional: true
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module SmsAero::Types
2
+ # Describes a user's birthday in year-month-date format
3
+ # Accepts dates, times, datetimes, and strings parceable to dates
4
+ #
5
+ # @example
6
+ # SmsAero::Types::Birthday["1901-12-9"] # => "1901-12-09"
7
+ # SmsAero::Types::Birthday[Time.new(1901, 12, 9, 10, 12)] # => "1901-12-09"
8
+ #
9
+ Birthday = Strict::String
10
+ .constrained(format: /\A\d{4}-\d{2}-\d{2}\z/)
11
+ .constructor do |value|
12
+ begin
13
+ date = value.to_date if value.respond_to? :to_date
14
+ date ||= ::Date.parse(value.to_s)
15
+ date.strftime "%Y-%m-%d"
16
+ rescue
17
+ raise TypeError, "#{value.inspect} cannot be coerced to date"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,4 @@
1
+ module SmsAero::Types
2
+ # Describes acceptable channel codes
3
+ Channel = Coercible::Int.constrained included_in: [1, 2, 3, 4, 6]
4
+ end
@@ -0,0 +1,8 @@
1
+ module SmsAero::Types
2
+ # Converts any value to either 1 or 0 flag for digital sending channel
3
+ #
4
+ # @example
5
+ # SmsAero::Types::Digital[true] # => 1
6
+ #
7
+ Digital = Strict::Int.constructor { |value| value ? 1 : 0 }
8
+ end
@@ -0,0 +1,3 @@
1
+ module SmsAero::Types
2
+ FilledString = Coercible::String.constrained(filled: true)
3
+ end
@@ -0,0 +1,16 @@
1
+ module SmsAero::Types
2
+ # Describes coercible Unix time in future
3
+ Future = Strict::Int.constructor do |value|
4
+ begin
5
+ error = TypeError.new "#{value.inspect} is not a valid time in future"
6
+
7
+ time = value.to_time if value.respond_to? :to_time
8
+ time ||= ::Time.parse(value.to_s) unless value.is_a? Numeric
9
+ number = time.to_i
10
+
11
+ (number > ::Time.now.to_i) ? number : raise(error)
12
+ rescue
13
+ raise error
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module SmsAero::Types
2
+ # Describes a valid phone containing digits without lead zeros
3
+ #
4
+ # @example
5
+ # SmsAero::Types::Phone["07 (123) 134-12-08"] # => "71231341208"
6
+ # SmsAero::Types::Phone["008"] # raises #<Dry::Types::ConstraintError ...>
7
+ #
8
+ Phone = Strict::String.constrained(format: /\A\d{11,13}\z/)
9
+ .constructor do |value|
10
+ value.to_s.scan(/\d/).join[/[^0].*/].to_s
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module SmsAero::Types
2
+ # Describes statuses of the sign
3
+ SignStatus = Strict::String.constrained included_in: %w(
4
+ accepted
5
+ approved
6
+ rejected
7
+ pending
8
+ )
9
+ end
data/sms_aero.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "sms_aero"
3
+ gem.version = "0.0.1"
4
+ gem.author = "Andrew Kozin (nepalez)"
5
+ gem.email = "andrew.kozin@gmail.com"
6
+ gem.homepage = "https://github.com/nepalez/sms_aero"
7
+ gem.summary = "HTTP(s) client to SMS Aero API"
8
+ gem.license = "MIT"
9
+
10
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
11
+ gem.test_files = gem.files.grep(/^spec/)
12
+ gem.extra_rdoc_files = Dir["README.md", "LICENSE", "CHANGELOG.md"]
13
+
14
+ gem.required_ruby_version = ">= 2.2"
15
+
16
+ gem.add_runtime_dependency "dry-types", "~> 0.9.1"
17
+ gem.add_runtime_dependency "evil-client", "~> 0.3.0"
18
+
19
+ gem.add_development_dependency "rake", "~> 11.0"
20
+ gem.add_development_dependency "rspec", "~> 3.0"
21
+ gem.add_development_dependency "rubocop", "~> 0.42"
22
+ gem.add_development_dependency "webmock", "~> 2.1"
23
+ end
@@ -0,0 +1,88 @@
1
+ RSpec.describe SmsAero, "#add_blacklist" do
2
+ let(:settings) { { user: "LOGIN", password: "PASSWORD" } }
3
+ let(:client) { described_class.new(settings) }
4
+ let(:params) { { phone: "+7 (018) 132-4388" } }
5
+ let(:answer) { { result: "accepted" } }
6
+
7
+ before { stub_request(:any, //).to_return(body: answer.to_json) }
8
+ subject { client.add_blacklist(params) }
9
+
10
+ context "using ssl via POST:" do
11
+ let(:host) { "https://gate.smsaero.ru/addblacklist" }
12
+ let(:query) { "answer=json&password=PASSWORD&phone=70181324388&user=LOGIN" }
13
+
14
+ it "sends a request" do
15
+ subject
16
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
17
+ end
18
+
19
+ it "returns success" do
20
+ expect(subject).to be_kind_of SmsAero::Answer
21
+ expect(subject.result).to eq "accepted"
22
+ end
23
+ end
24
+
25
+ context "via GET:" do
26
+ let(:host) { "https://gate.smsaero.ru/addblacklist" }
27
+ let(:query) { "answer=json&password=PASSWORD&phone=70181324388&user=LOGIN" }
28
+
29
+ before { settings[:use_post] = false }
30
+
31
+ it "sends a request" do
32
+ subject
33
+ expect(a_request(:get, "#{host}?#{query}")).to have_been_made
34
+ end
35
+ end
36
+
37
+ context "not using ssl:" do
38
+ let(:host) { "http://gate.smsaero.ru/addblacklist" }
39
+ let(:query) { "answer=json&password=PASSWORD&phone=70181324388&user=LOGIN" }
40
+
41
+ before { settings[:use_ssl] = false }
42
+
43
+ it "sends a request via http" do
44
+ subject
45
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
46
+ end
47
+ end
48
+
49
+ context "with custom user:" do
50
+ let(:host) { "https://gate.smsaero.ru/addblacklist" }
51
+ let(:query) { "answer=json&password=PASSWORD&phone=70181324388&user=USER" }
52
+
53
+ before { params[:user] = "USER" }
54
+
55
+ it "sends a request" do
56
+ subject
57
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
58
+ end
59
+ end
60
+
61
+ context "with custom password:" do
62
+ let(:host) { "https://gate.smsaero.ru/addblacklist" }
63
+ let(:query) { "answer=json&password=PSWD&phone=70181324388&user=LOGIN" }
64
+
65
+ before { params[:password] = "PSWD" }
66
+
67
+ it "sends a request" do
68
+ subject
69
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
70
+ end
71
+ end
72
+
73
+ context "with invalid phone:" do
74
+ before { params[:phone] = "foobar23" }
75
+
76
+ it "raises an exception" do
77
+ expect { subject }.to raise_error(TypeError, /23/)
78
+ end
79
+ end
80
+
81
+ context "without a phone:" do
82
+ before { params.delete :phone }
83
+
84
+ it "raises an exception" do
85
+ expect { subject }.to raise_error(KeyError)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,88 @@
1
+ RSpec.describe SmsAero, "#add_group" do
2
+ let(:settings) { { user: "LOGIN", password: "PASSWORD" } }
3
+ let(:client) { described_class.new(settings) }
4
+ let(:params) { { group: "foobar" } }
5
+ let(:answer) { { result: "accepted" } }
6
+
7
+ before { stub_request(:any, //).to_return(body: answer.to_json) }
8
+ subject { client.add_group(params) }
9
+
10
+ context "using ssl via POST:" do
11
+ let(:host) { "https://gate.smsaero.ru/addgroup" }
12
+ let(:query) { "answer=json&group=foobar&password=PASSWORD&user=LOGIN" }
13
+
14
+ it "sends a request" do
15
+ subject
16
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
17
+ end
18
+
19
+ it "returns success" do
20
+ expect(subject).to be_kind_of SmsAero::Answer
21
+ expect(subject.result).to eq "accepted"
22
+ end
23
+ end
24
+
25
+ context "via GET:" do
26
+ let(:host) { "https://gate.smsaero.ru/addgroup" }
27
+ let(:query) { "answer=json&group=foobar&password=PASSWORD&user=LOGIN" }
28
+
29
+ before { settings[:use_post] = false }
30
+
31
+ it "sends a request" do
32
+ subject
33
+ expect(a_request(:get, "#{host}?#{query}")).to have_been_made
34
+ end
35
+ end
36
+
37
+ context "not using ssl:" do
38
+ let(:host) { "http://gate.smsaero.ru/addgroup" }
39
+ let(:query) { "answer=json&group=foobar&password=PASSWORD&user=LOGIN" }
40
+
41
+ before { settings[:use_ssl] = false }
42
+
43
+ it "sends a request" do
44
+ subject
45
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
46
+ end
47
+ end
48
+
49
+ context "with custom user:" do
50
+ let(:host) { "https://gate.smsaero.ru/addgroup" }
51
+ let(:query) { "answer=json&group=foobar&password=PASSWORD&user=USER" }
52
+
53
+ before { params[:user] = "USER" }
54
+
55
+ it "sends a request" do
56
+ subject
57
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
58
+ end
59
+ end
60
+
61
+ context "with custom password:" do
62
+ let(:host) { "https://gate.smsaero.ru/addgroup" }
63
+ let(:query) { "answer=json&group=foobar&password=PSWD&user=LOGIN" }
64
+
65
+ before { params[:password] = "PSWD" }
66
+
67
+ it "sends a request" do
68
+ subject
69
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
70
+ end
71
+ end
72
+
73
+ context "with invalid group:" do
74
+ before { params[:group] = "" }
75
+
76
+ it "raises an exception" do
77
+ expect { subject }.to raise_error(TypeError)
78
+ end
79
+ end
80
+
81
+ context "without a group:" do
82
+ before { params.delete :group }
83
+
84
+ it "raises an exception" do
85
+ expect { subject }.to raise_error(KeyError)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,221 @@
1
+ RSpec.describe SmsAero, "#add_phone" do
2
+ let(:settings) { { user: "LOGIN", password: "PASSWORD" } }
3
+ let(:client) { described_class.new(settings) }
4
+ let(:params) { { phone: "+7 (909) 382-84-45" } }
5
+ let(:answer) { { result: "accepted" } }
6
+
7
+ before { stub_request(:any, //).to_return(body: answer.to_json) }
8
+ subject { client.add_phone(params) }
9
+
10
+ context "using ssl via POST:" do
11
+ let(:host) { "https://gate.smsaero.ru/addphone" }
12
+ let(:query) { "answer=json&password=PASSWORD&phone=79093828445&user=LOGIN" }
13
+
14
+ it "sends a request" do
15
+ subject
16
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
17
+ end
18
+
19
+ it "returns success" do
20
+ expect(subject).to be_kind_of SmsAero::Answer
21
+ expect(subject.result).to eq "accepted"
22
+ end
23
+ end
24
+
25
+ context "via GET:" do
26
+ let(:host) { "https://gate.smsaero.ru/addphone" }
27
+ let(:query) { "answer=json&password=PASSWORD&phone=79093828445&user=LOGIN" }
28
+
29
+ before { settings[:use_post] = false }
30
+
31
+ it "sends a request" do
32
+ subject
33
+ expect(a_request(:get, "#{host}?#{query}")).to have_been_made
34
+ end
35
+ end
36
+
37
+ context "not using ssl:" do
38
+ let(:host) { "http://gate.smsaero.ru/addphone" }
39
+ let(:query) { "answer=json&password=PASSWORD&phone=79093828445&user=LOGIN" }
40
+
41
+ before { settings[:use_ssl] = false }
42
+
43
+ it "sends a request" do
44
+ subject
45
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
46
+ end
47
+ end
48
+
49
+ context "with custom user:" do
50
+ let(:host) { "https://gate.smsaero.ru/addphone" }
51
+ let(:query) { "answer=json&password=PASSWORD&phone=79093828445&user=USER" }
52
+
53
+ before { params[:user] = "USER" }
54
+
55
+ it "sends a request" do
56
+ subject
57
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
58
+ end
59
+ end
60
+
61
+ context "with custom password:" do
62
+ let(:host) { "https://gate.smsaero.ru/addphone" }
63
+ let(:query) { "answer=json&password=PSWD&phone=79093828445&user=LOGIN" }
64
+
65
+ before { params[:password] = "PSWD" }
66
+
67
+ it "sends a request" do
68
+ subject
69
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
70
+ end
71
+ end
72
+
73
+ context "with invalid phone:" do
74
+ before { params[:phone] = "1324" }
75
+
76
+ it "raises an exception" do
77
+ expect { subject }.to raise_error(TypeError, /1324/)
78
+ end
79
+ end
80
+
81
+ context "without a phone:" do
82
+ before { params.delete :phone }
83
+
84
+ it "raises an exception" do
85
+ expect { subject }.to raise_error(KeyError)
86
+ end
87
+ end
88
+
89
+ context "with valid fname:" do
90
+ let(:host) { "https://gate.smsaero.ru/addphone" }
91
+ let(:query) do
92
+ "answer=json&fname=joe&password=PASSWORD&phone=79093828445&user=LOGIN"
93
+ end
94
+
95
+ before { params[:fname] = "joe" }
96
+
97
+ it "sends a request" do
98
+ subject
99
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
100
+ end
101
+ end
102
+
103
+ context "with invalid fname:" do
104
+ before { params[:fname] = "" }
105
+
106
+ it "raises an exception" do
107
+ expect { subject }.to raise_error(TypeError)
108
+ end
109
+ end
110
+
111
+ context "with valid sname:" do
112
+ let(:host) { "https://gate.smsaero.ru/addphone" }
113
+ let(:query) do
114
+ "answer=json&password=PASSWORD&phone=79093828445&sname=joe&user=LOGIN"
115
+ end
116
+
117
+ before { params[:sname] = "joe" }
118
+
119
+ it "sends a request" do
120
+ subject
121
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
122
+ end
123
+ end
124
+
125
+ context "with invalid sname:" do
126
+ before { params[:sname] = "" }
127
+
128
+ it "raises an exception" do
129
+ expect { subject }.to raise_error(TypeError)
130
+ end
131
+ end
132
+
133
+ context "with valid lname:" do
134
+ let(:host) { "https://gate.smsaero.ru/addphone" }
135
+ let(:query) do
136
+ "answer=json&lname=smith&password=PASSWORD&phone=79093828445&user=LOGIN"
137
+ end
138
+
139
+ before { params[:lname] = "smith" }
140
+
141
+ it "sends a request" do
142
+ subject
143
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
144
+ end
145
+ end
146
+
147
+ context "with invalid lname:" do
148
+ before { params[:lname] = "" }
149
+
150
+ it "raises an exception" do
151
+ expect { subject }.to raise_error(TypeError)
152
+ end
153
+ end
154
+
155
+ context "with valid group:" do
156
+ let(:host) { "https://gate.smsaero.ru/addphone" }
157
+ let(:query) do
158
+ "answer=json&group=qux&password=PASSWORD&phone=79093828445&user=LOGIN"
159
+ end
160
+
161
+ before { params[:group] = "qux" }
162
+
163
+ it "sends a request" do
164
+ subject
165
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
166
+ end
167
+ end
168
+
169
+ context "with invalid group:" do
170
+ before { params[:group] = "" }
171
+
172
+ it "raises an exception" do
173
+ expect { subject }.to raise_error(TypeError)
174
+ end
175
+ end
176
+
177
+ context "with valid param:" do
178
+ let(:host) { "https://gate.smsaero.ru/addphone" }
179
+ let(:query) do
180
+ "answer=json&param=qux&password=PASSWORD&phone=79093828445&user=LOGIN"
181
+ end
182
+
183
+ before { params[:param] = "qux" }
184
+
185
+ it "sends a request" do
186
+ subject
187
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
188
+ end
189
+ end
190
+
191
+ context "with invalid param:" do
192
+ before { params[:param] = "" }
193
+
194
+ it "raises an exception" do
195
+ expect { subject }.to raise_error(TypeError)
196
+ end
197
+ end
198
+
199
+ context "with valid bday:" do
200
+ let(:host) { "https://gate.smsaero.ru/addphone" }
201
+ let(:query) do
202
+ "answer=json&bday=1901-08-17&password=PASSWORD&" \
203
+ "phone=79093828445&user=LOGIN"
204
+ end
205
+
206
+ before { params[:bday] = Date.parse("1901-08-17") }
207
+
208
+ it "sends a request" do
209
+ subject
210
+ expect(a_request(:post, "#{host}?#{query}")).to have_been_made
211
+ end
212
+ end
213
+
214
+ context "with invalid bday:" do
215
+ before { params[:bday] = "foo" }
216
+
217
+ it "raises an exception" do
218
+ expect { subject }.to raise_error(TypeError, /foo/)
219
+ end
220
+ end
221
+ end