ruby_ami 0.1.1
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.
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +9 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +20 -0
- data/README.md +49 -0
- data/Rakefile +69 -0
- data/cucumber.yml +2 -0
- data/features/lexer.feature +260 -0
- data/features/step_definitions/lexer_steps.rb +207 -0
- data/features/support/ami_fixtures.yml +30 -0
- data/features/support/env.rb +16 -0
- data/features/support/introspective_lexer.rb +22 -0
- data/features/support/lexer_helper.rb +103 -0
- data/lib/ruby_ami.rb +29 -0
- data/lib/ruby_ami/Guardfile +6 -0
- data/lib/ruby_ami/action.rb +143 -0
- data/lib/ruby_ami/client.rb +187 -0
- data/lib/ruby_ami/error.rb +21 -0
- data/lib/ruby_ami/event.rb +10 -0
- data/lib/ruby_ami/lexer.rl.rb +302 -0
- data/lib/ruby_ami/lexer_machine.rl +87 -0
- data/lib/ruby_ami/metaprogramming.rb +17 -0
- data/lib/ruby_ami/response.rb +44 -0
- data/lib/ruby_ami/stream.rb +60 -0
- data/lib/ruby_ami/version.rb +3 -0
- data/ruby_ami.gemspec +40 -0
- data/spec/ruby_ami/action_spec.rb +163 -0
- data/spec/ruby_ami/client_spec.rb +324 -0
- data/spec/ruby_ami/error_spec.rb +7 -0
- data/spec/ruby_ami/event_spec.rb +7 -0
- data/spec/ruby_ami/response_spec.rb +7 -0
- data/spec/ruby_ami/stream_spec.rb +153 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/mock_server.rb +16 -0
- metadata +296 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
%%{ #%
|
2
|
+
|
3
|
+
#########
|
4
|
+
## This file is written with the Ragel programming language and parses the Asterisk Manager Interface protocol. It depends
|
5
|
+
## upon Ragel actions which should be implemented in another Ragel-parsed file which includes this file.
|
6
|
+
##
|
7
|
+
## Ragel was used because the AMI protocol is extremely non-deterministic and, in the edge cases, requires something both
|
8
|
+
## very robust and something which can recover from syntax errors.
|
9
|
+
##
|
10
|
+
## Note: This file is language agnostic. From this AMI parsers in many other languages can be generated.
|
11
|
+
#########
|
12
|
+
|
13
|
+
machine ami_protocol_parser_machine;
|
14
|
+
|
15
|
+
cr = "\r"; # A carriage return. Used before (almost) every newline character.
|
16
|
+
lf = "\n"; # Newline. Used (with cr) to separate key/value pairs and stanzas.
|
17
|
+
crlf = cr lf; # Means "carriage return and line feed". Used to separate key/value pairs and stanzas
|
18
|
+
loose_newline = cr? lf; # Used sometimes when the AMI protocol is nondeterministic about the delimiter
|
19
|
+
|
20
|
+
white = [\t ]; # Single whitespace character, either a tab or a space
|
21
|
+
colon = ":" [ ]**; # Separates keys from values. "A colon followed by any number of spaces"
|
22
|
+
stanza_break = crlf crlf; # The seperator between two stanzas.
|
23
|
+
rest_of_line = (any* -- crlf); # Match all characters until the next line seperator.
|
24
|
+
|
25
|
+
Prompt = "Asterisk Call Manager/" digit+ >version_starts "." digit+ %version_stops crlf;
|
26
|
+
|
27
|
+
Key = ((alnum | print) -- (cr | lf | ":"))+;
|
28
|
+
KeyValuePair = Key >key_starts %key_stops colon rest_of_line >value_starts %value_stops crlf;
|
29
|
+
|
30
|
+
FollowsDelimiter = loose_newline "--END COMMAND--";
|
31
|
+
|
32
|
+
Response = "Response"i colon;
|
33
|
+
|
34
|
+
Success = Response "Success"i %init_success crlf @{ fgoto success; };
|
35
|
+
Pong = Response "Pong"i %init_success crlf @{ fgoto success; };
|
36
|
+
Event = "Event"i colon %event_name_starts rest_of_line %event_name_stops crlf @{ fgoto success; };
|
37
|
+
Error = Response "Error"i %init_error crlf (("Message"i colon rest_of_line >error_reason_starts crlf >error_reason_stops) | KeyValuePair)+ crlf @error_received;
|
38
|
+
Follows = Response "Follows"i crlf @init_response_follows @{ fgoto response_follows; };
|
39
|
+
|
40
|
+
# For "Response: Follows"
|
41
|
+
FollowsBody = (any* -- FollowsDelimiter) >follows_text_starts FollowsDelimiter @follows_text_stops crlf;
|
42
|
+
|
43
|
+
ImmediateResponse = (any+ -- (loose_newline | ":")) >immediate_response_starts loose_newline @immediate_response_stops @{fret;};
|
44
|
+
SyntaxError = (any+ -- crlf) >syntax_error_starts crlf @syntax_error_stops;
|
45
|
+
|
46
|
+
irregularity := |*
|
47
|
+
ImmediateResponse; # Performs the fret in the ImmediateResponse FSM
|
48
|
+
SyntaxError => { fret; };
|
49
|
+
*|;
|
50
|
+
|
51
|
+
# When a new socket is established, Asterisk will send the version of the protocol per the Prompt machine. Because it's
|
52
|
+
# tedious for unit tests to always send this, we'll put some intelligence into this parser to support going straight into
|
53
|
+
# the protocol-parsing machine. It's also conceivable that a variant of AMI would not send this initial information.
|
54
|
+
main := |*
|
55
|
+
Prompt => { fgoto protocol; };
|
56
|
+
any => {
|
57
|
+
# If this scanner's look-ahead capability didn't match the prompt, let's ignore the need for a prompt
|
58
|
+
fhold;
|
59
|
+
fgoto protocol;
|
60
|
+
};
|
61
|
+
*|;
|
62
|
+
|
63
|
+
protocol := |*
|
64
|
+
Prompt;
|
65
|
+
Success;
|
66
|
+
Pong;
|
67
|
+
Event;
|
68
|
+
Error;
|
69
|
+
Follows crlf;
|
70
|
+
crlf => { fgoto protocol; }; # If we get a crlf out of place, let's just ignore it.
|
71
|
+
any => {
|
72
|
+
# If NONE of the above patterns match, we consider this a syntax error. The irregularity machine can recover gracefully.
|
73
|
+
fhold;
|
74
|
+
fcall irregularity;
|
75
|
+
};
|
76
|
+
*|;
|
77
|
+
|
78
|
+
success := KeyValuePair* crlf @message_received @{fgoto protocol;};
|
79
|
+
|
80
|
+
# For the "Response: Follows" protocol abnormality. What happens if there's a protocol irregularity in this state???
|
81
|
+
response_follows := |*
|
82
|
+
KeyValuePair+;
|
83
|
+
FollowsBody;
|
84
|
+
crlf @{ message_received @current_message; fgoto protocol; };
|
85
|
+
*|;
|
86
|
+
|
87
|
+
}%%
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module RubyAMI
|
2
|
+
##
|
3
|
+
# This is the object containing a response from Asterisk.
|
4
|
+
#
|
5
|
+
# Note: not all responses have an ActionID!
|
6
|
+
#
|
7
|
+
class Response
|
8
|
+
class << self
|
9
|
+
def from_immediate_response(text)
|
10
|
+
new.tap do |instance|
|
11
|
+
instance.text_body = text
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :action,
|
17
|
+
:text_body # For "Response: Follows" sections
|
18
|
+
attr_reader :events
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@headers = HashWithIndifferentAccess.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_text_body?
|
25
|
+
!!@text_body
|
26
|
+
end
|
27
|
+
|
28
|
+
def headers
|
29
|
+
@headers.clone
|
30
|
+
end
|
31
|
+
|
32
|
+
def [](arg)
|
33
|
+
@headers[arg]
|
34
|
+
end
|
35
|
+
|
36
|
+
def []=(key,value)
|
37
|
+
@headers[key] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
def action_id
|
41
|
+
@headers['ActionID']
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end # RubyAMI
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module RubyAMI
|
2
|
+
class Stream < EventMachine::Connection
|
3
|
+
class ConnectionStatus
|
4
|
+
def eql?(other)
|
5
|
+
other.is_a? self.class
|
6
|
+
end
|
7
|
+
|
8
|
+
alias :== :eql?
|
9
|
+
end
|
10
|
+
|
11
|
+
Connected = Class.new ConnectionStatus
|
12
|
+
Disconnected = Class.new ConnectionStatus
|
13
|
+
|
14
|
+
def self.start(host, port, event_callback)
|
15
|
+
EM.connect host, port, self, event_callback
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(event_callback)
|
19
|
+
super()
|
20
|
+
@event_callback = event_callback
|
21
|
+
@logger = Logger.new($stdout)
|
22
|
+
@logger.level = Logger::FATAL
|
23
|
+
@logger.debug "Starting up..."
|
24
|
+
@lexer = Lexer.new self
|
25
|
+
end
|
26
|
+
|
27
|
+
[:started, :stopped, :ready].each do |state|
|
28
|
+
define_method("#{state}?") { @state == state }
|
29
|
+
end
|
30
|
+
|
31
|
+
def post_init
|
32
|
+
@state = :started
|
33
|
+
@event_callback.call Connected.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def send_action(action)
|
37
|
+
@logger.debug "[SEND] #{action.to_s}"
|
38
|
+
send_data action.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
def receive_data(data)
|
42
|
+
@logger.debug "[RECV] #{data}"
|
43
|
+
@lexer << data
|
44
|
+
end
|
45
|
+
|
46
|
+
def message_received(message)
|
47
|
+
@logger.debug "[RECV] #{message.inspect}"
|
48
|
+
@event_callback.call message
|
49
|
+
end
|
50
|
+
|
51
|
+
alias :error_received :message_received
|
52
|
+
|
53
|
+
# Called by EM when the connection is closed
|
54
|
+
# @private
|
55
|
+
def unbind
|
56
|
+
@state = :stopped
|
57
|
+
@event_callback.call Disconnected.new
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/ruby_ami.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "ruby_ami/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "ruby_ami"
|
7
|
+
s.version = RubyAMI::VERSION
|
8
|
+
s.authors = ["Ben Langfeld"]
|
9
|
+
s.email = ["ben@langfeld.me"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Futzing with AMI so you don't have to}
|
12
|
+
s.description = %q{A Ruby client library for the Asterisk Management Interface build on eventmachine.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "ruby_ami"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n") << 'lib/ruby_ami/lexer.rb'
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency %q<activesupport>, [">= 3.0.9"]
|
22
|
+
s.add_runtime_dependency %q<uuidtools>, [">= 0"]
|
23
|
+
s.add_runtime_dependency %q<eventmachine>, [">= 0"]
|
24
|
+
s.add_runtime_dependency %q<future-resource>, [">= 0"]
|
25
|
+
s.add_runtime_dependency %q<girl_friday>, [">= 0"]
|
26
|
+
s.add_runtime_dependency %q<countdownlatch>, [">= 1.0.0"]
|
27
|
+
s.add_runtime_dependency %q<i18n>, [">= 0"]
|
28
|
+
|
29
|
+
s.add_development_dependency %q<bundler>, ["~> 1.0.0"]
|
30
|
+
s.add_development_dependency %q<rspec>, [">= 2.5.0"]
|
31
|
+
s.add_development_dependency %q<cucumber>, [">= 0"]
|
32
|
+
s.add_development_dependency %q<ci_reporter>, [">= 1.6.3"]
|
33
|
+
s.add_development_dependency %q<yard>, ["~> 0.6.0"]
|
34
|
+
s.add_development_dependency %q<rcov>, [">= 0"]
|
35
|
+
s.add_development_dependency %q<rake>, [">= 0"]
|
36
|
+
s.add_development_dependency %q<mocha>, [">= 0"]
|
37
|
+
s.add_development_dependency %q<simplecov>, [">= 0"]
|
38
|
+
s.add_development_dependency %q<simplecov-rcov>, [">= 0"]
|
39
|
+
s.add_development_dependency %q<guard-rspec>
|
40
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RubyAMI
|
4
|
+
describe Action do
|
5
|
+
let(:name) { 'foobar' }
|
6
|
+
let(:headers) { {'foo' => 'bar'} }
|
7
|
+
|
8
|
+
subject do
|
9
|
+
Action.new name, headers do |response|
|
10
|
+
@foo = response
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it { should be_new }
|
15
|
+
|
16
|
+
describe "SIPPeers actions" do
|
17
|
+
subject { Action.new('SIPPeers') }
|
18
|
+
its(:has_causal_events?) { should be true }
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "Queues actions" do
|
22
|
+
subject { Action.new('Queues') }
|
23
|
+
its(:replies_with_action_id?) { should == false }
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "IAXPeers actions" do
|
27
|
+
before { pending }
|
28
|
+
# FIXME: This test relies on the side effect that earlier tests have run
|
29
|
+
# and initialized the UnsupportedActionName::UNSUPPORTED_ACTION_NAMES
|
30
|
+
# constant for an "unknown" version of Asterisk. This should be fixed
|
31
|
+
# to be more specific about which version of Asterisk is under test.
|
32
|
+
# IAXPeers is supported (with Action IDs!) since Asterisk 1.8
|
33
|
+
subject { Action.new('IAXPeers') }
|
34
|
+
its(:replies_with_action_id?) { should == false }
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "the ParkedCalls terminator event" do
|
38
|
+
subject { Action.new('ParkedCalls') }
|
39
|
+
its(:causal_event_terminator_name) { should == "parkedcallscomplete" }
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should properly convert itself into a String when additional headers are given" do
|
43
|
+
string = Action.new("Hawtsawce", "Monkey" => "Zoo").to_s
|
44
|
+
string.should =~ /^Action: Hawtsawce\r\n/i
|
45
|
+
string.should =~ /[^\n]\r\n\r\n$/
|
46
|
+
string.should =~ /^(\w+:\s*[\w-]+\r\n){3}\r\n$/
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should properly convert itself into a String when no additional headers are given" do
|
50
|
+
Action.new("Ping").to_s.should =~ /^Action: Ping\r\nActionID: [\w-]+\r\n\r\n$/i
|
51
|
+
Action.new("ParkedCalls").to_s.should =~ /^Action: ParkedCalls\r\nActionID: [\w-]+\r\n\r\n$/i
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should be able to be marked as sent' do
|
55
|
+
subject.state = :sent
|
56
|
+
subject.should be_sent
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should be able to be marked as complete' do
|
60
|
+
subject.state = :complete
|
61
|
+
subject.should be_complete
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#<<' do
|
65
|
+
describe 'for a non-causal action' do
|
66
|
+
context 'with a response' do
|
67
|
+
let(:response) { Response.new }
|
68
|
+
|
69
|
+
it 'should set the response' do
|
70
|
+
subject << response
|
71
|
+
subject.response.should be response
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'with an error' do
|
76
|
+
let(:error) { Error.new }
|
77
|
+
|
78
|
+
it 'should set the response and raise the error when reading it' do
|
79
|
+
subject << error
|
80
|
+
lambda { subject.response }.should raise_error error
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'with an event' do
|
85
|
+
it 'should raise an error' do
|
86
|
+
lambda { subject << Event.new('foo') }.should raise_error StandardError, /causal action/
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'for a causal action' do
|
92
|
+
let(:name) { 'Status' }
|
93
|
+
|
94
|
+
context 'with a response' do
|
95
|
+
let(:message) { Response.new }
|
96
|
+
|
97
|
+
before { subject << message }
|
98
|
+
|
99
|
+
it { should_not be_complete }
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'with an event' do
|
103
|
+
let(:event) { Event.new 'foo' }
|
104
|
+
|
105
|
+
before { subject << event }
|
106
|
+
|
107
|
+
its(:events) { should == [event] }
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'with a terminating event' do
|
111
|
+
let(:response) { Response.new }
|
112
|
+
let(:event) { Event.new 'StatusComplete' }
|
113
|
+
|
114
|
+
before do
|
115
|
+
subject << response
|
116
|
+
subject.should_not be_complete
|
117
|
+
subject << event
|
118
|
+
end
|
119
|
+
|
120
|
+
its(:events) { should == [event] }
|
121
|
+
|
122
|
+
it { should be_complete }
|
123
|
+
|
124
|
+
its(:response) { should be response }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe 'setting the response' do
|
130
|
+
let(:response) { :bar }
|
131
|
+
|
132
|
+
before { subject.response = response }
|
133
|
+
|
134
|
+
it { should be_complete }
|
135
|
+
its(:response) { should == response }
|
136
|
+
|
137
|
+
it 'should call the response callback with the response' do
|
138
|
+
@foo.should == response
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe 'comparison' do
|
143
|
+
describe 'with another Action' do
|
144
|
+
context 'with identical name and headers' do
|
145
|
+
let(:other) { Action.new name, headers }
|
146
|
+
it { should == other }
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'with identical name and different headers' do
|
150
|
+
let(:other) { Action.new name, 'boo' => 'baz' }
|
151
|
+
it { should_not == other }
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'with different name and identical headers' do
|
155
|
+
let(:other) { Action.new 'BARBAZ', headers }
|
156
|
+
it { should_not == other }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
it { should_not == :foo }
|
161
|
+
end
|
162
|
+
end # Action
|
163
|
+
end # RubyAMI
|
@@ -0,0 +1,324 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RubyAMI
|
4
|
+
describe Client do
|
5
|
+
let(:event_handler) { [] }
|
6
|
+
|
7
|
+
let(:options) do
|
8
|
+
{
|
9
|
+
:host => '127.0.0.1',
|
10
|
+
:port => 50000 - rand(1000),
|
11
|
+
:username => 'username',
|
12
|
+
:password => 'password',
|
13
|
+
:event_handler => lambda { |event| event_handler << event }
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
subject { Client.new options }
|
18
|
+
|
19
|
+
it { should be_stopped }
|
20
|
+
|
21
|
+
its(:options) { should == options }
|
22
|
+
|
23
|
+
its(:action_queue) { should be_a GirlFriday::WorkQueue }
|
24
|
+
|
25
|
+
describe 'starting up' do
|
26
|
+
before do
|
27
|
+
MockServer.any_instance.stubs :receive_data
|
28
|
+
subject.start do
|
29
|
+
EM.start_server options[:host], options[:port], ServerMock
|
30
|
+
EM.add_timer(0.5) { EM.stop if EM.reactor_running? }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it { should be_started }
|
35
|
+
|
36
|
+
its(:events_stream) { should be_a Stream }
|
37
|
+
its(:actions_stream) { should be_a Stream }
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'logging in streams' do
|
41
|
+
context 'when the actions stream connects' do
|
42
|
+
let(:mock_actions_stream) { mock 'Actions Stream' }
|
43
|
+
|
44
|
+
let :expected_login_action do
|
45
|
+
Action.new 'Login',
|
46
|
+
'Username' => 'username',
|
47
|
+
'Secret' => 'password',
|
48
|
+
'Events' => 'Off'
|
49
|
+
end
|
50
|
+
|
51
|
+
before do
|
52
|
+
Action.any_instance.stubs(:response).returns(true)
|
53
|
+
subject.stubs(:actions_stream).returns mock_actions_stream
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should log in' do
|
57
|
+
mock_actions_stream.expects(:send_action).with do |action|
|
58
|
+
action.to_s.should == expected_login_action.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
GirlFriday::WorkQueue.immediate!
|
62
|
+
subject.handle_message Stream::Connected.new
|
63
|
+
GirlFriday::WorkQueue.queue!
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'when the events stream connects' do
|
68
|
+
let(:mock_events_stream) { mock 'Events Stream' }
|
69
|
+
|
70
|
+
let :expected_login_action do
|
71
|
+
Action.new 'Login',
|
72
|
+
'Username' => 'username',
|
73
|
+
'Secret' => 'password',
|
74
|
+
'Events' => 'On'
|
75
|
+
end
|
76
|
+
|
77
|
+
before do
|
78
|
+
subject.stubs(:events_stream).returns mock_events_stream
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should log in' do
|
82
|
+
mock_events_stream.expects(:send_action).with do |action|
|
83
|
+
action.to_s.should == expected_login_action.to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
subject.handle_event Stream::Connected.new
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'when the events stream disconnects' do
|
92
|
+
it 'should unbind' do
|
93
|
+
subject.expects(:unbind).once
|
94
|
+
subject.handle_event Stream::Disconnected.new
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe 'when the actions stream disconnects' do
|
99
|
+
before do
|
100
|
+
Action.any_instance.stubs(:response).returns(true)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should prevent further actions being sent' do
|
104
|
+
subject.expects(:_send_action).once
|
105
|
+
|
106
|
+
GirlFriday::WorkQueue.immediate!
|
107
|
+
subject.handle_message Stream::Connected.new
|
108
|
+
GirlFriday::WorkQueue.queue!
|
109
|
+
subject.handle_message Stream::Disconnected.new
|
110
|
+
|
111
|
+
action = Action.new 'foo'
|
112
|
+
subject.send_action action
|
113
|
+
|
114
|
+
sleep 2
|
115
|
+
|
116
|
+
action.should be_new
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should unbind' do
|
120
|
+
subject.expects(:unbind).once
|
121
|
+
subject.handle_message Stream::Disconnected.new
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe 'when an event is received' do
|
126
|
+
let(:event) { Event.new 'foobar' }
|
127
|
+
|
128
|
+
it 'should call the event handler' do
|
129
|
+
subject.handle_event event
|
130
|
+
event_handler.should == [event]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe 'sending actions' do
|
135
|
+
let(:action_name) { 'Login' }
|
136
|
+
let :headers do
|
137
|
+
{
|
138
|
+
'Username' => 'username',
|
139
|
+
'Secret' => 'password'
|
140
|
+
}
|
141
|
+
end
|
142
|
+
let(:expected_action) { Action.new action_name, headers }
|
143
|
+
|
144
|
+
let :expected_response do
|
145
|
+
Response.new.tap do |response|
|
146
|
+
response['ActionID'] = expected_action.action_id
|
147
|
+
response['Message'] = 'Action completed'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
let(:mock_actions_stream) { mock 'Actions Stream' }
|
152
|
+
|
153
|
+
before do
|
154
|
+
subject.stubs(:actions_stream).returns mock_actions_stream
|
155
|
+
subject.stubs(:login_actions).returns nil
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'should queue up actions to be sent' do
|
159
|
+
subject.handle_message Stream::Connected.new
|
160
|
+
subject.action_queue.expects(:<<).with expected_action
|
161
|
+
subject.send_action action_name, headers
|
162
|
+
end
|
163
|
+
|
164
|
+
describe 'forcibly for testing' do
|
165
|
+
before do
|
166
|
+
subject.actions_stream.expects(:send_action).with expected_action
|
167
|
+
subject._send_action expected_action
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should mark the action sent' do
|
171
|
+
expected_action.should be_sent
|
172
|
+
end
|
173
|
+
|
174
|
+
let(:receive_response) { subject.handle_message expected_response }
|
175
|
+
|
176
|
+
describe 'when a response is received' do
|
177
|
+
it 'should be sent to the action' do
|
178
|
+
expected_action.expects(:<<).once.with expected_response
|
179
|
+
receive_response
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should know its action' do
|
183
|
+
receive_response
|
184
|
+
expected_response.action.should be expected_action
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe 'when an error is received' do
|
189
|
+
let :expected_response do
|
190
|
+
Error.new.tap do |response|
|
191
|
+
response['ActionID'] = expected_action.action_id
|
192
|
+
response['Message'] = 'Action failed'
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'should be sent to the action' do
|
197
|
+
expected_action.expects(:<<).once.with expected_response
|
198
|
+
receive_response
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'should know its action' do
|
202
|
+
receive_response
|
203
|
+
expected_response.action.should be expected_action
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe 'when an event is received' do
|
208
|
+
let(:event) { Event.new 'foo' }
|
209
|
+
|
210
|
+
let(:receive_event) { subject.handle_message event }
|
211
|
+
|
212
|
+
context 'for a causal event' do
|
213
|
+
let(:expected_action) { Action.new 'Status' }
|
214
|
+
|
215
|
+
it 'should be sent to the action' do
|
216
|
+
expected_action.expects(:<<).once.with expected_response
|
217
|
+
expected_action.expects(:<<).once.with event
|
218
|
+
receive_response
|
219
|
+
receive_event
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'should know its action' do
|
223
|
+
expected_action.stubs :<<
|
224
|
+
receive_response
|
225
|
+
receive_event
|
226
|
+
event.action.should be expected_action
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
context 'for a causal action which is complete' do
|
231
|
+
let(:expected_action) { Action.new 'Status' }
|
232
|
+
|
233
|
+
before do
|
234
|
+
expected_action.stubs(:complete?).returns true
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'should raise an error' do
|
238
|
+
receive_response
|
239
|
+
receive_event
|
240
|
+
lambda { subject.handle_message Event.new('bar') }.should raise_error StandardError, /causal action/
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
context 'for a non-causal action' do
|
245
|
+
it 'should raise an error' do
|
246
|
+
lambda { receive_event }.should raise_error StandardError, /causal action/
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe 'from the queue' do
|
253
|
+
it 'should send actions to the stream and set their responses' do
|
254
|
+
subject.actions_stream.expects(:send_action).with expected_action
|
255
|
+
subject.handle_message Stream::Connected.new
|
256
|
+
|
257
|
+
Thread.new do
|
258
|
+
GirlFriday::WorkQueue.immediate!
|
259
|
+
subject.send_action expected_action
|
260
|
+
GirlFriday::WorkQueue.queue!
|
261
|
+
end
|
262
|
+
|
263
|
+
sleep 0.1
|
264
|
+
|
265
|
+
subject.handle_message expected_response
|
266
|
+
expected_action.response.should be expected_response
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'should not send another action if the first action has not yet received a response' do
|
270
|
+
subject.actions_stream.expects(:send_action).once.with expected_action
|
271
|
+
subject.handle_message Stream::Connected.new
|
272
|
+
actions = []
|
273
|
+
|
274
|
+
2.times do
|
275
|
+
action = Action.new action_name, headers
|
276
|
+
actions << action
|
277
|
+
subject.send_action action
|
278
|
+
end
|
279
|
+
|
280
|
+
sleep 2
|
281
|
+
|
282
|
+
actions.should have(2).actions
|
283
|
+
actions[0].should be_sent
|
284
|
+
actions[1].should be_new
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
describe '#stop' do
|
290
|
+
let(:mock_actions_stream) { mock 'Actions Stream' }
|
291
|
+
let(:mock_events_stream) { mock 'Events Stream' }
|
292
|
+
|
293
|
+
let(:streams) { [mock_actions_stream, mock_events_stream] }
|
294
|
+
|
295
|
+
before do
|
296
|
+
subject.stubs(:actions_stream).returns mock_actions_stream
|
297
|
+
subject.stubs(:events_stream).returns mock_events_stream
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'should close both streams' do
|
301
|
+
streams.each { |s| s.expects :close_connection_after_writing }
|
302
|
+
subject.stop
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
describe '#unbind' do
|
307
|
+
context 'if EM is running' do
|
308
|
+
it 'shuts down EM' do
|
309
|
+
EM.expects(:reactor_running?).returns true
|
310
|
+
EM.expects(:stop)
|
311
|
+
subject.unbind
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context 'if EM is not running' do
|
316
|
+
it 'does nothing' do
|
317
|
+
EM.expects(:reactor_running?).returns false
|
318
|
+
EM.expects(:stop).never
|
319
|
+
subject.unbind
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|