ruby-growl 3.0 → 4.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.
- data.tar.gz.sig +0 -0
- data/.autotest +16 -0
- data/.gemtest +0 -0
- data/History.txt +15 -0
- data/Manifest.txt +7 -1
- data/README.txt +15 -11
- data/Rakefile +4 -1
- data/bin/growl +1 -1
- data/lib/ruby-growl.rb +136 -262
- data/lib/ruby-growl/gntp.rb +553 -0
- data/lib/ruby-growl/ruby_logo.rb +3769 -0
- data/lib/ruby-growl/udp.rb +266 -0
- data/lib/uri/x_growl_resource.rb +76 -0
- data/test/test_growl_gntp.rb +1238 -0
- data/test/{test_ruby_growl.rb → test_growl_udp.rb} +30 -21
- metadata +86 -55
- metadata.gz.sig +0 -0
@@ -0,0 +1,266 @@
|
|
1
|
+
##
|
2
|
+
# Implements the UDP growl protocol used in growl 1.2 and older.
|
3
|
+
|
4
|
+
class Growl::UDP
|
5
|
+
|
6
|
+
##
|
7
|
+
# The Ruby that ships with Tiger has a broken #pack, so 'v' means network
|
8
|
+
# byte order instead of 'n'.
|
9
|
+
|
10
|
+
BROKEN_PACK = [1].pack("n") != "\000\001" # :nodoc:
|
11
|
+
|
12
|
+
little_endian = [1].pack('V*') == [1].pack('L*')
|
13
|
+
little_endian = !little_endian if BROKEN_PACK
|
14
|
+
|
15
|
+
##
|
16
|
+
# Endianness of this machine
|
17
|
+
|
18
|
+
LITTLE_ENDIAN = little_endian
|
19
|
+
|
20
|
+
##
|
21
|
+
# Growl Network Registration Packet +pack+ Format
|
22
|
+
#--
|
23
|
+
# Format:
|
24
|
+
#
|
25
|
+
# struct GrowlNetworkRegistration {
|
26
|
+
# struct GrowlNetworkPacket {
|
27
|
+
# unsigned char version;
|
28
|
+
# unsigned char type;
|
29
|
+
# } __attribute__((packed));
|
30
|
+
# unsigned short appNameLen;
|
31
|
+
# unsigned char numAllNotifications;
|
32
|
+
# unsigned char numDefaultNotifications;
|
33
|
+
# /*
|
34
|
+
# * Variable sized. Format:
|
35
|
+
# * <application name><all notifications><default notifications><checksum>
|
36
|
+
# * where <all notifications> is of the form (<length><name>){num} and
|
37
|
+
# * <default notifications> is an array of indices into the all notifications
|
38
|
+
# * array, each index being 8 bits.
|
39
|
+
# */
|
40
|
+
# unsigned char data[];
|
41
|
+
# } __attribute__((packed));
|
42
|
+
|
43
|
+
GNR_FORMAT = "CCnCCa*"
|
44
|
+
|
45
|
+
GNR_FORMAT.gsub!(/n/, 'v') if BROKEN_PACK
|
46
|
+
|
47
|
+
##
|
48
|
+
# Growl Network Notification Packet +pack+ Format
|
49
|
+
#--
|
50
|
+
# Format:
|
51
|
+
#
|
52
|
+
# struct GrowlNetworkNotification {
|
53
|
+
# struct GrowlNetworkPacket {
|
54
|
+
# unsigned char version;
|
55
|
+
# unsigned char type;
|
56
|
+
# } __attribute__((packed));
|
57
|
+
# struct GrowlNetworkNotificationFlags {
|
58
|
+
# unsigned reserved: 12;
|
59
|
+
# signed priority: 3;
|
60
|
+
# unsigned sticky: 1;
|
61
|
+
# } __attribute__((packed)) flags; //size = 16 (12 + 3 + 1)
|
62
|
+
# unsigned short nameLen;
|
63
|
+
# unsigned short titleLen;
|
64
|
+
# unsigned short descriptionLen;
|
65
|
+
# unsigned short appNameLen;
|
66
|
+
# /*
|
67
|
+
# * Variable sized. Format:
|
68
|
+
# * <notification name><title><description><application name><checksum>
|
69
|
+
# */
|
70
|
+
# unsigned char data[];
|
71
|
+
# } __attribute__((packed));
|
72
|
+
|
73
|
+
GNN_FORMAT = "CCnnnnna*"
|
74
|
+
|
75
|
+
GNN_FORMAT.gsub!(/n/, 'v') if BROKEN_PACK
|
76
|
+
|
77
|
+
# For litle endian machines the NetworkNotificationFlags aren't in network
|
78
|
+
# byte order
|
79
|
+
|
80
|
+
GNN_FORMAT.sub!((BROKEN_PACK ? 'v' : 'n'), 'v') if LITTLE_ENDIAN
|
81
|
+
|
82
|
+
##
|
83
|
+
# Growl Protocol Version
|
84
|
+
|
85
|
+
GROWL_PROTOCOL_VERSION = 1
|
86
|
+
|
87
|
+
##
|
88
|
+
# Growl Registration Packet Id
|
89
|
+
|
90
|
+
GROWL_TYPE_REGISTRATION = 0
|
91
|
+
|
92
|
+
##
|
93
|
+
# Growl Notification Packet Id
|
94
|
+
|
95
|
+
GROWL_TYPE_NOTIFICATION = 1
|
96
|
+
|
97
|
+
##
|
98
|
+
# Growl UDP Port
|
99
|
+
|
100
|
+
PORT = 9887
|
101
|
+
|
102
|
+
##
|
103
|
+
# Creates a new Growl UDP notifier and automatically registers any
|
104
|
+
# notifications with the remote machine.
|
105
|
+
#
|
106
|
+
# +host+ is the host to contact.
|
107
|
+
#
|
108
|
+
# +app_name+ is the name of the application sending the notifications.
|
109
|
+
#
|
110
|
+
# +all_notifies+ is a list of notification types your application sends.
|
111
|
+
#
|
112
|
+
# +default_notifies+ is a list of notification types that are turned on by
|
113
|
+
# default.
|
114
|
+
#
|
115
|
+
# I'm not sure about what +default_notifies+ is supposed to be set to, since
|
116
|
+
# there is a comment that says "not a subset of all_notifies" in the code.
|
117
|
+
#
|
118
|
+
# +password+ is the password needed to send notifications to +host+.
|
119
|
+
|
120
|
+
def initialize(host, app_name, all_notifies, default_notifies = nil,
|
121
|
+
password = nil)
|
122
|
+
@socket = UDPSocket.open
|
123
|
+
# FIXME This goes somewhere else
|
124
|
+
@socket.connect host, PORT
|
125
|
+
@app_name = app_name
|
126
|
+
@all_notifies = all_notifies
|
127
|
+
@default_notifies = default_notifies.nil? ? all_notifies : default_notifies
|
128
|
+
@password = password
|
129
|
+
|
130
|
+
register
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Sends a notification.
|
135
|
+
#
|
136
|
+
# +notify_type+ is the type of notification to send.
|
137
|
+
#
|
138
|
+
# +title+ is a title for the notification.
|
139
|
+
#
|
140
|
+
# +message+ is the body of the notification.
|
141
|
+
#
|
142
|
+
# +priority+ is the priorty of message to send.
|
143
|
+
#
|
144
|
+
# +sticky+ makes the notification stick until clicked.
|
145
|
+
|
146
|
+
def notify(notify_type, title, message, priority = 0, sticky = false)
|
147
|
+
raise Growl::Error, "Unknown Notification" unless
|
148
|
+
@all_notifies.include? notify_type
|
149
|
+
|
150
|
+
raise Growl::Error, "Invalid Priority" unless
|
151
|
+
priority >= -2 and priority <= 2
|
152
|
+
|
153
|
+
send notification_packet(notify_type, title, message, priority, sticky)
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Registers the notification types with +host+.
|
158
|
+
|
159
|
+
def register
|
160
|
+
send registration_packet
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# Sends a Growl packet
|
165
|
+
|
166
|
+
def send(packet)
|
167
|
+
set_sndbuf packet.length
|
168
|
+
@socket.send packet, 0
|
169
|
+
@socket.flush
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Builds a Growl registration packet
|
174
|
+
|
175
|
+
def registration_packet
|
176
|
+
data = []
|
177
|
+
data_format = ""
|
178
|
+
|
179
|
+
packet = [
|
180
|
+
GROWL_PROTOCOL_VERSION,
|
181
|
+
GROWL_TYPE_REGISTRATION
|
182
|
+
]
|
183
|
+
|
184
|
+
packet << @app_name.bytesize
|
185
|
+
packet << @all_notifies.length
|
186
|
+
packet << @default_notifies.length
|
187
|
+
|
188
|
+
data << @app_name
|
189
|
+
data_format = "a#{@app_name.bytesize}"
|
190
|
+
|
191
|
+
@all_notifies.each do |notify|
|
192
|
+
data << notify.length
|
193
|
+
data << notify
|
194
|
+
data_format << "na#{notify.length}"
|
195
|
+
end
|
196
|
+
|
197
|
+
@default_notifies.each do |notify|
|
198
|
+
data << @all_notifies.index(notify) if @all_notifies.include? notify
|
199
|
+
data_format << "C"
|
200
|
+
end
|
201
|
+
|
202
|
+
data_format.gsub!(/n/, 'v') if BROKEN_PACK
|
203
|
+
|
204
|
+
data = data.pack data_format
|
205
|
+
|
206
|
+
packet << data
|
207
|
+
|
208
|
+
packet = packet.pack GNR_FORMAT
|
209
|
+
|
210
|
+
checksum = Digest::MD5.new << packet
|
211
|
+
checksum.update @password unless @password.nil?
|
212
|
+
|
213
|
+
packet << checksum.digest
|
214
|
+
|
215
|
+
return packet
|
216
|
+
end
|
217
|
+
|
218
|
+
##
|
219
|
+
# Builds a Growl notification packet
|
220
|
+
|
221
|
+
def notification_packet(name, title, description, priority, sticky)
|
222
|
+
flags = 0
|
223
|
+
data = []
|
224
|
+
|
225
|
+
packet = [
|
226
|
+
GROWL_PROTOCOL_VERSION,
|
227
|
+
GROWL_TYPE_NOTIFICATION,
|
228
|
+
]
|
229
|
+
|
230
|
+
flags = 0
|
231
|
+
flags |= ((0x7 & priority) << 1) # 3 bits for priority
|
232
|
+
flags |= 1 if sticky # 1 bit for sticky
|
233
|
+
|
234
|
+
packet << flags
|
235
|
+
packet << name.bytesize
|
236
|
+
packet << title.length
|
237
|
+
packet << description.bytesize
|
238
|
+
packet << @app_name.bytesize
|
239
|
+
|
240
|
+
data << name
|
241
|
+
data << title
|
242
|
+
data << description
|
243
|
+
data << @app_name
|
244
|
+
|
245
|
+
packet << data.join
|
246
|
+
packet = packet.pack GNN_FORMAT
|
247
|
+
|
248
|
+
checksum = Digest::MD5.new << packet
|
249
|
+
checksum.update @password unless @password.nil?
|
250
|
+
|
251
|
+
packet << checksum.digest
|
252
|
+
|
253
|
+
return packet
|
254
|
+
end
|
255
|
+
|
256
|
+
##
|
257
|
+
# Set the size of the send buffer
|
258
|
+
#--
|
259
|
+
# Is this truly necessary?
|
260
|
+
|
261
|
+
def set_sndbuf(length)
|
262
|
+
@socket.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDBUF, length
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
266
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class URI::XGrowlResource < URI::Generic
|
2
|
+
|
3
|
+
DEFAULT_PORT = nil
|
4
|
+
|
5
|
+
COMPONENT = [ :scheme, :unique_id ]
|
6
|
+
|
7
|
+
UNIQUE_ID_REGEXP = /\A[\w-]+\z/
|
8
|
+
|
9
|
+
attr_reader :unique_id
|
10
|
+
|
11
|
+
def self.build args
|
12
|
+
tmp = URI::Util.make_components_hash self, args
|
13
|
+
|
14
|
+
if tmp[:unique_id] then
|
15
|
+
tmp[:host] = tmp[:unique_id]
|
16
|
+
else
|
17
|
+
tmp[:host] = ''
|
18
|
+
end
|
19
|
+
|
20
|
+
super tmp
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize *args
|
24
|
+
super
|
25
|
+
|
26
|
+
@unique_id = nil
|
27
|
+
|
28
|
+
if UNIQUE_ID_REGEXP =~ @host then
|
29
|
+
if args[-1] then # arg_check
|
30
|
+
self.unique_id = @host
|
31
|
+
else
|
32
|
+
set_unique_id @host
|
33
|
+
end
|
34
|
+
else
|
35
|
+
raise URI::InvalidComponentError,
|
36
|
+
"unrecognized opaque part for x-growl-resource URL: #{@host}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s # :nodoc:
|
41
|
+
"#{@scheme}://#{@unique_id}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def unique_id= v
|
45
|
+
check_unique_id v
|
46
|
+
set_unique_id v
|
47
|
+
end
|
48
|
+
|
49
|
+
# :stopdoc:
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
def set_unique_id v
|
54
|
+
@unique_id = v
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def check_unique_id v
|
60
|
+
return true unless v
|
61
|
+
return true if v.empty?
|
62
|
+
|
63
|
+
if parser.regexp[:HOST] !~ v or UNIQUE_ID_REGEXP !~ v then
|
64
|
+
raise InvalidComponentError,
|
65
|
+
"bad component (expected unique ID component): #{v}"
|
66
|
+
end
|
67
|
+
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
module URI # :nodoc:
|
74
|
+
@@schemes['X-GROWL-RESOURCE'] = URI::XGrowlResource
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,1238 @@
|
|
1
|
+
# coding: UTF-8
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'ruby-growl'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
class TestGrowlGNTP < MiniTest::Unit::TestCase
|
8
|
+
|
9
|
+
class Socket
|
10
|
+
attr_reader :_input, :_output
|
11
|
+
|
12
|
+
def initialize *a
|
13
|
+
@_input = StringIO.new
|
14
|
+
@_output = StringIO.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def gets separator
|
18
|
+
@_input.gets separator
|
19
|
+
end
|
20
|
+
|
21
|
+
def read *a
|
22
|
+
@_input.read(*a)
|
23
|
+
end
|
24
|
+
|
25
|
+
def write data
|
26
|
+
@_output.write data
|
27
|
+
end
|
28
|
+
|
29
|
+
def _input= data
|
30
|
+
@_input.write data
|
31
|
+
@_input.rewind
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class UUID
|
36
|
+
def generate() 4 end
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup
|
40
|
+
@gntp = Growl::GNTP.new 'localhost', 'test-app'
|
41
|
+
@gntp.uuid = UUID.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_add_notification
|
45
|
+
@gntp.add_notification 'test', 'Test Notification', 'PNG', true
|
46
|
+
|
47
|
+
expected = { 'test' => ['Test Notification', 'PNG', true] }
|
48
|
+
|
49
|
+
assert_equal expected, @gntp.notifications
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_cipher_des
|
53
|
+
@gntp.encrypt = 'DES'
|
54
|
+
key = "P>\a\x8AB\x01\xDF\xCET\x0F\xC7\xC9\xBC_^\xC0"
|
55
|
+
|
56
|
+
cipher, iv = @gntp.cipher key
|
57
|
+
|
58
|
+
assert_equal 'DES-CBC', cipher.name
|
59
|
+
assert_equal 8, cipher.iv_len
|
60
|
+
assert_equal 8, cipher.key_len
|
61
|
+
|
62
|
+
assert_kind_of String, iv
|
63
|
+
|
64
|
+
assert_endecrypt cipher, key, iv
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_cipher_iv
|
68
|
+
@gntp.encrypt = 'AES'
|
69
|
+
input_iv = 'junkjunkjunkjunk'
|
70
|
+
|
71
|
+
key = "\xF8\x93\xD4\xEB)u(\x06" \
|
72
|
+
"\x92\x88|)\x00\x97\xC73" \
|
73
|
+
"\x16/\xF3o\xB9@\xBA\x9D"
|
74
|
+
|
75
|
+
cipher, iv = @gntp.cipher key, input_iv
|
76
|
+
|
77
|
+
assert_equal 'AES-192-CBC', cipher.name
|
78
|
+
assert_equal 24, cipher.key_len
|
79
|
+
|
80
|
+
assert_equal input_iv, iv
|
81
|
+
|
82
|
+
assert_endecrypt cipher, key, iv
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_cipher_triple_des
|
86
|
+
@gntp.encrypt = '3DES'
|
87
|
+
key = "\xF8\x93\xD4\xEB)u(\x06" \
|
88
|
+
"\x92\x88|)\x00\x97\xC73" \
|
89
|
+
"\x16/\xF3o\xB9@\xBA\x9D"
|
90
|
+
|
91
|
+
cipher, iv = @gntp.cipher key
|
92
|
+
|
93
|
+
assert_equal 'DES-EDE3-CBC', cipher.name
|
94
|
+
assert_equal 8, cipher.iv_len
|
95
|
+
assert_equal 24, cipher.key_len
|
96
|
+
|
97
|
+
assert_kind_of String, iv
|
98
|
+
|
99
|
+
assert_endecrypt cipher, key, iv
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_cipher_aes
|
103
|
+
@gntp.encrypt = 'AES'
|
104
|
+
key = "\xF8\x93\xD4\xEB)u(\x06" \
|
105
|
+
"\x92\x88|)\x00\x97\xC73" \
|
106
|
+
"\x16/\xF3o\xB9@\xBA\x9D"
|
107
|
+
|
108
|
+
cipher, iv = @gntp.cipher key
|
109
|
+
|
110
|
+
assert_equal 'AES-192-CBC', cipher.name
|
111
|
+
assert_equal 16, cipher.iv_len
|
112
|
+
assert_equal 24, cipher.key_len
|
113
|
+
|
114
|
+
assert_kind_of String, iv
|
115
|
+
|
116
|
+
assert_endecrypt cipher, key, iv
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_key_hash_md5
|
120
|
+
stub_salt
|
121
|
+
@gntp.password = 'πassword'
|
122
|
+
algorithm = Digest::MD5
|
123
|
+
|
124
|
+
key, hash, = @gntp.key_hash algorithm
|
125
|
+
|
126
|
+
expected = [
|
127
|
+
80, 62, 7, 138, 66, 1, 223, 206,
|
128
|
+
84, 15, 199, 201, 188, 95, 94, 192,
|
129
|
+
]
|
130
|
+
|
131
|
+
assert_equal expected, key.unpack('C*'), 'key'
|
132
|
+
|
133
|
+
expected = 'c552e68e5d86772487f6014b02cb4a14'
|
134
|
+
|
135
|
+
assert_equal expected, hash, 'hash'
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_key_hash_sha1
|
139
|
+
stub_salt
|
140
|
+
@gntp.password = 'πassword'
|
141
|
+
algorithm = Digest::SHA1
|
142
|
+
|
143
|
+
key, hash, = @gntp.key_hash algorithm
|
144
|
+
|
145
|
+
expected = [
|
146
|
+
206, 111, 53, 40, 168, 195, 0, 193,
|
147
|
+
209, 5, 102, 197, 114, 212, 228, 64,
|
148
|
+
38, 168, 23, 187
|
149
|
+
]
|
150
|
+
|
151
|
+
assert_equal expected, key.unpack('C*'), 'key'
|
152
|
+
|
153
|
+
expected = '03247e7e5b3ae9033dba23cf4637023542bc10d3'
|
154
|
+
|
155
|
+
assert_equal expected, hash, 'hash'
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_key_hash_sha256
|
159
|
+
stub_salt
|
160
|
+
@gntp.password = 'πassword'
|
161
|
+
algorithm = Digest::SHA256
|
162
|
+
|
163
|
+
key, hash, = @gntp.key_hash algorithm
|
164
|
+
|
165
|
+
expected = [
|
166
|
+
248, 147, 212, 235, 41, 117, 40, 6,
|
167
|
+
146, 136, 124, 41, 0, 151, 199, 51,
|
168
|
+
22, 47, 243, 111, 185, 64, 186, 157,
|
169
|
+
227, 141, 213, 37, 127, 20, 155, 130
|
170
|
+
]
|
171
|
+
|
172
|
+
assert_equal expected, key.unpack('C*'), 'key'
|
173
|
+
|
174
|
+
expected = '88b55cd37083d87e' \
|
175
|
+
'cf79de12afe1c1b8' \
|
176
|
+
'8300c0d84c6ac35b' \
|
177
|
+
'cc6227c47a55087f'
|
178
|
+
|
179
|
+
assert_equal expected, hash, 'hash'
|
180
|
+
end
|
181
|
+
|
182
|
+
def test_key_hash_sha512
|
183
|
+
stub_salt
|
184
|
+
@gntp.password = 'πassword'
|
185
|
+
algorithm = Digest::SHA512
|
186
|
+
|
187
|
+
key, hash, = @gntp.key_hash algorithm
|
188
|
+
|
189
|
+
expected = [
|
190
|
+
134, 105, 63, 2, 240, 31, 36, 158,
|
191
|
+
20, 198, 246, 227, 240, 111, 158, 3,
|
192
|
+
37, 23, 1, 129, 27, 189, 68, 110,
|
193
|
+
105, 213, 90, 0, 23, 146, 218, 69,
|
194
|
+
253, 4, 57, 3, 152, 101, 22, 55,
|
195
|
+
89, 99, 133, 21, 95, 238, 181, 5,
|
196
|
+
67, 87, 108, 15, 128, 190, 137, 150,
|
197
|
+
151, 83, 245, 219, 21, 251, 95, 182,
|
198
|
+
]
|
199
|
+
|
200
|
+
assert_equal expected, key.unpack('C*'), 'key'
|
201
|
+
|
202
|
+
expected = '2407322ff8b1f13c' \
|
203
|
+
'75774ea8a954c74c' \
|
204
|
+
'fb5138813f49a7c5' \
|
205
|
+
'5e230cfad7426c42' \
|
206
|
+
'cc4771262331a559' \
|
207
|
+
'2ddc243462d7f6f8' \
|
208
|
+
'9ebd7581cb52c451' \
|
209
|
+
'7648834d624c3c60'
|
210
|
+
|
211
|
+
assert_equal expected, hash, 'hash'
|
212
|
+
end
|
213
|
+
|
214
|
+
def test_notify
|
215
|
+
stub_socket "GNTP/1.0 -OK NONE\r\n" \
|
216
|
+
"Response-Action: NOTIFY\r\n" \
|
217
|
+
"Notification-ID: (null)\r\n\r\n\r\n"
|
218
|
+
|
219
|
+
response = @gntp.notify 'test', 'title', 'message', 2, true
|
220
|
+
|
221
|
+
expected = {
|
222
|
+
'Response-Action' => 'NOTIFY',
|
223
|
+
'Notification-ID' => nil,
|
224
|
+
}
|
225
|
+
|
226
|
+
assert_equal expected, response
|
227
|
+
end
|
228
|
+
|
229
|
+
def test_notify_callback
|
230
|
+
callback_result = nil
|
231
|
+
stub_socket <<-STREAM
|
232
|
+
GNTP/1.0 -OK NONE\r
|
233
|
+
Response-Action: NOTIFY\r
|
234
|
+
Notification-ID: 4\r
|
235
|
+
\r
|
236
|
+
\r
|
237
|
+
\r
|
238
|
+
GNTP/1.0 -CALLBACK NONE\r
|
239
|
+
Response-Action: NOTIFY\r
|
240
|
+
Notification-ID: 4\r
|
241
|
+
Notification-Callback-Result: CLICKED\r
|
242
|
+
Notification-Callback-Timestamp: 2012-03-28\r
|
243
|
+
Notification-Callback-Context: context\r
|
244
|
+
Notification-Callback-Context-Type: type\r
|
245
|
+
Application-Name: test\r
|
246
|
+
\r
|
247
|
+
\r
|
248
|
+
STREAM
|
249
|
+
|
250
|
+
response = @gntp.notify 'test', 'title', 'message' do |result|
|
251
|
+
callback_result = result
|
252
|
+
end
|
253
|
+
|
254
|
+
expected = {
|
255
|
+
'Response-Action' => 'NOTIFY',
|
256
|
+
'Notification-ID' => '4',
|
257
|
+
'Notification-Callback-Result' => 'CLICKED',
|
258
|
+
'Notification-Callback-Timestamp' => Time.parse('2012-03-28'),
|
259
|
+
'Notification-Callback-Context' => 'context',
|
260
|
+
'Notification-Callback-Context-Type' => 'type',
|
261
|
+
'Application-Name' => 'test'
|
262
|
+
}
|
263
|
+
|
264
|
+
assert_equal expected, callback_result
|
265
|
+
|
266
|
+
expected = {
|
267
|
+
'Response-Action' => 'NOTIFY',
|
268
|
+
'Notification-ID' => '4',
|
269
|
+
}
|
270
|
+
|
271
|
+
assert_equal expected, response
|
272
|
+
end
|
273
|
+
|
274
|
+
def test_notify_callback_with_uri
|
275
|
+
e = assert_raises ArgumentError do
|
276
|
+
@gntp.notify 'test', 'title', 'message', 0, false, nil, 'uri' do end
|
277
|
+
end
|
278
|
+
|
279
|
+
assert_equal 'provide either a url or a block for callbacks, not both',
|
280
|
+
e.message
|
281
|
+
end
|
282
|
+
|
283
|
+
def test_notify_coalesce
|
284
|
+
stub_socket "GNTP/1.0 -OK NONE\r\n" \
|
285
|
+
"Response-Action: NOTIFY\r\n" \
|
286
|
+
"Notification-ID: (null)\r\n\r\n\r\n"
|
287
|
+
|
288
|
+
response = @gntp.notify 'test', 'title', 'message', 0, false, 'some_id'
|
289
|
+
|
290
|
+
expected = {
|
291
|
+
'Response-Action' => 'NOTIFY',
|
292
|
+
'Notification-ID' => nil,
|
293
|
+
}
|
294
|
+
|
295
|
+
assert_equal expected, response
|
296
|
+
end
|
297
|
+
|
298
|
+
def test_packet
|
299
|
+
expected = <<-EXPECTED
|
300
|
+
GNTP/1.0 REGISTER NONE\r
|
301
|
+
Application-Name: test-app\r
|
302
|
+
Origin-Software-Name: ruby-growl\r
|
303
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
304
|
+
Origin-Platform-Name: ruby\r
|
305
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
306
|
+
Connection: close\r
|
307
|
+
Foo: bar\r
|
308
|
+
\r
|
309
|
+
\r
|
310
|
+
EXPECTED
|
311
|
+
|
312
|
+
assert_equal expected, @gntp.packet('REGISTER', ["Foo: bar"])
|
313
|
+
end
|
314
|
+
|
315
|
+
def test_packet_encrypt_des
|
316
|
+
@gntp.encrypt = 'DES'
|
317
|
+
@gntp.password = 'password'
|
318
|
+
|
319
|
+
packet = @gntp.packet 'REGISTER', ["Foo: bar"]
|
320
|
+
|
321
|
+
info, body = packet.split "\r\n", 2
|
322
|
+
|
323
|
+
_, _, algorithm_info, key_info = info.split ' '
|
324
|
+
|
325
|
+
cipher, iv = algorithm_info.split ':'
|
326
|
+
|
327
|
+
assert_equal 'DES', cipher
|
328
|
+
|
329
|
+
iv = [iv].pack 'H*'
|
330
|
+
|
331
|
+
cipher = OpenSSL::Cipher.new Growl::GNTP::ENCRYPTION_ALGORITHMS[cipher]
|
332
|
+
|
333
|
+
assert_equal 'DES-CBC', cipher.name
|
334
|
+
|
335
|
+
_, salt = key_info.split '.', 2
|
336
|
+
|
337
|
+
salt = [salt].pack 'H*'
|
338
|
+
|
339
|
+
key = Digest::SHA512.digest "password#{salt}"
|
340
|
+
|
341
|
+
body = body.chomp "\r\n\r\n"
|
342
|
+
|
343
|
+
decrypted = decrypt cipher, key, iv, body
|
344
|
+
|
345
|
+
expected = <<-EXPECTED
|
346
|
+
Application-Name: test-app\r
|
347
|
+
Origin-Software-Name: ruby-growl\r
|
348
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
349
|
+
Origin-Platform-Name: ruby\r
|
350
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
351
|
+
Connection: close\r
|
352
|
+
Foo: bar\r
|
353
|
+
EXPECTED
|
354
|
+
|
355
|
+
assert_equal expected, decrypted
|
356
|
+
end
|
357
|
+
|
358
|
+
def test_packet_encrypt_3des
|
359
|
+
@gntp.encrypt = '3DES'
|
360
|
+
@gntp.password = 'password'
|
361
|
+
|
362
|
+
packet = @gntp.packet 'REGISTER', ["Foo: bar"]
|
363
|
+
|
364
|
+
info, body = packet.split "\r\n", 2
|
365
|
+
|
366
|
+
_, _, algorithm_info, key_info = info.split ' '
|
367
|
+
|
368
|
+
cipher, iv = algorithm_info.split ':'
|
369
|
+
|
370
|
+
assert_equal '3DES', cipher
|
371
|
+
|
372
|
+
iv = [iv].pack 'H*'
|
373
|
+
|
374
|
+
cipher = OpenSSL::Cipher.new Growl::GNTP::ENCRYPTION_ALGORITHMS[cipher]
|
375
|
+
|
376
|
+
assert_equal 'DES-EDE3-CBC', cipher.name
|
377
|
+
|
378
|
+
_, salt = key_info.split '.', 2
|
379
|
+
|
380
|
+
salt = [salt].pack 'H*'
|
381
|
+
|
382
|
+
key = Digest::SHA512.digest "password#{salt}"
|
383
|
+
|
384
|
+
body = body.chomp "\r\n\r\n"
|
385
|
+
|
386
|
+
decrypted = decrypt cipher, key, iv, body
|
387
|
+
|
388
|
+
expected = <<-EXPECTED
|
389
|
+
Application-Name: test-app\r
|
390
|
+
Origin-Software-Name: ruby-growl\r
|
391
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
392
|
+
Origin-Platform-Name: ruby\r
|
393
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
394
|
+
Connection: close\r
|
395
|
+
Foo: bar\r
|
396
|
+
EXPECTED
|
397
|
+
|
398
|
+
assert_equal expected, decrypted
|
399
|
+
end
|
400
|
+
|
401
|
+
def test_packet_encrypt_aes
|
402
|
+
@gntp.encrypt = 'AES'
|
403
|
+
@gntp.password = 'password'
|
404
|
+
|
405
|
+
packet = @gntp.packet 'REGISTER', ["Foo: bar"]
|
406
|
+
|
407
|
+
info, body = packet.split "\r\n", 2
|
408
|
+
|
409
|
+
_, _, algorithm_info, key_info = info.split ' '
|
410
|
+
|
411
|
+
cipher, iv = algorithm_info.split ':'
|
412
|
+
|
413
|
+
assert_equal 'AES', cipher
|
414
|
+
|
415
|
+
iv = [iv].pack 'H*'
|
416
|
+
|
417
|
+
cipher = OpenSSL::Cipher.new Growl::GNTP::ENCRYPTION_ALGORITHMS[cipher]
|
418
|
+
|
419
|
+
assert_equal 'AES-192-CBC', cipher.name
|
420
|
+
|
421
|
+
_, salt = key_info.split '.', 2
|
422
|
+
|
423
|
+
salt = [salt].pack 'H*'
|
424
|
+
|
425
|
+
key = Digest::SHA512.digest "password#{salt}"
|
426
|
+
|
427
|
+
body = body.chomp "\r\n\r\n"
|
428
|
+
|
429
|
+
decrypted = decrypt cipher, key, iv, body
|
430
|
+
|
431
|
+
expected = <<-EXPECTED
|
432
|
+
Application-Name: test-app\r
|
433
|
+
Origin-Software-Name: ruby-growl\r
|
434
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
435
|
+
Origin-Platform-Name: ruby\r
|
436
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
437
|
+
Connection: close\r
|
438
|
+
Foo: bar\r
|
439
|
+
EXPECTED
|
440
|
+
|
441
|
+
assert_equal expected, decrypted
|
442
|
+
end
|
443
|
+
|
444
|
+
def test_packet_encrypt_aes_icon
|
445
|
+
@gntp.encrypt = 'AES'
|
446
|
+
@gntp.password = 'password'
|
447
|
+
|
448
|
+
packet = @gntp.packet 'REGISTER', ["Foo: bar"], { 'icon' => 'PNG' }
|
449
|
+
|
450
|
+
info, body = packet.split "\r\n", 2
|
451
|
+
|
452
|
+
_, _, algorithm_info, key_info = info.split ' '
|
453
|
+
|
454
|
+
cipher, iv = algorithm_info.split ':'
|
455
|
+
|
456
|
+
assert_equal 'AES', cipher
|
457
|
+
|
458
|
+
iv = [iv].pack 'H*'
|
459
|
+
|
460
|
+
cipher = OpenSSL::Cipher.new Growl::GNTP::ENCRYPTION_ALGORITHMS[cipher]
|
461
|
+
|
462
|
+
assert_equal 'AES-192-CBC', cipher.name
|
463
|
+
|
464
|
+
_, salt = key_info.split '.', 2
|
465
|
+
|
466
|
+
salt = [salt].pack 'H*'
|
467
|
+
|
468
|
+
key = Digest::SHA512.digest "password#{salt}"
|
469
|
+
|
470
|
+
body = body.chomp "\r\n\r\n"
|
471
|
+
|
472
|
+
end_of_headers = body.index "\r\nIdentifier: "
|
473
|
+
headers = body.slice! 0, end_of_headers
|
474
|
+
|
475
|
+
decrypted = decrypt cipher, key, iv, headers
|
476
|
+
|
477
|
+
expected = <<-EXPECTED
|
478
|
+
Application-Name: test-app\r
|
479
|
+
Origin-Software-Name: ruby-growl\r
|
480
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
481
|
+
Origin-Platform-Name: ruby\r
|
482
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
483
|
+
Connection: close\r
|
484
|
+
Foo: bar\r
|
485
|
+
EXPECTED
|
486
|
+
|
487
|
+
assert_equal expected, decrypted
|
488
|
+
|
489
|
+
body =~ /Length: (\d+)\r\n\r\n/
|
490
|
+
|
491
|
+
data_length = $1.to_i
|
492
|
+
data_offset = $`.length + $&.length
|
493
|
+
|
494
|
+
data = body[data_offset, data_length]
|
495
|
+
|
496
|
+
decrypted = decrypt cipher, key, iv, data
|
497
|
+
|
498
|
+
assert_equal 'PNG', decrypted
|
499
|
+
end
|
500
|
+
|
501
|
+
def test_packet_hash
|
502
|
+
@gntp.password = 'password'
|
503
|
+
|
504
|
+
packet = @gntp.packet 'REGISTER', ["Foo: bar"]
|
505
|
+
|
506
|
+
info, body = packet.split "\r\n", 2
|
507
|
+
|
508
|
+
_, _, algorithm_info, key_info = info.split ' '
|
509
|
+
|
510
|
+
assert_equal 'NONE', algorithm_info
|
511
|
+
|
512
|
+
key_info =~ /:(.*)\./
|
513
|
+
|
514
|
+
digest = $`
|
515
|
+
key_hash = $1
|
516
|
+
salt = $'
|
517
|
+
|
518
|
+
salt = [salt].pack 'H*'
|
519
|
+
|
520
|
+
expected_key = Digest::SHA512.digest "password#{salt}"
|
521
|
+
expected_key_hash = Digest::SHA512.hexdigest expected_key
|
522
|
+
|
523
|
+
assert_equal expected_key_hash, key_hash
|
524
|
+
|
525
|
+
expected = <<-EXPECTED
|
526
|
+
Application-Name: test-app\r
|
527
|
+
Origin-Software-Name: ruby-growl\r
|
528
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
529
|
+
Origin-Platform-Name: ruby\r
|
530
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
531
|
+
Connection: close\r
|
532
|
+
Foo: bar\r
|
533
|
+
\r
|
534
|
+
\r
|
535
|
+
EXPECTED
|
536
|
+
|
537
|
+
assert_equal expected, body
|
538
|
+
end
|
539
|
+
|
540
|
+
def test_packet_notify
|
541
|
+
expected = <<-EXPECTED
|
542
|
+
GNTP/1.0 NOTIFY NONE\r
|
543
|
+
Application-Name: test-app\r
|
544
|
+
Origin-Software-Name: ruby-growl\r
|
545
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
546
|
+
Origin-Platform-Name: ruby\r
|
547
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
548
|
+
Connection: close\r
|
549
|
+
Notification-ID: 4\r
|
550
|
+
Notification-Name: test-note\r
|
551
|
+
Notification-Title: title\r
|
552
|
+
\r
|
553
|
+
\r
|
554
|
+
EXPECTED
|
555
|
+
|
556
|
+
assert_equal expected, @gntp.packet_notify('test-note', 'title',
|
557
|
+
nil, 0, false, nil, nil)
|
558
|
+
end
|
559
|
+
|
560
|
+
def test_packet_notify_callback
|
561
|
+
expected = <<-EXPECTED
|
562
|
+
GNTP/1.0 NOTIFY NONE\r
|
563
|
+
Application-Name: test-app\r
|
564
|
+
Origin-Software-Name: ruby-growl\r
|
565
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
566
|
+
Origin-Platform-Name: ruby\r
|
567
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
568
|
+
Connection: close\r
|
569
|
+
Notification-ID: 4\r
|
570
|
+
Notification-Name: test-note\r
|
571
|
+
Notification-Title: title\r
|
572
|
+
Notification-Callback-Context: context\r
|
573
|
+
Notification-Callback-Context-Type: type\r
|
574
|
+
\r
|
575
|
+
\r
|
576
|
+
EXPECTED
|
577
|
+
|
578
|
+
result = @gntp.packet_notify 'test-note', 'title', nil, 0, false, nil, true
|
579
|
+
|
580
|
+
assert_equal expected, result
|
581
|
+
end
|
582
|
+
|
583
|
+
def test_packet_notify_callback_url
|
584
|
+
expected = <<-EXPECTED
|
585
|
+
GNTP/1.0 NOTIFY NONE\r
|
586
|
+
Application-Name: test-app\r
|
587
|
+
Origin-Software-Name: ruby-growl\r
|
588
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
589
|
+
Origin-Platform-Name: ruby\r
|
590
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
591
|
+
Connection: close\r
|
592
|
+
Notification-ID: 4\r
|
593
|
+
Notification-Name: test-note\r
|
594
|
+
Notification-Title: title\r
|
595
|
+
Notification-Callback-Context: context\r
|
596
|
+
Notification-Callback-Context-Type: type\r
|
597
|
+
Notification-Callback-Target: http://example\r
|
598
|
+
\r
|
599
|
+
\r
|
600
|
+
EXPECTED
|
601
|
+
|
602
|
+
assert_equal expected, @gntp.packet_notify('test-note', 'title',
|
603
|
+
nil, 0, false, nil,
|
604
|
+
'http://example')
|
605
|
+
end
|
606
|
+
|
607
|
+
def test_packet_notify_coalesce
|
608
|
+
expected = <<-EXPECTED
|
609
|
+
GNTP/1.0 NOTIFY NONE\r
|
610
|
+
Application-Name: test-app\r
|
611
|
+
Origin-Software-Name: ruby-growl\r
|
612
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
613
|
+
Origin-Platform-Name: ruby\r
|
614
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
615
|
+
Connection: close\r
|
616
|
+
Notification-ID: 4\r
|
617
|
+
Notification-Coalescing-ID: 3\r
|
618
|
+
Notification-Name: test-note\r
|
619
|
+
Notification-Title: title\r
|
620
|
+
\r
|
621
|
+
\r
|
622
|
+
EXPECTED
|
623
|
+
|
624
|
+
assert_equal expected, @gntp.packet_notify('test-note', 'title',
|
625
|
+
nil, 0, false, 3, nil)
|
626
|
+
end
|
627
|
+
|
628
|
+
def test_packet_notify_description
|
629
|
+
expected = <<-EXPECTED
|
630
|
+
GNTP/1.0 NOTIFY NONE\r
|
631
|
+
Application-Name: test-app\r
|
632
|
+
Origin-Software-Name: ruby-growl\r
|
633
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
634
|
+
Origin-Platform-Name: ruby\r
|
635
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
636
|
+
Connection: close\r
|
637
|
+
Notification-ID: 4\r
|
638
|
+
Notification-Name: test-note\r
|
639
|
+
Notification-Title: title\r
|
640
|
+
Notification-Text: message\r
|
641
|
+
\r
|
642
|
+
\r
|
643
|
+
EXPECTED
|
644
|
+
|
645
|
+
assert_equal expected, @gntp.packet_notify('test-note', 'title', 'message',
|
646
|
+
0, false, nil, nil)
|
647
|
+
end
|
648
|
+
|
649
|
+
def test_packet_notify_icon
|
650
|
+
@gntp.add_notification 'test-note', nil, 'PNG'
|
651
|
+
|
652
|
+
expected = <<-EXPECTED
|
653
|
+
GNTP/1.0 NOTIFY NONE\r
|
654
|
+
Application-Name: test-app\r
|
655
|
+
Origin-Software-Name: ruby-growl\r
|
656
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
657
|
+
Origin-Platform-Name: ruby\r
|
658
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
659
|
+
Connection: close\r
|
660
|
+
Notification-ID: 4\r
|
661
|
+
Notification-Name: test-note\r
|
662
|
+
Notification-Title: title\r
|
663
|
+
Notification-Icon: x-growl-resource://4\r
|
664
|
+
\r
|
665
|
+
Identifier: 4\r
|
666
|
+
Length: 3\r
|
667
|
+
\r
|
668
|
+
PNG\r
|
669
|
+
\r
|
670
|
+
\r
|
671
|
+
EXPECTED
|
672
|
+
|
673
|
+
assert_equal expected, @gntp.packet_notify('test-note', 'title',
|
674
|
+
nil, 0, false, nil, nil)
|
675
|
+
end
|
676
|
+
|
677
|
+
def test_packet_notify_icon_uri
|
678
|
+
uri = URI 'http://example/icon.png'
|
679
|
+
@gntp.add_notification 'test-note', nil, uri
|
680
|
+
|
681
|
+
expected = <<-EXPECTED
|
682
|
+
GNTP/1.0 NOTIFY NONE\r
|
683
|
+
Application-Name: test-app\r
|
684
|
+
Origin-Software-Name: ruby-growl\r
|
685
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
686
|
+
Origin-Platform-Name: ruby\r
|
687
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
688
|
+
Connection: close\r
|
689
|
+
Notification-ID: 4\r
|
690
|
+
Notification-Name: test-note\r
|
691
|
+
Notification-Title: title\r
|
692
|
+
Notification-Icon: http://example/icon.png\r
|
693
|
+
\r
|
694
|
+
\r
|
695
|
+
EXPECTED
|
696
|
+
|
697
|
+
assert_equal expected, @gntp.packet_notify('test-note', 'title',
|
698
|
+
nil, 0, false, nil, nil)
|
699
|
+
end
|
700
|
+
|
701
|
+
def test_packet_notify_priority
|
702
|
+
expected = <<-EXPECTED
|
703
|
+
GNTP/1.0 NOTIFY NONE\r
|
704
|
+
Application-Name: test-app\r
|
705
|
+
Origin-Software-Name: ruby-growl\r
|
706
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
707
|
+
Origin-Platform-Name: ruby\r
|
708
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
709
|
+
Connection: close\r
|
710
|
+
Notification-ID: 4\r
|
711
|
+
Notification-Name: test-note\r
|
712
|
+
Notification-Title: title\r
|
713
|
+
Notification-Priority: 2\r
|
714
|
+
\r
|
715
|
+
\r
|
716
|
+
EXPECTED
|
717
|
+
|
718
|
+
assert_equal expected, @gntp.packet_notify('test-note', 'title',
|
719
|
+
nil, 2, false, nil, nil)
|
720
|
+
|
721
|
+
assert_match(%r%^Notification-Priority: -2%,
|
722
|
+
@gntp.packet_notify('test-note', 'title', nil,
|
723
|
+
-2, false, nil, nil))
|
724
|
+
assert_match(%r%^Notification-Priority: -1%,
|
725
|
+
@gntp.packet_notify('test-note', 'title', nil,
|
726
|
+
-1, false, nil, nil))
|
727
|
+
refute_match(%r%^Notification-Priority: 0%,
|
728
|
+
@gntp.packet_notify('test-note', 'title', nil,
|
729
|
+
0, false, nil, nil))
|
730
|
+
assert_match(%r%^Notification-Priority: 1%,
|
731
|
+
@gntp.packet_notify('test-note', 'title', nil,
|
732
|
+
1, false, nil, nil))
|
733
|
+
assert_match(%r%^Notification-Priority: 2%,
|
734
|
+
@gntp.packet_notify('test-note', 'title', nil,
|
735
|
+
2, false, nil, nil))
|
736
|
+
|
737
|
+
e = assert_raises ArgumentError do
|
738
|
+
@gntp.packet_notify 'test-note', 'title', nil, -3, false, nil, nil
|
739
|
+
end
|
740
|
+
|
741
|
+
assert_equal 'invalid priority level -3', e.message
|
742
|
+
|
743
|
+
e = assert_raises ArgumentError do
|
744
|
+
@gntp.packet_notify 'test-note', 'title', nil, 3, false, nil, nil
|
745
|
+
end
|
746
|
+
|
747
|
+
assert_equal 'invalid priority level 3', e.message
|
748
|
+
end
|
749
|
+
|
750
|
+
def test_packet_notify_sticky
|
751
|
+
expected = <<-EXPECTED
|
752
|
+
GNTP/1.0 NOTIFY NONE\r
|
753
|
+
Application-Name: test-app\r
|
754
|
+
Origin-Software-Name: ruby-growl\r
|
755
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
756
|
+
Origin-Platform-Name: ruby\r
|
757
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
758
|
+
Connection: close\r
|
759
|
+
Notification-ID: 4\r
|
760
|
+
Notification-Name: test-note\r
|
761
|
+
Notification-Title: title\r
|
762
|
+
Notification-Sticky: True\r
|
763
|
+
\r
|
764
|
+
\r
|
765
|
+
EXPECTED
|
766
|
+
|
767
|
+
assert_equal expected, @gntp.packet_notify('test-note', 'title',
|
768
|
+
nil, 0, true, nil, nil)
|
769
|
+
|
770
|
+
refute_match(%r%^Notification-Sticky:%,
|
771
|
+
@gntp.packet_notify('test-note', 'title', nil, 0, false,
|
772
|
+
nil, nil))
|
773
|
+
end
|
774
|
+
|
775
|
+
def test_packet_register
|
776
|
+
@gntp.add_notification 'test-note'
|
777
|
+
|
778
|
+
expected = <<-EXPECTED
|
779
|
+
GNTP/1.0 REGISTER NONE\r
|
780
|
+
Application-Name: test-app\r
|
781
|
+
Origin-Software-Name: ruby-growl\r
|
782
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
783
|
+
Origin-Platform-Name: ruby\r
|
784
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
785
|
+
Connection: close\r
|
786
|
+
Notifications-Count: 1\r
|
787
|
+
\r
|
788
|
+
Notification-Name: test-note\r
|
789
|
+
Notification-Enabled: true\r
|
790
|
+
\r
|
791
|
+
\r
|
792
|
+
EXPECTED
|
793
|
+
|
794
|
+
assert_equal expected, @gntp.packet_register
|
795
|
+
end
|
796
|
+
|
797
|
+
def test_packet_register_application_icon
|
798
|
+
@gntp.add_notification 'test-note'
|
799
|
+
@gntp.icon = 'PNG'
|
800
|
+
|
801
|
+
expected = <<-EXPECTED
|
802
|
+
GNTP/1.0 REGISTER NONE\r
|
803
|
+
Application-Name: test-app\r
|
804
|
+
Origin-Software-Name: ruby-growl\r
|
805
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
806
|
+
Origin-Platform-Name: ruby\r
|
807
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
808
|
+
Connection: close\r
|
809
|
+
Application-Icon: x-growl-resource://4\r
|
810
|
+
Notifications-Count: 1\r
|
811
|
+
\r
|
812
|
+
Notification-Name: test-note\r
|
813
|
+
Notification-Enabled: true\r
|
814
|
+
\r
|
815
|
+
Identifier: 4\r
|
816
|
+
Length: 3\r
|
817
|
+
\r
|
818
|
+
PNG\r
|
819
|
+
\r
|
820
|
+
\r
|
821
|
+
EXPECTED
|
822
|
+
|
823
|
+
assert_equal expected, @gntp.packet_register
|
824
|
+
end
|
825
|
+
|
826
|
+
def test_packet_register_application_icon_uri
|
827
|
+
@gntp.add_notification 'test-note'
|
828
|
+
@gntp.icon = URI 'http://example/icon.png'
|
829
|
+
|
830
|
+
expected = <<-EXPECTED
|
831
|
+
GNTP/1.0 REGISTER NONE\r
|
832
|
+
Application-Name: test-app\r
|
833
|
+
Origin-Software-Name: ruby-growl\r
|
834
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
835
|
+
Origin-Platform-Name: ruby\r
|
836
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
837
|
+
Connection: close\r
|
838
|
+
Application-Icon: http://example/icon.png\r
|
839
|
+
Notifications-Count: 1\r
|
840
|
+
\r
|
841
|
+
Notification-Name: test-note\r
|
842
|
+
Notification-Enabled: true\r
|
843
|
+
\r
|
844
|
+
\r
|
845
|
+
EXPECTED
|
846
|
+
|
847
|
+
assert_equal expected, @gntp.packet_register
|
848
|
+
end
|
849
|
+
|
850
|
+
def test_packet_register_disabled
|
851
|
+
@gntp.add_notification 'test-note', nil, nil, false
|
852
|
+
|
853
|
+
expected = <<-EXPECTED
|
854
|
+
GNTP/1.0 REGISTER NONE\r
|
855
|
+
Application-Name: test-app\r
|
856
|
+
Origin-Software-Name: ruby-growl\r
|
857
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
858
|
+
Origin-Platform-Name: ruby\r
|
859
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
860
|
+
Connection: close\r
|
861
|
+
Notifications-Count: 1\r
|
862
|
+
\r
|
863
|
+
Notification-Name: test-note\r
|
864
|
+
\r
|
865
|
+
\r
|
866
|
+
EXPECTED
|
867
|
+
|
868
|
+
assert_equal expected, @gntp.packet_register
|
869
|
+
end
|
870
|
+
|
871
|
+
def test_packet_register_display_name
|
872
|
+
@gntp.add_notification 'test-note', 'Test Note'
|
873
|
+
|
874
|
+
expected = <<-EXPECTED
|
875
|
+
GNTP/1.0 REGISTER NONE\r
|
876
|
+
Application-Name: test-app\r
|
877
|
+
Origin-Software-Name: ruby-growl\r
|
878
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
879
|
+
Origin-Platform-Name: ruby\r
|
880
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
881
|
+
Connection: close\r
|
882
|
+
Notifications-Count: 1\r
|
883
|
+
\r
|
884
|
+
Notification-Name: test-note\r
|
885
|
+
Notification-Display-Name: Test Note\r
|
886
|
+
Notification-Enabled: true\r
|
887
|
+
\r
|
888
|
+
\r
|
889
|
+
EXPECTED
|
890
|
+
|
891
|
+
assert_equal expected, @gntp.packet_register
|
892
|
+
end
|
893
|
+
|
894
|
+
def test_packet_register_notification_icon
|
895
|
+
@gntp.add_notification 'test-note', nil, 'PNG'
|
896
|
+
|
897
|
+
expected = <<-EXPECTED
|
898
|
+
GNTP/1.0 REGISTER NONE\r
|
899
|
+
Application-Name: test-app\r
|
900
|
+
Origin-Software-Name: ruby-growl\r
|
901
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
902
|
+
Origin-Platform-Name: ruby\r
|
903
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
904
|
+
Connection: close\r
|
905
|
+
Notifications-Count: 1\r
|
906
|
+
\r
|
907
|
+
Notification-Name: test-note\r
|
908
|
+
Notification-Enabled: true\r
|
909
|
+
Notification-Icon: x-growl-resource://4\r
|
910
|
+
\r
|
911
|
+
Identifier: 4\r
|
912
|
+
Length: 3\r
|
913
|
+
\r
|
914
|
+
PNG\r
|
915
|
+
\r
|
916
|
+
\r
|
917
|
+
EXPECTED
|
918
|
+
|
919
|
+
assert_equal expected, @gntp.packet_register
|
920
|
+
end
|
921
|
+
|
922
|
+
def test_packet_register_notification_icon_uri
|
923
|
+
uri = URI 'http://example/icon.png'
|
924
|
+
@gntp.add_notification 'test-note', nil, uri
|
925
|
+
|
926
|
+
expected = <<-EXPECTED
|
927
|
+
GNTP/1.0 REGISTER NONE\r
|
928
|
+
Application-Name: test-app\r
|
929
|
+
Origin-Software-Name: ruby-growl\r
|
930
|
+
Origin-Software-Version: #{Growl::VERSION}\r
|
931
|
+
Origin-Platform-Name: ruby\r
|
932
|
+
Origin-Platform-Version: #{RUBY_VERSION}\r
|
933
|
+
Connection: close\r
|
934
|
+
Notifications-Count: 1\r
|
935
|
+
\r
|
936
|
+
Notification-Name: test-note\r
|
937
|
+
Notification-Enabled: true\r
|
938
|
+
Notification-Icon: http://example/icon.png\r
|
939
|
+
\r
|
940
|
+
\r
|
941
|
+
EXPECTED
|
942
|
+
|
943
|
+
assert_equal expected, @gntp.packet_register
|
944
|
+
end
|
945
|
+
|
946
|
+
def test_parse_header_boolean
|
947
|
+
assert_equal ['Notification-Enabled', true],
|
948
|
+
@gntp.parse_header('Notification-Enabled', 'True')
|
949
|
+
assert_equal ['Notification-Enabled', true],
|
950
|
+
@gntp.parse_header('Notification-Enabled', 'Yes')
|
951
|
+
assert_equal ['Notification-Sticky', false],
|
952
|
+
@gntp.parse_header('Notification-Sticky', 'False')
|
953
|
+
assert_equal ['Notification-Sticky', false],
|
954
|
+
@gntp.parse_header('Notification-Sticky', 'No')
|
955
|
+
end
|
956
|
+
|
957
|
+
def test_parse_header_date
|
958
|
+
now = Time.at Time.now.to_i
|
959
|
+
now_8601 = now.iso8601
|
960
|
+
assert_equal ['Notification-Callback-Timestamp', now],
|
961
|
+
@gntp.parse_header('Notification-Callback-Timestamp', now_8601)
|
962
|
+
end
|
963
|
+
|
964
|
+
def test_parse_header_integer
|
965
|
+
assert_equal ['Error-Code', 200],
|
966
|
+
@gntp.parse_header('Error-Code', '200')
|
967
|
+
assert_equal ['Notifications-Count', 2],
|
968
|
+
@gntp.parse_header('Notifications-Count', '2')
|
969
|
+
assert_equal ['Notifications-Priority', 2],
|
970
|
+
@gntp.parse_header('Notifications-Priority', '2')
|
971
|
+
assert_equal ['Subscriber-Port', 23053],
|
972
|
+
@gntp.parse_header('Subscriber-Port', '23053')
|
973
|
+
assert_equal ['Subscription-TTL', 60],
|
974
|
+
@gntp.parse_header('Subscription-TTL', '60')
|
975
|
+
end
|
976
|
+
|
977
|
+
def test_parse_header_string
|
978
|
+
value = 'test'
|
979
|
+
value.encode! Encoding::BINARY
|
980
|
+
|
981
|
+
header = @gntp.parse_header('Application-Name', value)
|
982
|
+
assert_equal ['Application-Name', 'test'], header
|
983
|
+
assert_equal Encoding::UTF_8, header.last.encoding
|
984
|
+
|
985
|
+
header = @gntp.parse_header('Application-Name', '(null)')
|
986
|
+
assert_equal ['Application-Name', nil], header
|
987
|
+
|
988
|
+
assert_equal ['Application-Name', 'test'],
|
989
|
+
@gntp.parse_header('Application-Name', 'test')
|
990
|
+
assert_equal ['Error-Description', 'test'],
|
991
|
+
@gntp.parse_header('Error-Description', 'test')
|
992
|
+
assert_equal ['Notification-Name', 'test'],
|
993
|
+
@gntp.parse_header('Notification-Name', 'test')
|
994
|
+
assert_equal ['Notification-Display-Name', 'test'],
|
995
|
+
@gntp.parse_header('Notification-Display-Name', 'test')
|
996
|
+
assert_equal ['Notification-ID', 'test'],
|
997
|
+
@gntp.parse_header('Notification-ID', 'test')
|
998
|
+
assert_equal ['Notification-Title', 'test'],
|
999
|
+
@gntp.parse_header('Notification-Title', 'test')
|
1000
|
+
assert_equal ['Notification-Text', 'test'],
|
1001
|
+
@gntp.parse_header('Notification-Text', 'test')
|
1002
|
+
assert_equal ['Notification-Coalescing-ID', 'test'],
|
1003
|
+
@gntp.parse_header('Notification-Coalescing-ID', 'test')
|
1004
|
+
assert_equal ['Notification-Callback-Context', 'test'],
|
1005
|
+
@gntp.parse_header('Notification-Callback-Context', 'test')
|
1006
|
+
assert_equal ['Notification-Callback-Context-Type', 'test'],
|
1007
|
+
@gntp.parse_header('Notification-Callback-Context-Type', 'test')
|
1008
|
+
assert_equal ['Notification-Callback-Result', 'test'],
|
1009
|
+
@gntp.parse_header('Notification-Callback-Result', 'test')
|
1010
|
+
assert_equal ['Notification-Callback-Target', 'test'],
|
1011
|
+
@gntp.parse_header('Notification-Callback-Target', 'test')
|
1012
|
+
assert_equal ['Subscriber-ID', 'test'],
|
1013
|
+
@gntp.parse_header('Subscriber-ID', 'test')
|
1014
|
+
assert_equal ['Subscriber-Name', 'test'],
|
1015
|
+
@gntp.parse_header('Subscriber-Name', 'test')
|
1016
|
+
assert_equal ['Origin-Machine-Name', 'test'],
|
1017
|
+
@gntp.parse_header('Origin-Machine-Name', 'test')
|
1018
|
+
assert_equal ['Origin-Sofware-Name', 'test'],
|
1019
|
+
@gntp.parse_header('Origin-Sofware-Name', 'test')
|
1020
|
+
assert_equal ['Origin-Software-Version', 'test'],
|
1021
|
+
@gntp.parse_header('Origin-Software-Version', 'test')
|
1022
|
+
assert_equal ['Origin-Platform-Name', 'test'],
|
1023
|
+
@gntp.parse_header('Origin-Platform-Name', 'test')
|
1024
|
+
assert_equal ['Origin-Platform-Version', 'test'],
|
1025
|
+
@gntp.parse_header('Origin-Platform-Version', 'test')
|
1026
|
+
end
|
1027
|
+
|
1028
|
+
def test_parse_header_url
|
1029
|
+
http = URI 'http://example/some?page'
|
1030
|
+
|
1031
|
+
assert_equal ['Application-Icon', http],
|
1032
|
+
@gntp.parse_header('Application-Icon',
|
1033
|
+
'http://example/some?page')
|
1034
|
+
|
1035
|
+
res = URI 'x-growl-resource://unique'
|
1036
|
+
assert_equal ['Notification-Icon', res],
|
1037
|
+
@gntp.parse_header('Notification-Icon',
|
1038
|
+
'x-growl-resource://unique')
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
def test_receive_callback
|
1042
|
+
packet = <<-PACKET
|
1043
|
+
GNTP/1.0 -CALLBACK NONE\r
|
1044
|
+
Response-Action: NOTIFY\r
|
1045
|
+
Notification-ID: 4\r
|
1046
|
+
Notification-Callback-Result: CLICKED\r
|
1047
|
+
Notification-Callback-Timestamp: 2012-03-28\r
|
1048
|
+
Notification-Callback-Context: context\r
|
1049
|
+
Notification-Callback-Context-Type: type\r
|
1050
|
+
Application-Name: test\r
|
1051
|
+
PACKET
|
1052
|
+
|
1053
|
+
headers = @gntp.receive packet
|
1054
|
+
|
1055
|
+
expected = {
|
1056
|
+
'Response-Action' => 'NOTIFY',
|
1057
|
+
'Notification-ID' => '4',
|
1058
|
+
'Notification-Callback-Result' => 'CLICKED',
|
1059
|
+
'Notification-Callback-Timestamp' => Time.parse('2012-03-28'),
|
1060
|
+
'Notification-Callback-Context' => 'context',
|
1061
|
+
'Notification-Callback-Context-Type' => 'type',
|
1062
|
+
'Application-Name' => 'test'
|
1063
|
+
}
|
1064
|
+
|
1065
|
+
assert_equal expected, headers
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
def test_receive_error
|
1069
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1070
|
+
"Error-Description: (null)\r\nError-Code: 200\r\n\r\n\r\n"
|
1071
|
+
|
1072
|
+
e = assert_raises Growl::GNTP::TimedOut do
|
1073
|
+
@gntp.receive packet
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
expected = {
|
1077
|
+
'Error-Code' => 200,
|
1078
|
+
'Error-Description' => nil,
|
1079
|
+
'Response-Action' => nil,
|
1080
|
+
}
|
1081
|
+
|
1082
|
+
assert_equal expected, e.headers
|
1083
|
+
|
1084
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1085
|
+
"Error-Description: (null)\r\nError-Code: 201\r\n\r\n\r\n"
|
1086
|
+
|
1087
|
+
assert_raises Growl::GNTP::NetworkFailure do
|
1088
|
+
@gntp.receive packet
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1092
|
+
"Error-Description: (null)\r\nError-Code: 300\r\n\r\n\r\n"
|
1093
|
+
|
1094
|
+
assert_raises Growl::GNTP::InvalidRequest do
|
1095
|
+
@gntp.receive packet
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1099
|
+
"Error-Description: (null)\r\nError-Code: 301\r\n\r\n\r\n"
|
1100
|
+
|
1101
|
+
assert_raises Growl::GNTP::UnknownProtocol do
|
1102
|
+
@gntp.receive packet
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1106
|
+
"Error-Description: (null)\r\nError-Code: 302\r\n\r\n\r\n"
|
1107
|
+
|
1108
|
+
assert_raises Growl::GNTP::UnknownProtocolVersion do
|
1109
|
+
@gntp.receive packet
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1113
|
+
"Error-Description: (null)\r\nError-Code: 303\r\n\r\n\r\n"
|
1114
|
+
|
1115
|
+
assert_raises Growl::GNTP::RequiredHeaderMissing do
|
1116
|
+
@gntp.receive packet
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1120
|
+
"Error-Description: (null)\r\nError-Code: 400\r\n\r\n\r\n"
|
1121
|
+
|
1122
|
+
assert_raises Growl::GNTP::NotAuthorized do
|
1123
|
+
@gntp.receive packet
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1127
|
+
"Error-Description: (null)\r\nError-Code: 401\r\n\r\n\r\n"
|
1128
|
+
|
1129
|
+
assert_raises Growl::GNTP::UnknownApplication do
|
1130
|
+
@gntp.receive packet
|
1131
|
+
end
|
1132
|
+
|
1133
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1134
|
+
"Error-Description: (null)\r\nError-Code: 402\r\n\r\n\r\n"
|
1135
|
+
|
1136
|
+
assert_raises Growl::GNTP::UnknownNotification do
|
1137
|
+
@gntp.receive packet
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1141
|
+
"Error-Description: (null)\r\nError-Code: 403\r\n\r\n\r\n"
|
1142
|
+
|
1143
|
+
assert_raises Growl::GNTP::AlreadyProcessed do
|
1144
|
+
@gntp.receive packet
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1148
|
+
"Error-Description: (null)\r\nError-Code: 404\r\n\r\n\r\n"
|
1149
|
+
|
1150
|
+
assert_raises Growl::GNTP::NotificationDisabled do
|
1151
|
+
@gntp.receive packet
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
packet = "GNTP/1.0 -ERROR NONE\r\nResponse-Action: (null)\r\n" \
|
1155
|
+
"Error-Description: (null)\r\nError-Code: 500\r\n\r\n\r\n"
|
1156
|
+
|
1157
|
+
assert_raises Growl::GNTP::InternalServerError do
|
1158
|
+
@gntp.receive packet
|
1159
|
+
end
|
1160
|
+
end
|
1161
|
+
|
1162
|
+
def test_receive_ok
|
1163
|
+
packet = "\r\nGNTP/1.0 -OK NONE\r\nResponse-Action: REGISTER\r\n\r\n\r\n"
|
1164
|
+
|
1165
|
+
headers = @gntp.receive packet
|
1166
|
+
|
1167
|
+
expected = {
|
1168
|
+
'Response-Action' => 'REGISTER'
|
1169
|
+
}
|
1170
|
+
|
1171
|
+
assert_equal expected, headers
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
def test_salt
|
1175
|
+
salt = @gntp.salt
|
1176
|
+
|
1177
|
+
assert_kind_of String, salt
|
1178
|
+
assert_equal 16, salt.length
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
def test_send
|
1182
|
+
stub_socket "GNTP/1.0 -OK NONE\r\nResponse-Action: REGISTER\r\n\r\n\r\n"
|
1183
|
+
|
1184
|
+
result = @gntp.send "hello"
|
1185
|
+
|
1186
|
+
expected = {
|
1187
|
+
'Response-Action' => 'REGISTER'
|
1188
|
+
}
|
1189
|
+
|
1190
|
+
assert_equal expected, result
|
1191
|
+
|
1192
|
+
assert_equal "hello", @gntp._socket._output.string
|
1193
|
+
|
1194
|
+
assert_empty @gntp._socket.read.strip
|
1195
|
+
end
|
1196
|
+
|
1197
|
+
def assert_endecrypt cipher, key, iv
|
1198
|
+
encrypted = cipher.update 'this is a test payload'
|
1199
|
+
encrypted << cipher.final
|
1200
|
+
|
1201
|
+
plain = decrypt cipher, key, iv, encrypted
|
1202
|
+
|
1203
|
+
assert_equal 'this is a test payload', plain
|
1204
|
+
end
|
1205
|
+
|
1206
|
+
def decrypt cipher, key, iv, encrypted
|
1207
|
+
decipher = OpenSSL::Cipher.new cipher.name
|
1208
|
+
decipher.decrypt
|
1209
|
+
decipher.key = key
|
1210
|
+
decipher.iv = iv
|
1211
|
+
|
1212
|
+
plain = decipher.update encrypted
|
1213
|
+
plain << decipher.final
|
1214
|
+
|
1215
|
+
plain
|
1216
|
+
end
|
1217
|
+
|
1218
|
+
def stub_salt
|
1219
|
+
def @gntp.salt
|
1220
|
+
[152, 215, 233, 14, 170, 24, 254, 65].pack 'C*'
|
1221
|
+
end
|
1222
|
+
end
|
1223
|
+
|
1224
|
+
def stub_socket response
|
1225
|
+
@gntp.instance_variable_set :@_response, response
|
1226
|
+
def @gntp.connect
|
1227
|
+
@_socket = Socket.new
|
1228
|
+
@_socket._input = @_response
|
1229
|
+
@_socket
|
1230
|
+
end
|
1231
|
+
|
1232
|
+
def @gntp._socket
|
1233
|
+
@_socket
|
1234
|
+
end
|
1235
|
+
end
|
1236
|
+
|
1237
|
+
end
|
1238
|
+
|