spec/adhearsion/call_controller/dial_spec.rb in adhearsion-2.4.0 vs spec/adhearsion/call_controller/dial_spec.rb in adhearsion-2.5.0

- old
+ new

@@ -17,10 +17,12 @@ let(:mock_answered) { Punchblock::Event::Answered.new } let(:latch) { CountDownLatch.new 1 } + let(:join_options) { options[:join_options] || {} } + before do other_mock_call.wrapped_object.stub id: other_call_id, write_command: true second_other_mock_call.wrapped_object.stub id: second_other_call_id, write_command: true end @@ -94,11 +96,11 @@ latch.wait(1).should be_true end it "joins the new call to the existing one on answer" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) dial_in_thread latch.wait(1).should be_false @@ -106,13 +108,114 @@ other_mock_call << mock_end latch.wait(1).should be_true end + context "with a join target specified" do + let(:options) { { join_target: {mixer_name: 'foobar'} } } + + it "joins the calls to the specified target on answer" do + call.should_receive(:answer).once + call.should_receive(:join).once.with({mixer_name: 'foobar'}, {}) + other_mock_call.should_receive(:join).once.with({mixer_name: 'foobar'}, {}) + + dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_answered + other_mock_call << mock_end + + latch.wait(1).should be_true + end + end + + context "with a pre-join callback specified" do + let(:foo) { double } + let(:options) { { pre_join: ->(call) { foo.bar call } } } + + it "executes the callback prior to joining" do + foo.should_receive(:bar).once.with(other_mock_call).ordered + call.should_receive(:answer).once.ordered + other_mock_call.should_receive(:join).once.with(call, {}).ordered + + dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_answered + other_mock_call << mock_end + + latch.wait(1).should be_true + end + end + + context "with ringback specified" do + let(:component) { Punchblock::Component::Output.new } + let(:options) { { ringback: ['file://tt-monkeys'] } } + + before do + component.request! + component.execute! + end + + it "plays the ringback asynchronously, terminating prior to joining" do + subject.should_receive(:play!).once.with(['file://tt-monkeys'], repeat_times: 0).and_return(component) + component.should_receive(:stop!).twice + call.should_receive(:answer).once.ordered + other_mock_call.should_receive(:join).once.with(call, {}).ordered + + dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_answered + other_mock_call << mock_end + + latch.wait(1).should be_true + end + + context "as a callback" do + let(:foo) { double } + let(:options) { { ringback: -> { foo.bar; component } } } + + it "calls the callback to start, and uses the return value of the callback to stop the ringback" do + foo.should_receive(:bar).once.ordered + component.should_receive(:stop!).twice + call.should_receive(:answer).once.ordered + other_mock_call.should_receive(:join).once.with(call, {}).ordered + + dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_answered + other_mock_call << mock_end + + latch.wait(1).should be_true + end + end + + context "when the call is rejected" do + it "terminates the ringback before returning" do + subject.should_receive(:play!).once.with(['file://tt-monkeys'], repeat_times: 0).and_return(component) + component.should_receive(:stop!).once + + t = dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_end(:reject) + + latch.wait(1).should be_true + end + end + end + it "hangs up the new call when the root call ends" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) other_mock_call.should_receive(:hangup).once dial_in_thread latch.wait(1).should be_false @@ -160,11 +263,11 @@ end context "when the call is answered and joined" do it "has an overall dial status of :answer" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) t = dial_in_thread sleep 0.5 @@ -181,11 +284,11 @@ joined_status.result.should == :joined end it "records the duration of the join" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) other_mock_call.stub hangup: true t = dial_in_thread sleep 0.5 @@ -206,30 +309,55 @@ status = t.value status.result.should be == :answer joined_status = status.joins[status.calls.first] joined_status.duration.should == 37.0 end + + context "when join options are specified" do + let(:options) { { join_options: {media: :direct} } } + + it "joins the calls with those options" do + call.should_receive(:answer).once + other_mock_call.should_receive(:join).once.with(call, media: :direct) + other_mock_call.stub hangup: true + + t = dial_in_thread + + sleep 0.5 + + other_mock_call << mock_answered + + other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) + other_mock_call << mock_end + + latch.wait(1).should be_true + + t.join + end + end end context "when a dial is split" do + let(:join_target) { call } + before do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) - call.stub(:unjoin).and_return do + other_mock_call.should_receive(:join).once.with(join_target, join_options) + other_mock_call.stub(:unjoin).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end end it "should unjoin the calls" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_return do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end dial = Dial::Dial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -247,11 +375,11 @@ dial.status.result.should be == :answer end it "should not unblock immediately" do dial = Dial::Dial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -272,11 +400,11 @@ dial.status.result.should be == :answer end it "should set end time" do dial = Dial::Dial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -324,11 +452,11 @@ let(:main_split_controller) { Class.new(split_controller) } let(:others_split_controller) { Class.new(split_controller) } it "should execute the :main controller on the originating call and :others on the outbound calls" do dial = Dial::Dial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -360,17 +488,17 @@ end end context "when rejoining" do it "should rejoin the calls" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_return do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end dial = Dial::Dial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -379,32 +507,105 @@ other_mock_call << mock_answered dial.split - other_mock_call.should_receive(:join).once.ordered.with(call) + other_mock_call.should_receive(:join).once.ordered.with(call, {}) dial.rejoin other_mock_call << mock_end latch.wait(1).should be_true waiter_thread.join dial.status.result.should be == :answer end + context "when join options were set originally" do + let(:options) { { join_options: {media: :direct} } } + + it "should rejoin with the same parameters" do + other_mock_call.stub(:unjoin) + + dial = Dial::Dial.new to, options, call + dial.run subject + + other_mock_call << mock_answered + + dial.split + + other_mock_call.should_receive(:join).once.ordered.with(call, media: :direct) + dial.rejoin + end + end + + context "when join options are passed to rejoin" do + it "should rejoin with those parameters" do + other_mock_call.stub(:unjoin) + + dial = Dial::Dial.new to, options, call + dial.run subject + + other_mock_call << mock_answered + + dial.split + + other_mock_call.should_receive(:join).once.ordered.with(call, media: :direct) + dial.rejoin nil, media: :direct + end + end + + context "when a join target was originally specified" do + let(:join_target) { {mixer_name: 'foobar'} } + let(:options) { { join_target: join_target } } + + it "joins the calls to the specified target on answer" do + call.should_receive(:join).once.with(join_target, {}) + other_mock_call.should_receive(:unjoin).once.ordered.with(join_target) + call.should_receive(:unjoin).once.ordered.with(join_target).and_return do + call << Punchblock::Event::Unjoined.new(join_target) + other_mock_call << Punchblock::Event::Unjoined.new(join_target) + end + + dial = Dial::Dial.new to, options, call + dial.run subject + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + other_mock_call << mock_answered + + dial.split + + call.should_receive(:join).once.ordered.with({mixer_name: 'foobar'}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: 'foobar'}, {}) + dial.rejoin + + other_mock_call << mock_end + + latch.wait(1).should be_true + + waiter_thread.join + dial.status.result.should be == :answer + end + end + context "to a specified mixer" do let(:mixer) { SecureRandom.uuid } it "should join all calls to the mixer" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_return do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end dial = Dial::Dial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -413,21 +614,61 @@ other_mock_call << mock_answered dial.split - call.should_receive(:join).once.ordered.with(mixer_name: mixer) - other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) dial.rejoin mixer_name: mixer other_mock_call << mock_end latch.wait(1).should be_true waiter_thread.join dial.status.result.should be == :answer end + + it "#split should then unjoin calls from the mixer" do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do + call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) + other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) + end + + dial = Dial::Dial.new to, options, call + dial.run subject + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + other_mock_call << mock_answered + + dial.split + + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + dial.rejoin mixer_name: mixer + + other_mock_call.should_receive(:unjoin).once.ordered.with(mixer_name: mixer).and_return do + other_mock_call << Punchblock::Event::Unjoined.new(mixer_name: mixer) + end + call.should_receive(:unjoin).once.ordered.with(mixer_name: mixer).and_return do + call << Punchblock::Event::Unjoined.new(mixer_name: mixer) + end + dial.split + + other_mock_call << mock_end + + latch.wait(1).should be_true + + waiter_thread.join + dial.status.result.should be == :answer + end end end context "when another dial is merged in" do let(:second_root_call_id) { new_uuid } @@ -438,38 +679,38 @@ let(:other_dial) { Dial::Dial.new second_to, options, second_root_call } before do second_root_call.stub write_command: true, id: second_root_call_id OutboundCall.should_receive(:new).and_return second_other_mock_call - second_other_mock_call.should_receive(:join).once.with(second_root_call) + second_other_mock_call.should_receive(:join).once.with(second_root_call, {}) second_other_mock_call.should_receive(:dial).once.with(second_to, options) second_root_call.should_receive(:answer).once SecureRandom.stub uuid: mixer - dial.run - other_dial.run + dial.run subject + other_dial.run subject other_mock_call << mock_answered second_other_mock_call << mock_answered end it "should split calls, rejoin to a mixer, and rejoin other calls to mixer" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_return do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end - second_root_call.should_receive(:unjoin).once.ordered.with(second_other_mock_call.id).and_return do + second_other_mock_call.should_receive(:unjoin).once.ordered.with(second_root_call).and_return do second_root_call << Punchblock::Event::Unjoined.new(call_uri: second_other_mock_call.id) second_other_mock_call << Punchblock::Event::Unjoined.new(call_uri: second_root_call.id) end - call.should_receive(:join).once.ordered.with(mixer_name: mixer) - other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) - second_root_call.should_receive(:join).once.ordered.with(mixer_name: mixer) - second_other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + second_root_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + second_other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) dial.merge other_dial waiter_thread = Thread.new do dial.await_completion @@ -486,10 +727,27 @@ waiter_thread.join dial.status.result.should be == :answer end + context "when join options were specified originally" do + let(:options) { { join_options: {media: :direct} } } + + it "should rejoin with default options" do + other_mock_call.stub(:unjoin) + second_other_mock_call.stub(:unjoin) + + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + + second_root_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + second_other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + + dial.merge other_dial + end + end + it "should add the merged calls to the returned status" do [call, other_mock_call, second_root_call, second_other_mock_call].each { |c| c.stub join: true, unjoin: true } dial.merge other_dial waiter_thread = Thread.new do @@ -558,20 +816,168 @@ waiter_thread.join dial.status.result.should be == :answer end + it "should subsequently rejoin to a mixer" do + [call, other_mock_call, second_root_call, second_other_mock_call].each { |c| c.stub join: true, unjoin: true } + + dial.merge other_dial + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + other_mock_call << mock_end + latch.wait(1).should be_false + + [call, second_root_call, second_other_mock_call].each do |call| + call.should_receive(:unjoin).once.with(mixer_name: mixer).and_return do + call << Punchblock::Event::Unjoined.new(mixer_name: mixer) + end + end + + dial.split + + [call, second_root_call, second_other_mock_call].each do |call| + call.should_receive(:join).once.with({mixer_name: mixer}, {}).and_return do + call << Punchblock::Event::Joined.new(mixer_name: mixer) + end + end + + dial.rejoin + end + + describe "if splitting fails" do + it "should not add the merged calls to the returned status" do + [call, other_mock_call, second_root_call, second_other_mock_call].each { |c| c.stub join: true, unjoin: true } + other_dial.should_receive(:split).and_raise StandardError + expect { dial.merge other_dial }.to raise_error(StandardError) + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + other_mock_call.async << mock_end + second_root_call.async << mock_end + second_other_mock_call.async << mock_end + + latch.wait(1).should be_true + + waiter_thread.join + dial.status.result.should be == :answer + dial.status.calls.should_not include(second_root_call, second_other_mock_call) + end + + it "should unblock before all joined calls end" do + [call, other_mock_call, second_root_call, second_other_mock_call].each { |c| c.stub join: true, unjoin: true } + + other_dial.should_receive(:split).and_raise StandardError + expect { dial.merge other_dial }.to raise_error(StandardError) + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + other_mock_call << mock_end + latch.wait(1).should be_true + + second_other_mock_call << mock_end + latch.wait(1).should be_true + + second_root_call << mock_end + latch.wait(1).should be_true + + waiter_thread.join + dial.status.result.should be == :answer + end + + it "should not cleanup merged calls when the root call ends" do + [call, other_mock_call, second_root_call, second_other_mock_call].each do |c| + c.stub join: true, unjoin: true + end + other_mock_call.should_receive(:hangup).once + [second_root_call, second_other_mock_call].each do |c| + c.should_receive(:hangup).never + end + + other_dial.should_receive(:split).and_raise StandardError + expect { dial.merge other_dial }.to raise_error(StandardError) + + waiter_thread = Thread.new do + dial.await_completion + dial.cleanup_calls + latch.countdown! + end + + sleep 0.5 + + call << mock_end + latch.wait(1).should be_true + + waiter_thread.join + dial.status.result.should be == :answer + end + end + + context "if a call hangs up" do + it "should still allow splitting and rejoining" do + [call, other_mock_call, second_root_call, second_other_mock_call].each { |c| c.stub join: true, unjoin: true } + + dial.merge other_dial + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + [call, second_root_call, second_other_mock_call].each do |call| + call.should_receive(:unjoin).once.with(mixer_name: mixer).and_return do + call << Punchblock::Event::Unjoined.new(mixer_name: mixer) + end + end + + other_mock_call.should_receive(:unjoin).and_raise Adhearsion::Call::Hangup + + dial.split + + other_mock_call << mock_end + latch.wait(1).should be_false + + [call, second_root_call, second_other_mock_call].each do |call| + call.should_receive(:join).once.with({mixer_name: mixer}, {}).and_return do + call << Punchblock::Event::Joined.new(mixer_name: mixer) + end + end + + other_mock_call.should_receive(:join).and_raise Adhearsion::Call::ExpiredError + + dial.rejoin + end + end + context "if the calls were not joined" do it "should still join to mixer" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_raise Punchblock::ProtocolError.new.setup(:service_unavailable) - second_root_call.should_receive(:unjoin).once.ordered.with(second_other_mock_call.id).and_raise Punchblock::ProtocolError.new.setup(:service_unavailable) + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_raise Punchblock::ProtocolError.new.setup(:service_unavailable) + second_other_mock_call.should_receive(:unjoin).once.ordered.with(second_root_call).and_raise Punchblock::ProtocolError.new.setup(:service_unavailable) - call.should_receive(:join).once.ordered.with(mixer_name: mixer) - other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) - second_root_call.should_receive(:join).once.ordered.with(mixer_name: mixer) - second_other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + second_root_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + second_other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) dial.merge other_dial waiter_thread = Thread.new do dial.await_completion @@ -631,11 +1037,11 @@ end end it "dials all parties and joins the first one to answer, hanging up the rest" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) second_other_mock_call.should_receive(:hangup).once.and_return do second_other_mock_call << mock_end end t = dial_in_thread @@ -654,11 +1060,11 @@ status.calls.each { |c| c.should be_a OutboundCall } end it "unblocks when the joined call unjoins, allowing it to proceed further" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) other_mock_call.should_receive(:hangup).once second_other_mock_call.should_receive(:hangup).once.and_return do second_other_mock_call << mock_end end @@ -766,11 +1172,11 @@ end context "when a call is answered and joined, and the other ends with an error" do it "has an overall dial status of :answer" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) second_other_mock_call.should_receive(:hangup).once.and_return do second_other_mock_call << mock_end(:error) end t = dial_in_thread @@ -815,11 +1221,11 @@ describe "if someone answers before the timeout elapses" do it "should not abort until the far end hangs up" do other_mock_call.should_receive(:dial).once.with(to, hash_including(:timeout => timeout)) call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) OutboundCall.should_receive(:new).and_return other_mock_call time = Time.now t = Thread.new do @@ -918,11 +1324,11 @@ it "should join the calls if the call is still active after execution of the call controller" do other_mock_call.should_receive(:hangup).once other_mock_call['confirm'] = true call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) t = dial_in_thread latch.wait(1).should be_false @@ -990,11 +1396,11 @@ it "should only execute the confirmation controller on the first call to answer, immediately hanging up all others" do other_mock_call['confirm'] = true call.should_receive(:answer).once other_mock_call.should_receive(:dial).once.with(to, from: nil) - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) other_mock_call.should_receive(:hangup).once.and_return do other_mock_call << mock_end end second_other_mock_call.should_receive(:dial).once.with(second_to, from: nil) @@ -1093,11 +1499,11 @@ latch.wait(1).should be_true end it "joins the new call to the existing one on answer" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) dial_in_thread latch.wait(1).should be_false @@ -1105,14 +1511,115 @@ other_mock_call << mock_end latch.wait(1).should be_true end + context "with a join target specified" do + let(:options) { { join_target: {mixer_name: 'foobar'} } } + + it "joins the calls to the specified target on answer" do + call.should_receive(:answer).once + call.should_receive(:join).once.with({mixer_name: 'foobar'}, {}) + other_mock_call.should_receive(:join).once.with({mixer_name: 'foobar'}, {}) + + dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_answered + other_mock_call << mock_end + + latch.wait(1).should be_true + end + end + + context "with a pre-join callback specified" do + let(:foo) { double } + let(:options) { { pre_join: ->(call) { foo.bar call } } } + + it "executes the callback prior to joining" do + foo.should_receive(:bar).once.with(other_mock_call).ordered + call.should_receive(:answer).once.ordered + other_mock_call.should_receive(:join).once.with(call, {}).ordered + + dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_answered + other_mock_call << mock_end + + latch.wait(1).should be_true + end + end + + context "with ringback specified" do + let(:component) { Punchblock::Component::Output.new } + let(:options) { { ringback: ['file://tt-monkeys'] } } + + before do + component.request! + component.execute! + end + + it "plays the ringback asynchronously, terminating prior to joining" do + subject.should_receive(:play!).once.with(['file://tt-monkeys'], repeat_times: 0).and_return(component) + component.should_receive(:stop!).twice + call.should_receive(:answer).once.ordered + other_mock_call.should_receive(:join).once.with(call, {}).ordered + + dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_answered + other_mock_call << mock_end + + latch.wait(1).should be_true + end + + context "as a callback" do + let(:foo) { double } + let(:options) { { ringback: -> { foo.bar; component } } } + + it "calls the callback to start, and uses the return value of the callback to stop the ringback" do + foo.should_receive(:bar).once.ordered + component.should_receive(:stop!).twice + call.should_receive(:answer).once.ordered + other_mock_call.should_receive(:join).once.with(call, {}).ordered + + dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_answered + other_mock_call << mock_end + + latch.wait(1).should be_true + end + end + + context "when the call is rejected" do + it "terminates the ringback before returning" do + subject.should_receive(:play!).once.with(['file://tt-monkeys'], repeat_times: 0).and_return(component) + component.should_receive(:stop!).once + + t = dial_in_thread + + latch.wait(1).should be_false + + other_mock_call << mock_end(:reject) + + latch.wait(1).should be_true + end + end + end + it "hangs up the new call when the root call ends" do other_mock_call.should_receive(:hangup).once call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) dial_in_thread latch.wait(1).should be_false @@ -1159,11 +1666,11 @@ end context "when the call is answered and joined" do it "has an overall dial status of :answer" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) t = dial_in_thread sleep 0.5 @@ -1180,11 +1687,11 @@ joined_status.result.should == :joined end it "records the duration of the join" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) other_mock_call.stub hangup: true t = dial_in_thread sleep 0.5 @@ -1205,30 +1712,55 @@ status = t.value status.result.should be == :answer joined_status = status.joins[status.calls.first] joined_status.duration.should == 37.0 end + + context "when join options are specified" do + let(:options) { { join_options: {media: :direct} } } + + it "joins the calls with those options" do + call.should_receive(:answer).once + other_mock_call.should_receive(:join).once.with(call, media: :direct) + other_mock_call.stub hangup: true + + t = dial_in_thread + + sleep 0.5 + + other_mock_call << mock_answered + + other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) + other_mock_call << mock_end + + latch.wait(1).should be_true + + t.join + end + end end context "when a dial is split" do + let(:join_target) { call } + before do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) - call.stub(:unjoin).and_return do + other_mock_call.should_receive(:join).once.with(join_target, join_options) + other_mock_call.stub(:unjoin).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end end it "should unjoin the calls" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_return do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end dial = Dial::ParallelConfirmationDial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -1246,11 +1778,11 @@ dial.status.result.should be == :answer end it "should not unblock immediately" do dial = Dial::ParallelConfirmationDial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -1271,11 +1803,11 @@ dial.status.result.should be == :answer end it "should set end time" do dial = Dial::ParallelConfirmationDial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -1323,11 +1855,11 @@ let(:main_split_controller) { Class.new(split_controller) } let(:others_split_controller) { Class.new(split_controller) } it "should execute the :main controller on the originating call and :others on the outbound calls" do dial = Dial::ParallelConfirmationDial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -1359,17 +1891,17 @@ end end context "when rejoining" do it "should rejoin the calls" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_return do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end dial = Dial::ParallelConfirmationDial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -1378,32 +1910,105 @@ other_mock_call << mock_answered dial.split - other_mock_call.should_receive(:join).once.ordered.with(call) + other_mock_call.should_receive(:join).once.ordered.with(call, {}) dial.rejoin other_mock_call << mock_end latch.wait(1).should be_true waiter_thread.join dial.status.result.should be == :answer end + context "when join options were set originally" do + let(:options) { { join_options: {media: :direct} } } + + it "should rejoin with the same parameters" do + other_mock_call.stub(:unjoin) + + dial = Dial::ParallelConfirmationDial.new to, options, call + dial.run subject + + other_mock_call << mock_answered + + dial.split + + other_mock_call.should_receive(:join).once.ordered.with(call, media: :direct) + dial.rejoin + end + end + + context "when join options are passed to rejoin" do + it "should rejoin with those parameters" do + other_mock_call.stub(:unjoin) + + dial = Dial::ParallelConfirmationDial.new to, options, call + dial.run subject + + other_mock_call << mock_answered + + dial.split + + other_mock_call.should_receive(:join).once.ordered.with(call, media: :direct) + dial.rejoin nil, media: :direct + end + end + + context "when a join target was originally specified" do + let(:join_target) { {mixer_name: 'foobar'} } + let(:options) { { join_target: join_target } } + + it "joins the calls to the specified target on answer" do + call.should_receive(:join).once.with(join_target, {}) + other_mock_call.should_receive(:unjoin).once.ordered.with(join_target) + call.should_receive(:unjoin).once.ordered.with(join_target).and_return do + call << Punchblock::Event::Unjoined.new(join_target) + other_mock_call << Punchblock::Event::Unjoined.new(join_target) + end + + dial = Dial::ParallelConfirmationDial.new to, options, call + dial.run subject + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + other_mock_call << mock_answered + + dial.split + + call.should_receive(:join).once.ordered.with({mixer_name: 'foobar'}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: 'foobar'}, {}) + dial.rejoin + + other_mock_call << mock_end + + latch.wait(1).should be_true + + waiter_thread.join + dial.status.result.should be == :answer + end + end + context "to a specified mixer" do let(:mixer) { SecureRandom.uuid } it "should join all calls to the mixer" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_return do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end dial = Dial::ParallelConfirmationDial.new to, options, call - dial.run + dial.run subject waiter_thread = Thread.new do dial.await_completion latch.countdown! end @@ -1412,21 +2017,61 @@ other_mock_call << mock_answered dial.split - call.should_receive(:join).once.ordered.with(mixer_name: mixer) - other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) dial.rejoin mixer_name: mixer other_mock_call << mock_end latch.wait(1).should be_true waiter_thread.join dial.status.result.should be == :answer end + + it "#split should then unjoin calls from the mixer" do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do + call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) + other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) + end + + dial = Dial::ParallelConfirmationDial.new to, options, call + dial.run subject + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + other_mock_call << mock_answered + + dial.split + + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + dial.rejoin mixer_name: mixer + + other_mock_call.should_receive(:unjoin).once.ordered.with(mixer_name: mixer).and_return do + other_mock_call << Punchblock::Event::Unjoined.new(mixer_name: mixer) + end + call.should_receive(:unjoin).once.ordered.with(mixer_name: mixer).and_return do + call << Punchblock::Event::Unjoined.new(mixer_name: mixer) + end + dial.split + + other_mock_call << mock_end + + latch.wait(1).should be_true + + waiter_thread.join + dial.status.result.should be == :answer + end end end context "when another dial is merged in" do let(:second_root_call_id) { new_uuid } @@ -1437,38 +2082,38 @@ let(:other_dial) { Dial::ParallelConfirmationDial.new second_to, options, second_root_call } before do second_root_call.stub write_command: true, id: second_root_call_id OutboundCall.should_receive(:new).and_return second_other_mock_call - second_other_mock_call.should_receive(:join).once.with(second_root_call) + second_other_mock_call.should_receive(:join).once.with(second_root_call, {}) second_other_mock_call.should_receive(:dial).once.with(second_to, options) second_root_call.should_receive(:answer).once SecureRandom.stub uuid: mixer - dial.run - other_dial.run + dial.run subject + other_dial.run subject other_mock_call << mock_answered second_other_mock_call << mock_answered end it "should split calls, rejoin to a mixer, and rejoin other calls to mixer" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_return do + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_return do call << Punchblock::Event::Unjoined.new(call_uri: other_mock_call.id) other_mock_call << Punchblock::Event::Unjoined.new(call_uri: call.id) end - second_root_call.should_receive(:unjoin).once.ordered.with(second_other_mock_call.id).and_return do + second_other_mock_call.should_receive(:unjoin).once.ordered.with(second_root_call).and_return do second_root_call << Punchblock::Event::Unjoined.new(call_uri: second_other_mock_call.id) second_other_mock_call << Punchblock::Event::Unjoined.new(call_uri: second_root_call.id) end - call.should_receive(:join).once.ordered.with(mixer_name: mixer) - other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) - second_root_call.should_receive(:join).once.ordered.with(mixer_name: mixer) - second_other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + second_root_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + second_other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) dial.merge other_dial waiter_thread = Thread.new do dial.await_completion @@ -1485,10 +2130,27 @@ waiter_thread.join dial.status.result.should be == :answer end + context "when join options were specified originally" do + let(:options) { { join_options: {media: :direct} } } + + it "should rejoin with default options" do + other_mock_call.stub(:unjoin) + second_other_mock_call.stub(:unjoin) + + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + + second_root_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + second_other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + + dial.merge other_dial + end + end + it "should add the merged calls to the returned status" do [call, other_mock_call, second_root_call, second_other_mock_call].each { |c| c.stub join: true, unjoin: true } dial.merge other_dial waiter_thread = Thread.new do @@ -1557,20 +2219,90 @@ waiter_thread.join dial.status.result.should be == :answer end + it "should subsequently rejoin to a mixer" do + [call, other_mock_call, second_root_call, second_other_mock_call].each { |c| c.stub join: true, unjoin: true } + + dial.merge other_dial + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + other_mock_call << mock_end + latch.wait(1).should be_false + + [call, second_root_call, second_other_mock_call].each do |call| + call.should_receive(:unjoin).once.with(mixer_name: mixer).and_return do + call << Punchblock::Event::Unjoined.new(mixer_name: mixer) + end + end + + dial.split + + [call, other_mock_call, second_root_call, second_other_mock_call].each do |call| + call.should_receive(:join).once.with({mixer_name: mixer}, {}).and_return do + call << Punchblock::Event::Joined.new(mixer_name: mixer) + end + end + + dial.rejoin + end + + context "if a call hangs up" do + it "should still allow splitting and rejoining" do + [call, other_mock_call, second_root_call, second_other_mock_call].each { |c| c.stub join: true, unjoin: true } + + dial.merge other_dial + + waiter_thread = Thread.new do + dial.await_completion + latch.countdown! + end + + sleep 0.5 + + [call, second_root_call, second_other_mock_call].each do |call| + call.should_receive(:unjoin).once.with(mixer_name: mixer).and_return do + call << Punchblock::Event::Unjoined.new(mixer_name: mixer) + end + end + + other_mock_call.should_receive(:unjoin).and_raise Adhearsion::Call::Hangup + + dial.split + + other_mock_call << mock_end + latch.wait(1).should be_false + + [call, second_root_call, second_other_mock_call].each do |call| + call.should_receive(:join).once.with({mixer_name: mixer}, {}).and_return do + call << Punchblock::Event::Joined.new(mixer_name: mixer) + end + end + + other_mock_call.should_receive(:join).and_raise Adhearsion::Call::ExpiredError + + dial.rejoin + end + end + context "if the calls were not joined" do it "should still join to mixer" do - call.should_receive(:unjoin).once.ordered.with(other_mock_call.id).and_raise Punchblock::ProtocolError.new.setup(:service_unavailable) - second_root_call.should_receive(:unjoin).once.ordered.with(second_other_mock_call.id).and_raise Punchblock::ProtocolError.new.setup(:service_unavailable) + other_mock_call.should_receive(:unjoin).once.ordered.with(call).and_raise Punchblock::ProtocolError.new.setup(:service_unavailable) + second_other_mock_call.should_receive(:unjoin).once.ordered.with(second_root_call).and_raise Punchblock::ProtocolError.new.setup(:service_unavailable) - call.should_receive(:join).once.ordered.with(mixer_name: mixer) - other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) - second_root_call.should_receive(:join).once.ordered.with(mixer_name: mixer) - second_other_mock_call.should_receive(:join).once.ordered.with(mixer_name: mixer) + second_root_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) + second_other_mock_call.should_receive(:join).once.ordered.with({mixer_name: mixer}, {}) dial.merge other_dial waiter_thread = Thread.new do dial.await_completion @@ -1630,11 +2362,11 @@ end end it "dials all parties and joins the first one to answer, hanging up the rest" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) second_other_mock_call.should_receive(:hangup).once.and_return do second_other_mock_call << mock_end end t = dial_in_thread @@ -1653,11 +2385,11 @@ status.calls.each { |c| c.should be_a OutboundCall } end it "unblocks when the joined call unjoins, allowing it to proceed further" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) other_mock_call.should_receive(:hangup).once second_other_mock_call.should_receive(:hangup).once.and_return do second_other_mock_call << mock_end end @@ -1765,11 +2497,11 @@ end context "when a call is answered and joined, and the other ends with an error" do it "has an overall dial status of :answer" do call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) second_other_mock_call.should_receive(:hangup).once.and_return do second_other_mock_call << mock_end(:error) end t = dial_in_thread @@ -1814,11 +2546,11 @@ describe "if someone answers before the timeout elapses" do it "should not abort until the far end hangs up" do other_mock_call.should_receive(:dial).once.with(to, hash_including(:timeout => timeout)) call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) OutboundCall.should_receive(:new).and_return other_mock_call time = Time.now t = Thread.new do @@ -1922,11 +2654,11 @@ other_mock_call.should_receive(:hangup).once.and_return do other_mock_call << mock_end end other_mock_call['confirm'] = true call.should_receive(:answer).once - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) t = dial_in_thread latch.wait(1).should be_false @@ -2009,11 +2741,11 @@ second_other_mock_call['confirmation_delay'] = 1.3 call.should_receive(:answer).once other_mock_call.should_receive(:dial).once.with(to, from: nil) - other_mock_call.should_receive(:join).once.with(call) + other_mock_call.should_receive(:join).once.with(call, {}) other_mock_call.should_receive(:hangup).once.and_return do other_mock_call.async.deliver_message mock_end end second_other_mock_call.should_receive(:dial).once.with(second_to, from: nil) @@ -2073,10 +2805,10 @@ call.stub answer: true other_mock_call.stub dial: true, join: true other_mock_call.should_receive(:hangup).never - subject.run + subject.run double('controller') subject.skip_cleanup Thread.new do subject.await_completion