ruby_ami 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|