sippy_cup 0.2.3 → 0.3.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.
- checksums.yaml +9 -9
- data/.gitignore +3 -0
- data/.rspec +1 -0
- data/.travis.yml +15 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +2 -0
- data/Guardfile +4 -9
- data/README.markdown +28 -28
- data/Rakefile +2 -3
- data/bin/sippy_cup +16 -26
- data/lib/sippy_cup/media.rb +3 -1
- data/lib/sippy_cup/runner.rb +148 -50
- data/lib/sippy_cup/scenario.rb +436 -206
- data/lib/sippy_cup/tasks.rb +3 -3
- data/lib/sippy_cup/version.rb +1 -1
- data/lib/sippy_cup/xml_scenario.rb +57 -0
- data/lib/sippy_cup.rb +1 -0
- data/sippy_cup.gemspec +2 -1
- data/spec/fixtures/dtmf_2833_1.pcap +0 -0
- data/spec/fixtures/scenario.xml +73 -0
- data/spec/sippy_cup/fixtures/test.yml +16 -0
- data/spec/sippy_cup/runner_spec.rb +425 -71
- data/spec/sippy_cup/scenario_spec.rb +820 -71
- data/spec/sippy_cup/xml_scenario_spec.rb +103 -0
- data/spec/spec_helper.rb +5 -2
- metadata +30 -5
- data/tmp/rspec_guard_result +0 -1
@@ -1,141 +1,890 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe SippyCup::Scenario do
|
4
|
+
include FakeFS::SpecHelpers
|
5
|
+
|
6
|
+
before do
|
7
|
+
Dir.mkdir("/tmp") unless Dir.exist?("/tmp")
|
8
|
+
Dir.chdir "/tmp"
|
9
|
+
end
|
10
|
+
|
4
11
|
let(:default_args) { {source: '127.0.0.1:5060', destination: '10.0.0.1:5080'} }
|
12
|
+
let(:args) { {} }
|
13
|
+
|
14
|
+
subject(:scenario) { described_class.new 'Test', default_args.merge(args) }
|
5
15
|
|
6
|
-
it
|
16
|
+
it "creates a media stream on initialization" do
|
7
17
|
SippyCup::Media.should_receive(:new).once
|
8
|
-
|
18
|
+
subject
|
9
19
|
end
|
10
20
|
|
11
|
-
it
|
12
|
-
s =
|
21
|
+
it "takes a block to generate a scenario" do
|
22
|
+
s = described_class.new 'Test', default_args do
|
13
23
|
invite
|
14
24
|
end
|
15
25
|
|
16
26
|
s.to_xml.should =~ %r{INVITE sip:\[service\]@\[remote_ip\]:\[remote_port\] SIP/2.0}
|
17
27
|
end
|
18
28
|
|
19
|
-
it
|
20
|
-
|
21
|
-
s.invite
|
22
|
-
s.to_xml.should =~ %r{INVITE sip:\[service\]@\[remote_ip\]:\[remote_port\] SIP/2.0}
|
29
|
+
it "allows creating a blank scenario with no block" do
|
30
|
+
subject.to_xml.should =~ %r{<scenario name="Test"/>}
|
23
31
|
end
|
24
32
|
|
25
|
-
describe '#
|
26
|
-
|
33
|
+
describe '#invite' do
|
34
|
+
it "sends an INVITE message" do
|
35
|
+
subject.invite
|
36
|
+
|
37
|
+
subject.to_xml.should match(%r{<send .*>})
|
38
|
+
subject.to_xml.should match(%r{INVITE})
|
39
|
+
end
|
40
|
+
|
41
|
+
it "allows setting options on the send instruction" do
|
42
|
+
subject.invite foo: 'bar'
|
43
|
+
|
44
|
+
subject.to_xml.should match(%r{<send foo="bar".*>})
|
45
|
+
end
|
46
|
+
|
47
|
+
it "defaults to retrans of 500" do
|
48
|
+
subject.invite
|
49
|
+
subject.to_xml.should match(%r{<send retrans="500".*>})
|
50
|
+
end
|
51
|
+
|
52
|
+
it "allows setting retrans" do
|
53
|
+
subject.invite retrans: 200
|
54
|
+
subject.to_xml.should match(%r{<send retrans="200".*>})
|
55
|
+
end
|
56
|
+
|
57
|
+
context "with extra headers specified" do
|
58
|
+
it "adds the headers to the end of the message" do
|
59
|
+
subject.invite headers: "Foo: <bar>\nBar: <baz>"
|
60
|
+
subject.to_xml.should match(%r{Foo: <bar>\nBar: <baz>})
|
61
|
+
end
|
62
|
+
|
63
|
+
it "only has one blank line between headers and SDP" do
|
64
|
+
subject.invite headers: "Foo: <bar>\n\n\n"
|
65
|
+
subject.to_xml.should match(%r{Foo: <bar>\n\nv=0})
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "with no extra headers" do
|
70
|
+
it "only has one blank line between headers and SDP" do
|
71
|
+
subject.invite
|
72
|
+
subject.to_xml.should match(%r{Content-Length: \[len\]\n\nv=0})
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "uses [media_port+1] as the RTCP port in the SDP" do
|
77
|
+
subject.invite
|
78
|
+
subject.to_xml.should match(%r{m=audio \[media_port\] RTP/AVP 0 101\n})
|
79
|
+
end
|
80
|
+
|
81
|
+
context "when a from user is specified" do
|
82
|
+
let(:args) { {from_user: 'frank'} }
|
83
|
+
|
84
|
+
it "includes the specified user in the From and Contact headers" do
|
85
|
+
subject.invite
|
86
|
+
subject.to_xml.should match(%r{From: "frank" <sip:frank@})
|
87
|
+
subject.to_xml.should match(%r{Contact: <sip:frank@})
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "when no from user is specified" do
|
92
|
+
it "uses a default of 'sipp' in the From and Contact headers" do
|
93
|
+
subject.invite
|
94
|
+
subject.to_xml.should match(%r{From: "sipp" <sip:sipp@})
|
95
|
+
subject.to_xml.should match(%r{Contact: <sip:sipp@})
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "#register" do
|
101
|
+
it "sends a REGISTER message" do
|
102
|
+
subject.register 'frank'
|
103
|
+
|
104
|
+
subject.to_xml.should match(%r{<send .*>})
|
105
|
+
subject.to_xml.should match(%r{REGISTER})
|
106
|
+
end
|
107
|
+
|
108
|
+
it "allows setting options on the send instruction" do
|
109
|
+
subject.register 'frank', nil, foo: 'bar'
|
110
|
+
subject.to_xml.should match(%r{<send foo="bar".*>})
|
111
|
+
end
|
112
|
+
|
113
|
+
it "defaults to retrans of 500" do
|
114
|
+
subject.register 'frank'
|
115
|
+
subject.to_xml.should match(%r{<send retrans="500".*>})
|
116
|
+
end
|
117
|
+
|
118
|
+
it "allows setting retrans" do
|
119
|
+
subject.register 'frank', nil, retrans: 200
|
120
|
+
subject.to_xml.should match(%r{<send retrans="200".*>})
|
121
|
+
end
|
122
|
+
|
123
|
+
context "when a domain is provided" do
|
124
|
+
it "uses the specified user and domain" do
|
125
|
+
subject.register 'frank@foobar.com'
|
126
|
+
subject.to_xml.should match(%r{REGISTER sip:foobar.com})
|
127
|
+
subject.to_xml.should match(%r{From: <sip:frank@foobar.com})
|
128
|
+
subject.to_xml.should match(%r{To: <sip:frank@foobar.com})
|
129
|
+
subject.to_xml.should match(%r{Contact: <sip:sipp@\[local_ip\]})
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "when a domain is not provided" do
|
134
|
+
it "uses the remote IP" do
|
135
|
+
subject.register 'frank'
|
136
|
+
subject.to_xml.should match(%r{REGISTER sip:\[remote_ip\]})
|
137
|
+
subject.to_xml.should match(%r{From: <sip:frank@\[remote_ip\]})
|
138
|
+
subject.to_xml.should match(%r{To: <sip:frank@\[remote_ip\]})
|
139
|
+
subject.to_xml.should match(%r{Contact: <sip:sipp@\[local_ip\]})
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "when a password is provided" do
|
144
|
+
it "expects a 401 response" do
|
145
|
+
subject.register 'frank', 'abc123'
|
146
|
+
subject.to_xml.should match(%r{<recv response="401" auth="true" optional="false"/>})
|
147
|
+
end
|
148
|
+
|
149
|
+
it "adds authentication data to the REGISTER message" do
|
150
|
+
subject.register 'frank', 'abc123'
|
151
|
+
subject.to_xml.should match(%r{\[authentication username=frank password=abc123\]})
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe '#receive_trying' do
|
157
|
+
it "expects an optional 100" do
|
158
|
+
subject.receive_trying
|
159
|
+
|
160
|
+
scenario.to_xml.should match(%q{<recv response="100" optional="true"/>})
|
161
|
+
end
|
27
162
|
|
28
|
-
it
|
163
|
+
it "allows passing options to the recv expectation" do
|
164
|
+
subject.receive_trying foo: 'bar'
|
165
|
+
|
166
|
+
scenario.to_xml.should match(%q{<recv foo="bar" response="100" optional="true"/>})
|
167
|
+
end
|
168
|
+
|
169
|
+
it "allows overriding options" do
|
170
|
+
subject.receive_trying optional: false
|
171
|
+
|
172
|
+
scenario.to_xml.should match(%q{<recv optional="false" response="100"/>})
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe '#receive_ringing' do
|
177
|
+
it "expects an optional 180" do
|
178
|
+
subject.receive_ringing
|
179
|
+
|
180
|
+
scenario.to_xml.should match(%q{<recv response="180" optional="true"/>})
|
181
|
+
end
|
182
|
+
|
183
|
+
it "allows passing options to the recv expectation" do
|
184
|
+
subject.receive_ringing foo: 'bar'
|
185
|
+
|
186
|
+
scenario.to_xml.should match(%q{<recv foo="bar" response="180" optional="true"/>})
|
187
|
+
end
|
188
|
+
|
189
|
+
it "allows overriding options" do
|
190
|
+
subject.receive_ringing optional: false
|
191
|
+
|
192
|
+
scenario.to_xml.should match(%q{<recv optional="false" response="180"/>})
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe '#receive_progress' do
|
197
|
+
it "expects an optional 183" do
|
198
|
+
subject.receive_progress
|
199
|
+
|
200
|
+
scenario.to_xml.should match(%q{<recv response="183" optional="true"/>})
|
201
|
+
end
|
202
|
+
|
203
|
+
it "allows passing options to the recv expectation" do
|
204
|
+
subject.receive_progress foo: 'bar'
|
205
|
+
|
206
|
+
scenario.to_xml.should match(%q{<recv foo="bar" response="183" optional="true"/>})
|
207
|
+
end
|
208
|
+
|
209
|
+
it "allows overriding options" do
|
210
|
+
subject.receive_progress optional: false
|
211
|
+
|
212
|
+
scenario.to_xml.should match(%q{<recv optional="false" response="183"/>})
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe '#receive_answer' do
|
217
|
+
it "expects a 200 with rrs and rtd true" do
|
218
|
+
subject.receive_answer
|
219
|
+
|
220
|
+
scenario.to_xml.should match(%q{<recv response="200" rrs="true" rtd="true"/>})
|
221
|
+
end
|
222
|
+
|
223
|
+
it "allows passing options to the recv expectation" do
|
224
|
+
subject.receive_answer foo: 'bar'
|
225
|
+
|
226
|
+
scenario.to_xml.should match(%q{<recv response="200" rrs="true" rtd="true" foo="bar"/>})
|
227
|
+
end
|
228
|
+
|
229
|
+
it "allows overriding options" do
|
230
|
+
subject.receive_answer rtd: false
|
231
|
+
|
232
|
+
scenario.to_xml.should match(%q{<recv response="200" rrs="true" rtd="false"/>})
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe '#ack_answer' do
|
237
|
+
it "sends an ACK message" do
|
238
|
+
subject.ack_answer
|
239
|
+
|
240
|
+
subject.to_xml.should match(%r{<send>})
|
241
|
+
subject.to_xml.should match(%r{ACK})
|
242
|
+
end
|
243
|
+
|
244
|
+
it "allows setting options on the send instruction" do
|
245
|
+
subject.ack_answer foo: 'bar'
|
246
|
+
subject.to_xml.should match(%r{<send foo="bar".*>})
|
247
|
+
end
|
248
|
+
|
249
|
+
it "starts the PCAP media" do
|
250
|
+
subject.ack_answer
|
251
|
+
|
252
|
+
subject.to_xml.should match(%r{<nop>\n.*<action>\n.*<exec play_pcap_audio="/tmp/test.pcap"/>\n.*</action>\n.*</nop>})
|
253
|
+
end
|
254
|
+
|
255
|
+
context "when a from user is specified" do
|
256
|
+
let(:args) { {from_user: 'frank'} }
|
257
|
+
|
258
|
+
it "includes the specified user in the From and Contact headers" do
|
259
|
+
subject.ack_answer
|
260
|
+
subject.to_xml.should match(%r{From: "frank" <sip:frank@})
|
261
|
+
subject.to_xml.should match(%r{Contact: <sip:frank@})
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
context "when no from user is specified" do
|
266
|
+
it "uses a default of 'sipp' in the From and Contact headers" do
|
267
|
+
subject.ack_answer
|
268
|
+
subject.to_xml.should match(%r{From: "sipp" <sip:sipp@})
|
269
|
+
subject.to_xml.should match(%r{Contact: <sip:sipp@})
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
describe '#wait_for_answer' do
|
275
|
+
it "tells SIPp to optionally receive a SIP 100, 180 and 183 by default, while requiring a 200" do
|
29
276
|
scenario.wait_for_answer
|
30
277
|
|
31
278
|
xml = scenario.to_xml
|
32
|
-
xml.should =~ /recv
|
33
|
-
xml.should =~ /recv
|
34
|
-
xml.should =~ /recv
|
279
|
+
xml.should =~ /recv optional="true".*response="100"/
|
280
|
+
xml.should =~ /recv optional="true".*response="180"/
|
281
|
+
xml.should =~ /recv optional="true".*response="183"/
|
35
282
|
xml.should =~ /recv response="200"/
|
36
|
-
xml.should_not =~ /recv
|
283
|
+
xml.should_not =~ /recv optional="true".*response="200"/
|
37
284
|
end
|
38
285
|
|
39
|
-
it
|
286
|
+
it "passes through additional options" do
|
40
287
|
scenario.wait_for_answer foo: 'bar'
|
41
288
|
|
42
289
|
xml = scenario.to_xml
|
43
|
-
xml.should =~ /recv
|
44
|
-
xml.should =~ /recv
|
45
|
-
xml.should =~ /recv
|
46
|
-
xml.should =~ /recv response="200".*foo="bar"/
|
290
|
+
xml.should =~ /recv .*foo="bar".*response="100"/
|
291
|
+
xml.should =~ /recv .*foo="bar".*response="180"/
|
292
|
+
xml.should =~ /recv .*foo="bar".*response="183"/
|
293
|
+
xml.should =~ /recv .*response="200" .*foo="bar"/
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
describe '#send_bye' do
|
298
|
+
it "sends a BYE message" do
|
299
|
+
subject.send_bye
|
300
|
+
|
301
|
+
subject.to_xml.should match(%r{<send>})
|
302
|
+
subject.to_xml.should match(%r{BYE})
|
303
|
+
end
|
304
|
+
|
305
|
+
it "allows setting options on the send instruction" do
|
306
|
+
subject.send_bye foo: 'bar'
|
307
|
+
subject.to_xml.should match(%r{<send foo="bar".*>})
|
308
|
+
end
|
309
|
+
|
310
|
+
context "when a from user is specified" do
|
311
|
+
let(:args) { {from_user: 'frank'} }
|
312
|
+
|
313
|
+
it "includes the specified user in the From and Contact headers" do
|
314
|
+
subject.send_bye
|
315
|
+
subject.to_xml.should match(%r{From: "frank" <sip:frank@})
|
316
|
+
subject.to_xml.should match(%r{Contact: <sip:frank@})
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
context "when no from user is specified" do
|
321
|
+
it "uses a default of 'sipp' in the From and Contact headers" do
|
322
|
+
subject.send_bye
|
323
|
+
subject.to_xml.should match(%r{From: "sipp" <sip:sipp@})
|
324
|
+
subject.to_xml.should match(%r{Contact: <sip:sipp@})
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
describe '#receive_bye' do
|
330
|
+
it "expects a BYE" do
|
331
|
+
subject.receive_bye
|
332
|
+
|
333
|
+
scenario.to_xml.should match(%q{<recv request="BYE"/>})
|
334
|
+
end
|
335
|
+
|
336
|
+
it "allows passing options to the recv expectation" do
|
337
|
+
subject.receive_bye foo: 'bar'
|
338
|
+
|
339
|
+
scenario.to_xml.should match(%q{<recv foo="bar" request="BYE"/>})
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
describe '#ack_bye' do
|
344
|
+
it "sends a 200 OK" do
|
345
|
+
subject.ack_bye
|
346
|
+
|
347
|
+
subject.to_xml.should match(%r{<send>})
|
348
|
+
subject.to_xml.should match(%r{SIP/2.0 200 OK})
|
349
|
+
end
|
350
|
+
|
351
|
+
it "allows setting options on the send instruction" do
|
352
|
+
subject.ack_bye foo: 'bar'
|
353
|
+
subject.to_xml.should match(%r{<send foo="bar".*>})
|
354
|
+
end
|
355
|
+
|
356
|
+
context "when a from user is specified" do
|
357
|
+
let(:args) { {from_user: 'frank'} }
|
358
|
+
|
359
|
+
it "includes the specified user in the Contact header" do
|
360
|
+
subject.ack_bye
|
361
|
+
subject.to_xml.should match(%r{Contact: <sip:frank@})
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
context "when no from user is specified" do
|
366
|
+
it "uses a default of 'sipp' in the Contact header" do
|
367
|
+
subject.ack_bye
|
368
|
+
subject.to_xml.should match(%r{Contact: <sip:sipp@})
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
describe '#wait_for_hangup' do
|
374
|
+
it "expects a BYE and acks it" do
|
375
|
+
subject.receive_bye foo: 'bar'
|
376
|
+
|
377
|
+
scenario.to_xml.should match(%q{<recv foo="bar" request="BYE"/>})
|
378
|
+
scenario.to_xml.should match(%q{<recv foo="bar" request="BYE"/>})
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
describe '#ack_bye' do
|
383
|
+
it "sends a 200 OK" do
|
384
|
+
subject.ack_bye
|
385
|
+
|
386
|
+
subject.to_xml.should match(%r{<send>})
|
387
|
+
subject.to_xml.should match(%r{SIP/2.0 200 OK})
|
47
388
|
end
|
48
389
|
end
|
49
390
|
|
50
391
|
describe 'media-dependent operations' do
|
51
392
|
let(:media) { double :media }
|
52
|
-
|
393
|
+
before do
|
53
394
|
SippyCup::Media.should_receive(:new).once.and_return media
|
54
|
-
|
395
|
+
media.stub :<<
|
55
396
|
end
|
56
397
|
|
57
|
-
|
58
|
-
|
59
|
-
|
398
|
+
describe '#sleep' do
|
399
|
+
it "creates the proper amount of silent audio'" do
|
400
|
+
media.should_receive(:<<).once.with 'silence:5000'
|
401
|
+
scenario.sleep 5
|
402
|
+
end
|
403
|
+
|
404
|
+
it "should insert a pause into the scenario" do
|
405
|
+
scenario.sleep 5
|
406
|
+
scenario.to_xml.should match(%r{<pause milliseconds="5000"/>})
|
407
|
+
end
|
408
|
+
|
409
|
+
context "when passed fractional seconds" do
|
410
|
+
it "creates the proper amount of silent audio" do
|
411
|
+
media.should_receive(:<<).once.with 'silence:500'
|
412
|
+
scenario.sleep '0.5'
|
413
|
+
end
|
414
|
+
|
415
|
+
it "should insert a pause into the scenario" do
|
416
|
+
scenario.sleep 0.5
|
417
|
+
scenario.to_xml.should match(%r{<pause milliseconds="500"/>})
|
418
|
+
end
|
419
|
+
end
|
60
420
|
end
|
61
421
|
|
62
|
-
|
63
|
-
media
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
422
|
+
describe '#send_digits' do
|
423
|
+
it "creates the requested DTMF string in media, with 250ms pauses between" do
|
424
|
+
media.should_receive(:<<).ordered.with 'dtmf:1'
|
425
|
+
media.should_receive(:<<).ordered.with 'silence:250'
|
426
|
+
media.should_receive(:<<).ordered.with 'dtmf:3'
|
427
|
+
media.should_receive(:<<).ordered.with 'silence:250'
|
428
|
+
media.should_receive(:<<).ordered.with 'dtmf:6'
|
429
|
+
media.should_receive(:<<).ordered.with 'silence:250'
|
430
|
+
scenario.send_digits '136'
|
431
|
+
end
|
432
|
+
|
433
|
+
it "should insert a pause into the scenario to cover the DTMF duration (250ms) and the pause" do
|
434
|
+
scenario.send_digits '136'
|
435
|
+
scenario.to_xml.should match(%r{<pause milliseconds="1500"/>})
|
436
|
+
end
|
70
437
|
end
|
71
438
|
end
|
72
439
|
|
73
|
-
describe "#
|
74
|
-
|
440
|
+
describe "#compile!" do
|
441
|
+
context "when a filename is not provided" do
|
442
|
+
it "writes the scenario XML to disk at name.xml" do
|
443
|
+
scenario.invite
|
444
|
+
|
445
|
+
scenario.compile!
|
446
|
+
|
447
|
+
File.read("/tmp/test.xml").should == scenario.to_xml
|
448
|
+
end
|
449
|
+
|
450
|
+
it "writes the PCAP media to disk at name.pcap" do
|
451
|
+
scenario.send_digits '123'
|
452
|
+
|
453
|
+
scenario.compile!
|
454
|
+
|
455
|
+
File.read("/tmp/test.pcap").should_not be_empty
|
456
|
+
end
|
457
|
+
|
458
|
+
it "returns the path to the scenario file" do
|
459
|
+
scenario.compile!.should == "/tmp/test.xml"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
context "when a filename is provided" do
|
464
|
+
let(:args) { {filename: 'foobar'} }
|
465
|
+
|
466
|
+
it "writes the scenario XML to disk at filename.xml" do
|
467
|
+
scenario.invite
|
468
|
+
|
469
|
+
scenario.compile!
|
470
|
+
|
471
|
+
File.read("/tmp/foobar.xml").should == scenario.to_xml
|
472
|
+
end
|
473
|
+
|
474
|
+
it "writes the PCAP media to disk at filename.pcap" do
|
475
|
+
scenario.send_digits '123'
|
476
|
+
|
477
|
+
scenario.compile!
|
75
478
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
scenario
|
479
|
+
File.read("/tmp/foobar.pcap").should_not be_empty
|
480
|
+
end
|
481
|
+
|
482
|
+
it "returns the path to the scenario file" do
|
483
|
+
scenario.compile!.should == "/tmp/foobar.xml"
|
484
|
+
end
|
80
485
|
end
|
486
|
+
end
|
81
487
|
|
82
|
-
|
83
|
-
|
84
|
-
|
488
|
+
describe "#to_tmpfiles" do
|
489
|
+
before { scenario.invite }
|
490
|
+
|
491
|
+
it "writes the scenario XML to a Tempfile and returns it" do
|
492
|
+
files = scenario.to_tmpfiles
|
493
|
+
files[:scenario].should be_a(Tempfile)
|
494
|
+
files[:scenario].read.should eql(scenario.to_xml)
|
85
495
|
end
|
86
496
|
|
87
|
-
it
|
88
|
-
scenario.
|
497
|
+
it "allows the scenario XML to be read from disk independently" do
|
498
|
+
files = scenario.to_tmpfiles
|
499
|
+
File.read(files[:scenario].path).should eql(scenario.to_xml)
|
500
|
+
end
|
89
501
|
|
90
|
-
|
91
|
-
|
502
|
+
it "writes the PCAP media to a Tempfile and returns it" do
|
503
|
+
files = scenario.to_tmpfiles
|
504
|
+
files[:media].should be_a(Tempfile)
|
505
|
+
files[:media].read.should_not be_empty
|
92
506
|
end
|
93
507
|
|
94
|
-
it
|
95
|
-
scenario.
|
508
|
+
it "allows the PCAP media to be read from disk independently" do
|
509
|
+
files = scenario.to_tmpfiles
|
510
|
+
File.read(files[:media].path).should_not be_empty
|
511
|
+
end
|
512
|
+
end
|
96
513
|
|
97
|
-
|
98
|
-
|
514
|
+
describe "#build" do
|
515
|
+
let(:scenario_xml) do <<-END
|
516
|
+
<?xml version="1.0"?>
|
517
|
+
<scenario name="Test">
|
518
|
+
<send retrans="500">
|
519
|
+
<![CDATA[
|
520
|
+
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
|
521
|
+
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
522
|
+
From: "sipp" <sip:sipp@[local_ip]>;tag=[call_number]
|
523
|
+
To: <sip:[service]@[remote_ip]:[remote_port]>
|
524
|
+
Call-ID: [call_id]
|
525
|
+
CSeq: [cseq] INVITE
|
526
|
+
Contact: <sip:sipp@[local_ip]:[local_port];transport=[transport]>
|
527
|
+
Max-Forwards: 100
|
528
|
+
User-Agent: SIPp/sippy_cup
|
529
|
+
Content-Type: application/sdp
|
530
|
+
Content-Length: [len]
|
531
|
+
|
532
|
+
v=0
|
533
|
+
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
|
534
|
+
s=-
|
535
|
+
c=IN IP[media_ip_type] [media_ip]
|
536
|
+
t=0 0
|
537
|
+
m=audio [media_port] RTP/AVP 0 101
|
538
|
+
a=rtpmap:0 PCMU/8000
|
539
|
+
a=rtpmap:101 telephone-event/8000
|
540
|
+
a=fmtp:101 0-15
|
541
|
+
]]>
|
542
|
+
</send>
|
543
|
+
<recv optional="true" response="100"/>
|
544
|
+
<recv optional="true" response="180"/>
|
545
|
+
<recv optional="true" response="183"/>
|
546
|
+
<recv response="200" rrs="true" rtd="true"/>
|
547
|
+
<send>
|
548
|
+
<![CDATA[
|
549
|
+
ACK [next_url] SIP/2.0
|
550
|
+
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
551
|
+
From: "sipp" <sip:sipp@[local_ip]>;tag=[call_number]
|
552
|
+
[last_To:]
|
553
|
+
Call-ID: [call_id]
|
554
|
+
CSeq: [cseq] ACK
|
555
|
+
Contact: <sip:sipp@[local_ip]:[local_port];transport=[transport]>
|
556
|
+
Max-Forwards: 100
|
557
|
+
User-Agent: SIPp/sippy_cup
|
558
|
+
Content-Length: 0
|
559
|
+
[routes]
|
560
|
+
]]>
|
561
|
+
</send>
|
562
|
+
<nop>
|
563
|
+
<action>
|
564
|
+
<exec play_pcap_audio="/tmp/test.pcap"/>
|
565
|
+
</action>
|
566
|
+
</nop>
|
567
|
+
<recv request="BYE"/>
|
568
|
+
<send>
|
569
|
+
<![CDATA[
|
570
|
+
SIP/2.0 200 OK
|
571
|
+
[last_Via:]
|
572
|
+
[last_From:]
|
573
|
+
[last_To:]
|
574
|
+
[last_Call-ID:]
|
575
|
+
[last_CSeq:]
|
576
|
+
Contact: <sip:sipp@[local_ip]:[local_port];transport=[transport]>
|
577
|
+
Max-Forwards: 100
|
578
|
+
User-Agent: SIPp/sippy_cup
|
579
|
+
Content-Length: 0
|
580
|
+
[routes]
|
581
|
+
]]>
|
582
|
+
</send>
|
583
|
+
</scenario>
|
584
|
+
END
|
99
585
|
end
|
100
586
|
|
101
|
-
|
102
|
-
|
587
|
+
context "with a valid steps definition" do
|
588
|
+
let(:steps) { ['invite', 'wait_for_answer', 'ack_answer', 'wait_for_hangup'] }
|
103
589
|
|
104
|
-
|
105
|
-
|
106
|
-
|
590
|
+
it "runs each step" do
|
591
|
+
subject.build(steps)
|
592
|
+
subject.to_xml.should == scenario_xml
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
context "with an invalid steps definition" do
|
597
|
+
let(:steps) { ["send_digits 'b'"] }
|
598
|
+
|
599
|
+
it "doesn't raise errors" do
|
600
|
+
expect { subject.build(steps) }.to_not raise_error
|
601
|
+
end
|
107
602
|
end
|
108
603
|
end
|
109
604
|
|
110
|
-
describe "
|
111
|
-
let(:
|
605
|
+
describe ".from_manifest" do
|
606
|
+
let(:specs_from) { 'specs' }
|
112
607
|
|
113
|
-
|
608
|
+
let(:scenario_yaml) do <<-END
|
609
|
+
name: spec scenario
|
610
|
+
source: 192.0.2.15
|
611
|
+
destination: 192.0.2.200
|
612
|
+
max_concurrent: 10
|
613
|
+
calls_per_second: 5
|
614
|
+
number_of_calls: 20
|
615
|
+
from_user: #{specs_from}
|
616
|
+
steps:
|
617
|
+
- invite
|
618
|
+
- wait_for_answer
|
619
|
+
- ack_answer
|
620
|
+
- sleep 3
|
621
|
+
- send_digits '3125551234'
|
622
|
+
- sleep 5
|
623
|
+
- send_digits '#'
|
624
|
+
- wait_for_hangup
|
625
|
+
END
|
626
|
+
end
|
627
|
+
|
628
|
+
let(:scenario_xml) do <<-END
|
629
|
+
<?xml version="1.0"?>
|
630
|
+
<scenario name="spec scenario">
|
631
|
+
<send retrans="500">
|
632
|
+
<![CDATA[
|
633
|
+
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
|
634
|
+
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
635
|
+
From: "#{specs_from}" <sip:#{specs_from}@[local_ip]>;tag=[call_number]
|
636
|
+
To: <sip:[service]@[remote_ip]:[remote_port]>
|
637
|
+
Call-ID: [call_id]
|
638
|
+
CSeq: [cseq] INVITE
|
639
|
+
Contact: <sip:#{specs_from}@[local_ip]:[local_port];transport=[transport]>
|
640
|
+
Max-Forwards: 100
|
641
|
+
User-Agent: SIPp/sippy_cup
|
642
|
+
Content-Type: application/sdp
|
643
|
+
Content-Length: [len]
|
644
|
+
|
645
|
+
v=0
|
646
|
+
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
|
647
|
+
s=-
|
648
|
+
c=IN IP[media_ip_type] [media_ip]
|
649
|
+
t=0 0
|
650
|
+
m=audio [media_port] RTP/AVP 0 101
|
651
|
+
a=rtpmap:0 PCMU/8000
|
652
|
+
a=rtpmap:101 telephone-event/8000
|
653
|
+
a=fmtp:101 0-15
|
654
|
+
]]>
|
655
|
+
</send>
|
656
|
+
<recv optional="true" response="100"/>
|
657
|
+
<recv optional="true" response="180"/>
|
658
|
+
<recv optional="true" response="183"/>
|
659
|
+
<recv response="200" rrs="true" rtd="true"/>
|
660
|
+
<send>
|
661
|
+
<![CDATA[
|
662
|
+
ACK [next_url] SIP/2.0
|
663
|
+
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
664
|
+
From: "#{specs_from}" <sip:#{specs_from}@[local_ip]>;tag=[call_number]
|
665
|
+
[last_To:]
|
666
|
+
Call-ID: [call_id]
|
667
|
+
CSeq: [cseq] ACK
|
668
|
+
Contact: <sip:#{specs_from}@[local_ip]:[local_port];transport=[transport]>
|
669
|
+
Max-Forwards: 100
|
670
|
+
User-Agent: SIPp/sippy_cup
|
671
|
+
Content-Length: 0
|
672
|
+
[routes]
|
673
|
+
]]>
|
674
|
+
</send>
|
675
|
+
<nop>
|
676
|
+
<action>
|
677
|
+
<exec play_pcap_audio="/tmp/spec_scenario.pcap"/>
|
678
|
+
</action>
|
679
|
+
</nop>
|
680
|
+
<pause milliseconds="3000"/>
|
681
|
+
<pause milliseconds="5000"/>
|
682
|
+
<pause milliseconds="5000"/>
|
683
|
+
<pause milliseconds="500"/>
|
684
|
+
<recv request="BYE"/>
|
685
|
+
<send>
|
686
|
+
<![CDATA[
|
687
|
+
SIP/2.0 200 OK
|
688
|
+
[last_Via:]
|
689
|
+
[last_From:]
|
690
|
+
[last_To:]
|
691
|
+
[last_Call-ID:]
|
692
|
+
[last_CSeq:]
|
693
|
+
Contact: <sip:#{specs_from}@[local_ip]:[local_port];transport=[transport]>
|
694
|
+
Max-Forwards: 100
|
695
|
+
User-Agent: SIPp/sippy_cup
|
696
|
+
Content-Length: 0
|
697
|
+
[routes]
|
698
|
+
]]>
|
699
|
+
</send>
|
700
|
+
</scenario>
|
701
|
+
END
|
702
|
+
end
|
114
703
|
|
115
|
-
|
116
|
-
|
704
|
+
let(:override_options) { { number_of_calls: 10 } }
|
705
|
+
|
706
|
+
it "generates the correct XML" do
|
707
|
+
scenario = described_class.from_manifest(scenario_yaml)
|
708
|
+
scenario.to_xml.should == scenario_xml
|
709
|
+
end
|
710
|
+
|
711
|
+
it "sets the proper options" do
|
712
|
+
scenario = described_class.from_manifest(scenario_yaml)
|
713
|
+
scenario.scenario_options.should == {
|
714
|
+
'name' => 'spec scenario',
|
715
|
+
'source' => '192.0.2.15',
|
716
|
+
'destination' => '192.0.2.200',
|
717
|
+
'max_concurrent' => 10,
|
718
|
+
'calls_per_second' => 5,
|
719
|
+
'number_of_calls' => 20,
|
720
|
+
'from_user' => "#{specs_from}"
|
721
|
+
}
|
722
|
+
end
|
723
|
+
|
724
|
+
context "when the :scenario key is provided in the manifest" do
|
725
|
+
let(:scenario_path) { File.expand_path('scenario.xml', File.join(File.dirname(__FILE__), '..', 'fixtures')) }
|
726
|
+
let(:scenario_yaml) do <<-END
|
727
|
+
name: spec scenario
|
728
|
+
source: 192.0.2.15
|
729
|
+
destination: 192.0.2.200
|
730
|
+
max_concurrent: 10
|
731
|
+
calls_per_second: 5
|
732
|
+
number_of_calls: 20
|
733
|
+
from_user: #{specs_from}
|
734
|
+
scenario: #{scenario_path}
|
735
|
+
END
|
736
|
+
end
|
737
|
+
|
738
|
+
before { FakeFS.deactivate! }
|
739
|
+
|
740
|
+
it "creates an XMLScenario with the scenario XML and nil media" do
|
741
|
+
scenario = described_class.from_manifest(scenario_yaml)
|
742
|
+
scenario.should be_a(SippyCup::XMLScenario)
|
743
|
+
scenario.to_xml.should == File.read(scenario_path)
|
744
|
+
end
|
745
|
+
|
746
|
+
context "and the :media key is provided" do
|
747
|
+
let(:media_path) { File.expand_path('dtmf_2833_1.pcap', File.join(File.dirname(__FILE__), '..', 'fixtures')) }
|
748
|
+
let(:scenario_yaml) do <<-END
|
749
|
+
name: spec scenario
|
750
|
+
source: 192.0.2.15
|
751
|
+
destination: 192.0.2.200
|
752
|
+
max_concurrent: 10
|
753
|
+
calls_per_second: 5
|
754
|
+
number_of_calls: 20
|
755
|
+
from_user: #{specs_from}
|
756
|
+
scenario: #{scenario_path}
|
757
|
+
media: #{media_path}
|
758
|
+
END
|
759
|
+
end
|
760
|
+
|
761
|
+
it "creates an XMLScenario with the scenario XML and media from the filesystem" do
|
762
|
+
scenario = described_class.from_manifest(scenario_yaml)
|
763
|
+
|
764
|
+
media = File.read(media_path, mode: 'rb')
|
765
|
+
|
766
|
+
files = scenario.to_tmpfiles
|
767
|
+
files[:media].read.should eql(media)
|
768
|
+
end
|
769
|
+
end
|
770
|
+
end
|
771
|
+
|
772
|
+
context "without a name specified" do
|
773
|
+
let(:scenario_yaml) do <<-END
|
774
|
+
source: 192.0.2.15
|
775
|
+
destination: 192.0.2.200
|
776
|
+
max_concurrent: 10
|
777
|
+
calls_per_second: 5
|
778
|
+
number_of_calls: 20
|
779
|
+
from_user: #{specs_from}
|
780
|
+
steps:
|
781
|
+
- invite
|
782
|
+
- wait_for_answer
|
783
|
+
- ack_answer
|
784
|
+
- sleep 3
|
785
|
+
- send_digits '3125551234'
|
786
|
+
- sleep 5
|
787
|
+
- send_digits '#'
|
788
|
+
- wait_for_hangup
|
789
|
+
END
|
790
|
+
end
|
791
|
+
|
792
|
+
it "should default to 'My Scenario'" do
|
793
|
+
scenario = described_class.from_manifest(scenario_yaml)
|
794
|
+
scenario.scenario_options[:name].should == 'My Scenario'
|
795
|
+
end
|
796
|
+
end
|
797
|
+
|
798
|
+
context "with an input filename specified" do
|
799
|
+
context "and a name in the manifest" do
|
800
|
+
it "uses the name from the manifest" do
|
801
|
+
scenario = described_class.from_manifest(scenario_yaml, input_filename: '/tmp/foobar.yml')
|
802
|
+
scenario.scenario_options[:name].should == 'spec scenario'
|
803
|
+
end
|
117
804
|
end
|
118
805
|
|
119
|
-
|
120
|
-
|
806
|
+
context "and no name in the manifest" do
|
807
|
+
let(:scenario_yaml) do <<-END
|
808
|
+
source: 192.0.2.15
|
809
|
+
destination: 192.0.2.200
|
810
|
+
max_concurrent: 10
|
811
|
+
calls_per_second: 5
|
812
|
+
number_of_calls: 20
|
813
|
+
from_user: #{specs_from}
|
814
|
+
steps:
|
815
|
+
- invite
|
816
|
+
- wait_for_answer
|
817
|
+
- ack_answer
|
818
|
+
- sleep 3
|
819
|
+
- send_digits '3125551234'
|
820
|
+
- sleep 5
|
821
|
+
- send_digits '#'
|
822
|
+
- wait_for_hangup
|
823
|
+
END
|
824
|
+
end
|
825
|
+
|
826
|
+
it "uses the input filename" do
|
827
|
+
scenario = described_class.from_manifest(scenario_yaml, input_filename: '/tmp/foobar.yml')
|
828
|
+
scenario.scenario_options[:name].should == 'foobar'
|
829
|
+
end
|
121
830
|
end
|
831
|
+
end
|
122
832
|
|
123
|
-
|
124
|
-
|
833
|
+
context "overriding some value" do
|
834
|
+
let(:specs_from) { 'other_user' }
|
835
|
+
|
836
|
+
it "overrides keys with values from the options hash" do
|
837
|
+
scenario = described_class.from_manifest(scenario_yaml, override_options)
|
838
|
+
scenario.to_xml.should == scenario_xml
|
839
|
+
end
|
840
|
+
|
841
|
+
it "sets the proper options" do
|
842
|
+
scenario = described_class.from_manifest(scenario_yaml, override_options)
|
843
|
+
scenario.scenario_options.should == {
|
844
|
+
'name' => 'spec scenario',
|
845
|
+
'source' => '192.0.2.15',
|
846
|
+
'destination' => '192.0.2.200',
|
847
|
+
'max_concurrent' => 10,
|
848
|
+
'calls_per_second' => 5,
|
849
|
+
'number_of_calls' => override_options[:number_of_calls],
|
850
|
+
'from_user' => "#{specs_from}"
|
851
|
+
}
|
125
852
|
end
|
126
853
|
end
|
127
854
|
|
128
|
-
context "
|
129
|
-
|
130
|
-
|
855
|
+
context "with an invalid scenario" do
|
856
|
+
let(:scenario_yaml) do <<-END
|
857
|
+
name: spec scenario
|
858
|
+
source: 192.0.2.15
|
859
|
+
destination: 192.0.2.200
|
860
|
+
max_concurrent: 10
|
861
|
+
calls_per_second: 5
|
862
|
+
number_of_calls: 20
|
863
|
+
from_user: #{specs_from}
|
864
|
+
steps:
|
865
|
+
- invite
|
866
|
+
- wait_for_answer
|
867
|
+
- ack_answer
|
868
|
+
- sleep 3
|
869
|
+
- send_digits 'abc'
|
870
|
+
- sleep 5
|
871
|
+
- send_digits '#'
|
872
|
+
- wait_for_hangup
|
873
|
+
END
|
874
|
+
end
|
875
|
+
|
876
|
+
it "does not raise errors" do
|
877
|
+
expect { SippyCup::Scenario.from_manifest(scenario_yaml) }.to_not raise_error
|
131
878
|
end
|
132
879
|
|
133
|
-
it
|
134
|
-
scenario.
|
880
|
+
it "sets the validity of the scenario" do
|
881
|
+
scenario = SippyCup::Scenario.from_manifest(scenario_yaml)
|
882
|
+
scenario.should_not be_valid
|
135
883
|
end
|
136
884
|
|
137
|
-
it
|
138
|
-
scenario.
|
885
|
+
it "sets the error messages for the scenario" do
|
886
|
+
scenario = SippyCup::Scenario.from_manifest(scenario_yaml)
|
887
|
+
scenario.errors.should == [{step: 5, message: "send_digits 'abc': Invalid DTMF digit requested: a"}]
|
139
888
|
end
|
140
889
|
end
|
141
890
|
end
|