ruby-growl 3.0 → 4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+