ruby-asterisk 0.1.0 → 1.0.0

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.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/.claude/skills/ruby-asterisk-agi/SKILL.md +317 -0
  3. data/.claude/skills/ruby-asterisk-ami/SKILL.md +187 -0
  4. data/.claude/skills/ruby-asterisk-ari/SKILL.md +174 -0
  5. data/.claude/skills/ruby-asterisk-ari-websocket/SKILL.md +148 -0
  6. data/.github/workflows/ci.yml +79 -0
  7. data/.gitignore +50 -3
  8. data/.rubocop.yml +84 -0
  9. data/CHANGELOG.md +52 -0
  10. data/Gemfile +3 -1
  11. data/LICENCE +21 -0
  12. data/README.md +454 -70
  13. data/Rakefile +3 -1
  14. data/lib/ruby-asterisk/agi/protocol.rb +160 -0
  15. data/lib/ruby-asterisk/agi/server.rb +108 -0
  16. data/lib/ruby-asterisk/agi/session.rb +187 -0
  17. data/lib/ruby-asterisk/ami/client.rb +135 -0
  18. data/lib/ruby-asterisk/ami/commands/channel.rb +55 -0
  19. data/lib/ruby-asterisk/ami/commands/conference.rb +37 -0
  20. data/lib/ruby-asterisk/ami/commands/extension.rb +17 -0
  21. data/lib/ruby-asterisk/ami/commands/mailbox.rb +21 -0
  22. data/lib/ruby-asterisk/ami/commands/monitor.rb +33 -0
  23. data/lib/ruby-asterisk/ami/commands/queue.rb +39 -0
  24. data/lib/ruby-asterisk/ami/commands/sip.rb +25 -0
  25. data/lib/ruby-asterisk/ami/commands/system.rb +50 -0
  26. data/lib/ruby-asterisk/ami/event.rb +22 -0
  27. data/lib/ruby-asterisk/ami/event_list_aggregation.rb +82 -0
  28. data/lib/ruby-asterisk/ami/parser.rb +60 -0
  29. data/lib/ruby-asterisk/ami/promise.rb +108 -0
  30. data/lib/ruby-asterisk/ami/reactor.rb +162 -0
  31. data/lib/ruby-asterisk/ari/client.rb +94 -0
  32. data/lib/ruby-asterisk/ari/resources/base.rb +23 -0
  33. data/lib/ruby-asterisk/ari/resources/bridge.rb +22 -0
  34. data/lib/ruby-asterisk/ari/resources/channel.rb +26 -0
  35. data/lib/ruby-asterisk/ari/resources/collection.rb +27 -0
  36. data/lib/ruby-asterisk/ari/resources/endpoint.rb +22 -0
  37. data/lib/ruby-asterisk/ari/resources/playback.rb +18 -0
  38. data/lib/ruby-asterisk/ari/websocket/connection.rb +143 -0
  39. data/lib/ruby-asterisk/ari/websocket/event_handlers.rb +80 -0
  40. data/lib/ruby-asterisk/ari/websocket/heartbeat.rb +65 -0
  41. data/lib/ruby-asterisk/ari/websocket/reconnect.rb +54 -0
  42. data/lib/ruby-asterisk/ari/websocket/socket_adapter.rb +23 -0
  43. data/lib/ruby-asterisk/ari/websocket.rb +155 -0
  44. data/lib/ruby-asterisk/compat.rb +7 -0
  45. data/lib/ruby-asterisk/error.rb +10 -0
  46. data/lib/ruby-asterisk/parsing_constants.rb +100 -0
  47. data/lib/ruby-asterisk/request.rb +38 -20
  48. data/lib/ruby-asterisk/response.rb +50 -137
  49. data/lib/ruby-asterisk/response_builder.rb +18 -0
  50. data/lib/ruby-asterisk/response_parser.rb +43 -0
  51. data/lib/ruby-asterisk/version.rb +4 -2
  52. data/lib/ruby-asterisk.rb +5 -147
  53. data/ruby-asterisk.gemspec +26 -17
  54. data/spec/ruby-asterisk/agi/protocol_spec.rb +239 -0
  55. data/spec/ruby-asterisk/agi/server_spec.rb +199 -0
  56. data/spec/ruby-asterisk/agi/session_spec.rb +293 -0
  57. data/spec/ruby-asterisk/ami/async_client_spec.rb +256 -0
  58. data/spec/ruby-asterisk/ami/multi_event_spec.rb +57 -0
  59. data/spec/ruby-asterisk/ami/parser_spec.rb +190 -0
  60. data/spec/ruby-asterisk/ami/reactor_spec.rb +290 -0
  61. data/spec/ruby-asterisk/ami_client_spec.rb +68 -0
  62. data/spec/ruby-asterisk/ari/client_spec.rb +168 -0
  63. data/spec/ruby-asterisk/ari/resources/bridge_spec.rb +42 -0
  64. data/spec/ruby-asterisk/ari/resources/channel_spec.rb +57 -0
  65. data/spec/ruby-asterisk/ari/resources/collection_spec.rb +66 -0
  66. data/spec/ruby-asterisk/ari/resources/endpoint_spec.rb +30 -0
  67. data/spec/ruby-asterisk/ari/resources/playback_spec.rb +33 -0
  68. data/spec/ruby-asterisk/ari/websocket_integration_spec.rb +86 -0
  69. data/spec/ruby-asterisk/ari/websocket_spec.rb +374 -0
  70. data/spec/ruby-asterisk/immutability_spec.rb +148 -0
  71. data/spec/ruby-asterisk/request_spec.rb +29 -9
  72. data/spec/ruby-asterisk/response_spec.rb +147 -148
  73. data/spec/spec_helper.rb +14 -0
  74. data/spec/support/mock_ami_server.rb +50 -0
  75. data/spec/support/mock_ari_websocket_server.rb +114 -0
  76. metadata +177 -21
  77. data/CHANGELOG +0 -3
  78. data/spec/ruby-asterisk/ruby_asterisk_spec.rb +0 -150
