playful 0.1.0.alpha.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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +19 -0
  4. data/.rspec +1 -0
  5. data/.travis.yml +5 -0
  6. data/Gemfile +6 -0
  7. data/History.rdoc +3 -0
  8. data/LICENSE.rdoc +22 -0
  9. data/README.rdoc +194 -0
  10. data/Rakefile +20 -0
  11. data/features/control_point.feature +13 -0
  12. data/features/device.feature +22 -0
  13. data/features/device_discovery.feature +9 -0
  14. data/features/step_definitions/control_point_steps.rb +19 -0
  15. data/features/step_definitions/device_discovery_steps.rb +40 -0
  16. data/features/step_definitions/device_steps.rb +28 -0
  17. data/features/support/common.rb +9 -0
  18. data/features/support/env.rb +17 -0
  19. data/features/support/fake_upnp_device_collection.rb +108 -0
  20. data/features/support/world_extensions.rb +15 -0
  21. data/lib/core_ext/hash_patch.rb +5 -0
  22. data/lib/core_ext/socket_patch.rb +16 -0
  23. data/lib/core_ext/to_upnp_s.rb +65 -0
  24. data/lib/playful.rb +5 -0
  25. data/lib/playful/control_point.rb +175 -0
  26. data/lib/playful/control_point/base.rb +74 -0
  27. data/lib/playful/control_point/device.rb +511 -0
  28. data/lib/playful/control_point/error.rb +13 -0
  29. data/lib/playful/control_point/service.rb +404 -0
  30. data/lib/playful/device.rb +28 -0
  31. data/lib/playful/logger.rb +8 -0
  32. data/lib/playful/ssdp.rb +195 -0
  33. data/lib/playful/ssdp/broadcast_searcher.rb +114 -0
  34. data/lib/playful/ssdp/error.rb +6 -0
  35. data/lib/playful/ssdp/listener.rb +38 -0
  36. data/lib/playful/ssdp/multicast_connection.rb +112 -0
  37. data/lib/playful/ssdp/network_constants.rb +17 -0
  38. data/lib/playful/ssdp/notifier.rb +41 -0
  39. data/lib/playful/ssdp/searcher.rb +87 -0
  40. data/lib/playful/version.rb +3 -0
  41. data/lib/rack/upnp_control_point.rb +70 -0
  42. data/playful.gemspec +38 -0
  43. data/spec/spec_helper.rb +16 -0
  44. data/spec/support/search_responses.rb +134 -0
  45. data/spec/unit/core_ext/to_upnp_s_spec.rb +105 -0
  46. data/spec/unit/playful/control_point/device_spec.rb +7 -0
  47. data/spec/unit/playful/control_point_spec.rb +45 -0
  48. data/spec/unit/playful/ssdp/listener_spec.rb +29 -0
  49. data/spec/unit/playful/ssdp/multicast_connection_spec.rb +157 -0
  50. data/spec/unit/playful/ssdp/notifier_spec.rb +76 -0
  51. data/spec/unit/playful/ssdp/searcher_spec.rb +110 -0
  52. data/spec/unit/playful/ssdp_spec.rb +214 -0
  53. data/tasks/control_point.html +30 -0
  54. data/tasks/control_point.thor +43 -0
  55. data/tasks/search.thor +128 -0
  56. data/tasks/test_js/FABridge.js +1425 -0
  57. data/tasks/test_js/WebSocketMain.swf +807 -0
  58. data/tasks/test_js/swfobject.js +825 -0
  59. data/tasks/test_js/web_socket.js +1133 -0
  60. data/test/test_ssdp.rb +298 -0
  61. data/test/test_ssdp_notification.rb +74 -0
  62. data/test/test_ssdp_response.rb +31 -0
  63. data/test/test_ssdp_search.rb +23 -0
  64. metadata +339 -0
