twilio-test-toolkit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +223 -0
  3. data/Rakefile +38 -0
  4. data/lib/twilio-test-toolkit.rb +4 -0
  5. data/lib/twilio-test-toolkit/call_in_progress.rb +48 -0
  6. data/lib/twilio-test-toolkit/call_scope.rb +163 -0
  7. data/lib/twilio-test-toolkit/dsl.rb +12 -0
  8. data/lib/twilio-test-toolkit/version.rb +3 -0
  9. data/spec/dummy/README.rdoc +261 -0
  10. data/spec/dummy/Rakefile +7 -0
  11. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  12. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  13. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  14. data/spec/dummy/app/controllers/twilio_controller.rb +8 -0
  15. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  16. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  17. data/spec/dummy/app/views/layouts/twilio.layout.xml.erb +4 -0
  18. data/spec/dummy/app/views/twilio/testaction.xml.erb +2 -0
  19. data/spec/dummy/app/views/twilio/testdial.xml.erb +1 -0
  20. data/spec/dummy/app/views/twilio/testhangup.xml.erb +1 -0
  21. data/spec/dummy/app/views/twilio/testredirect.xml.erb +1 -0
  22. data/spec/dummy/app/views/twilio/testsay.xml.erb +1 -0
  23. data/spec/dummy/app/views/twilio/teststart.xml.erb +5 -0
  24. data/spec/dummy/config.ru +4 -0
  25. data/spec/dummy/config/application.rb +59 -0
  26. data/spec/dummy/config/boot.rb +10 -0
  27. data/spec/dummy/config/database.yml +25 -0
  28. data/spec/dummy/config/environment.rb +5 -0
  29. data/spec/dummy/config/environments/development.rb +37 -0
  30. data/spec/dummy/config/environments/production.rb +67 -0
  31. data/spec/dummy/config/environments/test.rb +37 -0
  32. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  33. data/spec/dummy/config/initializers/inflections.rb +15 -0
  34. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  35. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  36. data/spec/dummy/config/initializers/session_store.rb +8 -0
  37. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  38. data/spec/dummy/config/locales/en.yml +5 -0
  39. data/spec/dummy/config/routes.rb +11 -0
  40. data/spec/dummy/db/test.sqlite3 +0 -0
  41. data/spec/dummy/log/test.log +1475 -0
  42. data/spec/dummy/public/404.html +26 -0
  43. data/spec/dummy/public/422.html +26 -0
  44. data/spec/dummy/public/500.html +25 -0
  45. data/spec/dummy/public/favicon.ico +0 -0
  46. data/spec/dummy/script/rails +6 -0
  47. data/spec/requests/call_scope_spec.rb +258 -0
  48. data/spec/requests/dsl_spec.rb +46 -0
  49. data/spec/spec_helper.rb +20 -0
  50. metadata +224 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,223 @@
