# encoding: utf-8 require 'spec_helper' describe SippyCup::Scenario do include FakeFS::SpecHelpers before do Dir.mkdir("/tmp") unless Dir.exist?("/tmp") Dir.chdir "/tmp" end let(:default_args) { {source: '127.0.0.1:5060', destination: '10.0.0.1:5080'} } let(:args) { {} } subject(:scenario) { described_class.new 'Test', default_args.merge(args) } it "takes a block to generate a scenario" do s = described_class.new 'Test', default_args do invite end s.to_xml.should =~ %r{INVITE sip:\[service\]@\[remote_ip\]:\[remote_port\] SIP/2.0} end it "allows creating a blank scenario with no block" do subject.to_xml.should =~ %r{} end describe '#invite' do it "sends an INVITE message" do subject.invite subject.to_xml.should match(%r{}) subject.to_xml.should match(%r{INVITE}) end it "allows setting options on the send instruction" do subject.invite foo: 'bar' subject.to_xml.should match(%r{}) end it "defaults to retrans of 500" do subject.invite subject.to_xml.should match(%r{}) end it "allows setting retrans" do subject.invite retrans: 200 subject.to_xml.should match(%r{}) end context "with extra headers specified" do it "adds the headers to the end of the message" do subject.invite headers: "Foo: \nBar: " subject.to_xml.should match(%r{Foo: \nBar: }) end it "only has one blank line between headers and SDP" do subject.invite headers: "Foo: \n\n\n" subject.to_xml.should match(%r{Foo: \n\nv=0}) end end context "with no extra headers" do it "only has one blank line between headers and SDP" do subject.invite subject.to_xml.should match(%r{Content-Length: \[len\]\n\nv=0}) end end it "uses [media_port+1] as the RTCP port in the SDP" do subject.invite subject.to_xml.should match(%r{m=audio \[media_port\] RTP/AVP 0 101\n}) end context "when a from user is specified" do let(:args) { {from_user: 'frank'} } it "includes the specified user in the From and Contact headers" do subject.invite subject.to_xml.should match(%r{From: "frank" }) subject.to_xml.should match(%r{REGISTER}) end it "allows setting options on the send instruction" do subject.register 'frank', nil, foo: 'bar' subject.to_xml.should match(%r{}) end it "defaults to retrans of 500" do subject.register 'frank' subject.to_xml.should match(%r{}) end it "allows setting retrans" do subject.register 'frank', nil, retrans: 200 subject.to_xml.should match(%r{}) end context "when a domain is provided" do it "uses the specified user and domain" do subject.register 'frank@foobar.com' subject.to_xml.should match(%r{REGISTER sip:foobar.com}) subject.to_xml.should match(%r{From: }) end it "adds authentication data to the REGISTER message" do subject.register 'frank', 'abc123' subject.to_xml.should match(%r{\[authentication username=frank password=abc123\]}) end end end describe '#receive_trying' do it "expects an optional 100" do subject.receive_trying scenario.to_xml.should match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_trying foo: 'bar' scenario.to_xml.should match(%q{}) end it "allows overriding options" do subject.receive_trying optional: false scenario.to_xml.should match(%q{}) end end describe '#receive_ringing' do it "expects an optional 180" do subject.receive_ringing scenario.to_xml.should match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_ringing foo: 'bar' scenario.to_xml.should match(%q{}) end it "allows overriding options" do subject.receive_ringing optional: false scenario.to_xml.should match(%q{}) end end describe '#receive_progress' do it "expects an optional 183" do subject.receive_progress scenario.to_xml.should match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_progress foo: 'bar' scenario.to_xml.should match(%q{}) end it "allows overriding options" do subject.receive_progress optional: false scenario.to_xml.should match(%q{}) end end describe '#receive_answer' do it "expects a 200 with rrs and rtd true" do subject.receive_answer scenario.to_xml.should match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_answer foo: 'bar' scenario.to_xml.should match(%q{}) end it "allows overriding options" do subject.receive_answer rtd: false scenario.to_xml.should match(%q{}) end end describe '#receive_200' do it "expects a 200" do subject.receive_200 scenario.to_xml.should match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_200 foo: 'bar' scenario.to_xml.should match(%q{}) end it "allows overriding options" do subject.receive_200 response: 999 # Silly but still... scenario.to_xml.should match(%q{}) end end describe '#ack_answer' do it "sends an ACK message" do subject.ack_answer subject.to_xml.should match(%r{}) subject.to_xml.should match(%r{ACK}) end it "allows setting options on the send instruction" do subject.ack_answer foo: 'bar' subject.to_xml.should match(%r{}) end context "when media is present" do before do subject.answer subject.sleep 1 end it "starts the PCAP media" do subject.ack_answer subject.sleep 1 subject.to_xml(:pcap_path => "/dev/null").should match(%r{\n.*\n.*\n.*\n.*}) end end context "when media is not present" do it "does not start the PCAP media" do subject.ack_answer subject.to_xml(:pcap_path => "/dev/null").should_not match(%r{\n.*\n.*\n.*\n.*}) end end end describe '#wait_for_answer' do it "tells SIPp to optionally receive a SIP 100, 180 and 183 by default, while requiring a 200" do scenario.wait_for_answer xml = scenario.to_xml xml.should =~ /recv response="100".*optional="true"/ xml.should =~ /recv response="180".*optional="true"/ xml.should =~ /recv response="183".*optional="true"/ xml.should =~ /recv response="200"/ xml.should_not =~ /recv response="200".*optional="true"/ xml.should match(%r{}) xml.should match(%r{ACK}) end it "passes through additional options" do scenario.wait_for_answer foo: 'bar' xml = scenario.to_xml xml.should =~ /recv .*foo="bar".*response="100"/ xml.should =~ /recv .*foo="bar".*response="180"/ xml.should =~ /recv .*foo="bar".*response="183"/ xml.should =~ /recv .*response="200" .*foo="bar"/ xml.should match(%r{}) xml.should match(%r{ACK}) end end describe '#receive_message' do it "expects a MESSAGE and acks it" do subject.receive_message subject.to_xml.should match(%r{.*SIP/2\.0 200 OK}m) end it "allows a string to be given as a regexp for matching" do subject.receive_message "Hello World!" subject.to_xml.should match(%r{\s*\s*}m) end it "increments the variable name used for regexp matching because SIPp requires it to be unique" do subject.receive_message "Hello World!" subject.receive_message "Hello Again World!" subject.receive_message "Goodbye World!" subject.to_xml.should match(%r{]* assign_to="([^"]+)_1"/>.*]* assign_to="\1_2"/>.*]* assign_to="\1_3"/>}m) end it "declares the variable used for regexp matching so that SIPp doesn't complain that it's unused" do subject.receive_message "Hello World!" subject.to_xml.should match(%r{]* assign_to="([^"]+)"/>.*}m) end end describe '#send_bye' do it "sends a BYE message" do subject.send_bye subject.to_xml.should match(%r{}) subject.to_xml.should match(%r{BYE}) end it "allows setting options on the send instruction" do subject.send_bye foo: 'bar' subject.to_xml.should match(%r{}) end end describe '#receive_bye' do it "expects a BYE" do subject.receive_bye scenario.to_xml.should match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_bye foo: 'bar' scenario.to_xml.should match(%q{}) end end describe '#okay' do it "sends a 200 OK" do subject.okay subject.to_xml.should match(%r{}) subject.to_xml.should match(%r{SIP/2.0 200 OK}) end it "allows setting options on the send instruction" do subject.okay foo: 'bar' subject.to_xml.should match(%r{}) end end describe '#wait_for_hangup' do it "expects a BYE and acks it" do subject.receive_bye foo: 'bar' scenario.to_xml.should match(%q{}) scenario.to_xml.should match(%q{}) end end describe '#call_length_repartition' do it 'create a partition table' do subject.call_length_repartition('1', '10', '2') scenario.to_xml.should match('') end end describe '#response_time_repartition' do it 'create a partition table' do subject.response_time_repartition('1', '10', '2') scenario.to_xml.should match('') end end describe 'media-dependent operations' do let(:media) { double :media } before do SippyCup::Media.should_receive(:new).once.and_return media scenario.ack_answer media.stub :<< end describe '#sleep' do it "creates the proper amount of silent audio'" do media.should_receive(:<<).once.with 'silence:5000' scenario.sleep 5 end it "should insert a pause into the scenario" do scenario.sleep 5 scenario.to_xml.should match(%r{}) end context "when passed fractional seconds" do it "creates the proper amount of silent audio" do media.should_receive(:<<).once.with 'silence:500' scenario.sleep '0.5' end it "should insert a pause into the scenario" do scenario.sleep 0.5 scenario.to_xml.should match(%r{}) end end end describe '#send_digits' do it "creates the requested DTMF string in media, with 250ms pauses between" do media.should_receive(:<<).ordered.with 'dtmf:1' media.should_receive(:<<).ordered.with 'silence:250' media.should_receive(:<<).ordered.with 'dtmf:3' media.should_receive(:<<).ordered.with 'silence:250' media.should_receive(:<<).ordered.with 'dtmf:6' media.should_receive(:<<).ordered.with 'silence:250' scenario.send_digits '136' end it "should insert a pause into the scenario to cover the DTMF duration (250ms) and the pause" do scenario.send_digits '136' scenario.to_xml.should match(%r{}) end end end describe "#send_digits with a SIP INFO DTMF mode" do let(:args) { {dtmf_mode: 'info'} } before { scenario.answer } it "creates the requested DTMF string as SIP INFO messages" do scenario.send_digits '136' xml = scenario.to_xml scenario.to_xml.should match(%r{(.*INFO \[next_url\] SIP/2\.0.*.*){3}}m) scenario.to_xml.should match(%r{Signal=1(\nDuration=250\n).*Signal=3\1.*Signal=6\1}m) end it "expects a response for each digit sent" do scenario.send_digits '123' scenario.to_xml.should match(%r{(.*INFO.*.*.*){3}}m) end it "inserts 250ms pauses between each digit" do scenario.send_digits '321' scenario.to_xml.should match(%r{(.*INFO.*.*.*){3}}m) end end describe "#compile!" do context "when a filename is not provided" do it "writes the scenario XML to disk at name.xml" do scenario.invite scenario.compile! File.read("/tmp/test.xml").should == scenario.to_xml end it "writes the PCAP media to disk at name.pcap" do scenario.ack_answer scenario.send_digits '123' scenario.compile! File.read("/tmp/test.pcap").should_not be_empty end it "returns the path to the scenario file" do scenario.compile!.should == "/tmp/test.xml" end end context "when a filename is provided" do let(:args) { {filename: 'foobar'} } it "writes the scenario XML to disk at filename.xml" do scenario.invite scenario.compile! File.read("/tmp/foobar.xml").should == scenario.to_xml end it "writes the PCAP media to disk at filename.pcap" do scenario.ack_answer scenario.send_digits '123' scenario.compile! File.read("/tmp/foobar.pcap").should_not be_empty end it "returns the path to the scenario file" do scenario.compile!.should == "/tmp/foobar.xml" end end end describe "#to_tmpfiles" do before { scenario.invite } it "writes the scenario XML to a Tempfile and returns it" do files = scenario.to_tmpfiles files[:scenario].should be_a(Tempfile) files[:scenario].read.should eql(scenario.to_xml) end it "allows the scenario XML to be read from disk independently" do files = scenario.to_tmpfiles File.read(files[:scenario].path).should eql(scenario.to_xml) end context "without media" do it "does not write a PCAP media file" do files = scenario.to_tmpfiles files[:media].should be_nil end end context "with media" do before do scenario.ack_answer scenario.sleep 1 end it "writes the PCAP media to a Tempfile and returns it" do files = scenario.to_tmpfiles files[:media].should be_a(Tempfile) files[:media].read.should_not be_empty end it "allows the PCAP media to be read from disk independently" do files = scenario.to_tmpfiles File.read(files[:media].path).should_not be_empty end it "puts the PCAP file path into the scenario XML" do files = scenario.to_tmpfiles files[:scenario].read.should match(%r{play_pcap_audio="#{files[:media].path}"}) end end end describe "#build" do let(:scenario_xml) do <<-END ;tag=[call_number] To: Call-ID: [call_id] CSeq: [cseq] INVITE Contact: Max-Forwards: 100 User-Agent: SIPp/sippy_cup Content-Type: application/sdp Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-15 ]]> ;tag=[call_number] To: [peer_tag_param] Call-ID: [call_id] CSeq: [cseq] ACK Contact: Max-Forwards: 100 User-Agent: SIPp/sippy_cup Content-Length: 0 [routes] ]]> Max-Forwards: 100 User-Agent: SIPp/sippy_cup Content-Length: 0 [routes] ]]> END end context "with a valid steps definition" do let(:steps) { ['invite', 'wait_for_answer', 'wait_for_hangup'] } it "runs each step" do subject.build(steps) subject.to_xml(:pcap_path => "/dev/null").should == scenario_xml end end context "having steps with arguments" do let(:steps) do [ %q(register 'user@domain.com' "my password has spaces"), %q(sleep 3), %q(send_digits 12345) ] end it "each method should receive the correct arguments" do subject.should_receive(:register).once.ordered.with('user@domain.com', 'my password has spaces') subject.should_receive(:sleep).once.ordered.with('3') subject.should_receive(:send_digits).once.ordered.with('12345') subject.build steps end end context "with an invalid steps definition" do let(:steps) { ["send_digits 'b'"] } it "doesn't raise errors" do expect { subject.build(steps) }.to_not raise_error end end end describe ".from_manifest" do let(:specs_from) { 'specs' } let(:scenario_yaml) do <<-END name: spec scenario source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 calls_per_second: 5 number_of_calls: 20 from_user: #{specs_from} steps: - invite - wait_for_answer - sleep 3 - send_digits '3125551234' - sleep 5 - send_digits '#' - wait_for_hangup END end let(:scenario_xml) do <<-END ;tag=[call_number] To: Call-ID: [call_id] CSeq: [cseq] INVITE Contact: Max-Forwards: 100 User-Agent: SIPp/sippy_cup Content-Type: application/sdp Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-15 ]]> ;tag=[call_number] To: [peer_tag_param] Call-ID: [call_id] CSeq: [cseq] ACK Contact: Max-Forwards: 100 User-Agent: SIPp/sippy_cup Content-Length: 0 [routes] ]]> Max-Forwards: 100 User-Agent: SIPp/sippy_cup Content-Length: 0 [routes] ]]> END end let(:override_options) { { number_of_calls: 10 } } it "generates the correct XML" do scenario = described_class.from_manifest(scenario_yaml) scenario.to_xml(:pcap_path => "/dev/null").should == scenario_xml end it "sets the proper options" do scenario = described_class.from_manifest(scenario_yaml) scenario.scenario_options.should == { 'name' => 'spec scenario', 'source' => '192.0.2.15', 'destination' => '192.0.2.200', 'max_concurrent' => 10, 'calls_per_second' => 5, 'number_of_calls' => 20, 'from_user' => "#{specs_from}" } end context "when the :scenario key is provided in the manifest" do let(:scenario_path) { File.expand_path('scenario.xml', File.join(File.dirname(__FILE__), '..', 'fixtures')) } let(:scenario_yaml) do <<-END name: spec scenario source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 calls_per_second: 5 number_of_calls: 20 from_user: #{specs_from} scenario: #{scenario_path} END end before { FakeFS.deactivate! } it "creates an XMLScenario with the scenario XML and nil media" do scenario = described_class.from_manifest(scenario_yaml) scenario.should be_a(SippyCup::XMLScenario) scenario.to_xml.should == File.read(scenario_path) end context "and the :media key is provided" do let(:media_path) { File.expand_path('dtmf_2833_1.pcap', File.join(File.dirname(__FILE__), '..', 'fixtures')) } let(:scenario_yaml) do <<-END name: spec scenario source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 calls_per_second: 5 number_of_calls: 20 from_user: #{specs_from} scenario: #{scenario_path} media: #{media_path} END end it "creates an XMLScenario with the scenario XML and media from the filesystem" do scenario = described_class.from_manifest(scenario_yaml) media = File.read(media_path, mode: 'rb') files = scenario.to_tmpfiles files[:media].read.should eql(media) end end end context "without a name specified" do let(:scenario_yaml) do <<-END source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 calls_per_second: 5 number_of_calls: 20 from_user: #{specs_from} steps: - invite - wait_for_answer - sleep 3 - send_digits '3125551234' - sleep 5 - send_digits '#' - wait_for_hangup END end it "should default to 'My Scenario'" do scenario = described_class.from_manifest(scenario_yaml) scenario.scenario_options[:name].should == 'My Scenario' end end context "with an input filename specified" do context "and a name in the manifest" do it "uses the name from the manifest" do scenario = described_class.from_manifest(scenario_yaml, input_filename: '/tmp/foobar.yml') scenario.scenario_options[:name].should == 'spec scenario' end end context "and no name in the manifest" do let(:scenario_yaml) do <<-END source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 calls_per_second: 5 number_of_calls: 20 from_user: #{specs_from} steps: - invite - wait_for_answer - sleep 3 - send_digits '3125551234' - sleep 5 - send_digits '#' - wait_for_hangup END end it "uses the input filename" do scenario = described_class.from_manifest(scenario_yaml, input_filename: '/tmp/foobar.yml') scenario.scenario_options[:name].should == 'foobar' end end end context "overriding some value" do let(:specs_from) { 'other_user' } it "overrides keys with values from the options hash" do scenario = described_class.from_manifest(scenario_yaml, override_options) scenario.to_xml(:pcap_path => "/dev/null").should == scenario_xml end it "sets the proper options" do scenario = described_class.from_manifest(scenario_yaml, override_options) scenario.scenario_options.should == { 'name' => 'spec scenario', 'source' => '192.0.2.15', 'destination' => '192.0.2.200', 'max_concurrent' => 10, 'calls_per_second' => 5, 'number_of_calls' => override_options[:number_of_calls], 'from_user' => "#{specs_from}" } end end context "with an invalid scenario" do let(:scenario_yaml) do <<-END name: spec scenario source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 calls_per_second: 5 number_of_calls: 20 from_user: #{specs_from} steps: - invite - wait_for_answer - sleep 3 - send_digits 'xyz' - sleep 5 - send_digits '#' - wait_for_hangup END end it "does not raise errors" do expect { SippyCup::Scenario.from_manifest(scenario_yaml) }.to_not raise_error end it "sets the validity of the scenario" do scenario = SippyCup::Scenario.from_manifest(scenario_yaml) scenario.should_not be_valid end it "sets the error messages for the scenario" do scenario = SippyCup::Scenario.from_manifest(scenario_yaml) scenario.errors.should == [{step: 4, message: "send_digits 'xyz': Invalid DTMF digit requested: x"}] end end end end