jschat 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. data/MIT-LICENSE +23 -0
  2. data/README.textile +71 -0
  3. data/bin/jschat-client +3 -0
  4. data/bin/jschat-server +18 -0
  5. data/bin/jschat-web +7 -0
  6. data/lib/jschat/client.rb +745 -0
  7. data/lib/jschat/errors.rb +41 -0
  8. data/lib/jschat/flood_protection.rb +39 -0
  9. data/lib/jschat/http/config/sprockets.yml +7 -0
  10. data/lib/jschat/http/config.ru +12 -0
  11. data/lib/jschat/http/jschat.rb +264 -0
  12. data/lib/jschat/http/public/favicon.ico +0 -0
  13. data/lib/jschat/http/public/images/emoticons/angry.gif +0 -0
  14. data/lib/jschat/http/public/images/emoticons/arr.gif +0 -0
  15. data/lib/jschat/http/public/images/emoticons/blink.gif +0 -0
  16. data/lib/jschat/http/public/images/emoticons/blush.gif +0 -0
  17. data/lib/jschat/http/public/images/emoticons/brucelee.gif +0 -0
  18. data/lib/jschat/http/public/images/emoticons/btw.gif +0 -0
  19. data/lib/jschat/http/public/images/emoticons/chuckle.gif +0 -0
  20. data/lib/jschat/http/public/images/emoticons/clap.gif +0 -0
  21. data/lib/jschat/http/public/images/emoticons/cool.gif +0 -0
  22. data/lib/jschat/http/public/images/emoticons/drool.gif +0 -0
  23. data/lib/jschat/http/public/images/emoticons/drunk.gif +0 -0
  24. data/lib/jschat/http/public/images/emoticons/dry.gif +0 -0
  25. data/lib/jschat/http/public/images/emoticons/eek.gif +0 -0
  26. data/lib/jschat/http/public/images/emoticons/flex.gif +0 -0
  27. data/lib/jschat/http/public/images/emoticons/happy.gif +0 -0
  28. data/lib/jschat/http/public/images/emoticons/holmes.gif +0 -0
  29. data/lib/jschat/http/public/images/emoticons/huh.gif +0 -0
  30. data/lib/jschat/http/public/images/emoticons/laugh.gif +0 -0
  31. data/lib/jschat/http/public/images/emoticons/lol.gif +0 -0
  32. data/lib/jschat/http/public/images/emoticons/mad.gif +0 -0
  33. data/lib/jschat/http/public/images/emoticons/mellow.gif +0 -0
  34. data/lib/jschat/http/public/images/emoticons/noclue.gif +0 -0
  35. data/lib/jschat/http/public/images/emoticons/oh.gif +0 -0
  36. data/lib/jschat/http/public/images/emoticons/ohmy.gif +0 -0
  37. data/lib/jschat/http/public/images/emoticons/ph34r.gif +0 -0
  38. data/lib/jschat/http/public/images/emoticons/pimp.gif +0 -0
  39. data/lib/jschat/http/public/images/emoticons/punch.gif +0 -0
  40. data/lib/jschat/http/public/images/emoticons/realmad.gif +0 -0
  41. data/lib/jschat/http/public/images/emoticons/rock.gif +0 -0
  42. data/lib/jschat/http/public/images/emoticons/rofl.gif +0 -0
  43. data/lib/jschat/http/public/images/emoticons/rolleyes.gif +0 -0
  44. data/lib/jschat/http/public/images/emoticons/sad.gif +0 -0
  45. data/lib/jschat/http/public/images/emoticons/scratch.gif +0 -0
  46. data/lib/jschat/http/public/images/emoticons/shifty.gif +0 -0
  47. data/lib/jschat/http/public/images/emoticons/shock.gif +0 -0
  48. data/lib/jschat/http/public/images/emoticons/shrug.gif +0 -0
  49. data/lib/jschat/http/public/images/emoticons/sleep.gif +0 -0
  50. data/lib/jschat/http/public/images/emoticons/sleeping.gif +0 -0
  51. data/lib/jschat/http/public/images/emoticons/smile.gif +0 -0
  52. data/lib/jschat/http/public/images/emoticons/suicide.gif +0 -0
  53. data/lib/jschat/http/public/images/emoticons/sweat.gif +0 -0
  54. data/lib/jschat/http/public/images/emoticons/thumbs.gif +0 -0
  55. data/lib/jschat/http/public/images/emoticons/tongue.gif +0 -0
  56. data/lib/jschat/http/public/images/emoticons/unsure.gif +0 -0
  57. data/lib/jschat/http/public/images/emoticons/w00t.gif +0 -0
  58. data/lib/jschat/http/public/images/emoticons/wacko.gif +0 -0
  59. data/lib/jschat/http/public/images/emoticons/whistling.gif +0 -0
  60. data/lib/jschat/http/public/images/emoticons/wink.gif +0 -0
  61. data/lib/jschat/http/public/images/emoticons/worship.gif +0 -0
  62. data/lib/jschat/http/public/images/emoticons/yucky.gif +0 -0
  63. data/lib/jschat/http/public/images/jschat.gif +0 -0
  64. data/lib/jschat/http/public/images/shadow.png +0 -0
  65. data/lib/jschat/http/public/javascripts/app/controllers/chat_controller.js +191 -0
  66. data/lib/jschat/http/public/javascripts/app/controllers/signon_controller.js +56 -0
  67. data/lib/jschat/http/public/javascripts/app/helpers/emote_helper.js +23 -0
  68. data/lib/jschat/http/public/javascripts/app/helpers/form_helpers.js +37 -0
  69. data/lib/jschat/http/public/javascripts/app/helpers/link_helper.js +47 -0
  70. data/lib/jschat/http/public/javascripts/app/helpers/page_helper.js +27 -0
  71. data/lib/jschat/http/public/javascripts/app/helpers/text_helper.js +92 -0
  72. data/lib/jschat/http/public/javascripts/app/lib/split.js +78 -0
  73. data/lib/jschat/http/public/javascripts/app/models/cookie.js +27 -0
  74. data/lib/jschat/http/public/javascripts/app/protocol/change.js +15 -0
  75. data/lib/jschat/http/public/javascripts/app/protocol/chat_request.js +13 -0
  76. data/lib/jschat/http/public/javascripts/app/protocol/display.js +147 -0
  77. data/lib/jschat/http/public/javascripts/app/ui/commands.js +55 -0
  78. data/lib/jschat/http/public/javascripts/app/ui/tab_completion.js +122 -0
  79. data/lib/jschat/http/public/javascripts/init.js +19 -0
  80. data/lib/jschat/http/public/stylesheets/iphone.css +3 -0
  81. data/lib/jschat/http/public/stylesheets/screen.css +68 -0
  82. data/lib/jschat/http/script/sprockets.rb +14 -0
  83. data/lib/jschat/http/tmp/restart.txt +0 -0
  84. data/lib/jschat/http/views/index.erb +23 -0
  85. data/lib/jschat/http/views/iphone.erb +29 -0
  86. data/lib/jschat/http/views/layout.erb +29 -0
  87. data/lib/jschat/http/views/message_form.erb +15 -0
  88. data/lib/jschat/server.rb +503 -0
  89. data/test/server_test.rb +175 -0
  90. data/test/stateless_test.rb +33 -0
  91. data/test/test_helper.rb +61 -0
  92. metadata +223 -0