@@ -0,0 +1,17 @@
1
+ module Playful
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 Playful::SSDP::Notifier < Playful::SSDP::MulticastConnection
6
+ include LogSwitch::Mixin
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} Playful/#{Playful::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 Playful
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::Mixin
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 Playful
2
+ VERSION = '0.1.0.alpha.1'
3
+ end
@@ -0,0 +1,70 @@
1
+ require 'rack'
2
+ require_relative '../playful/control_point'
3
+
4
+ module Rack
5
+
6
+ # Middleware that allows your Rack app to keep tabs on devices that the
7
+ # {Playful::ControlPoint} has found. UPnP devices that match the +search_type+
8
+ # are discovered and added to the list, then removed as those devices send out
9
+ # +ssdp:byebye+ notifications. All of this depends on +EventMachine::Channel+s,
10
+ # and thus requires that an EventMachine reactor is running. If you don't
11
+ # have one running, the {Playful::ControlPoint} will start one for you.
12
+ #
13
+ # @example Control all root devices
14
+ #
15
+ # Thin::Server.start('0.0.0.0', 3000) do
16
+ # use Rack::UPnPControlPoint, search_type: :root
17
+ #
18
+ # map "/devices" do
19
+ # run lambda { |env|
20
+ # devices = env['upnp.devices']
21
+ # friendly_names = devices.map(&:friendly_name).join("\n")
22
+ # [200, {'Content-Type' => 'text/plain'}, [friendly_names]]
23
+ # }
24
+ # end
25
+ # end
26
+ #
27
+ class UPnPControlPoint
28
+
29
+ # @param [Rack::Builder] app Your Rack application.
30
+ # @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
31
+ # @see Playful::SSDP::Searcher
32
+ def initialize(app, options = {})
33
+ @app = app
34
+ @devices = []
35
+ options[:search_type] ||= :root
36
+ EM.next_tick { start_control_point(options[:search_type], options) }
37
+ end
38
+
39
+ # Creates and starts the {Playful::ControlPoint}, then manages the list of
40
+ # devices using the +EventMachine::Channel+ objects yielded in.
41
+ #
42
+ # @param [Symbol,String] search_type The device(s) you want to search for
43
+ # and control.
44
+ # @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
45
+ # @see Playful::SSDP::Searcher
46
+ def start_control_point(search_type, options)
47
+ @cp = ::Playful::ControlPoint.new(search_type, options)
48
+
49
+ @cp.start do |new_device_channel, old_device_channel|
50
+ new_device_channel.subscribe do |notification|
51
+ @devices << notification
52
+ end
53
+
54
+ old_device_channel.subscribe do |old_device|
55
+ @devices.reject! { |d| d.usn == old_device[:usn] }
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ # Adds the whole list of devices to <tt>env['upnp.devices']</tt> so that
62
+ # that list can be accessed from within your app.
63
+ #
64
+ # @param [Hash] env The Rack environment.
65
+ def call(env)
66
+ env['upnp.devices'] = @devices
67
+ @app.call(env)
68
+ end
69
+ end
70
+ end
data/playful.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'playful/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'playful'
7
+ s.version = Playful::VERSION
8
+ s.author = 'turboladen'
9
+ s.email = 'steve.loveless@gmail.com'
10
+ s.homepage = 'http://github.com/turboladen/playful'
11
+ s.summary = 'Use me to build a UPnP app!'
12
+ s.description = %q{playful provides the tools you need to build an app that runs
13
+ in a UPnP environment.}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.extra_rdoc_files = %w(History.rdoc README.rdoc)
19
+ s.require_paths = ['lib']
20
+ s.required_ruby_version = Gem::Requirement.new('>=1.9.1')
21
+
22
+ s.add_dependency 'eventmachine', '>=1.0.0'
23
+ s.add_dependency 'em-http-request', '>=1.0.2'
24
+ s.add_dependency 'em-synchrony'
25
+ s.add_dependency 'nori', '>=2.0.2'
26
+ s.add_dependency 'log_switch', '>=0.4.0'
27
+ s.add_dependency 'savon', '~>2.0'
28
+
29
+ s.add_development_dependency 'bundler'
30
+ s.add_development_dependency 'cucumber', '>=1.0.0'
31
+ s.add_development_dependency 'em-websocket', '>=0.3.6'
32
+ s.add_development_dependency 'rake'
33
+ s.add_development_dependency 'rspec', '>=3.0.0.beta'
34
+ s.add_development_dependency 'simplecov', '>=0.4.2'
35
+ s.add_development_dependency 'thin'
36
+ s.add_development_dependency 'thor', '>=0.1.6'
37
+ s.add_development_dependency 'yard', '>=0.7.0'
38
+ end
@@ -0,0 +1,16 @@
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
@@ -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
+