1
+ twilio-test-toolkit
2
+ ===================
3
+
4
+ Twilio Test Toolkit (TTT) makes RSpec integration tests for Twilio phone callbacks easy to write and understand.
5
+
6
+ When you initiate a [phone call with Twilio](http://www.twilio.com/docs/api/rest/making-calls), you must POST either a URL or an ApplicationSid that is configured with a URL. Twilio then POSTs to your URL, and you're expected to return a 200 OK and a valid [TwiML](http://www.twilio.com/docs/api/twiml) response. That response can be any valid TwiML - speak something, gather keystrokes, redirect, etc.
7
+
8
+ Although it's pretty easy to test individual controller actions with the existing RSpec gem, testing more complex scenarios that use many controller actions (or controllers) is syntax-heavy and usually repetitive. TTT exists to make these larger scale integration tests easy and fun to write. TTT emulates the Twilio end of the Twilo phone callbacks, and allows to to emulate a user listening for a specific Say element, pressing keys on their phone, etc. With TTT, you can test your whole phone system built on Twilio.
9
+
10
+ For instance, let's say you have a controller that handles inbound Twilio calls and asks the user to enter an account number, then a PIN code, and then finally makes a menu selection. With TTT, you can just do this:
11
+
12
+ @call = ttt_call(some_controller_action_path, from_number, to_number)
13
+
14
+ @call.should have_say("Welcome to AwesomeCo.")
15
+ @call.within_gather do |gather|
16
+ gather.should have_say("Please enter your account number, followed by the pound sign.")
17
+ gather.press "12345678"
18
+ end
19
+
20
+ @call.current_path.should == some_other_controller_action_path
21
+ @call.within_gather do |gather|
22
+ gather.should have_say("Please enter your 4 digit PIN code.")
23
+ gather.press "9876"
24
+ end
25
+
26
+ @call.current_path.should == yet_another_controller_action_path
27
+ @call.should have_say "Please choose from one of the following menu options"
28
+
29
+ TTT was originally built to handle testing a complex, large-scale phone tree system. Without TTT, it was necessary to do lots of deployments to staging to test how the controller actions worked together, and there wasn't a good automated way to verify any new changes. When new functionality was introduced, TTT tests were written for the full project, and everything that worked in TTT worked great the first time it was pushed to staging. It's a great timesaver.
30
+
31
+ What's supported
32
+ ================
33
+
34
+ TTT supports most of the more common Twilio scenarios:
35
+
36
+ * Checking for Say elements and their content
37
+ * Taking action on Gather elements and querying their contents
38
+ * Following and querying redirects
39
+ * Dial and Hangup
40
+
41
+ TTT doesn't yet support (but you should contribute them!)
42
+
43
+ * Play
44
+ * Queue
45
+ * Any of the other conference calling features
46
+
47
+ What's required
48
+ ================
49
+
50
+ TTT depends on [Capybara](https://github.com/jnicklas/capybara). It uses Capybara's session object to POST requests to your controllers.
51
+
52
+ TTT expects your controller actions to behave like well-behaved Twilio callbacks. That is, you need to respond to XML-formatted requests, and need to respond with a 200 OK. TTT also requires that your controller actions be wired for POST, not GET (Twilio does support GET, but TTT lacks this support). Twilio will not follow 301 or 302 redirects properly, and neither will TTT (see below for more details).
53
+
54
+ TTT has only been tested with RSpec on Rails. It might work on other test frameworks or other Rack-based frameworks. Feel free to submit pull requests to improve compatibility with these.
55
+
56
+ If it works with Twilio, it should work with TTT. If not, open an issue/pull request.
57
+
58
+ Getting started
59
+ ================
60
+
61
+ First, get RSpec and Capybara working for your project. Then, you'll need to add this to your gemfile:
62
+
63
+ group :test do
64
+ ...
65
+ gem 'twilio-test-toolkit'
66
+ ...
67
+ end
68
+
69
+ Since TTT is test-only code, it should be in the :test group.
70
+
71
+ You'll have to make one more change in spec/spec_helper.rb:
72
+
73
+ RSpec.configure do |config|
74
+ ...
75
+ # Configure Twilio Test Toolkit
76
+ config.include TwilioTestToolkit::DSL, :type => :request
77
+ ...
78
+ end
79
+
80
+ This line is required in order to get TTT's DSL to work with your tests.
81
+
82
+ Finally, since TTT deals with integration tests, you should write your tests in spec/requests (or whatever directory you've configured for this type of test).
83
+
84
+ How to use
85
+ ================
86
+
87
+ This section describes how to use TTT and its basic functionality.
88
+
89
+ ttt_call
90
+ -------------
91
+
92
+ The *ttt_call* method is the main entry point for working with TTT. You call this method to initiate a "Twilio phone call" to your controller actions. TTT simulates Twilio's behavior by POSTing to your action with the expected Twilio parameters (From, To, CallSid, etc).
93
+
94
+ *ttt_call* has three required parameters, and two optional ones:
95
+
96
+ @call = ttt_call(action_path, from_number, to_number, call_sid = nil, is_machine = false)
97
+
98
+ * **action_path**. Where to POST your request. It should be obvious by now, but whatever action you specify here should be a POST action.
99
+ * **from_number**. What to fill params[:From] with. If you don't care about this value, you can pass a blank one, as this is only used to pass along to your actions.
100
+ * **to_number**. What to fill params[:To] with.
101
+ * **call_sid**. Specify an optional fixed value to be passed as params[:CallSid]. This is useful if you are expecting a specific SID. For instance, a common pattern is to initiate a call, store the SID in your database, and look up the call when you get the callback. If you don't pass a SID, TTT will generate one for you that's just a UUID.
102
+ * **is_machine**. Controls params[:AnsweredBy]. See Twilio's documentation for more information on how Twilio uses this.
103
+
104
+ *ttt_call* returns a *TwilioTestToolkit::CallInProgress* object, which is a descendent of *TwilioTestToolkit::CallScope*. You'll want to save this object as it's how you interact with TTT.
105
+
106
+ It's worth noting that TTT won't pass any of the parameters you need to [validate that a request comes from Twilio](http://www.twilio.com/docs/security). Most people seem to only do this check in production, so if that applies to you, this won't be an issue. If you do this check in dev and test, you might want to consider submitting a pull request with a fix.
107
+
108
+ Getting call information
109
+ --------------
110
+
111
+ You can use the CallInProgress object returned from *ttt_call* to inspect some basic properties about the call:
112
+
113
+ @call.sid # Returns the SID of the call
114
+ @call.initial_path # Returns the path that you used to start the call (the first parameter to ttt_call)
115
+ @call.from_number # Returns the from number
116
+ @call.to_number # Returns the to number
117
+ @call.is_machine # Returns the answering machine state (passed to ttt_call)
118
+
119
+ Call scopes
120
+ --------------
121
+
122
+ The CallInProgress object returned from *ttt_call* is a descendent of CallScope. A CallScope represents a scope within a call. For instance, the root TwiML Response element is a scope. A Gather within that is a scope.
123
+
124
+ For instance, let's say you have TwiML like this:
125
+
126
+ <Response>
127
+ <Say>Foo</Say>
128
+ <Gather action="baz">
129
+ <Say>Bar</Say>
130
+ </Gather>
131
+ </Response>
132
+
133
+ The scope referred to by the CallInProgress object is the Response object. Only items that directly descend from this scope are seen by TTT. That is, the say for "Foo" is in the scope, but the say for "Bar" is not. The Gather is its own scope, and it contains the say for "Bar". TTT intentionally restricts you to accessing only what's in your scope because it helps you enforce a more rigid structure in your call, and allows you to handle multiple Gathers or similar in a given TwiML markup.
134
+
135
+ CallScope has some properties that are also useful:
136
+
137
+ @call.current_path # Returns the current path that the call is on.
138
+ @call.response_xml # Returns the raw XML response that your action returned to TTT
139
+ @call.root_call # Returns the original CallInProgress returned from ttt_call.
140
+
141
+ Inspecting the contents of the call
142
+ --------------
143
+
144
+ A common thing you'll want to do is inspect the various Say elements, and check for control elements like Dial and Hangup.
145
+
146
+ @call.has_say?("Foo") # Returns true if there's a <Say> in the current scope
147
+ # that contains the text. Partial matches are OK, but
148
+ # the call is case sensitive.
149
+ @call.has_dial?("911") # Returns true if there's a <Dial> in the current scope
150
+ # for the number. Partial matches are OK.
151
+ @call.has_hangup? # Returns true if there's a <Hangup> in the current scope.
152
+
153
+ These methods are available on any CallScope.
154
+
155
+ Gathers
156
+ --------------
157
+
158
+ Gathers are used to collect digits from the caller (e.g. "press 1 to speak with a representative, press 2 to cancel"). Twilio handles Gathers by speaking the contents of the Gather and waiting for a digit or digits to be pressed. If nothing is pressed, it continues with the script. If something is pressed, it aborts processing the current script and POSTs the digits (via params[:Digits]) to the path specified by the action attribute. TTT handles Gathers in a similar way.
159
+
160
+ You can only interact with a gather by calling *within_gather*:
161
+
162
+ @call.within_gather do |gather|
163
+ gather.should have_say("Enter your account number")
164
+ gather.press "12345"
165
+ end
166
+
167
+ *within_gather* creates a new CallScope and passes it to the yielded parameter (gather in the example above). *within_gather* will fail if there is no Gather element in the current scope.
168
+
169
+ You can verify the existence of a Gather in the current scope with:
170
+
171
+ @call.has_gather? # Returns true if the current scope has a gather.
172
+
173
+ Within a gather CallScope, you can use the following methods:
174
+
175
+ @call.gather? # Returns true if the current scope **is** a gather. Compare with the has_gather? method.
176
+ @call.gather_action # Returns the value of the action attribute for the gather.
177
+ @call.press("1") # Simulates pressing the specified digits.
178
+
179
+ You can also use other CallScope methods (e.g. *has_say?* and similar.)
180
+
181
+ The *press* method has a few caveats worth knowing. It's only callable once per gather - when you call it, TTT will immediately POST to the gather's action. There is no way to simulate pressing buttons slowly, and you don't really need to do this anyways - Twilio doesn't care and just passes them all at once. *press* simply fills out the value of params[:Digits] and POSTs that to your method, just like Twilio does.
182
+
183
+ Although you can technically pass whatever you want to *press*, in practice Twilio only sends digits and #. Still it's probably a good idea to test garbage data in this parameter with your actions, so TTT doesn't get in your way if you want to call press with "UNICORNSANDPONIES" as a parameter.
184
+
185
+ TTT doesn't attempt to validate your TwiML, so it's worth knowing that Gather only allows Say, Pause, and Play as child elements. Nested Gathers are not supported.
186
+
187
+ Redirects
188
+ --------------
189
+
190
+ The Redirect element is used to tell Twilio to POST to a different page. It differs from a standard 301 or 302 redirect (created by a *redirect_to*) in that the 301/302 redirects don't support a POST, and if you try a *redirect_to* within a Twilio action, Twilio will fail and your caller will get the dreaded "I'm sorry, an application error has occurred" message. Because Twilio doesn't support 301/302 redirects, TTT doesn't either, and if you use one, TTT will complain.
191
+
192
+ There are several methods you can use within a CallScope related to redirects:
193
+
194
+ @call.has_redirect? # Returns true if there's a <Redirect> element in the current scope.
195
+ @call.has_redirect_to?(path) # Returns true if there's a <Redirect> element to the specified path
196
+ # in the current scope.
197
+ @call.follow_redirect # Follows the <Redirect> in the current scope and returns a new
198
+ # CallScope object. The original scope is not modified.
199
+ @call.follow_redirect! # Follows the <Redirect> in the current scope and updates the scope
200
+ # to the new path.
201
+
202
+ Although it's allowed by TwiML (as of this writing), multiple Redirects in a scope aren't effectively allowed, as only the first one will ever be used. Thus, TTT only looks at the first Redirect it finds in the given scope.
203
+
204
+ Contributing
205
+ ================
206
+
207
+ TTT is pretty basic, but it should work for most people's needs. You might consider helping to improve it. Some things that could be done:
208
+
209
+ * Support more Twilio functionality
210
+ * Support GET actions. Twilio technically supports this, although it doesn't seem to be used very often.
211
+ * Build more stringent checks into says and plays - e.g. verify that there's enough of a pause between sentences.
212
+ * Build checks to make sure that numbers, dates, and other special text is spoken properly (e.g. "one two three" instead of "one hundred twenty three").
213
+ * Build checks to validate the correctness of your TwiML.
214
+ * Refactor wonky code (there's probably some in there somewhere)
215
+ * Write more tests. There are basic tests, but more involved ones might be nice
216
+ * Add support as needed for other test frameworks or Rack-based frameworks
217
+
218
+ Contributions are welcome and encouraged. The usual deal applies - fork a branch, add tests, add your changes, submit a pull request. If I haven't done anything with your pull request in a reasonable amount of time, ping me on Twitter or email and I'll get on it.
219
+
220
+ Credits
221
+ ================
222
+
223
+ TTT was put together by Jack Nichols [@jmongol](http://twitter.com/jmongol). MIT license.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'TwilioTestToolkit'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,4 @@
1
+ module TwilioTestToolkit
2
+ require "uuidtools"
3
+ require 'twilio-test-toolkit/dsl'
4
+ end
@@ -0,0 +1,48 @@
1
+ require "twilio-test-toolkit/call_scope"
2
+
3
+ module TwilioTestToolkit
4
+ # Models a call
5
+ class CallInProgress < CallScope
6
+ # Init the call
7
+ def initialize(initial_path, from_number, to_number, call_sid, is_machine)
8
+ # Save our variables for later
9
+ @initial_path = initial_path
10
+ @from_number = from_number
11
+ @to_number = to_number
12
+ @is_machine = is_machine
13
+
14
+ # Generate an initial call SID if we don't have one
15
+ if (call_sid.nil?)
16
+ @sid = UUIDTools::UUID.random_create.to_s
17
+ else
18
+ @sid = call_sid
19
+ end
20
+
21
+ # We are the root call
22
+ self.root_call = self
23
+
24
+ # Create the request
25
+ post_for_twiml!(@initial_path, "", @is_machine)
26
+ end
27
+
28
+ def sid
29
+ @sid
30
+ end
31
+
32
+ def initial_path
33
+ @initial_path
34
+ end
35
+
36
+ def from_number
37
+ @from_number
38
+ end
39
+
40
+ def to_number
41
+ @to_number
42
+ end
43
+
44
+ def is_machine
45
+ @is_machine
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,163 @@
1
+ module TwilioTestToolkit
2
+ # Models a scope within a call.
3
+ class CallScope
4
+ # Stuff for redirects
5
+ def has_redirect_to?(url)
6
+ el = get_redirect_node
7
+ return false if el.nil?
8
+ return normalize_redirect_path(el.text) == normalize_redirect_path(url)
9
+ end
10
+
11
+ def follow_redirect
12
+ el = get_redirect_node
13
+ raise "No redirect" if el.nil?
14
+
15
+ return CallScope.from_post(self, el.text)
16
+ end
17
+
18
+ def follow_redirect!
19
+ el = get_redirect_node
20
+ raise "No redirect" if el.nil?
21
+
22
+ post_for_twiml!(normalize_redirect_path(el.text))
23
+ end
24
+
25
+ # Stuff for Says
26
+ def has_say?(say)
27
+ @xml.xpath("Say").each do |s|
28
+ return true if s.inner_text.include?(say)
29
+ end
30
+
31
+ return false
32
+ end
33
+
34
+ # Stuff for Dials
35
+ def has_dial?(number)
36
+ @xml.xpath("Dial").each do |s|
37
+ return true if s.inner_text.include?(number)
38
+ end
39
+
40
+ return false
41
+ end
42
+
43
+ # Stuff for hangups
44
+ def has_redirect?
45
+ return !(@xml.at_xpath("Redirect").nil?)
46
+ end
47
+
48
+ def has_hangup?
49
+ return !(@xml.at_xpath("Hangup").nil?)
50
+ end
51
+
52
+ def has_gather?
53
+ return !(@xml.at_xpath("Gather").nil?)
54
+ end
55
+
56
+ # Within gather returns a scope that's tied to the specified gather.
57
+ def within_gather(&block)
58
+ gather_el = get_gather_node
59
+ raise "No gather in scope" if gather_el.nil?
60
+ yield(CallScope.from_xml(self, gather_el))
61
+ end
62
+
63
+ # Stuff for gatherers
64
+ def gather?
65
+ @xml.name == "Gather"
66
+ end
67
+
68
+ def gather_action
69
+ rasie "Not a gather" unless gather?
70
+ return @xml["action"]
71
+ end
72
+
73
+ def press(digits)
74
+ raise "Not a gather" unless gather?
75
+
76
+ # Fetch the path and then post
77
+ path = gather_action
78
+
79
+ # Update the root call
80
+ root_call.post_for_twiml!(path, digits)
81
+ end
82
+
83
+ # Some basic accessors
84
+ def current_path
85
+ @current_path
86
+ end
87
+
88
+ def response_xml
89
+ @response_xml
90
+ end
91
+
92
+ def root_call
93
+ @root_call
94
+ end
95
+
96
+ private
97
+ def get_redirect_node
98
+ @xml.at_xpath("Redirect")
99
+ end
100
+
101
+ def get_gather_node
102
+ @xml.at_xpath("Gather")
103
+ end
104
+
105
+ protected
106
+ # New object creation
107
+ def self.from_xml(parent, xml)
108
+ new_scope = CallScope.new
109
+ new_scope.send(:set_xml, xml)
110
+ new_scope.send(:root_call=, parent.root_call)
111
+ return new_scope
112
+ end
113
+
114
+ def set_xml(xml)
115
+ @xml = xml
116
+ end
117
+
118
+ # Create a new object from a post
119
+ def self.from_post(parent, path, digits = "")
120
+ new_scope = CallScope.new
121
+ new_scope.send(:root_call=, parent.root_call)
122
+ new_scope.send(:post_for_twiml!, path, digits)
123
+ return new_scope
124
+ end
125
+
126
+ def normalize_redirect_path(path)
127
+ p = path
128
+
129
+ # Strip off ".xml" off of the end of any path
130
+ p = path[0...path.length - ".xml".length] if path.downcase.ends_with?(".xml")
131
+ return p
132
+ end
133
+
134
+ # Post and update the scope
135
+ def post_for_twiml!(path, digits = "", is_machine = false)
136
+ @current_path = normalize_redirect_path(path)
137
+
138
+ # Post the query
139
+ rack_test_session_wrapper = Capybara.current_session.driver
140
+ @response = rack_test_session_wrapper.post(@current_path,
141
+ :format => :xml,
142
+ :CallSid => @root_call.sid,
143
+ :Digits => digits,
144
+ :From => @root_call.from_number,
145
+ :To => @root_call.to_number,
146
+ :AnsweredBy => (is_machine ? "machine" : "human")
147
+ )
148
+
149
+ # All Twilio responses must be a success.
150
+ raise "Bad response: #{@response.status}" unless @response.status == 200
151
+
152
+ # Load the xml
153
+ data = @response.body
154
+ @response_xml = Nokogiri::XML.parse(data)
155
+ set_xml(@response_xml.at_xpath("Response"))
156
+ end
157
+
158
+ # Parent call control
159
+ def root_call=(val)
160
+ @root_call = val
161
+ end
162
+ end
163
+ end