@@ -0,0 +1,503 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'json'
4
+ require 'time'
5
+ require 'socket'
6
+
7
+ # JsChat libraries
8
+ require 'jschat/errors'
9
+ require 'jschat/flood_protection'
10
+
11
+ module JsChat
12
+ STATELESS_TIMEOUT = 60
13
+
14
+ class User
15
+ include JsChat::FloodProtection
16
+
17
+ attr_accessor :name, :connection, :rooms, :last_activity,
18
+ :identified, :ip, :last_poll
19
+
20
+ def initialize(connection)
21
+ @name = nil
22
+ @connection = connection
23
+ @rooms = []
24
+ @last_activity = Time.now.utc
25
+ @last_poll = Time.now.utc
26
+ @identified = false
27
+ @ip = ''
28
+ end
29
+
30
+ def to_json
31
+ { 'name' => @name, 'last_activity' => @last_activity }.to_json
32
+ end
33
+
34
+ def name=(name)
35
+ if @connection and @connection.name_taken? name
36
+ raise JsChat::Errors::InvalidName.new(:name_taken, 'Name taken')
37
+ elsif User.valid_name?(name)
38
+ @identified = true
39
+ @name = name
40
+ else
41
+ raise JsChat::Errors::InvalidName.new(:invalid_name, 'Invalid name')
42
+ end
43
+ end
44
+
45
+ def self.valid_name?(name)
46
+ not name.match /[^[:alnum:]._\-\[\]^C]/ and name.size > 0
47
+ end
48
+
49
+ def private_message(message)
50
+ response = { 'display' => 'message', 'message' => message }
51
+ @connection.send_response response
52
+ end
53
+
54
+ def change(params)
55
+ # Valid options for change
56
+ ['name'].each do |field|
57
+ if params[field]
58
+ old_value = send(field)
59
+ send "#{field}=", params[field]
60
+ @rooms.each do |room|
61
+ response = { 'change' => 'user',
62
+ 'room' => room.name,
63
+ 'user' => { field => { old_value => params[field] } } }
64
+ room.change_notice self, response
65
+ return [field, params[field]]
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ class Room
73
+ attr_accessor :name, :users
74
+
75
+ def initialize(name)
76
+ @name = name
77
+ @users = []
78
+ end
79
+
80
+ def self.valid_name?(name)
81
+ User.valid_name?(name[1..-1]) and name[0].chr == '#'
82
+ end
83
+
84
+ def self.find(item)
85
+ @@rooms ||= []
86
+
87
+ if item.kind_of? String
88
+ @@rooms.find { |room| room.name.downcase == item.downcase if room.name }
89
+ elsif item.kind_of? User
90
+ @@rooms.find_all { |room| room.users.include? item }
91
+ end
92
+ end
93
+
94
+ def self.find_or_create(room_name)
95
+ room = find room_name
96
+ if room.nil?
97
+ room = new(room_name)
98
+ @@rooms << room
99
+ end
100
+ room
101
+ end
102
+
103
+ def lastlog(since = nil)
104
+ { 'display' => 'messages', 'messages' => messages_since(since) }
105
+ end
106
+
107
+ def messages_since(since)
108
+ if since.nil?
109
+ @messages
110
+ else
111
+ @messages.select { |m| message_time(m) > since }
112
+ end
113
+ end
114
+
115
+ def message_time(message)
116
+ if message.has_key? 'display'
117
+ message[message['display']]['time']
118
+ elsif message.has_key? 'change'
119
+ message[message['change']]['time']
120
+ end
121
+ end
122
+
123
+ def add_to_lastlog(message)
124
+ @messages ||= []
125
+ if message
126
+ if message.has_key? 'display'
127
+ message[message['display']]['time'] = Time.now.utc
128
+ elsif message.has_key? 'change'
129
+ message[message['change']]['time'] = Time.now.utc
130
+ end
131
+ @messages.push message
132
+ @messages = @messages[-100..-1] if @messages.size > 100
133
+ end
134
+ end
135
+
136
+ def join(user)
137
+ if @users.include? user
138
+ Error.new(:already_joined, 'Already in that room')
139
+ else
140
+ @users << user
141
+ user.rooms << self
142
+ join_notice user
143
+ { 'display' => 'join', 'join' => { 'user' => user.name, 'room' => @name } }
144
+ end
145
+ end
146
+
147
+ def part(user)
148
+ if not @users.include?(user)
149
+ Error.new(:not_in_room, 'Not in that room')
150
+ else
151
+ user.rooms.delete_if { |r| r == self }
152
+ @users.delete_if { |u| u == user }
153
+ part_notice user
154
+ { 'display' => 'part', 'part' => { 'user' => user.name, 'room' => @name } }
155
+ end
156
+ end
157
+
158
+ def send_message(message)
159
+ message['room'] = name
160
+ response = { 'display' => 'message', 'message' => message }
161
+
162
+ add_to_lastlog response
163
+
164
+ @users.each do |user|
165
+ user.connection.send_response response
166
+ end
167
+ end
168
+
169
+ def member_names
170
+ @users.collect { |user| user.name }
171
+ end
172
+
173
+ def to_json
174
+ { 'name' => @name, 'members' => member_names }.to_json
175
+ end
176
+
177
+ def notice(user, message, all = false)
178
+ add_to_lastlog message
179
+
180
+ @users.each do |u|
181
+ if (u != user and !all) or all
182
+ u.connection.send_response(message)
183
+ end
184
+ end
185
+ end
186
+
187
+ def change_notice(user, response)
188
+ notice(user, response, true)
189
+ end
190
+
191
+ def join_notice(user)
192
+ notice(user, { 'display' => 'join_notice', 'join_notice' => { 'user' => user.name, 'room' => @name } })
193
+ end
194
+
195
+ def part_notice(user)
196
+ notice(user, { 'display' => 'part_notice', 'part_notice' => { 'user' => user.name, 'room' => @name } })
197
+ end
198
+
199
+ def quit_notice(user)
200
+ notice(user, { 'display' => 'quit_notice', 'quit_notice' => { 'user' => user.name, 'room' => @name } })
201
+ @users.delete_if { |u| u == user }
202
+ end
203
+ end
204
+
205
+ # User initially has a nil name
206
+ def users_with_names
207
+ @@users.find_all { |u| u.name }
208
+ end
209
+
210
+ def name_taken?(name)
211
+ users_with_names.find { |user| user.name.downcase == name.downcase }
212
+ end
213
+
214
+ # {"identify":"alex"}
215
+ def identify(name, ip, options = {})
216
+ if @user and @user.identified
217
+ Error.new :already_identified, 'You have already identified'
218
+ elsif name_taken? name
219
+ Error.new :name_taken, 'Name already taken'
220
+ else
221
+ @user.name = name
222
+ @user.ip = ip
223
+ register_stateless_user if @stateless
224
+ { 'display' => 'identified', 'identified' => @user }
225
+ end
226
+ rescue JsChat::Errors::InvalidName => exception
227
+ exception
228
+ end
229
+
230
+ def lastlog(room, options = {})
231
+ room = Room.find room
232
+ if room and room.users.include? @user
233
+ room.lastlog
234
+ else
235
+ Error.new(:not_in_room, "Please join this room first")
236
+ end
237
+ end
238
+
239
+ def since(room, options = {})
240
+ room = Room.find room
241
+ if room and room.users.include? @user
242
+ response = room.lastlog(@user.last_poll)
243
+ @user.last_poll = Time.now.utc
244
+ response
245
+ else
246
+ Error.new(:not_in_room, "Please join this room first")
247
+ end
248
+ end
249
+
250
+ def ping(message, options = {})
251
+ if @user and @user.last_poll and Time.now.utc > @user.last_poll
252
+ time = Time.now.utc
253
+ { 'pong' => time }
254
+ else
255
+ # TODO: HANDLE PING OUTS
256
+ Error.new(:ping_out, 'Your connection has been lost')
257
+ end
258
+ end
259
+
260
+ def quit(message, options = {})
261
+ if @user
262
+ disconnect_user @user
263
+ end
264
+ end
265
+
266
+ def room_message(message, options)
267
+ room = Room.find options['to']
268
+ if room and room.users.include? @user
269
+ room.send_message({ 'message' => message, 'user' => @user.name, 'time' => Time.now.utc })
270
+ else
271
+ send_response Error.new(:not_in_room, "Please join this room first")
272
+ end
273
+ end
274
+
275
+ def private_message(message, options)
276
+ user = users_with_names.find { |u| u.name.downcase == options['to'].downcase }
277
+ if user
278
+ # Return the message to the user, and send it to the other person too
279
+ now = Time.now.utc
280
+ user.private_message({ 'message' => message, 'user' => @user.name, 'time' => now })
281
+ @user.private_message({ 'message' => message, 'user' => @user.name, 'time' => now })
282
+ else
283
+ Error.new(:not_online, 'User not online')
284
+ end
285
+ end
286
+
287
+ def send_message(message, options)
288
+ if options['to'].nil?
289
+ send_response Error.new(:to_required, 'Please specify who to send the message to or join a channel')
290
+ elsif options['to'][0].chr == '#'
291
+ room_message message, options
292
+ else
293
+ private_message message, options
294
+ end
295
+ end
296
+
297
+ def join(room_name, options = {})
298
+ if Room.valid_name? room_name
299
+ room = Room.find_or_create(room_name)
300
+ room.join @user
301
+ else
302
+ Error.new(:invalid_room, 'Invalid room name')
303
+ end
304
+ end
305
+
306
+ def part(room_name, options = {})
307
+ room = @user.rooms.find { |r| r.name == room_name }
308
+ if room
309
+ room.part @user
310
+ else
311
+ Error.new(:not_in_room, "You are not in that room")
312
+ end
313
+ end
314
+
315
+ def names(room_name, options = {})
316
+ room = Room.find(room_name)
317
+ if room
318
+ { 'display' => 'names', 'names' => room.users, 'room' => room.name }
319
+ else
320
+ Error.new(:room_not_available, 'No such room')
321
+ end
322
+ end
323
+
324
+ def new_cookie
325
+ chars = ("a".."z").to_a + ("1".."9").to_a
326
+ Array.new(8, '').collect { chars[rand(chars.size)] }.join
327
+ end
328
+
329
+ def register_stateless_cient
330
+ @stateless_cookie = new_cookie
331
+ user = User.new(self)
332
+ @@stateless_cookies << { :cookie => @stateless_cookie, :user => user }
333
+ @@users << user
334
+ { 'cookie' => @stateless_cookie }
335
+ end
336
+
337
+ def current_stateless_client
338
+ @@stateless_cookies.find { |c| c[:cookie] == @stateless_cookie }
339
+ end
340
+
341
+ def register_stateless_user
342
+ current_stateless_client[:user] = @user
343
+ end
344
+
345
+ def valid_stateless_user?
346
+ current_stateless_client
347
+ end
348
+
349
+ def load_stateless_user
350
+ if client = current_stateless_client
351
+ @user = client[:user]
352
+ @stateless = true
353
+ else
354
+ raise JsChat::Errors::InvalidCookie.new(:invalid_cookie, 'Invalid cookie')
355
+ end
356
+ end
357
+
358
+ def disconnect_lagged_users
359
+ @@stateless_cookies.delete_if do |cookie|
360
+ lagged?(cookie[:user].last_poll) ? disconnect_user(cookie[:user]) && true : false
361
+ end
362
+ end
363
+
364
+ def lagged?(time)
365
+ Time.now.utc - time > STATELESS_TIMEOUT
366
+ end
367
+
368
+ def unbind
369
+ return if @stateless
370
+ disconnect_user(@user)
371
+ @user = nil
372
+ end
373
+
374
+ def disconnect_user(user)
375
+ log :info, "Removing a connection"
376
+ Room.find(user).each do |room|
377
+ room.quit_notice user
378
+ end
379
+
380
+ @@users.delete_if { |u| u == user }
381
+ end
382
+
383
+ def post_init
384
+ @@users ||= []
385
+ @@stateless_cookies ||= []
386
+ @user = User.new(self)
387
+ @@users << @user
388
+ end
389
+
390
+ def log(level, message)
391
+ if Object.const_defined? :ServerConfig and ServerConfig[:logger]
392
+ if @user
393
+ message = "#{@user.name} (#{@user.ip}): #{message}"
394
+ end
395
+ ServerConfig[:logger].send level, message
396
+ end
397
+ end
398
+
399
+ def change(change, options = {})
400
+ if change == 'user'
401
+ field, value = @user.send :change, options[change]
402
+ { 'display' => 'notice', 'notice' => "Your #{field} has been changed to: #{value}" }
403
+ else
404
+ Error.new(:invalid_request, "Invalid change request")
405
+ end
406
+ rescue JsChat::Errors::InvalidName => exception
407
+ exception
408
+ end
409
+
410
+ def send_response(data)
411
+ response = ''
412
+ case data
413
+ when String
414
+ response = data
415
+ when Error
416
+ response = data.to_json + "\n"
417
+ log :error, data.message
418
+ else
419
+ # Other objects should be safe for to_json
420
+ response = data.to_json + "\n"
421
+ log :info, response.strip
422
+ end
423
+
424
+ send_data response
425
+ end
426
+
427
+ include EM::Protocols::LineText2
428
+
429
+ def get_remote_ip
430
+ Socket.unpack_sockaddr_in(get_peername)[1]
431
+ end
432
+
433
+ def receive_line(data)
434
+ response = ''
435
+ disconnect_lagged_users
436
+
437
+ if data and data.size > ServerConfig[:max_message_length]
438
+ raise JsChat::Errors::MessageTooLong.new(:message_too_long, 'Message too long')
439
+ end
440
+
441
+ data.chomp.split("\n").each do |line|
442
+ # Receive the identify request
443
+ input = JSON.parse line
444
+
445
+ @user.seen!
446
+
447
+ # Unbind when a stateless connection doesn't match the cookie
448
+ if input.has_key?('cookie')
449
+ @stateless_cookie = input['cookie']
450
+ load_stateless_user
451
+ end
452
+
453
+ if input.has_key? 'protocol'
454
+ if input['protocol'] == 'stateless'
455
+ @stateless = true
456
+ response << send_response(register_stateless_cient)
457
+ end
458
+ elsif input.has_key? 'identify'
459
+ input['ip'] ||= get_remote_ip
460
+ response << send_response(identify(input['identify'], input['ip']))
461
+ else
462
+ ['lastlog', 'change', 'send', 'join', 'names', 'part', 'since', 'ping', 'quit'].each do |command|
463
+ if @user.name.nil?
464
+ response << send_response(Error.new(:identity_required, "Identify first"))
465
+ return response
466
+ end
467
+
468
+ if input.has_key? command
469
+ if command == 'send'
470
+ @user.last_activity = Time.now.utc
471
+ message_result = send('send_message', input[command], input)
472
+ response << message_result if message_result.kind_of? String
473
+ else
474
+ result = send_response(send(command, input[command], input))
475
+ response << result if result.kind_of? String
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end
481
+
482
+ response
483
+ rescue JsChat::Errors::StillFlooding
484
+ ""
485
+ rescue JsChat::Errors::Flooding => exception
486
+ send_response exception
487
+ rescue JsChat::Errors::MessageTooLong => exception
488
+ send_response exception
489
+ rescue JsChat::Errors::InvalidCookie => exception
490
+ send_response exception
491
+ rescue Exception => exception
492
+ puts "Data that raised exception: #{exception}"
493
+ p data
494
+ print_call_stack
495
+ end
496
+
497
+ def print_call_stack(from = 2, to = 5)
498
+ puts "Stack:"
499
+ (from..to).each do |index|
500
+ puts "\t#{caller[index]}"
501
+ end
502
+ end
503
+ end
@@ -0,0 +1,175 @@
1
+ require 'test_helper'
2
+
3
+ class TestJsChat < Test::Unit::TestCase
4
+ include JsChatHelpers
5
+
6
+ def setup
7
+ @jschat = JsChatMock.new
8
+ @jschat.post_init
9
+ end
10
+
11
+ def teardown
12
+ @jschat.reset
13
+ end
14
+
15
+ def test_identify
16
+ response = JSON.parse @jschat.receive_line({ 'identify' => 'alex' }.to_json)
17
+ assert_equal 'identified', response['display']
18
+ end
19
+
20
+ def test_invalid_identify
21
+ response = JSON.parse @jschat.receive_line({ 'identify' => '@lex' }.to_json)
22
+ assert_equal JsChat::Errors::Codes.invert[:invalid_name], response['error']['code']
23
+ end
24
+
25
+ def test_identify_twice_fails
26
+ identify_as 'alex'
27
+ result = JSON.parse identify_as('alex')
28
+ assert_equal 106, result['error']['code']
29
+ end
30
+
31
+ def test_ensure_nicks_are_longer_than_0
32
+ result = JSON.parse identify_as('')
33
+ assert result['error']
34
+ end
35
+
36
+ def test_ensure_nicks_are_unique
37
+ identify_as 'alex'
38
+
39
+ # Obvious duplicate
40
+ result = identify_as 'alex'
41
+ assert result['error']
42
+
43
+ # Case
44
+ result = identify_as 'Alex'
45
+ assert result['error']
46
+ end
47
+
48
+ def test_invalid_room_name
49
+ identify_as 'bob'
50
+ response = JSON.parse @jschat.receive_line({ 'join' => 'oublinet' }.to_json)
51
+ assert_equal 'Invalid room name', response['error']['message']
52
+ end
53
+
54
+ def test_join
55
+ identify_as 'bob'
56
+ expected = { 'display' => 'join', 'join' => { 'user' => 'bob', 'room' => '#oublinet' } }.to_json + "\n"
57
+ assert_equal expected, @jschat.receive_line({ 'join' => '#oublinet' }.to_json)
58
+ end
59
+
60
+ def test_join_without_identifying
61
+ response = JSON.parse @jschat.receive_line({ 'join' => '#oublinet' }.to_json)
62
+ assert response['error']
63
+ end
64
+
65
+ def test_join_more_than_once
66
+ identify_as 'bob'
67
+
68
+ expected = { 'display' => 'error', 'error' => { 'message' => 'Already in that room' } }.to_json + "\n"
69
+ @jschat.receive_line({ 'join' => '#oublinet' }.to_json)
70
+ response = JSON.parse @jschat.receive_line({ 'join' => '#oublinet' }.to_json)
71
+ assert_equal JsChat::Errors::Codes.invert[:already_joined], response['error']['code']
72
+ end
73
+
74
+ def test_names
75
+ identify_as 'nick', '#oublinet'
76
+
77
+ # Add a user
78
+ @jschat.add_user 'alex', '#oublinet'
79
+
80
+ response = JSON.parse(@jschat.receive_line({ 'names' => '#oublinet' }.to_json))
81
+ assert response['names']
82
+ end
83
+
84
+ def test_valid_names
85
+ user = JsChat::User.new nil
86
+ ['alex*', "alex\n"].each do |name|
87
+ assert_raises JsChat::Errors::InvalidName do
88
+ user.name = name
89
+ end
90
+ end
91
+ end
92
+
93
+ def test_message_not_in_room
94
+ identify_as 'nick', '#oublinet'
95
+ @jschat.add_user 'alex', '#oublinet'
96
+ response = JSON.parse @jschat.receive_line({ 'send' => 'hello', 'to' => '#merk' }.to_json)
97
+ assert_equal 'Please join this room first', response['error']['message']
98
+ end
99
+
100
+ def test_message
101
+ identify_as 'nick', '#oublinet'
102
+ @jschat.add_user 'alex', '#oublinet'
103
+ assert @jschat.receive_line({ 'send' => 'hello', 'to' => '#oublinet' }.to_json)
104
+ end
105
+
106
+ def test_message_ignores_case
107
+ identify_as 'nick', '#oublinet'
108
+ @jschat.add_user 'alex', '#oublinet'
109
+ response = @jschat.receive_line({ 'send' => 'hello', 'to' => '#Oublinet' }.to_json)
110
+ assert response
111
+ end
112
+
113
+ def test_part
114
+ identify_as 'nick', '#oublinet'
115
+ @jschat.add_user 'alex', '#oublinet'
116
+ response = JSON.parse @jschat.receive_line({ 'part' => '#oublinet'}.to_json)
117
+ assert_equal '#oublinet', response['part']['room']
118
+ end
119
+
120
+ def test_private_message
121
+ identify_as 'nick'
122
+ @jschat.add_user 'alex', '#oublinet'
123
+ response = JSON.parse @jschat.receive_line({ 'send' => 'hello', 'to' => 'alex' }.to_json)
124
+ assert_equal 'hello', response['message']['message']
125
+ end
126
+
127
+ def test_private_message_ignores_case
128
+ identify_as 'nick'
129
+ @jschat.add_user 'alex', '#oublinet'
130
+ response = JSON.parse @jschat.receive_line({ 'send' => 'hello', 'to' => 'Alex' }.to_json)
131
+ assert_equal 'hello', response['message']['message']
132
+ end
133
+
134
+ def test_log_request
135
+ identify_as 'nick', '#oublinet'
136
+ @jschat.receive_line({ 'send' => 'hello', 'to' => '#oublinet' }.to_json)
137
+ response = JSON.parse @jschat.receive_line({ 'lastlog' => '#oublinet' }.to_json)
138
+ assert_equal 'hello', response['messages'].last['message']['message']
139
+ end
140
+
141
+ def test_name_change
142
+ identify_as 'nick', '#oublinet'
143
+ @jschat.add_user 'alex', '#oublinet'
144
+ response = JSON.parse @jschat.receive_line({ 'change' => 'user', 'user' => { 'name' => 'bob' }}.to_json)
145
+ assert_equal 'notice', response['display']
146
+ end
147
+
148
+ def test_name_change_duplicate
149
+ identify_as 'nick', '#oublinet'
150
+ @jschat.add_user 'alex', '#oublinet'
151
+ response = JSON.parse @jschat.receive_line({ 'change' => 'user', 'user' => { 'name' => 'alex' }}.to_json)
152
+ assert_equal 'error', response['display']
153
+ end
154
+
155
+ def test_max_message_length
156
+ identify_as 'nick', '#oublinet'
157
+ response = JSON.parse @jschat.receive_line({ 'send' => 'a' * 1000, 'to' => '#oublinet' }.to_json)
158
+ assert response['error']
159
+ end
160
+
161
+ def test_flood_protection
162
+ identify_as 'nick', '#oublinet'
163
+ response = ''
164
+ # simulate a flood and extract the error response
165
+ (1..50).detect do
166
+ response = @jschat.receive_line({ 'send' => 'a' * 10, 'to' => '#oublinet' }.to_json)
167
+ response.match /error/
168
+ end
169
+ response = JSON.parse response
170
+ assert response['error']
171
+ assert_match /wait a few seconds/i, response['error']['message']
172
+ end
173
+
174
+ end
175
+
@@ -0,0 +1,33 @@
1
+ require 'test_helper'
2
+
3
+ class TestJsChat < Test::Unit::TestCase
4
+ include JsChatHelpers
5
+
6
+ def setup
7
+ @jschat = JsChatMock.new
8
+ @jschat.post_init
9
+ @cookie = get_cookie
10
+ end
11
+
12
+ def test_identify
13
+ response = send_to_jschat({ 'identify' => 'alex', 'cookie' => @cookie })
14
+ assert_equal 'identified', response['display']
15
+ end
16
+
17
+ def test_join
18
+ response = identify_as 'alex2', '#jschat'
19
+ assert JSON.parse(response)['join']
20
+ end
21
+
22
+ def test_message
23
+ response = identify_as 'nick', '#jschat'
24
+ assert send_to_jschat({ 'cookie' => @cookie, 'send' => 'hello', 'to' => '#jschat' }, false)
25
+ end
26
+
27
+ private
28
+
29
+ def get_cookie
30
+ JSON.parse(@jschat.receive_line({ 'protocol' => 'stateless' }.to_json))['cookie']
31
+ end
32
+
33
+ end