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