# encoding: utf-8 require 'spec_helper' module Punchblock module Translator class Asterisk module Component describe Output do include HasMockCallbackConnection let(:renderer) { nil } let(:ami_client) { double('AMI') } let(:translator) { Punchblock::Translator::Asterisk.new ami_client, connection } let(:mock_call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator, ami_client, connection } let :original_command do Punchblock::Component::Output.new command_options end let :ssml_doc do RubySpeech::SSML.draw do say_as(:interpret_as => :cardinal) { 'FOO' } end end let(:command_opts) { {} } let :command_options do { :render_document => {:value => ssml_doc}, renderer: renderer } end subject { Output.new original_command, mock_call } def expect_answered(value = true) allow(mock_call).to receive(:answered?).and_return(value) end def expect_mrcpsynth_with_options(options) expect(mock_call).to receive(:execute_agi_command).once.with { |*args| expect(args[0]).to eq('EXEC MRCPSynth') expect(args[1]).to match options }.and_return code: 200, result: 1 end describe '#execute' do before { original_command.request! } context 'with an invalid renderer' do let(:renderer) { 'foobar' } it "should return an error and not execute any actions" do subject.execute error = ProtocolError.new.setup 'option error', 'The renderer foobar is unsupported.' expect(original_command.response(0.1)).to eq(error) end end context 'with a renderer of :swift' do let(:renderer) { 'swift' } let(:audio_filename) { 'http://foo.com/bar.mp3' } let :ssml_doc do RubySpeech::SSML.draw do audio :src => audio_filename say_as(:interpret_as => :cardinal) { 'FOO' } end end let :command_options do { :render_document => {:value => ssml_doc}, renderer: renderer }.merge(command_opts) end def ssml_with_options(prefix = '', postfix = '') base_doc = ssml_doc.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" } prefix + base_doc + postfix end before { expect_answered } it "should execute Swift" do expect(mock_call).to receive(:execute_agi_command).once.with 'EXEC Swift', ssml_with_options subject.execute end it 'should send a complete event when Swift completes' do expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1 subject.execute expect(original_command.complete_event(0.1).reason).to be_a Punchblock::Component::Output::Complete::Finish end context "when we get a RubyAMI Error" do it "should send an error complete event" do error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' } expect(mock_call).to receive(:execute_agi_command).and_raise error subject.execute complete_reason = original_command.complete_event(0.1).reason expect(complete_reason).to be_a Punchblock::Event::Complete::Error expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'") end end context "when the channel is gone" do it "should send an error complete event" do error = ChannelGoneError.new 'FooBar' expect(mock_call).to receive(:execute_agi_command).and_raise error subject.execute complete_reason = original_command.complete_event(0.1).reason expect(complete_reason).to be_a Punchblock::Event::Complete::Hangup end end context "when the call is not answered" do before { expect_answered false } it "should send progress" do expect(mock_call).to receive(:send_progress) expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1 subject.execute end end describe 'interrupt_on' do context "set to nil" do let(:command_opts) { { :interrupt_on => nil } } it "should not add interrupt arguments" do expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options).and_return code: 200, result: 1 subject.execute end end context "set to :any" do let(:command_opts) { { :interrupt_on => :any } } it "should add the interrupt options to the argument" do expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options('', '|1|1')).and_return code: 200, result: 1 subject.execute end end context "set to :dtmf" do let(:command_opts) { { :interrupt_on => :dtmf } } it "should add the interrupt options to the argument" do expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options('', '|1|1')).and_return code: 200, result: 1 subject.execute end end context "set to :voice" do let(:command_opts) { { :interrupt_on => :voice } } it "should return an error and not execute any actions" do subject.execute error = ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.' expect(original_command.response(0.1)).to eq(error) end end end describe 'voice' do context "set to nil" do let(:command_opts) { { :voice => nil } } it "should not add a voice at the beginning of the argument" do expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options).and_return code: 200, result: 1 subject.execute end end context "set to Leonard" do let(:command_opts) { { :voice => "Leonard" } } it "should add a voice at the beginning of the argument" do expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options('Leonard^', '')).and_return code: 200, result: 1 subject.execute end end end describe "with multiple documents" do let :first_ssml_doc do RubySpeech::SSML.draw do audio :src => audio_filename end end let :second_ssml_doc do RubySpeech::SSML.draw do say_as(:interpret_as => :cardinal) { 'FOO' } end end let(:command_opts) { { render_documents: [{value: first_ssml_doc}, {value: second_ssml_doc}] } } it "executes Swift with a concatenated version of the documents" do expect(mock_call).to receive(:execute_agi_command).once.with 'EXEC Swift', ssml_with_options subject.execute end end end context 'with a renderer of :unimrcp' do let(:renderer) { :unimrcp } let(:audio_filename) { 'http://foo.com/bar.mp3' } let :ssml_doc do RubySpeech::SSML.draw do audio :src => audio_filename say_as(:interpret_as => :cardinal) { 'FOO' } end end let(:command_opts) { {} } let :command_options do { :render_document => {:value => ssml_doc}, renderer: renderer }.merge(command_opts) end let(:synthstatus) { 'OK' } before { allow(mock_call).to receive(:channel_var).with('SYNTHSTATUS').and_return synthstatus } before { expect_answered } it "should execute MRCPSynth" do expect(mock_call).to receive(:execute_agi_command).once.with('EXEC MRCPSynth', ["\"#{ssml_doc.to_s.squish.gsub('"', '\"')}\"", ''].join(',')).and_return code: 200, result: 1 subject.execute end it 'should send a complete event when MRCPSynth completes' do expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1 subject.execute expect(original_command.complete_event(0.1).reason).to be_a Punchblock::Component::Output::Complete::Finish end context "when we get a RubyAMI Error" do it "should send an error complete event" do error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' } expect(mock_call).to receive(:execute_agi_command).and_raise error subject.execute complete_reason = original_command.complete_event(0.1).reason expect(complete_reason).to be_a Punchblock::Event::Complete::Error expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'") end end context "when the channel is gone" do it "should send an error complete event" do error = ChannelGoneError.new 'FooBar' expect(mock_call).to receive(:execute_agi_command).and_raise error subject.execute complete_reason = original_command.complete_event(0.1).reason expect(complete_reason).to be_a Punchblock::Event::Complete::Hangup end end context "when the call is not answered" do before { expect_answered false } it "should send progress" do expect(mock_call).to receive(:send_progress) expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1 subject.execute end end context "when the SYNTHSTATUS variable is set to 'ERROR'" do let(:synthstatus) { 'ERROR' } it "should send an error complete event" do expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1 subject.execute complete_reason = original_command.complete_event(0.1).reason expect(complete_reason).to be_a Punchblock::Event::Complete::Error expect(complete_reason.details).to eq("Terminated due to UniMRCP error") end end describe 'document' do context 'unset' do let(:ssml_doc) { nil } it "should return an error and not execute any actions" do subject.execute error = ProtocolError.new.setup 'option error', 'An SSML document is required.' expect(original_command.response(0.1)).to eq(error) end end context 'with multiple documents' do let(:command_opts) { { :render_documents => [{:value => ssml_doc}, {:value => ssml_doc}] } } it "should execute MRCPSynth once with each document" do param = ["\"#{ssml_doc.to_s.squish.gsub('"', '\"')}\"", ''].join(',') expect(mock_call).to receive(:execute_agi_command).once.with('EXEC MRCPSynth', param).and_return code: 200, result: 1 expect(mock_call).to receive(:execute_agi_command).once.with('EXEC MRCPSynth', param).and_return code: 200, result: 1 subject.execute end it 'should not execute further output after a stop command' do expect(mock_call).to receive(:execute_agi_command).once.ordered do sleep 0.5 end latch = CountDownLatch.new 1 expect(original_command).to receive(:add_event).once.with { |e| expect(e.reason).to be_a Punchblock::Component::Output::Complete::Finish latch.countdown! } Celluloid::Future.new { subject.execute } sleep 0.2 expect(mock_call).to receive(:redirect_back).ordered stop_command = Punchblock::Component::Stop.new stop_command.request! subject.execute_command stop_command expect(latch.wait(2)).to be_true end end end describe 'start-offset' do context 'unset' do let(:command_opts) { { :start_offset => nil } } it 'should not pass any options to MRCPSynth' do expect_mrcpsynth_with_options(//) subject.execute end end context 'set' do let(:command_opts) { { :start_offset => 10 } } it "should return an error and not execute any actions" do subject.execute error = ProtocolError.new.setup 'option error', 'A start_offset value is unsupported on Asterisk.' expect(original_command.response(0.1)).to eq(error) end end end describe 'start-paused' do context 'false' do let(:command_opts) { { :start_paused => false } } it 'should not pass any options to MRCPSynth' do expect_mrcpsynth_with_options(//) subject.execute end end context 'true' do let(:command_opts) { { :start_paused => true } } it "should return an error and not execute any actions" do subject.execute error = ProtocolError.new.setup 'option error', 'A start_paused value is unsupported on Asterisk.' expect(original_command.response(0.1)).to eq(error) end end end describe 'repeat-interval' do context 'unset' do let(:command_opts) { { :repeat_interval => nil } } it 'should not pass any options to MRCPSynth' do expect_mrcpsynth_with_options(//) subject.execute end end context 'set' do let(:command_opts) { { :repeat_interval => 10 } } it "should return an error and not execute any actions" do subject.execute error = ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported on Asterisk.' expect(original_command.response(0.1)).to eq(error) end end end describe 'repeat-times' do context 'unset' do let(:command_opts) { { :repeat_times => nil } } it 'should not pass any options to MRCPSynth' do expect_mrcpsynth_with_options(//) subject.execute end end context 'set' do let(:command_opts) { { :repeat_times => 2 } } it "should render the specified number of times" do 2.times { expect_mrcpsynth_with_options(//) } subject.execute expect(original_command.complete_event(0.1).reason).to be_a Punchblock::Component::Output::Complete::Finish end context 'to 0' do let(:command_opts) { { :repeat_times => 0 } } it "should render 10,000 the specified number of times" do expect_answered 1000.times { expect_mrcpsynth_with_options(//) } subject.execute expect(original_command.complete_event(0.1).reason).to be_a Punchblock::Component::Output::Complete::Finish end end it 'should not execute further output after a stop command' do expect(mock_call).to receive(:execute_agi_command).once.ordered do sleep 0.2 end latch = CountDownLatch.new 1 expect(original_command).to receive(:add_event).once.with { |e| expect(e.reason).to be_a Punchblock::Component::Output::Complete::Finish latch.countdown! } Celluloid::Future.new { subject.execute } sleep 0.1 expect(mock_call).to receive(:redirect_back).ordered stop_command = Punchblock::Component::Stop.new stop_command.request! subject.execute_command stop_command expect(latch.wait(2)).to be_true end end end describe 'max-time' do context 'unset' do let(:command_opts) { { :max_time => nil } } it 'should not pass any options to MRCPSynth' do expect_mrcpsynth_with_options(//) subject.execute end end context 'set' do let(:command_opts) { { :max_time => 30 } } it "should return an error and not execute any actions" do subject.execute error = ProtocolError.new.setup 'option error', 'A max_time value is unsupported on Asterisk.' expect(original_command.response(0.1)).to eq(error) end end end describe 'voice' do context 'unset' do let(:command_opts) { { :voice => nil } } it 'should not pass the v option to MRCPSynth' do expect_mrcpsynth_with_options(//) subject.execute end end context 'set' do let(:command_opts) { { :voice => 'alison' } } it 'should pass the v option to MRCPSynth' do expect_mrcpsynth_with_options(/v=alison/) subject.execute end end end describe 'interrupt_on' do context "set to nil" do let(:command_opts) { { :interrupt_on => nil } } it "should not pass the i option to MRCPSynth" do expect_mrcpsynth_with_options(//) subject.execute end end context "set to :any" do let(:command_opts) { { :interrupt_on => :any } } it "should pass the i option to MRCPSynth" do expect_mrcpsynth_with_options(/i=any/) subject.execute end end context "set to :dtmf" do let(:command_opts) { { :interrupt_on => :dtmf } } it "should pass the i option to MRCPSynth" do expect_mrcpsynth_with_options(/i=any/) subject.execute end end context "set to :voice" do let(:command_opts) { { :interrupt_on => :voice } } it "should return an error and not execute any actions" do subject.execute error = ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.' expect(original_command.response(0.1)).to eq(error) end end end end [:asterisk, nil].each do |renderer| context "with a renderer of #{renderer.inspect}" do def expect_playback(filename = audio_filename) expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Playback', filename).and_return code: 200 end def expect_playback_noanswer expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Playback', audio_filename + ',noanswer').and_return code: 200 end let(:audio_filename) { 'tt-monkeys' } let :ssml_doc do RubySpeech::SSML.draw do audio :src => audio_filename end end let(:command_opts) { {} } let :command_options do { :render_document => {:value => ssml_doc}, renderer: renderer }.merge(command_opts) end let :original_command do Punchblock::Component::Output.new command_options end let(:playbackstatus) { 'SUCCESS' } before { allow(mock_call).to receive(:channel_var).with('PLAYBACKSTATUS').and_return playbackstatus } describe 'ssml' do context 'unset' do let(:ssml_doc) { nil } it "should return an error and not execute any actions" do subject.execute error = ProtocolError.new.setup 'option error', 'An SSML document is required.' expect(original_command.response(0.1)).to eq(error) end end context 'with a single audio SSML node' do let(:audio_filename) { 'tt-monkeys' } let :ssml_doc do RubySpeech::SSML.draw { audio :src => audio_filename } end it 'should playback the audio file using Playback' do expect_answered expect_playback subject.execute end it 'should send a complete event when the file finishes playback' do def mock_call.answered? true end expect_playback subject.execute expect(original_command.complete_event(0.1).reason).to be_a Punchblock::Component::Output::Complete::Finish end context "when the audio filename is prefixed by file://" do let(:audio_filename) { 'file://tt-monkeys' } it 'should playback the audio file using Playback' do expect_answered expect_playback 'tt-monkeys' subject.execute end end context "when the audio filename has an extension" do let(:audio_filename) { 'tt-monkeys.wav' } it 'should playback the audio file using Playback' do expect_answered expect_playback 'tt-monkeys' subject.execute end context "when there are other dots in the filename" do let(:audio_filename) { 'blue.tt-monkeys.wav' } it 'should playback the audio file using Playback' do expect_answered expect_playback 'blue.tt-monkeys' subject.execute end context "and no file extension" do let(:audio_filename) { '/var/lib/gems/1.9.1/gems/myapp-1.0.0/prompts/greeting' } it 'should playback the audio file using Playback' do expect_answered expect_playback '/var/lib/gems/1.9.1/gems/myapp-1.0.0/prompts/greeting' subject.execute end end end end context "when we get a RubyAMI Error" do it "should send an error complete event" do expect_answered error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' } expect(mock_call).to receive(:execute_agi_command).and_raise error subject.execute complete_reason = original_command.complete_event(0.1).reason expect(complete_reason).to be_a Punchblock::Event::Complete::Error expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'") end end context "when the channel is gone" do it "should send an error complete event" do expect_answered error = ChannelGoneError.new 'FooBar' expect(mock_call).to receive(:execute_agi_command).and_raise error subject.execute complete_reason = original_command.complete_event(0.1).reason expect(complete_reason).to be_a Punchblock::Event::Complete::Hangup end end context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do let(:playbackstatus) { 'FAILED' } it "should send an error complete event" do expect_answered expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1 subject.execute complete_reason = original_command.complete_event(0.1).reason expect(complete_reason).to be_a Punchblock::Event::Complete::Error expect(complete_reason.details).to eq("Terminated due to playback error") end end end context 'with a single text node without spaces' do let(:audio_filename) { 'tt-monkeys' } let :ssml_doc do RubySpeech::SSML.draw { string audio_filename } end it 'should playback the audio file using Playback' do expect_answered expect_playback subject.execute end it 'should send a complete event when the file finishes playback' do expect_answered expect_playback subject.execute expect(original_command.complete_event(0.1).reason).to be_a Punchblock::Component::Output::Complete::Finish end context "when we get a RubyAMI Error" do it "should send an error complete event" do expect_answered error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' } expect(mock_call).to receive(:execute_agi_command).and_raise error subject.execute complete_reason = original_command.complete_event(0.1).reason expect(complete_reason).to be_a Punchblock::Event::Complete::Error expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'") end end context "with early media playback" do it "should play the file with Playback" do expect_answered false expect_playback_noanswer expect(mock_call).to receive(:send_progress) subject.execute end context "with interrupt_on set to something that is not nil" do let(:audio_filename) { 'tt-monkeys' } let :command_options do { :render_document => { :value => RubySpeech::SSML.draw { string audio_filename }, }, :interrupt_on => :any } end it "should return an error when the output is interruptible and it is early media" do expect_answered false error = ProtocolError.new.setup 'option error', 'Interrupt digits are not allowed with early media.' subject.execute expect(original_command.response(0.1)).to eq(error) end end end end context 'with multiple audio SSML nodes' do let(:audio_filename1) { 'foo' } let(:audio_filename2) { 'bar' } let :ssml_doc do RubySpeech::SSML.draw do audio :src => audio_filename1 audio :src => audio_filename2 end end it 'should playback all audio files using Playback' do latch = CountDownLatch.new 2 expect_playback [audio_filename1, audio_filename2].join('&') expect_answered subject.execute latch.wait 2 sleep 2 end it 'should send a complete event after the final file has finished playback' do expect_answered expect_playback [audio_filename1, audio_filename2].join('&') latch = CountDownLatch.new 1 expect(original_command).to receive(:add_event).once.with { |e| expect(e.reason).to be_a Punchblock::Component::Output::Complete::Finish latch.countdown! } subject.execute expect(latch.wait(2)).to be_true end end context "with an SSML document containing elements other than