-
Notifications
You must be signed in to change notification settings - Fork 456
/
browserchannel.coffee
387 lines (311 loc) · 14.3 KB
/
browserchannel.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# Tests for the server's browserchannel frontend. The protocol is documented here:
#
# https://github.com/josephg/ShareJS/wiki/Wire-Protocol
testCase = require('nodeunit').testCase
assert = require 'assert'
{BCSocket} = require 'browserchannel'
server = require '../src/server'
types = require '../src/types'
helpers = require './helpers'
newDocName = helpers.newDocName
applyOps = helpers.applyOps
makePassPart = helpers.makePassPart
ANYOBJECT = new Object
# Helper method to check that subsequent data received by the callback is a particular
# set of values.
expectData = (socket, expectedData, callback) ->
expectedData = [expectedData] unless Array.isArray expectedData
socket.onmessage = (data) ->
expected = expectedData.shift()
if expected.meta == ANYOBJECT
assert.strictEqual typeof data.meta, 'object'
delete data.meta
delete expected.meta
assert.deepEqual expected, data
if expectedData.length == 0
socket.onmessage = (data) -> console.warn 'xxxx', data
callback()
module.exports = testCase
setUp: (callback) ->
@auth = (client, action) -> action.accept()
options =
browserchannel: {}
rest: null
socketio: null
db: {type: 'none'}
auth: (client, action) => @auth client, action
try
@model = server.createModel options
@server = server options, @model
@server.listen =>
@name = 'testingdoc'
# Open a new browserchannel session to the server
@socket = new BCSocket "http://localhost:#{@server.address().port}/channel"
@socket.onmessage = (data) =>
@id = data.auth
assert.ok @id
callback()
@socket.onerror = (e) -> console.warn 'eeee', e
@expect = (data, callback) =>
expectData @socket, data, callback
catch e
console.log e.stack
throw e
tearDown: (callback) ->
@socket.close()
# Its important the port has closed before the next test is run.
@server.on 'close', callback
@server.close()
'open an existing document with no version specified opens the document': (test) ->
@model.create @name, 'simple', =>
@socket.send {doc:@name, open:true}
@expect {doc:@name, v:0, open:true}, =>
@model.applyOp @name, {op:{position:0, text:'hi'}, v:0}, =>
@expect {v:0, op:{position:0, text:'hi'}, meta:ANYOBJECT}, ->
test.done()
'open an existing document with version specified opens the document': (test) ->
@model.create @name, 'simple', =>
@socket.send {doc:@name, open:true, v:0}
@expect {doc:@name, v:0, open:true}, =>
@model.applyOp @name, {op:{position:0, text:'hi'}, v:0}, =>
@expect {v:0, op:{position:0, text:'hi'}, meta:ANYOBJECT}, ->
test.done()
'open a nonexistant document with create:true creates the document': (test) ->
@socket.send {doc:@name, open:true, create:true, type:'simple'}
@expect {doc:@name, open:true, create:true, v:0}, =>
@model.getSnapshot @name, (error, docData) ->
test.deepEqual docData, {snapshot:{str:''}, v:0, type:types.simple, meta:{}}
test.done()
'open a nonexistant document without create fails': (test) ->
@socket.send {doc:@name, open:true}
@expect {doc:@name, open:false, error:'Document does not exist'}, =>
test.done()
'open a nonexistant document at a particular version without create fails': (test) ->
@socket.send {doc:@name, open:true, v:0}
@expect {doc:@name, open:false, error:'Document does not exist'}, =>
test.done()
'open a nonexistant document with snapshot:null fails normally': (test) ->
@socket.send {doc:@name, open:true, snapshot:null}
@expect {doc:@name, open:false, snapshot:null, error:'Document does not exist'}, =>
test.done()
'get a snapshot of a nonexistant document fails normally': (test) ->
@socket.send {doc:@name, snapshot:null}
@expect {doc:@name, snapshot:null, error:'Document does not exist'}, =>
test.done()
'open a nonexistant document with create:true and snapshot:null does not return the snapshot': (test) ->
# The snapshot can be inferred.
@socket.send {doc:@name, open:true, create:true, type:'text', snapshot:null}
@expect {doc:@name, open:true, create:true, v:0}, =>
test.done()
'open a document with a different type fails': (test) ->
@model.create @name, 'simple', =>
@socket.send {doc:@name, open:true, type:'text'}
@expect {doc:@name, open:false, error:'Type mismatch'}, =>
test.done()
'open an existing document with create:true opens the current document': (test) ->
@model.create @name, 'simple', =>
@model.applyOp @name, {op:{position:0, text:'hi'}, v:0}, =>
@socket.send {doc:@name, open:true, create:true, type:'simple', snapshot:null}
# The type isn't sent if it can be inferred.
@expect {doc:@name, create:false, open:true, v:1, snapshot:{str:'hi'}}, ->
test.done()
'open a document at a previous version and get ops since': (test) ->
@model.create @name, 'simple', =>
@model.applyOp @name, {op:{position:0, text:'hi'}, v:0}, =>
@socket.send {doc:@name, v:0, open:true, type:'simple'}
@expect [{doc:@name, v:0, open:true}, {v:0, op:{position:0, text:'hi'}, meta:ANYOBJECT}], ->
test.done()
'create a document without opening it': (test) ->
@socket.send {doc:@name, create:true, type:'simple'}
@expect {doc:@name, create:true}, =>
@model.getSnapshot @name, (error, docData) ->
test.deepEqual docData, {snapshot:{str:''}, v:0, type:types.simple, meta:{}}
test.done()
'create a document that already exists returns create:false': (test) ->
@model.create @name, 'simple', =>
@socket.send {doc:@name, create:true, type:'simple'}
@expect {doc:@name, create:false}, =>
test.done()
'create a document with snapshot:null returns create:true and no snapshot': (test) ->
@socket.send {doc:@name, create:true, type:'simple', snapshot:null}
@expect {doc:@name, create:true}, =>
test.done()
'receive ops through an open document': (test) ->
@socket.send {doc:@name, v:0, open:true, create:true, type:'simple'}
@expect {doc:@name, v:0, open:true, create:true}, =>
@model.applyOp @name, {op:{position:0, text:'hi'}, v:0}
@expect {v:0, op:{position:0, text:'hi'}, meta:ANYOBJECT}, ->
test.done()
'send an op': (test) ->
@model.create @name, 'simple', =>
listener = (opData) ->
test.strictEqual opData.v, 0
test.deepEqual opData.op, {position:0, text:'hi'}
test.done()
@model.listen @name, listener, (error, v) -> test.strictEqual v, 0
@socket.send {doc:@name, v:0, op:{position:0, text:'hi'}}
'send an op with metadata': (test) ->
@model.create @name, 'simple', =>
listener = (opData) ->
test.strictEqual opData.v, 0
test.strictEqual opData.meta.x, 5
test.deepEqual opData.op, {position:0, text:'hi'}
test.done()
@model.listen @name, listener, (error, v) -> test.strictEqual v, 0
@socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, meta:{x:5}}
'receive confirmation when an op is sent': (test) ->
@model.create @name, 'simple', =>
@socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, meta:{x:5}}
@expect {doc:@name, v:0}, ->
test.done()
'not be sent your own ops back': (test) ->
# @socket.onmessage = (data) ->
# test.notDeepEqual data.op, {position:0, text:'hi'} if data.op?
#
@socket.send {doc:@name, open:true, create:true, type:'simple'}
@socket.send {doc:@name, v:0, op:{position:0, text:'hi'}}
@expect [{doc:@name, v:0, open:true, create:true}, {v:0}], =>
# Gonna do this a dodgy way. Because I don't want to wait an undefined amount of time
# to make sure the op doesn't come, I'll trigger another op and make sure it recieves that.
# The second op should come after the first.
@expect {v:1, op:{position:0, text:'yo '}, meta:ANYOBJECT}, ->
test.done()
@model.applyOp @name, {v:1, op:{position:0, text:'yo '}}
'get a document snapshot': (test) ->
@model.create @name, 'simple', =>
@model.applyOp @name, {v:0, op:{position:0, text:'internet'}}, (error, _) =>
test.ifError(error)
@socket.send {doc:@name, snapshot:null}
@expect {doc:@name, snapshot:{str:'internet'}, v:1, type:'simple'}, ->
test.done()
'be able to close a document': (test) ->
name1 = newDocName()
name2 = newDocName()
@socket.send {doc:name1, open:true, create:true, type:'simple'}
@socket.send {open:false}
@socket.send {doc:name2, open:true, create:true, type:'text'}
@expect [{doc:name1, open:true, create:true, v:0}, {open:false}, {doc:name2, open:true, create:true, v:0}], =>
# name1 should be closed, and name2 should be open.
# We should only get the op for name2.
@model.applyOp name1, {v:0, op:{position:0, text:'Blargh!'}}, (error, appliedVersion) ->
test.fail error if error
@model.applyOp name2, {v:0, op:[{i:'hi', p:0}]}, (error, appliedVersion) ->
test.fail error if error
@expect {v:0, op:[{i:'hi', p:0}], meta:ANYOBJECT}, ->
test.done()
'doc names are sent in ops when necessary': (test) ->
name1 = newDocName()
name2 = newDocName()
@socket.send {doc:name1, open:true, create:true, type:'simple'}
@socket.send {doc:name2, open:true, create:true, type:'simple'}
passPart = makePassPart test, 3
@expect [{doc:name1, open:true, create:true, v:0}, {doc:name2, open:true, create:true, v:0}], =>
@model.applyOp name1, {v:0, op:{position:0, text:'a'}}, (error) =>
test.fail error if error
@model.applyOp name2, {v:0, op:{position:0, text:'b'}}, (error) =>
test.fail error if error
@model.applyOp name1, {v:1, op:{position:0, text:'c'}}, (error) =>
test.fail error if error
# All the ops that come through the socket should have the doc name set.
@socket.onmessage = (data) =>
test.strictEqual data.doc?, true
passPart()
"don't repeat document names": (test) ->
passPart = makePassPart test, 3
@socket.send {doc:@name, open:true, create:true, type:'simple'}
@expect {doc:@name, open:true, create:true, v:0}, =>
@socket.onmessage = (data) =>
# This time, none of the ops should have the document name set.
test.strictEqual data.doc?, false
passPart()
@socket.send {doc:@name, op:{position: 0, text:'a'}, v:0}
@socket.send {doc:@name, op:{position: 0, text:'b'}, v:1}
@socket.send {doc:@name, op:{position: 0, text:'c'}, v:2}
'an error message is sent through the socket if the operation is invalid': (test) ->
@model.create @name, 'simple', =>
# This might cause the model code to print out an error stack trace
@socket.send {doc:@name, v:0, op:{position:-100, text:'asdf'}}
@expect {doc:@name, v:null, error:'Invalid position'}, ->
test.done()
'creating a document with a null doc name creates a new doc': (test) ->
@socket.send {doc:null, create:true, type:'simple'}
@socket.onmessage = (data) =>
test.strictEqual data.create, true
test.equal typeof data.doc, 'string'
test.ok data.doc.length > 8
@model.getSnapshot data.doc, (error, docData) ->
test.deepEqual docData, {snapshot:{str:''}, v:0, type:types.simple, meta:{}}
test.done()
# ---- Auth-related tests
'The auth client object is persisted across requests': (test) ->
c = null
@auth = (client, action) =>
if c
test.strictEqual c, client
else
c = client
action.accept()
@socket.send {doc:@name, open:true, create:true, type:'simple'}
@expect {doc:@name, open:true, create:true, v:0}, =>
@socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, meta:{x:5}}
@expect {v:0}, ->
test.expect 3
test.done()
'Cannot connect if auth rejects you': (test) ->
@auth = (client, action) ->
test.strictEqual action.type, 'connect'
test.ok client.remoteAddress in ['localhost', '127.0.0.1'] # Is there a nicer way to do this?
test.strictEqual typeof client.id, 'string'
test.ok client.id.length > 5
test.ok client.connectTime
test.strictEqual typeof client.headers, 'object'
# I can't edit the headers using socket.io-client's API. I'd test the default headers in this
# object, but the default XHR headers aren't part of socket.io's API, so they could change between
# versions and break the test.
test.strictEqual client.headers['user-agent'], 'node.js'
action.reject()
socket = new BCSocket "http://localhost:#{@server.address().port}/channel"
expectData socket, {auth:null, error:'forbidden'}, ->
socket.onclose = ->
test.expect 7
test.done()
'Cannot open a document if auth rejects you': (test) ->
@auth = (client, action) =>
if action.name == 'open'
action.reject()
else
action.accept()
@model.create @name, 'simple', =>
@socket.send {doc:@name, open:true}
@expect {doc:@name, open:false, error:'forbidden'}, ->
test.done()
'Cannot open a document if you cannot get a snapshot': (test) ->
@auth = (client, action) =>
if action.name == 'get snapshot'
action.reject()
else
action.accept()
@model.create @name, 'simple', =>
@socket.send {doc:@name, open:true, snapshot:null}
@expect {doc:@name, open:false, snapshot:null, error:'forbidden'}, ->
test.done()
'Cannot create a document if youre not allowed to create': (test) ->
@auth = (client, action) =>
if action.name == 'create'
action.reject()
else
action.accept()
@socket.send {doc:@name, open:true, create:true, type:'simple'}
@expect {doc:@name, open:false, error:'forbidden'}, ->
test.done()
'Cannot submit an op if auth rejects you': (test) ->
@auth = (client, action) ->
if action.type == 'update'
action.reject()
else
action.accept()
@socket.send {doc:@name, open:true, create:true, type:'simple', snapshot:null}
@expect {doc:@name, open:true, create:true, v:0}, =>
@socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, meta:{}}
@expect {v:null, error:'forbidden'}, ->
test.done()