frisky 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ module Frisky
2
+ class SSDP
3
+ module NetworkConstants
4
+
5
+ BROADCAST_IP = '255.255.255.255'
6
+
7
+ # Default multicast IP address
8
+ MULTICAST_IP = '239.255.255.250'
9
+
10
+ # Default multicast port
11
+ MULTICAST_PORT = 1900
12
+
13
+ # Default TTL
14
+ TTL = 4
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ require_relative '../logger'
2
+ require_relative 'multicast_connection'
3
+
4
+
5
+ class Frisky::SSDP::Notifier < Frisky::SSDP::MulticastConnection
6
+ include LogSwitch
7
+
8
+ def initialize(nt, usn, ddf_url, valid_for_duration)
9
+ @os = RbConfig::CONFIG['host_vendor'].capitalize + '/' +
10
+ RbConfig::CONFIG['host_os']
11
+ @upnp_version = '1.0'
12
+ @notification = notification(nt, usn, ddf_url, valid_for_duration)
13
+ end
14
+
15
+ def post_init
16
+ if send_datagram(@notification, MULTICAST_IP, MULTICAST_PORT) > 0
17
+ log "Sent notification:\n#{@notification}"
18
+ end
19
+ end
20
+
21
+ # @param [String] nt "Notification Type"; a potential search target. Used in
22
+ # +NT+ header.
23
+ # @param [String] usn "Unique Service Name"; a composite identifier for the
24
+ # advertisement. Used in +USN+ header.
25
+ # @param [String] ddf_url Device Description File URL for the root device.
26
+ # @param [Fixnum] valid_for_duration Duration in seconds for which the
27
+ # advertisement is valid. Used in +CACHE-CONTROL+ header.
28
+ def notification(nt, usn, ddf_url, valid_for_duration)
29
+ <<-NOTIFICATION
30
+ NOTIFY * HTTP/1.1\r
31
+ HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
32
+ CACHE-CONTROL: max-age=#{valid_for_duration}\r
33
+ LOCATION: #{ddf_url}\r
34
+ NT: #{nt}\r
35
+ NTS: ssdp:alive\r
36
+ SERVER: #{@os} UPnP/#{@upnp_version} Frisky/#{Frisky::VERSION}\r
37
+ USN: #{usn}\r
38
+ \r
39
+ NOTIFICATION
40
+ end
41
+ end
@@ -0,0 +1,87 @@
1
+ require_relative '../logger'
2
+ require_relative 'multicast_connection'
3
+
4
+ module Frisky
5
+
6
+ # A subclass of an EventMachine::Connection, this handles doing M-SEARCHes.
7
+ #
8
+ # Search types:
9
+ # ssdp:all
10
+ # upnp:rootdevice
11
+ # uuid:[device-uuid]
12
+ # urn:schemas-upnp-org:device:[deviceType-version]
13
+ # urn:schemas-upnp-org:service:[serviceType-version]
14
+ # urn:[custom-schema]:device:[deviceType-version]
15
+ # urn:[custom-schema]:service:[serviceType-version]
16
+ class SSDP::Searcher < SSDP::MulticastConnection
17
+ include LogSwitch
18
+
19
+ DEFAULT_RESPONSE_WAIT_TIME = 5
20
+ DEFAULT_M_SEARCH_COUNT = 2
21
+
22
+ # @return [EventMachine::Channel] Provides subscribers with responses from
23
+ # their search request.
24
+ attr_reader :discovery_responses
25
+
26
+ # @param [String] search_target
27
+ # @param [Hash] options
28
+ # @option options [Fixnum] response_wait_time
29
+ # @option options [Fixnum] ttl
30
+ # @option options [Fixnum] m_search_count The number of times to send the
31
+ # M-SEARCH. UPnP 1.0 suggests to send the request more than once.
32
+ def initialize(search_target, options = {})
33
+ options[:ttl] ||= TTL
34
+ options[:response_wait_time] ||= DEFAULT_RESPONSE_WAIT_TIME
35
+ @m_search_count = options[:m_search_count] ||= DEFAULT_M_SEARCH_COUNT
36
+
37
+ @search = m_search(search_target, options[:response_wait_time])
38
+
39
+ super options[:ttl]
40
+ end
41
+
42
+ # This is the callback called by EventMachine when it receives data on the
43
+ # socket that's been opened for this connection. In this case, the method
44
+ # parses the SSDP responses/notifications into Hashes and adds them to the
45
+ # appropriate EventMachine::Channel (provided as accessor methods). This
46
+ # effectively means that in each Channel, you get a Hash that represents
47
+ # the headers for each response/notification that comes in on the socket.
48
+ #
49
+ # @param [String] response The data received on this connection's socket.
50
+ def receive_data(response)
51
+ ip, port = peer_info
52
+ log "Response from #{ip}:#{port}:\n#{response}\n"
53
+ parsed_response = parse(response)
54
+
55
+ return if parsed_response.has_key? :nts
56
+ return if parsed_response[:man] &&
57
+ parsed_response[:man] =~ /ssdp:discover/
58
+
59
+ @discovery_responses << parsed_response
60
+ end
61
+
62
+ # Sends the M-SEARCH that was built during init. Logs what was sent if the
63
+ # send was successful.
64
+ def post_init
65
+ @m_search_count.times do
66
+ if send_datagram(@search, MULTICAST_IP, MULTICAST_PORT) > 0
67
+ log "Sent datagram search:\n#{@search}"
68
+ end
69
+ end
70
+ end
71
+
72
+ # Builds the M-SEARCH request string.
73
+ #
74
+ # @param [String] search_target
75
+ # @param [Fixnum] response_wait_time
76
+ def m_search(search_target, response_wait_time)
77
+ <<-MSEARCH
78
+ M-SEARCH * HTTP/1.1\r
79
+ HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
80
+ MAN: "ssdp:discover"\r
81
+ MX: #{response_wait_time}\r
82
+ ST: #{search_target}\r
83
+ \r
84
+ MSEARCH
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module Frisky
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,18 @@
1
+ require 'simplecov'
2
+
3
+ SimpleCov.start
4
+
5
+ require 'coveralls'
6
+ Coveralls.wear!
7
+
8
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
+
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |file| require file }
11
+
12
+ ENV['RUBY_UPNP_ENV'] = 'testing'
13
+
14
+ RSpec.configure do |config|
15
+ #config.raise_errors_for_deprecations!
16
+ end
17
+
18
+ require_relative "../lib/frisky/logger"
@@ -0,0 +1,134 @@
1
+ SSDP_SEARCH_RESPONSES_PARSED = {
2
+ root_device1: {
3
+ cache_control: 'max-age=1200',
4
+ date: 'Mon, 26 Sep 2011 06:40:19 GMT',
5
+ location: 'http://192.168.10.3:5001/description/fetch',
6
+ server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
7
+ st: 'upnp:rootdevice',
8
+ ext: nil,
9
+ usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice',
10
+ content_length: 0
11
+ },
12
+ root_device2: {
13
+ cache_control: 'max-age=1200',
14
+ date: 'Mon, 26 Sep 2011 06:40:20 GMT',
15
+ location: 'http://192.168.10.4:5001/description/fetch',
16
+ server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
17
+ st: 'upnp:rootdevice',
18
+ ext: nil,
19
+ usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136e::upnp:rootdevice',
20
+ content_length: 0
21
+ },
22
+ media_server: {
23
+ cache_control: 'max-age=1200',
24
+ date: 'Mon, 26 Sep 2011 06:40:21 GMT',
25
+ location: 'http://192.168.10.3:5001/description/fetch',
26
+ server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
27
+ st: 'urn:schemas-upnp-org:device:MediaServer:1',
28
+ ext: nil,
29
+ usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::urn:schemas-upnp-org:device:MediaServer:1',
30
+ content_length: 0
31
+ }
32
+ }
33
+
34
+ SSDP_DESCRIPTIONS = {
35
+ root_device1: {
36
+ 'root' => {
37
+ 'specVersion' => {
38
+ 'major' => '1',
39
+ 'minor' => '0'
40
+ },
41
+ 'URLBase' => 'http://192.168.10.3:5001/',
42
+ 'device' => {
43
+ 'dlna:X_DLNADOC' => %w[DMS-1.50 M-DMS-1.50],
44
+ 'deviceType' => 'urn:schemas-upnp-org:device:MediaServer:1',
45
+ 'friendlyName' => 'PS3 Media Server [gutenberg]',
46
+ 'manufacturer' => 'PMS',
47
+ 'manufacturerURL' => 'http://ps3mediaserver.blogspot.com',
48
+ 'modelDescription' => 'UPnP/AV 1.0 Compliant Media Server',
49
+ 'modelName' => 'PMS',
50
+ 'modelNumber' => '01',
51
+ 'modelURL' => 'http://ps3mediaserver.blogspot.com',
52
+ 'serialNumber' => nil,
53
+ 'UPC' => nil,
54
+ 'UDN' => 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d',
55
+ 'iconList' => {
56
+ 'icon' => {
57
+ 'mimetype' => 'image/jpeg',
58
+ 'width' => '120',
59
+ 'height' => '120',
60
+ 'depth' => '24',
61
+ 'url' => '/images/icon-256.png'
62
+ }
63
+ },
64
+ 'presentationURL' => 'http://192.168.10.3:5001/console/index.html',
65
+ 'serviceList' => {
66
+ 'service' => [
67
+ {
68
+ 'serviceType' => 'urn:schemas-upnp-org:service:ContentDirectory:1',
69
+ 'serviceId' => 'urn:upnp-org:serviceId:ContentDirectory',
70
+ 'SCPDURL' => '/UPnP_AV_ContentDirectory_1.0.xml',
71
+ 'controlURL' => '/upnp/control/content_directory',
72
+ 'eventSubURL' => '/upnp/event/content_directory'
73
+ },
74
+ {
75
+ 'serviceType' => 'urn:schemas-upnp-org:service:ConnectionManager:1',
76
+ 'serviceId' => 'urn:upnp-org:serviceId:ConnectionManager',
77
+ 'SCPDURL' => '/UPnP_AV_ConnectionManager_1.0.xml',
78
+ 'controlURL' => '/upnp/control/connection_manager',
79
+ 'eventSubURL' => '/upnp/event/connection_manager'
80
+ }
81
+ ]
82
+ }
83
+ },
84
+ '@xmlns:dlna' => 'urn:schemas-dlna-org:device-1-0',
85
+ '@xmlns' => 'urn:schemas-upnp-org:device-1-0'
86
+ }
87
+ }
88
+ }
89
+
90
+ ROOT_DEVICE1 = <<-RD
91
+ HTTP/1.1 200 OK
92
+ CACHE-CONTROL: max-age=1200
93
+ DATE: Mon, 26 Sep 2011 06:40:19 GMT
94
+ LOCATION: http://1.2.3.4:5678/description/fetch
95
+ SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
96
+ ST: upnp:rootdevice
97
+ EXT:
98
+ USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice
99
+ Content-Length: 0
100
+
101
+ RD
102
+
103
+ ROOT_DEVICE2 = <<-RD
104
+ HTTP/1.1 200 OK
105
+ CACHE-CONTROL: max-age=1200
106
+ DATE: Mon, 26 Sep 2011 06:40:20 GMT
107
+ LOCATION: http://1.2.3.4:5678/description/fetch
108
+ SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
109
+ ST: upnp:rootdevice
110
+ EXT:
111
+ USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136e::upnp:rootdevice
112
+ Content-Length: 0
113
+
114
+ RD
115
+
116
+ MEDIA_SERVER = <<-MD
117
+ HTTP/1.1 200 OK
118
+ CACHE-CONTROL: max-age=1200
119
+ DATE: Mon, 26 Sep 2011 06:40:21 GMT
120
+ LOCATION: http://1.2.3.4:5678/description/fetch
121
+ SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
122
+ ST: urn:schemas-upnp-org:device:MediaServer:1
123
+ EXT:
124
+ USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136d::urn:schemas-upnp-org:device:MediaServer:
125
+ Content-Length: 0
126
+
127
+ MD
128
+
129
+ RESPONSES = {
130
+ root_device1: ROOT_DEVICE1,
131
+ root_device2: ROOT_DEVICE2,
132
+ media_server: MEDIA_SERVER
133
+ }
134
+
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+ require 'core_ext/to_upnp_s'
3
+
4
+
5
+ describe Hash do
6
+ describe '#to_upnp_s' do
7
+ context ':uuid as key' do
8
+ it "returns a String like 'uuid:[Hash value]'" do
9
+ expect({ uuid: '12345' }.to_upnp_s).to eq 'uuid:12345'
10
+ end
11
+
12
+ it "doesn't check if the Hash value is legit" do
13
+ expect({ uuid: '' }.to_upnp_s).to eq 'uuid:'
14
+ end
15
+ end
16
+
17
+ context ':device_type as key' do
18
+ context 'domain name not given' do
19
+ it "returns a String like 'urn:schemas-upnp-org:device:[device type]'" do
20
+ expect({ device_type: '12345' }.to_upnp_s).
21
+ to eq 'urn:schemas-upnp-org:device:12345'
22
+ end
23
+
24
+ it "doesn't check if the Hash value is legit" do
25
+ expect({ device_type: '' }.to_upnp_s).to eq 'urn:schemas-upnp-org:device:'
26
+ end
27
+ end
28
+
29
+ context 'domain name given' do
30
+ it "returns a String like 'urn:[domain name]:device:[device type]'" do
31
+ expect({ device_type: '12345', domain_name: 'my-domain' }.to_upnp_s).
32
+ to eq 'urn:my-domain:device:12345'
33
+ end
34
+
35
+ it "doesn't check if the Hash value is legit" do
36
+ expect({ device_type: '', domain_name: 'stuff' }.to_upnp_s).
37
+ to eq 'urn:stuff:device:'
38
+ end
39
+ end
40
+ end
41
+
42
+ context ':service_type as key' do
43
+ context 'domain name not given' do
44
+ it "returns a String like 'urn:schemas-upnp-org:service:[service type]'" do
45
+ expect({ service_type: '12345' }.to_upnp_s).
46
+ to eq 'urn:schemas-upnp-org:service:12345'
47
+ end
48
+
49
+ it "doesn't check if the Hash value is legit" do
50
+ expect({ service_type: '' }.to_upnp_s).to eq 'urn:schemas-upnp-org:service:'
51
+ end
52
+ end
53
+
54
+ context 'domain name given' do
55
+ it "returns a String like 'urn:[domain name]:service:[service type]'" do
56
+ expect({ service_type: '12345', domain_name: 'my-domain' }.to_upnp_s).
57
+ to eq 'urn:my-domain:service:12345'
58
+ end
59
+
60
+ it "doesn't check if the Hash value is legit" do
61
+ expect({ service_type: '', domain_name: 'my-domain' }.to_upnp_s).
62
+ to eq 'urn:my-domain:service:'
63
+ end
64
+ end
65
+ end
66
+
67
+ context 'some other Hash key' do
68
+ context 'domain name not given' do
69
+ it 'returns self.to_s' do
70
+ expect({ firestorm: 12345 }.to_upnp_s).to eq '{:firestorm=>12345}'
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ describe Symbol do
78
+ context ':all' do
79
+ describe '#to_upnp_s' do
80
+ it "returns 'ssdp:all'" do
81
+ expect(:all.to_upnp_s).to eq 'ssdp:all'
82
+ end
83
+ end
84
+ end
85
+
86
+ context ':root' do
87
+ describe '#to_upnp_s' do
88
+ it "returns 'upnp:rootdevice'" do
89
+ expect(:root.to_upnp_s).to eq 'upnp:rootdevice'
90
+ end
91
+ end
92
+ end
93
+
94
+ it "returns itself if one of the defined shortcuts wasn't given" do
95
+ expect(:firestorm.to_upnp_s).to eq :firestorm
96
+ end
97
+ end
98
+
99
+ describe String do
100
+ it 'returns itself' do
101
+ expect('Stuff and things'.to_upnp_s).to eq 'Stuff and things'
102
+ end
103
+ end
104
+
105
+
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'frisky/ssdp/listener'
3
+
4
+
5
+ describe Frisky::SSDP::Listener do
6
+ around(:each) do |example|
7
+ EM.synchrony do
8
+ example.run
9
+ EM.stop
10
+ end
11
+ end
12
+
13
+ before do
14
+ allow_any_instance_of(Frisky::SSDP::Listener).to receive(:setup_multicast_socket)
15
+ end
16
+
17
+ subject { Frisky::SSDP::Listener.new(1) }
18
+
19
+ describe '#receive_data' do
20
+ it 'logs the IP and port from which the request came from' do
21
+ expect(subject).to receive(:peer_info).and_return %w[ip port]
22
+ expect(subject).to receive(:log).
23
+ with("Response from ip:port:\nmessage\n")
24
+ allow(subject).to receive(:parse).and_return({})
25
+
26
+ subject.receive_data('message')
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,157 @@
1
+ require 'spec_helper'
2
+ require 'frisky/ssdp/multicast_connection'
3
+
4
+
5
+ describe Frisky::SSDP::MulticastConnection do
6
+ around(:each) do |example|
7
+ EM.synchrony do
8
+ example.run
9
+ EM.stop
10
+ end
11
+ end
12
+
13
+ subject { Frisky::SSDP::MulticastConnection.new(1) }
14
+
15
+ before do
16
+ Frisky.logging_enabled = false
17
+ end
18
+
19
+ describe '#peer_info' do
20
+ before do
21
+ allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:setup_multicast_socket)
22
+ subject.stub_chain(:get_peername, :[], :unpack).
23
+ and_return(%w[1234 1 2 3 4])
24
+ end
25
+
26
+ it 'returns an Array with IP and port' do
27
+ expect(subject.peer_info).to eq ['1.2.3.4', 1234]
28
+ end
29
+
30
+ it 'returns IP as a String' do
31
+ expect(subject.peer_info.first).to be_a String
32
+ end
33
+
34
+ it 'returns port as a Fixnum' do
35
+ expect(subject.peer_info.last).to be_a Fixnum
36
+ end
37
+ end
38
+
39
+ describe '#parse' do
40
+ before do
41
+ allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:setup_multicast_socket)
42
+ end
43
+
44
+ it 'turns headers into Hash keys' do
45
+ result = subject.parse ROOT_DEVICE1
46
+ expect(result).to have_key :cache_control
47
+ expect(result).to have_key :date
48
+ expect(result).to have_key :location
49
+ expect(result).to have_key :server
50
+ expect(result).to have_key :st
51
+ expect(result).to have_key :ext
52
+ expect(result).to have_key :usn
53
+ expect(result).to have_key :content_length
54
+ end
55
+
56
+ it 'turns header values into Hash values' do
57
+ result = subject.parse ROOT_DEVICE1
58
+ expect(result[:cache_control]).to eq 'max-age=1200'
59
+ expect(result[:date]).to eq 'Mon, 26 Sep 2011 06:40:19 GMT'
60
+ expect(result[:location]).to eq 'http://1.2.3.4:5678/description/fetch'
61
+ expect(result[:server]).to eq 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1'
62
+ expect(result[:st]).to eq 'upnp:rootdevice'
63
+ expect(result[:ext]).to be_empty
64
+ expect(result[:usn]).to eq 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice'
65
+ expect(result[:content_length]).to eq '0'
66
+ end
67
+
68
+ context 'single line String as response data' do
69
+ before { @data = ROOT_DEVICE1.gsub("\n", ' ') }
70
+
71
+ it 'returns an empty Hash' do
72
+ expect(subject.parse(@data)).to eq({ })
73
+ end
74
+
75
+ it "logs the 'bad' response" do
76
+ subject.should_receive(:log).twice
77
+ subject.parse @data
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '#setup_multicast_socket' do
83
+ before do
84
+ allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:set_membership)
85
+ allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:switch_multicast_loop)
86
+ allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:set_multicast_ttl)
87
+ allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:set_ttl)
88
+ end
89
+
90
+ it 'adds 0.0.0.0 and 239.255.255.250 to the membership group' do
91
+ expect(subject).to receive(:set_membership).with(
92
+ IPAddr.new('239.255.255.250').hton + IPAddr.new('0.0.0.0').hton
93
+ )
94
+ subject.setup_multicast_socket
95
+ end
96
+
97
+ it 'sets multicast TTL to 4' do
98
+ expect(subject).to receive(:set_multicast_ttl).with(4)
99
+ subject.setup_multicast_socket
100
+ end
101
+
102
+ it 'sets TTL to 4' do
103
+ expect(subject).to receive(:set_ttl).with(4)
104
+ subject.setup_multicast_socket
105
+ end
106
+
107
+ context "ENV['RUBY_UPNP_ENV'] != testing" do
108
+ after { ENV['RUBY_UPNP_ENV'] = 'testing' }
109
+
110
+ it 'turns multicast loop off' do
111
+ ENV['RUBY_UPNP_ENV'] = 'development'
112
+ expect(subject).to receive(:switch_multicast_loop).with(:off)
113
+ subject.setup_multicast_socket
114
+ end
115
+ end
116
+ end
117
+
118
+ describe '#switch_multicast_loop' do
119
+ before do
120
+ allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:setup_multicast_socket)
121
+ end
122
+
123
+ it "passes '\\001' to the socket option call when param == :on" do
124
+ expect(subject).to receive(:set_sock_opt).with(
125
+ Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\001"
126
+ )
127
+ subject.switch_multicast_loop :on
128
+ end
129
+
130
+ it "passes '\\001' to the socket option call when param == '\\001'" do
131
+ expect(subject).to receive(:set_sock_opt).with(
132
+ Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\001"
133
+ )
134
+ subject.switch_multicast_loop "\001"
135
+ end
136
+
137
+ it "passes '\\000' to the socket option call when param == :off" do
138
+ expect(subject).to receive(:set_sock_opt).with(
139
+ Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\000"
140
+ )
141
+ subject.switch_multicast_loop :off
142
+ end
143
+
144
+ it "passes '\\000' to the socket option call when param == '\\000'" do
145
+ expect(subject).to receive(:set_sock_opt).with(
146
+ Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\000"
147
+ )
148
+ subject.switch_multicast_loop "\000"
149
+ end
150
+
151
+ it "raises when not :on, :off, '\\000', or '\\001'" do
152
+ expect { subject.switch_multicast_loop 12312312 }.
153
+ to raise_error(Frisky::SSDP::Error)
154
+ end
155
+ end
156
+ end
157
+