frisky 0.1.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.
@@ -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
+