@@ -0,0 +1,374 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyAsterisk::ARI::WebSocket do
6
+ let(:base_url) { 'http://localhost:8088' }
7
+ let(:api_key) { 'asterisk_api_key' }
8
+ let(:app_name) { 'stasis_app' }
9
+ let(:logger) { instance_double(Logger, info: nil, warn: nil, error: nil, debug: nil) }
10
+
11
+ subject(:websocket) { described_class.new(base_url, api_key, app_name, logger: logger) }
12
+
13
+ describe '#initialize' do
14
+ it 'sets the base_url' do
15
+ expect(websocket.instance_variable_get(:@base_url)).to eq(base_url)
16
+ end
17
+
18
+ it 'sets the app_name' do
19
+ expect(websocket.app_name).to eq(app_name)
20
+ end
21
+
22
+ it 'initializes callbacks as empty hash' do
23
+ expect(websocket.callbacks).to eq({})
24
+ end
25
+
26
+ it 'sets connected to false' do
27
+ expect(websocket.connected).to be(false)
28
+ end
29
+
30
+ it 'sets auto_reconnect to true by default' do
31
+ expect(websocket.instance_variable_get(:@should_reconnect)).to be(true)
32
+ end
33
+
34
+ it 'allows disabling auto_reconnect' do
35
+ ws = described_class.new(base_url, api_key, app_name, auto_reconnect: false)
36
+ expect(ws.instance_variable_get(:@should_reconnect)).to be(false)
37
+ end
38
+
39
+ it 'uses default ping interval' do
40
+ expect(websocket.instance_variable_get(:@ping_interval)).to eq(described_class::PING_INTERVAL)
41
+ end
42
+
43
+ it 'allows custom ping interval' do
44
+ ws = described_class.new(base_url, api_key, app_name, ping_interval: 60)
45
+ expect(ws.instance_variable_get(:@ping_interval)).to eq(60)
46
+ end
47
+ end
48
+
49
+ describe '#on' do
50
+ it 'registers a callback for an event type' do
51
+ callback = proc { |data| puts data }
52
+ websocket.on('StasisStart', &callback)
53
+
54
+ expect(websocket.callbacks['StasisStart']).to include(callback)
55
+ end
56
+
57
+ it 'allows multiple callbacks for the same event' do
58
+ callback1 = proc { |data| puts data }
59
+ callback2 = proc { |data| puts data }
60
+
61
+ websocket.on('StasisStart', &callback1)
62
+ websocket.on('StasisStart', &callback2)
63
+
64
+ expect(websocket.callbacks['StasisStart']).to include(callback1, callback2)
65
+ end
66
+
67
+ it 'converts symbol event types to strings' do
68
+ callback = proc { |data| puts data }
69
+ websocket.on(:StasisStart, &callback)
70
+
71
+ expect(websocket.callbacks['StasisStart']).to include(callback)
72
+ end
73
+
74
+ it 'returns self for method chaining' do
75
+ result = websocket.on('StasisStart') { |data| puts data }
76
+ expect(result).to eq(websocket)
77
+ end
78
+ end
79
+
80
+ describe '#connected?' do
81
+ context 'when not connected' do
82
+ it 'returns false' do
83
+ expect(websocket.connected?).to be(false)
84
+ end
85
+ end
86
+
87
+ context 'when connected flag is true but no driver' do
88
+ before do
89
+ websocket.instance_variable_set(:@connected, true)
90
+ end
91
+
92
+ it 'returns false' do
93
+ expect(websocket.connected?).to be(false)
94
+ end
95
+ end
96
+
97
+ context 'when connected with an open driver' do
98
+ let(:driver) { instance_double(WebSocket::Driver::Client, state: :open) }
99
+
100
+ before do
101
+ websocket.instance_variable_set(:@connected, true)
102
+ websocket.instance_variable_set(:@driver, driver)
103
+ end
104
+
105
+ it 'returns true' do
106
+ expect(websocket.connected?).to be(true)
107
+ end
108
+ end
109
+ end
110
+
111
+ describe '#send_message?' do
112
+ context 'when not connected' do
113
+ it 'returns false' do
114
+ expect(websocket.send_message?('test')).to be(false)
115
+ end
116
+ end
117
+
118
+ context 'when connected' do
119
+ let(:driver) { instance_double(WebSocket::Driver::Client, state: :open, text: true) }
120
+
121
+ before do
122
+ websocket.instance_variable_set(:@connected, true)
123
+ websocket.instance_variable_set(:@driver, driver)
124
+ end
125
+
126
+ it 'sends string message' do
127
+ websocket.send_message?('test message')
128
+ expect(driver).to have_received(:text).with('test message')
129
+ end
130
+
131
+ it 'converts hash to JSON' do
132
+ websocket.send_message?({ type: 'test', data: 'value' })
133
+ expect(driver).to have_received(:text).with('{"type":"test","data":"value"}')
134
+ end
135
+
136
+ it 'returns true' do
137
+ expect(websocket.send_message?('test')).to be(true)
138
+ end
139
+
140
+ it 'returns false when the write raises IOError' do
141
+ allow(driver).to receive(:text).and_raise(IOError)
142
+ expect(websocket.send_message?('test')).to be(false)
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '#disconnect' do
148
+ let(:driver) { instance_double(WebSocket::Driver::Client, state: :open, close: nil) }
149
+ let(:socket) { instance_double(TCPSocket, close: nil) }
150
+
151
+ before do
152
+ websocket.instance_variable_set(:@driver, driver)
153
+ websocket.instance_variable_set(:@socket, socket)
154
+ websocket.instance_variable_set(:@connected, true)
155
+ end
156
+
157
+ it 'sets should_reconnect to false' do
158
+ websocket.disconnect
159
+ expect(websocket.instance_variable_get(:@should_reconnect)).to be(false)
160
+ end
161
+
162
+ it 'stops the ping timer' do
163
+ websocket.disconnect
164
+ expect(websocket.instance_variable_get(:@ping_token)).to be_nil
165
+ expect(websocket.instance_variable_get(:@ping_thread)).to be_nil
166
+ end
167
+
168
+ it 'sends a close frame' do
169
+ websocket.disconnect
170
+ expect(driver).to have_received(:close)
171
+ end
172
+
173
+ it 'closes the socket' do
174
+ websocket.disconnect
175
+ expect(socket).to have_received(:close)
176
+ end
177
+
178
+ it 'sets connected to false' do
179
+ websocket.disconnect
180
+ expect(websocket.connected).to be(false)
181
+ end
182
+
183
+ it 'logs disconnection' do
184
+ websocket.disconnect
185
+ expect(logger).to have_received(:info).with('WebSocket disconnected')
186
+ end
187
+
188
+ it 'returns self' do
189
+ expect(websocket.disconnect).to eq(websocket)
190
+ end
191
+ end
192
+
193
+ describe 'private #build_url' do
194
+ it 'builds WebSocket URL with ws scheme for http' do
195
+ url = websocket.send(:build_url)
196
+ expect(url).to eq("ws://#{api_key}:@localhost:8088/ari/events?app=#{app_name}")
197
+ end
198
+
199
+ it 'builds WebSocket URL with wss scheme for https' do
200
+ ws = described_class.new('https://localhost:8089', api_key, app_name)
201
+ url = ws.send(:build_url)
202
+ expect(url).to eq("wss://#{api_key}:@localhost:8089/ari/events?app=#{app_name}")
203
+ end
204
+
205
+ it 'includes app name in query string' do
206
+ url = websocket.send(:build_url)
207
+ expect(url).to include("app=#{app_name}")
208
+ end
209
+ end
210
+
211
+ describe 'private #dispatch_event' do
212
+ let(:event_data) { { 'type' => 'StasisStart', 'channel' => { 'id' => '123' } } }
213
+
214
+ it 'calls registered callback for event type' do
215
+ called = false
216
+ received_data = nil
217
+
218
+ websocket.on('StasisStart') do |data|
219
+ called = true
220
+ received_data = data
221
+ end
222
+
223
+ websocket.send(:dispatch_event, 'StasisStart', event_data)
224
+
225
+ expect(called).to be(true)
226
+ expect(received_data).to eq(event_data)
227
+ end
228
+
229
+ it 'calls multiple callbacks for the same event' do
230
+ call_count = 0
231
+
232
+ websocket.on('StasisStart') { call_count += 1 }
233
+ websocket.on('StasisStart') { call_count += 1 }
234
+
235
+ websocket.send(:dispatch_event, 'StasisStart', event_data)
236
+
237
+ expect(call_count).to eq(2)
238
+ end
239
+
240
+ it 'calls wildcard callbacks for any event' do
241
+ called = false
242
+ received_data = nil
243
+
244
+ websocket.on('*') do |data|
245
+ called = true
246
+ received_data = data
247
+ end
248
+
249
+ websocket.send(:dispatch_event, 'StasisStart', event_data)
250
+
251
+ expect(called).to be(true)
252
+ expect(received_data).to eq(event_data)
253
+ end
254
+
255
+ it 'logs errors in callbacks but does not raise' do
256
+ websocket.on('StasisStart') { raise 'Callback error' }
257
+
258
+ expect { websocket.send(:dispatch_event, 'StasisStart', event_data) }.not_to raise_error
259
+ expect(logger).to have_received(:error).with(/Error in event handler/)
260
+ end
261
+
262
+ it 'does not call callbacks for different event types' do
263
+ called = false
264
+ websocket.on('ChannelHangup') { called = true }
265
+
266
+ websocket.send(:dispatch_event, 'StasisStart', event_data)
267
+
268
+ expect(called).to be(false)
269
+ end
270
+ end
271
+
272
+ describe 'private #handle_message' do
273
+ let(:event_json) { '{"type":"StasisStart","channel":{"id":"123"}}' }
274
+ let(:event) { double('WebSocket::Driver::MessageEvent', data: event_json) }
275
+
276
+ it 'parses JSON and dispatches event' do
277
+ called = false
278
+ websocket.on('StasisStart') { called = true }
279
+
280
+ websocket.send(:handle_message, event)
281
+
282
+ expect(called).to be(true)
283
+ end
284
+
285
+ it 'logs received event' do
286
+ websocket.send(:handle_message, event)
287
+ expect(logger).to have_received(:debug).with('Received event: StasisStart')
288
+ end
289
+
290
+ it 'logs error for invalid JSON' do
291
+ invalid_event = double('WebSocket::Driver::MessageEvent', data: 'invalid json')
292
+
293
+ websocket.send(:handle_message, invalid_event)
294
+
295
+ expect(logger).to have_received(:error).with(/Failed to parse message/)
296
+ end
297
+ end
298
+
299
+ describe 'private #handle_open' do
300
+ let(:driver) { instance_double(WebSocket::Driver::Client) }
301
+ let(:connect_callback) { proc { |ws| } }
302
+
303
+ before do
304
+ websocket.instance_variable_set(:@driver, driver)
305
+ websocket.instance_variable_set(:@on_connect_callback, connect_callback)
306
+ allow(websocket).to receive(:start_ping_timer)
307
+ end
308
+
309
+ it 'sets connected to true' do
310
+ websocket.send(:handle_open)
311
+ expect(websocket.connected).to be(true)
312
+ end
313
+
314
+ it 'resets reconnect attempts' do
315
+ websocket.instance_variable_set(:@reconnect_attempts, 5)
316
+ websocket.send(:handle_open)
317
+ expect(websocket.instance_variable_get(:@reconnect_attempts)).to eq(0)
318
+ end
319
+
320
+ it 'logs successful connection' do
321
+ websocket.send(:handle_open)
322
+ expect(logger).to have_received(:info).with('WebSocket connected successfully')
323
+ end
324
+
325
+ it 'starts ping timer' do
326
+ websocket.send(:handle_open)
327
+ expect(websocket).to have_received(:start_ping_timer)
328
+ end
329
+
330
+ it 'calls on_connect callback' do
331
+ expect(connect_callback).to receive(:call).with(websocket)
332
+ websocket.send(:handle_open)
333
+ end
334
+ end
335
+
336
+ describe 'private #handle_close' do
337
+ let(:event) { double('WebSocket::Driver::CloseEvent', code: 1000, reason: 'Normal closure') }
338
+
339
+ before do
340
+ websocket.instance_variable_set(:@connected, true)
341
+ allow(websocket).to receive(:stop_ping_timer)
342
+ allow(websocket).to receive(:close_socket)
343
+ end
344
+
345
+ it 'sets connected to false' do
346
+ websocket.send(:handle_close, event)
347
+ expect(websocket.connected).to be(false)
348
+ end
349
+
350
+ it 'stops ping timer' do
351
+ websocket.send(:handle_close, event)
352
+ expect(websocket).to have_received(:stop_ping_timer)
353
+ end
354
+
355
+ it 'logs close event' do
356
+ websocket.send(:handle_close, event)
357
+ expect(logger).to have_received(:warn).with(/WebSocket closed/)
358
+ end
359
+
360
+ it 'closes the socket so the read loop unblocks' do
361
+ websocket.send(:handle_close, event)
362
+ expect(websocket).to have_received(:close_socket)
363
+ end
364
+ end
365
+
366
+ describe 'private #handle_error' do
367
+ let(:event) { double('WebSocket::Driver::ProtocolError', message: 'Connection failed') }
368
+
369
+ it 'logs error message' do
370
+ websocket.send(:handle_error, event)
371
+ expect(logger).to have_received(:error).with('WebSocket error: Connection failed')
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Immutability of value objects' do
6
+ describe RubyAsterisk::Request do
7
+ subject { RubyAsterisk::Request.new('Originate', { 'Channel' => '1234', 'Context' => 'test' }) }
8
+
9
+ it 'is frozen' do
10
+ subject.should be_frozen
11
+ end
12
+
13
+ it 'has a frozen action' do
14
+ subject.action.should be_frozen
15
+ end
16
+
17
+ it 'has a frozen action_id' do
18
+ subject.action_id.should be_frozen
19
+ end
20
+
21
+ it 'has a frozen parameters hash' do
22
+ subject.parameters.should be_frozen
23
+ end
24
+
25
+ it 'has frozen parameter keys and values' do
26
+ subject.parameters.each do |key, value|
27
+ key.should be_frozen
28
+ value.should be_frozen
29
+ end
30
+ end
31
+
32
+ it 'raises FrozenError when attempting to modify instance variables' do
33
+ -> { subject.instance_variable_set(:@action, 'Login') }.should raise_error(FrozenError)
34
+ end
35
+
36
+ it 'raises FrozenError when attempting to modify parameters' do
37
+ -> { subject.parameters['NewKey'] = 'val' }.should raise_error(FrozenError)
38
+ end
39
+
40
+ it 'does not expose writer methods' do
41
+ subject.respond_to?(:action=).should be false
42
+ subject.respond_to?(:parameters=).should be false
43
+ subject.respond_to?(:action_id=).should be false
44
+ end
45
+
46
+ it 'does not expose response_data' do
47
+ subject.respond_to?(:response_data).should be false
48
+ subject.respond_to?(:response_data=).should be false
49
+ end
50
+ end
51
+
52
+ describe RubyAsterisk::Response do
53
+ let(:raw_response) do
54
+ "Response: Success\nActionID: 123\nMessage: Authentication accepted\n\n"
55
+ end
56
+ subject { RubyAsterisk::Response.new('Login', raw_response) }
57
+
58
+ it 'is frozen' do
59
+ subject.should be_frozen
60
+ end
61
+
62
+ it 'has a frozen type' do
63
+ subject.type.should be_frozen
64
+ end
65
+
66
+ it 'has a frozen action_id' do
67
+ subject.action_id.should be_frozen
68
+ end
69
+
70
+ it 'has a frozen message' do
71
+ subject.message.should be_frozen
72
+ end
73
+
74
+ it 'has a frozen raw_response' do
75
+ subject.raw_response.should be_frozen
76
+ subject.raw_response.each do |element|
77
+ element.should be_frozen
78
+ end
79
+ end
80
+
81
+ it 'has frozen data' do
82
+ subject.data.should be_frozen
83
+ end
84
+
85
+ it 'raises FrozenError when attempting to modify instance variables' do
86
+ -> { subject.instance_variable_set(:@type, 'Other') }.should raise_error(FrozenError)
87
+ end
88
+
89
+ it 'does not expose writer methods' do
90
+ subject.respond_to?(:type=).should be false
91
+ subject.respond_to?(:action_id=).should be false
92
+ subject.respond_to?(:message=).should be false
93
+ subject.respond_to?(:data=).should be false
94
+ subject.respond_to?(:raw_response=).should be false
95
+ end
96
+
97
+ context 'with parsed data containing nested structures' do
98
+ let(:raw_response) do
99
+ "Event: CoreShowChannel\nActionID: 839\nChannel: SIP/test\nCallerIDnum: 123\n\n" \
100
+ "Event: CoreShowChannelsComplete\nActionID: 839\n\n"
101
+ end
102
+ subject { RubyAsterisk::Response.new('CoreShowChannels', raw_response) }
103
+
104
+ it 'has deeply frozen data structures' do
105
+ subject.data.should be_frozen
106
+ subject.data[:channels].should be_frozen
107
+ subject.data[:channels].each do |channel|
108
+ channel.should be_frozen
109
+ channel.each do |key, value|
110
+ key.should be_frozen
111
+ value.should be_frozen
112
+ end
113
+ end
114
+ end
115
+
116
+ it 'raises FrozenError when attempting to modify nested data' do
117
+ -> { subject.data[:channels] << {} }.should raise_error(FrozenError)
118
+ end
119
+ end
120
+ end
121
+
122
+ describe RubyAsterisk::ResponseBuilder do
123
+ it 'builds a frozen Response' do
124
+ builder = RubyAsterisk::ResponseBuilder.new
125
+ builder.type = 'Login'
126
+ builder.raw_response = "Response: Success\nActionID: 123\n\n"
127
+ response = builder.build
128
+ response.should be_frozen
129
+ end
130
+
131
+ it 'raises ArgumentError when type is missing' do
132
+ builder = RubyAsterisk::ResponseBuilder.new
133
+ builder.raw_response = "Response: Success\n\n"
134
+ -> { builder.build }.should raise_error(ArgumentError, 'type is required')
135
+ end
136
+
137
+ it 'raises ArgumentError when raw_response is missing' do
138
+ builder = RubyAsterisk::ResponseBuilder.new
139
+ builder.type = 'Login'
140
+ -> { builder.build }.should raise_error(ArgumentError, 'raw_response is required')
141
+ end
142
+
143
+ it 'is not frozen itself' do
144
+ builder = RubyAsterisk::ResponseBuilder.new
145
+ builder.should_not be_frozen
146
+ end
147
+ end
148
+ end
@@ -1,17 +1,37 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  require 'spec_helper'
3
4
  describe RubyAsterisk::Request do
