jschat 0.1.1

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