4
-
5
- describe "of type Originate" do
6
- it "should set a Variable option if set" do
7
- request = RubyAsterisk::Request.new("Originate",{"Channel" => "1234", "Context" => "test", "Exten" => "1", "Priority" => "1", "Callerid" => "1234", "Timeout" => "30000", "Variable" => "var1=15" })
5
+ describe 'of type Originate' do
6
+ it 'should set a Variable option if set' do
7
+ request = RubyAsterisk::Request.new('Originate', { 'Channel' => '1234', 'Context' => 'test', 'Exten' => '1', 'Priority' => '1', 'Callerid' => '1234', 'Timeout' => '30000', 'Variable' => 'var1=15' })
8
8
  request.commands.include?("Variable: var1=15\r\n").should_not be_nil
9
9
  end
10
10
 
11
- it "should not set a Variable option if not set" do
12
- request = RubyAsterisk::Request.new("Originate",{"Channel" => "1234", "Context" => "test", "Exten" => "1", "Priority" => "1", "Callerid" => "1234", "Timeout" => "30000", "Variable" => nil })
13
- request.commands.include?("Variable: var=15").should==false
11
+ it 'should not set a Variable option if not set' do
12
+ request = RubyAsterisk::Request.new('Originate', { 'Channel' => '1234', 'Context' => 'test', 'Exten' => '1', 'Priority' => '1', 'Callerid' => '1234', 'Timeout' => '30000', 'Variable' => nil })
13
+ request.commands.include?('Variable: var=15').should == false
14
14
  end
15
15
  end
16
- end
17
16
 
17
+ describe 'action_id' do
18
+ it 'generates one when none is supplied' do
19
+ request = RubyAsterisk::Request.new('Ping')
20
+
21
+ expect(request.action_id).not_to be_empty
22
+ expect(request.commands.join).to include("ActionID: #{request.action_id}\r\n")
23
+ end
24
+
25
+ it 'uses the supplied action_id' do
26
+ request = RubyAsterisk::Request.new('Status', {}, action_id: 'my-own-id')
27
+
28
+ expect(request.action_id).to eq('my-own-id')
29
+ end
30
+
31
+ it 'emits exactly one ActionID header' do
32
+ request = RubyAsterisk::Request.new('Status', { 'Channel' => 'SIP/alice' }, action_id: 'my-own-id')
33
+
34
+ expect(request.commands.join.scan(/^ActionID:/).size).to eq(1)
35
+ end
36
+ end
37
+ end