1 | var assert = require('assert');
|
2 | var http = require('http');
|
3 | var os = require('os');
|
4 | var request = require('supertest');
|
5 | var spdy = require('spdy');
|
6 | var zetta = require('../');
|
7 | var Query = require('calypso').Query;
|
8 | var rels = require('zetta-rels');
|
9 | var zettacluster = require('zetta-cluster');
|
10 | var Scout = require('./fixture/example_scout');
|
11 | var Driver = require('./fixture/example_driver');
|
12 | var HttpDriver = require('./fixture/example_http_driver');
|
13 | var Registry = require('./fixture/mem_registry');
|
14 | var PeerRegistry = require('./fixture/mem_peer_registry');
|
15 |
|
16 | function getHttpServer(app) {
|
17 | return app.httpServer.server;
|
18 | }
|
19 |
|
20 | function getBody(fn) {
|
21 | return function(res) {
|
22 | try {
|
23 | if(res.text) {
|
24 | var body = JSON.parse(res.text);
|
25 | } else {
|
26 | var body = '';
|
27 | }
|
28 | } catch(err) {
|
29 | throw new Error('Failed to parse json body');
|
30 | }
|
31 |
|
32 | fn(res, body);
|
33 | }
|
34 | }
|
35 |
|
36 | function checkDeviceOnRootUri(entity) {
|
37 | assert(entity.class.indexOf('device') >= 0);
|
38 | assert(entity.class.indexOf(entity.properties.type) >= 0);
|
39 | assert.deepEqual(entity.rel, ["http://rels.zettajs.io/device"]);
|
40 |
|
41 | assert(entity.properties.name);
|
42 | assert(entity.properties.type);
|
43 | assert(entity.properties.state);
|
44 | assert(!entity.actions);
|
45 |
|
46 | assert(entity.links);
|
47 | hasLinkRel(entity.links, rels.self);
|
48 | hasLinkRel(entity.links, rels.server);
|
49 | hasLinkRel(entity.links, rels.type);
|
50 | hasLinkRel(entity.links, rels.edit);
|
51 | }
|
52 |
|
53 | function hasLinkRel(links, rel, title, href) {
|
54 | var found = false;
|
55 |
|
56 | links.forEach(function(link) {
|
57 | if(link.rel.indexOf(rel) != -1) {
|
58 | found = true;
|
59 |
|
60 | if(title !== undefined && link.title !== title) {
|
61 | throw new Error('link title does not match');
|
62 | }
|
63 |
|
64 | if(href !== undefined && link.href !== href) {
|
65 | throw new Error('link href does not match');
|
66 | }
|
67 | }
|
68 | });
|
69 |
|
70 | if(!found) {
|
71 | throw new Error('Link rel:'+rel+' not found in links');
|
72 | }
|
73 | }
|
74 |
|
75 |
|
76 | describe('Zetta Api', function() {
|
77 | var reg = null;
|
78 | var peerRegistry = null;
|
79 |
|
80 | beforeEach(function() {
|
81 | reg = new Registry();
|
82 | peerRegistry = new PeerRegistry();
|
83 | });
|
84 |
|
85 | it('updates href hosts using x-forwarded-host header', function(done) {
|
86 | var app = zetta({ registry: reg, peerRegistry: peerRegistry })
|
87 | .silent()
|
88 | .name('local')
|
89 | ._run(function(err) {
|
90 | if (err) {
|
91 | return done(err);
|
92 | }
|
93 |
|
94 | request(getHttpServer(app))
|
95 | .get('/')
|
96 | .set('x-forwarded-host', 'google.com')
|
97 | .expect(getBody(function(res, body) {
|
98 | var self = body.links.filter(function(link) { return link.rel.indexOf('self') >= 0; })[0];
|
99 | assert.equal(self.href, 'http://google.com/');
|
100 | }))
|
101 | .end(done);
|
102 | });
|
103 | })
|
104 |
|
105 | it('allow for x-forwarded-host header to be disabled', function(done) {
|
106 | var app = zetta({ registry: reg, peerRegistry: peerRegistry, useXForwardedHostHeader: false })
|
107 | .silent()
|
108 | .name('local')
|
109 | ._run(function(err) {
|
110 | if (err) {
|
111 | return done(err);
|
112 | }
|
113 |
|
114 | request(getHttpServer(app))
|
115 | .get('/')
|
116 | .set('x-forwarded-host', 'google.com')
|
117 | .expect(getBody(function(res, body) {
|
118 | var self = body.links.filter(function(link) { return link.rel.indexOf('self') >= 0; })[0];
|
119 | assert.notEqual(self.href, 'http://google.com/');
|
120 | }))
|
121 | .end(done);
|
122 | });
|
123 | })
|
124 |
|
125 | describe('/servers/<peer id> ', function() {
|
126 | var app = null;
|
127 | var url = null;
|
128 |
|
129 | beforeEach(function(done) {
|
130 | app = zetta({ registry: reg, peerRegistry: peerRegistry })
|
131 | .silent()
|
132 | .properties({ custom: 123 })
|
133 | .use(Scout)
|
134 | .use(HttpDriver)
|
135 | .name('local')
|
136 | .expose('*')
|
137 | ._run(done);
|
138 |
|
139 | url = '/servers/'+app._name;
|
140 | });
|
141 |
|
142 | it('should have content type application/vnd.siren+json', function(done) {
|
143 | request(getHttpServer(app))
|
144 | .get(url)
|
145 | .expect('Content-Type', 'application/vnd.siren+json', done);
|
146 | });
|
147 |
|
148 | it('should return status code 200', function(done) {
|
149 | request(getHttpServer(app))
|
150 | .get(url)
|
151 | .expect(200, done);
|
152 | });
|
153 |
|
154 | it('should have class ["server"]', function(done) {
|
155 | request(getHttpServer(app))
|
156 | .get(url)
|
157 | .expect(getBody(function(res, body) {
|
158 | assert.deepEqual(body.class, ['server']);
|
159 | }))
|
160 | .end(done);
|
161 | });
|
162 |
|
163 | it('should have proper name and id property', function(done) {
|
164 | request(getHttpServer(app))
|
165 | .get(url)
|
166 | .expect(getBody(function(res, body) {
|
167 | assert.equal(body.properties.name, 'local');
|
168 | }))
|
169 | .end(done);
|
170 | });
|
171 |
|
172 | it('should have custom properties in resp', function(done) {
|
173 | request(getHttpServer(app))
|
174 | .get(url)
|
175 | .expect(getBody(function(res, body) {
|
176 | assert.equal(body.properties.name, 'local');
|
177 | assert.equal(body.properties.custom, 123);
|
178 | }))
|
179 | .end(done);
|
180 | });
|
181 |
|
182 | it('should have self link and log link', function(done) {
|
183 | request(getHttpServer(app))
|
184 | .get(url)
|
185 | .expect(getBody(function(res, body) {
|
186 | assert(body.links);
|
187 | hasLinkRel(body.links, 'self');
|
188 | hasLinkRel(body.links, 'monitor');
|
189 | }))
|
190 | .end(done);
|
191 | });
|
192 |
|
193 | it('should have a metadata link', function(done) {
|
194 | request(getHttpServer(app))
|
195 | .get(url)
|
196 | .expect(getBody(function(res, body) {
|
197 | assert(body.links);
|
198 | hasLinkRel(body.links, rels.metadata);
|
199 | }))
|
200 | .end(done);
|
201 | });
|
202 |
|
203 | it('should have monitor log link formatted correctly for HTTP requests', function(done) {
|
204 | request(getHttpServer(app))
|
205 | .get(url)
|
206 | .expect(getBody(function(res, body) {
|
207 | var link = body.links.filter(function(l) {
|
208 | return l.rel.indexOf('monitor') > -1;
|
209 | })[0];
|
210 | var obj = require('url').parse(link.href, true);
|
211 | assert.equal(obj.protocol, 'ws:');
|
212 | assert(obj.query.topic);
|
213 | }))
|
214 | .end(done);
|
215 | });
|
216 |
|
217 | it('should have monitor log link formatted correctly for SPDY requests', function(done) {
|
218 | var a = getHttpServer(app);
|
219 |
|
220 | if (!a.address()) a.listen(0);
|
221 |
|
222 | var agent = spdy.createAgent({
|
223 | host: '127.0.0.1',
|
224 | port: a.address().port,
|
225 | spdy: {
|
226 | plain: true,
|
227 | ssl: false
|
228 | }
|
229 | });
|
230 |
|
231 | var request = http.get({
|
232 | host: '127.0.0.1',
|
233 | port: a.address().port,
|
234 | path: url,
|
235 | agent: agent
|
236 | }, function(response) {
|
237 |
|
238 | var buffers = [];
|
239 | response.on('readable', function() {
|
240 | var data;
|
241 | while ((data = response.read()) !== null) {
|
242 | buffers.push(data);
|
243 | }
|
244 | });
|
245 |
|
246 | response.on('end', function() {
|
247 | var body = JSON.parse(Buffer.concat(buffers));
|
248 | var link = body.links.filter(function(l) {
|
249 | return l.rel.indexOf('monitor') > -1;
|
250 | })[0];
|
251 | var obj = require('url').parse(link.href, true);
|
252 | assert.equal(obj.protocol, 'http:');
|
253 | assert(obj.query.topic);
|
254 | agent.close();
|
255 | });
|
256 |
|
257 | response.on('end', done);
|
258 | }).end();
|
259 | });
|
260 |
|
261 | it('should not have an events link for SPDY requests', function(done) {
|
262 | var a = getHttpServer(app);
|
263 |
|
264 | if (!a.address()) a.listen(0);
|
265 |
|
266 | var agent = spdy.createAgent({
|
267 | host: '127.0.0.1',
|
268 | port: a.address().port,
|
269 | spdy: {
|
270 | plain: true,
|
271 | ssl: false
|
272 | }
|
273 | });
|
274 |
|
275 | var request = http.get({
|
276 | host: '127.0.0.1',
|
277 | port: a.address().port,
|
278 | path: '/',
|
279 | agent: agent
|
280 | }, function(response) {
|
281 |
|
282 | var buffers = [];
|
283 | response.on('readable', function() {
|
284 | var data;
|
285 | while ((data = response.read()) !== null) {
|
286 | buffers.push(data);
|
287 | }
|
288 | });
|
289 |
|
290 | response.on('end', function() {
|
291 | var body = JSON.parse(Buffer.concat(buffers));
|
292 | var links = body.links.filter(function(l) {
|
293 | return l.rel.indexOf('http://rels.zettajs.io/events') > -1;
|
294 | });
|
295 | assert.equal(links.length, 0);
|
296 | agent.close();
|
297 | });
|
298 |
|
299 | response.on('end', done);
|
300 | }).end();
|
301 | });
|
302 |
|
303 | it('should have valid entities', function(done) {
|
304 | request(getHttpServer(app))
|
305 | .get(url)
|
306 | .expect(getBody(function(res, body) {
|
307 | assert(body.entities);
|
308 | assert.equal(body.entities.length, 1);
|
309 | checkDeviceOnRootUri(body.entities[0]);
|
310 | }))
|
311 | .end(done);
|
312 | });
|
313 |
|
314 | it('should have one action', function(done) {
|
315 | request(getHttpServer(app))
|
316 | .get(url)
|
317 | .expect(getBody(function(res, body) {
|
318 | assert(body.actions);
|
319 | assert.equal(body.actions.length, 1);
|
320 | }))
|
321 | .end(done);
|
322 | });
|
323 |
|
324 | it('should accept remote devices of type testdriver', function(done) {
|
325 | request(getHttpServer(app))
|
326 | .post(url + '/devices')
|
327 | .send('type=testdriver')
|
328 | .end(function(err, res) {
|
329 | getBody(function(res, body) {
|
330 | assert.equal(res.statusCode, 201);
|
331 | var query = Query.of('devices');
|
332 | reg.find(query, function(err, machines) {
|
333 | assert.equal(machines.length, 2);
|
334 | assert.equal(machines[1].type, 'testdriver');
|
335 | done();
|
336 | });
|
337 | })(res);
|
338 | });
|
339 | });
|
340 |
|
341 | it('should not accept a remote device of type foo', function(done) {
|
342 | request(getHttpServer(app))
|
343 | .post(url + '/devices')
|
344 | .send('type=foo')
|
345 | .expect(getBody(function(res, body) {
|
346 | assert.equal(res.statusCode, 404);
|
347 | }))
|
348 | .end(done);
|
349 | });
|
350 |
|
351 | it('should accept remote devices of type testdriver, and allow them to set their own id properties', function(done) {
|
352 | request(getHttpServer(app))
|
353 | .post(url + '/devices')
|
354 | .send('type=testdriver&id=12345&name=test')
|
355 | .end(function(err, res) {
|
356 | getBody(function(res, body) {
|
357 | assert.equal(res.statusCode, 201);
|
358 | var query = Query.of('devices').where('id', { eq: '12345'});
|
359 | reg.find(query, function(err, machines) {
|
360 | assert.equal(machines.length, 1);
|
361 | assert.equal(machines[0].type, 'testdriver');
|
362 | assert.equal(machines[0].id, '12345');
|
363 | done();
|
364 | });
|
365 | })(res);
|
366 | });
|
367 | });
|
368 |
|
369 | it('query for device should respond with properly formated api response', function(done) {
|
370 | request(getHttpServer(app))
|
371 | .get(url+'?server=local&ql=where%20type="testdriver"')
|
372 | .expect(getBody(function(res, body) {
|
373 | assert(body.entities);
|
374 | assert.equal(body.entities.length, 1);
|
375 | checkDeviceOnRootUri(body.entities[0]);
|
376 | }))
|
377 | .end(done);
|
378 | });
|
379 | });
|
380 |
|
381 | describe('/', function() {
|
382 | var app = null;
|
383 |
|
384 | beforeEach(function() {
|
385 | app = zetta({ registry: reg, peerRegistry: peerRegistry })
|
386 | .silent()
|
387 | .use(Scout)
|
388 | .name('local')
|
389 | .expose('*')
|
390 | ._run();
|
391 | });
|
392 |
|
393 | it('should have content type application/vnd.siren+json', function(done) {
|
394 | request(getHttpServer(app))
|
395 | .get('/')
|
396 | .expect('Content-Type', 'application/vnd.siren+json', done);
|
397 | });
|
398 |
|
399 | it('should have status code 200', function(done) {
|
400 | request(getHttpServer(app))
|
401 | .get('/')
|
402 | .expect(200, done);
|
403 | });
|
404 |
|
405 | it('body should contain class ["root"]', function(done) {
|
406 | request(getHttpServer(app))
|
407 | .get('/')
|
408 | .expect(getBody(function(res, body) {
|
409 | assert.deepEqual(body.class, ['root']);
|
410 | }))
|
411 | .end(done)
|
412 | });
|
413 |
|
414 |
|
415 | it('body should contain links property', function(done) {
|
416 | request(getHttpServer(app))
|
417 | .get('/')
|
418 | .expect(getBody(function(res, body) {
|
419 | assert.equal(body.links.length, 4);
|
420 | hasLinkRel(body.links, 'self');
|
421 | }))
|
422 | .end(done)
|
423 | });
|
424 |
|
425 | it('links should contain rel to server', function(done) {
|
426 | request(getHttpServer(app))
|
427 | .get('/')
|
428 | .expect(getBody(function(res, body) {
|
429 | hasLinkRel(body.links, rels.server);
|
430 | }))
|
431 | .end(done)
|
432 | });
|
433 |
|
434 | it('should contain link for event stream', function(done) {
|
435 | request(getHttpServer(app))
|
436 | .get('/')
|
437 | .expect(getBody(function(res, body) {
|
438 | hasLinkRel(body.links, rels.events);
|
439 | }))
|
440 | .end(done)
|
441 | });
|
442 |
|
443 | it('should use a default server name if none has been provided', function(done) {
|
444 | var app = zetta({ registry: reg, peerRegistry: peerRegistry }).silent()._run();
|
445 |
|
446 | request(getHttpServer(app))
|
447 | .get('/')
|
448 | .expect(getBody(function(res, body) {
|
449 | var self = body.links.filter(function(link) {
|
450 | return link.rel.indexOf(rels.server) !== -1;
|
451 | })[0];
|
452 |
|
453 | assert.equal(self.title, os.hostname());
|
454 | }))
|
455 | .end(done);
|
456 | });
|
457 | });
|
458 |
|
459 | describe('/peer-management', function() {
|
460 | var app = null;
|
461 |
|
462 | before(function(done) {
|
463 | peerRegistry.save({
|
464 | id: '12341234',
|
465 | name: 'test-peer'
|
466 | }, done);
|
467 | });
|
468 |
|
469 | beforeEach(function(done) {
|
470 | app = zetta({ registry: reg, peerRegistry: peerRegistry })
|
471 | .silent()
|
472 | .use(Scout)
|
473 | .name('local')
|
474 | .expose('*')
|
475 | ._run(done);
|
476 | });
|
477 |
|
478 | it('should have content type application/vnd.siren+json', function(done) {
|
479 | request(getHttpServer(app))
|
480 | .get('/peer-management')
|
481 | .expect('Content-Type', 'application/vnd.siren+json', done);
|
482 | });
|
483 |
|
484 | it('should return status code 200', function(done) {
|
485 | request(getHttpServer(app))
|
486 | .get('/peer-management')
|
487 | .expect(200, done);
|
488 | });
|
489 |
|
490 | it('should have class ["peer-management"]', function(done) {
|
491 | request(getHttpServer(app))
|
492 | .get('/peer-management')
|
493 | .expect(getBody(function(err, body) {
|
494 | assert.deepEqual(body.class, ['peer-management']);
|
495 | }))
|
496 | .end(done);
|
497 | });
|
498 |
|
499 | it('subentities should have rel ["item"]', function(done) {
|
500 | peerRegistry.save({ id: '0' }, function() {
|
501 | request(getHttpServer(app))
|
502 | .get('/peer-management')
|
503 | .expect(getBody(function(err, body) {
|
504 | body.entities.forEach(function(entity) {
|
505 | assert(entity.rel.indexOf('item') >= 0)
|
506 | })
|
507 | }))
|
508 | .end(done);
|
509 | });
|
510 | });
|
511 |
|
512 | it('should list saved peers', function(done) {
|
513 | peerRegistry.save({ id: '0' }, function() {
|
514 | request(getHttpServer(app))
|
515 | .get('/peer-management')
|
516 | .expect(getBody(function(err, body) {
|
517 | assert.equal(body.entities.length, 1);
|
518 | }))
|
519 | .end(done);
|
520 | });
|
521 | });
|
522 |
|
523 | it('should allow the querying of peers with the ql parameter', function(done) {
|
524 | peerRegistry.save({ id: '1', type: 'initiator'}, function() {
|
525 | request(getHttpServer(app))
|
526 | .get('/peer-management?ql=where%20type%3D%22initiator%22')
|
527 | .expect(getBody(function(err, body) {
|
528 | assert.equal(body.entities.length, 1);
|
529 | var entity = body.entities[0];
|
530 | assert.equal(entity.properties.id, '1');
|
531 | }))
|
532 | .end(done);
|
533 | });
|
534 | });
|
535 |
|
536 | describe('#link', function() {
|
537 | it('should return status code 202', function(done) {
|
538 | request(getHttpServer(app))
|
539 | .post('/peer-management')
|
540 | .send('url=http://testurl')
|
541 | .expect(202, done);
|
542 | });
|
543 |
|
544 | it('should return a Location header', function(done) {
|
545 | request(getHttpServer(app))
|
546 | .post('/peer-management')
|
547 | .send('url=http://testurl')
|
548 | .expect('Location', /^http.+/)
|
549 | .end(done);
|
550 | });
|
551 | });
|
552 |
|
553 | describe('#show', function() {
|
554 | it('should return the peer item representation', function(done) {
|
555 | var id = '1234-5678-9ABCD';
|
556 | peerRegistry.save({ id: id }, function() {
|
557 | request(getHttpServer(app))
|
558 | .get('/peer-management/' + id)
|
559 | .expect(200, done);
|
560 | });
|
561 | });
|
562 | });
|
563 | });
|
564 |
|
565 | describe('/devices of server', function() {
|
566 | var app = null;
|
567 |
|
568 | beforeEach(function(done) {
|
569 | app = zetta({ registry: reg, peerRegistry: peerRegistry })
|
570 | .silent()
|
571 | .use(Scout)
|
572 | .name('local')
|
573 | .expose('*')
|
574 | ._run(done);
|
575 | });
|
576 |
|
577 | it('should have content type application/vnd.siren+json', function(done) {
|
578 | request(getHttpServer(app))
|
579 | .get('/devices')
|
580 | .expect('Content-Type', 'application/vnd.siren+json', done);
|
581 | });
|
582 |
|
583 | it('should return status code 200', function(done) {
|
584 | request(getHttpServer(app))
|
585 | .get('/devices')
|
586 | .expect(200, done);
|
587 | });
|
588 |
|
589 | it('should have class ["devices"]', function(done) {
|
590 | request(getHttpServer(app))
|
591 | .get('/devices')
|
592 | .expect(getBody(function(res, body) {
|
593 | assert.deepEqual(body.class, ['devices']);
|
594 | }))
|
595 | .end(done);
|
596 | });
|
597 |
|
598 | it('should have one valid entity', function(done) {
|
599 | request(getHttpServer(app))
|
600 | .get('/devices')
|
601 | .expect(getBody(function(res, body) {
|
602 | assert(body.entities);
|
603 | assert.equal(body.entities.length, 1);
|
604 | checkDeviceOnRootUri(body.entities[0]);
|
605 | hasLinkRel(body.links, 'self');
|
606 | }))
|
607 | .end(done);
|
608 | });
|
609 | });
|
610 |
|
611 |
|
612 |
|
613 |
|
614 | describe('/servers/:id/devices/:id', function() {
|
615 | var app = null;
|
616 | var url = null;
|
617 | var device = null;
|
618 |
|
619 | beforeEach(function(done) {
|
620 | app = zetta({ registry: reg, peerRegistry: peerRegistry })
|
621 | .silent()
|
622 | .use(Scout)
|
623 | .name('local')
|
624 | .expose('*')
|
625 | ._run(function() {
|
626 | device = app.runtime._jsDevices[Object.keys(app.runtime._jsDevices)[0]];
|
627 | url = '/servers/' + app._name + '/devices/' + device.id;
|
628 | done();
|
629 | });
|
630 | });
|
631 |
|
632 | it('should have content type application/vnd.siren+json', function(done) {
|
633 | request(getHttpServer(app))
|
634 | .get(url)
|
635 | .expect('Content-Type', 'application/vnd.siren+json', done);
|
636 | });
|
637 |
|
638 | it('class should be ["device", ":type"]', function(done) {
|
639 | request(getHttpServer(app))
|
640 | .get(url)
|
641 | .expect(getBody(function(res, body) {
|
642 | assert(body.class.indexOf('device') >= 0);
|
643 | assert(body.class.indexOf(body.properties.type) >= 0);
|
644 | }))
|
645 | .end(done);
|
646 | });
|
647 |
|
648 | |
649 |
|
650 |
|
651 |
|
652 |
|
653 |
|
654 | it('properties should match expected', function(done) {
|
655 | request(getHttpServer(app))
|
656 | .get(url)
|
657 | .expect(getBody(function(res, body) {
|
658 | assert(body.properties);
|
659 | assert.equal(body.properties.name, device.name);
|
660 | assert.equal(body.properties.type, device.type);
|
661 | assert.equal(body.properties.id, device.id);
|
662 | assert.equal(body.properties.state, device.state);
|
663 | }))
|
664 | .end(done);
|
665 | });
|
666 |
|
667 | it('device should have action change', function(done) {
|
668 | request(getHttpServer(app))
|
669 | .get(url)
|
670 | .expect(getBody(function(res, body) {
|
671 | assert.equal(body.actions.length, 7);
|
672 | var action = body.actions[0];
|
673 | assert.equal(action.name, 'change');
|
674 | assert.equal(action.method, 'POST');
|
675 | assert(action.href);
|
676 | assert.deepEqual(action.fields[0], { name: 'action', type: 'hidden', value: 'change' });
|
677 | }))
|
678 | .end(done);
|
679 | });
|
680 |
|
681 | it('device actions should have class "transition"', function(done) {
|
682 | request(getHttpServer(app))
|
683 | .get(url)
|
684 | .expect(getBody(function(res, body) {
|
685 | assert.equal(body.actions.length, 7);
|
686 | body.actions.forEach(function(action) {
|
687 | assert(action.class.indexOf('transition') >= 0);
|
688 | })
|
689 | }))
|
690 | .end(done);
|
691 | });
|
692 |
|
693 |
|
694 | it('device should have self link', function(done) {
|
695 | request(getHttpServer(app))
|
696 | .get(url)
|
697 | .expect(getBody(function(res, body) {
|
698 | hasLinkRel(body.links, 'self');
|
699 | }))
|
700 | .end(done);
|
701 | });
|
702 |
|
703 | it('device should have edit link', function(done) {
|
704 | request(getHttpServer(app))
|
705 | .get(url)
|
706 | .expect(getBody(function(res, body) {
|
707 | hasLinkRel(body.links, 'edit');
|
708 | }))
|
709 | .end(done);
|
710 | });
|
711 |
|
712 | it('device should have up link to server', function(done) {
|
713 | request(getHttpServer(app))
|
714 | .get(url)
|
715 | .expect(getBody(function(res, body) {
|
716 | hasLinkRel(body.links, 'up', 'local');
|
717 | }))
|
718 | .end(done);
|
719 | });
|
720 |
|
721 | it('device should have monitor link for bar', function(done) {
|
722 | request(getHttpServer(app))
|
723 | .get(url)
|
724 | .expect(getBody(function(res, body) {
|
725 | hasLinkRel(body.links, 'monitor');
|
726 | }))
|
727 | .end(done);
|
728 | });
|
729 |
|
730 | it('disabling a stream should remove it from the API.', function(done) {
|
731 | Object.keys(app.runtime._jsDevices).forEach(function(name) {
|
732 | var device = app.runtime._jsDevices[name];
|
733 | device.disableStream('foo');
|
734 | });
|
735 |
|
736 | request(getHttpServer(app))
|
737 | .get(url)
|
738 | .expect(getBody(function(res, body) {
|
739 | var foo = body.links.filter(function(link) {
|
740 | return link.title === 'foo';
|
741 | });
|
742 |
|
743 | assert.equal(foo.length, 0);
|
744 | }))
|
745 | .end(done);
|
746 | });
|
747 |
|
748 | it('enabling a stream should show it in the API.', function(done) {
|
749 | var device = null;
|
750 | Object.keys(app.runtime._jsDevices).forEach(function(name) {
|
751 | device = app.runtime._jsDevices[name];
|
752 | device.disableStream('foo');
|
753 | device.enableStream('foo');
|
754 | });
|
755 |
|
756 | request(getHttpServer(app))
|
757 | .get(url)
|
758 | .expect(getBody(function(res, body) {
|
759 | var foo = body.links.filter(function(link) {
|
760 | return link.title === 'foo';
|
761 | });
|
762 |
|
763 | assert.equal(foo.length, 1);
|
764 | }))
|
765 | .end(done);
|
766 | });
|
767 |
|
768 | it('device should have monitor link for bar formatted correctly for HTTP requests', function(done) {
|
769 | request(getHttpServer(app))
|
770 | .get(url)
|
771 | .expect(getBody(function(res, body) {
|
772 | var fooBar = body.links.filter(function(link) {
|
773 | return link.title === 'foobar';
|
774 | });
|
775 |
|
776 | hasLinkRel(fooBar, rels.binaryStream);
|
777 | var parsed = require('url').parse(fooBar[0].href);
|
778 | assert.equal(parsed.protocol, 'ws:');
|
779 | }))
|
780 | .end(done);
|
781 | });
|
782 |
|
783 | it('should have a monitor link for bar formatted correctly for SPDY requests', function(done) {
|
784 | var a = getHttpServer(app);
|
785 |
|
786 | if (!a.address()) a.listen(0);
|
787 |
|
788 | var agent = spdy.createAgent({
|
789 | host: '127.0.0.1',
|
790 | port: a.address().port,
|
791 | spdy: {
|
792 | plain: true,
|
793 | ssl: false
|
794 | }
|
795 | });
|
796 |
|
797 | var request = http.get({
|
798 | host: '127.0.0.1',
|
799 | port: a.address().port,
|
800 | path: url,
|
801 | agent: agent
|
802 | }, function(response) {
|
803 |
|
804 | var buffers = [];
|
805 | response.on('readable', function() {
|
806 | var data;
|
807 | while ((data = response.read()) !== null) {
|
808 | buffers.push(data);
|
809 | }
|
810 | });
|
811 |
|
812 | response.on('end', function() {
|
813 | var body = JSON.parse(Buffer.concat(buffers));
|
814 | var fooBar = body.links.filter(function(link) {
|
815 | return link.title === 'foobar';
|
816 | });
|
817 |
|
818 | hasLinkRel(fooBar, rels.binaryStream);
|
819 | var parsed = require('url').parse(fooBar[0].href);
|
820 | assert.equal(parsed.protocol, 'http:');
|
821 | agent.close();
|
822 | });
|
823 |
|
824 | response.on('end', done);
|
825 | }).end();
|
826 | });
|
827 |
|
828 | it('device action should return a 400 status code on a missing request body', function(done) {
|
829 | request(getHttpServer(app))
|
830 | .post(url)
|
831 | .send()
|
832 | .expect(getBody(function(res, body) {
|
833 | assert.equal(res.statusCode, 400);
|
834 | }))
|
835 | .end(done);
|
836 | });
|
837 |
|
838 | it('device action should return a 400 status code on an invalid request body', function(done) {
|
839 | request(getHttpServer(app))
|
840 | .post(url)
|
841 | .type('form')
|
842 | .send('{ "what": "invalid" }')
|
843 | .expect(getBody(function(res, body) {
|
844 | assert.equal(res.statusCode, 400);
|
845 | }))
|
846 | .end(done);
|
847 | });
|
848 |
|
849 | it('device action should work', function(done) {
|
850 | request(getHttpServer(app))
|
851 | .post(url)
|
852 | .type('form')
|
853 | .send({ action: 'test', value: 123 })
|
854 | .expect(getBody(function(res, body) {
|
855 | assert.equal(body.properties.value, 123);
|
856 | hasLinkRel(body.links, 'monitor');
|
857 | }))
|
858 | .end(done);
|
859 | });
|
860 |
|
861 | it('device action should support extended characters', function(done) {
|
862 | request(getHttpServer(app))
|
863 | .post(url)
|
864 | .type('form')
|
865 | .send({ action: 'test-text', value: "ππΈπ" })
|
866 | .expect(getBody(function(res, body) {
|
867 | assert.equal(body.properties.message, "ππΈπ");
|
868 | }))
|
869 | .end(done);
|
870 | });
|
871 |
|
872 | var createTransitionArgTest = function(action, testType, input) {
|
873 | it('api should decode transition args to ' + testType + ' for ' + action, function(done) {
|
874 | var device = app.runtime._jsDevices[Object.keys(app.runtime._jsDevices)[0]];
|
875 |
|
876 | var orig = device._transitions[action].handler;
|
877 | device._transitions[action].handler = function(x) {
|
878 | assert.equal(typeof x, testType);
|
879 | orig.apply(device, arguments);
|
880 | };
|
881 |
|
882 | request(getHttpServer(app))
|
883 | .post(url)
|
884 | .type('form')
|
885 | .expect(200)
|
886 | .send({ action: action, value: input })
|
887 | .end(done);
|
888 | });
|
889 | };
|
890 |
|
891 | createTransitionArgTest('test-number', 'number', 123)
|
892 | createTransitionArgTest('test-text', 'string', 'Hello');
|
893 | createTransitionArgTest('test-none', 'string', 'Anything');
|
894 | createTransitionArgTest('test-date', 'object', '2015-01-02');
|
895 |
|
896 | it('api should respond with 400 when argument is not expected number', function(done) {
|
897 | request(getHttpServer(app))
|
898 | .post(url)
|
899 | .type('form')
|
900 | .expect(400)
|
901 | .expect(getBody(function(res, body) {
|
902 | assert(body.class.indexOf('input-error') > -1);
|
903 | assert.equal(body.properties.errors.length, 1);
|
904 | }))
|
905 | .send({ action: 'test-number', value: 'some string' })
|
906 | .end(done);
|
907 | })
|
908 |
|
909 | it('api should respond with 400 when argument is not expected Date', function(done) {
|
910 | request(getHttpServer(app))
|
911 | .post(url)
|
912 | .type('form')
|
913 | .expect(400)
|
914 | .expect(getBody(function(res, body) {
|
915 | assert(body.class.indexOf('input-error') > -1);
|
916 | assert.equal(body.properties.errors.length, 1);
|
917 | }))
|
918 | .send({ action: 'test-date', value: 'some string' })
|
919 | .end(done);
|
920 | })
|
921 |
|
922 | it('device action should return 400 when not available.', function(done) {
|
923 | request(getHttpServer(app))
|
924 | .post(url)
|
925 | .type('form')
|
926 | .send({ action: 'prepare' })
|
927 | .expect(getBody(function(res, body) {
|
928 | assert.equal(res.statusCode, 400);
|
929 | }))
|
930 | .end(done);
|
931 | });
|
932 |
|
933 | it('should return 500 when a error is passed in a callback of device driver', function(done) {
|
934 | request(getHttpServer(app))
|
935 | .post(url)
|
936 | .type('form')
|
937 | .send({ action: 'error', value: 'some error' })
|
938 | .expect(getBody(function(res, body) {
|
939 | assert.equal(res.statusCode, 500);
|
940 | }))
|
941 | .end(done);
|
942 | });
|
943 |
|
944 | it('should support device updates using PUT', function(done) {
|
945 | request(getHttpServer(app))
|
946 | .put(url)
|
947 | .type('json')
|
948 | .send({ bar: 2, value: 3 })
|
949 | .expect(getBody(function(res, body) {
|
950 | assert.equal(res.statusCode, 200);
|
951 | assert.equal(body.properties.bar, 2);
|
952 | assert.equal(body.properties.value, 3);
|
953 | }))
|
954 | .end(done);
|
955 | });
|
956 |
|
957 | it('should support device deletes using DELETE', function(done) {
|
958 | request(getHttpServer(app))
|
959 | .del(url)
|
960 | .expect(getBody(function(res, body) {
|
961 | assert.equal(res.statusCode, 204);
|
962 | assert.equal(Object.keys(app.runtime._jsDevices).length, 0);
|
963 | }))
|
964 | .end(done);
|
965 |
|
966 | });
|
967 |
|
968 | it('remoteDestroy hook should prevent the device from being destroyed with a DELETE', function(done) {
|
969 | var deviceKey = Object.keys(app.runtime._jsDevices)[0];
|
970 | var device = app.runtime._jsDevices[deviceKey];
|
971 |
|
972 | var remoteDestroy = function(cb) {
|
973 | cb(null, false);
|
974 | }
|
975 |
|
976 | device._remoteDestroy = remoteDestroy.bind(device);
|
977 |
|
978 | request(getHttpServer(app))
|
979 | .del(url)
|
980 | .expect(getBody(function(res, body) {
|
981 | assert.equal(res.statusCode, 500);
|
982 | assert.equal(Object.keys(app.runtime._jsDevices).length, 1);
|
983 | }))
|
984 | .end(done);
|
985 |
|
986 | });
|
987 |
|
988 | it('remoteDestroy hook should prevent the device from being destroyed with a DELETE if callback has an error', function(done) {
|
989 | var deviceKey = Object.keys(app.runtime._jsDevices)[0];
|
990 | var device = app.runtime._jsDevices[deviceKey];
|
991 |
|
992 | var remoteDestroy = function(cb) {
|
993 | cb(new Error('Oof! Ouch!'));
|
994 | }
|
995 |
|
996 | device._remoteDestroy = remoteDestroy.bind(device);
|
997 |
|
998 | request(getHttpServer(app))
|
999 | .del(url)
|
1000 | .expect(getBody(function(res, body) {
|
1001 | assert.equal(res.statusCode, 500);
|
1002 | assert.equal(Object.keys(app.runtime._jsDevices).length, 1);
|
1003 | }))
|
1004 | .end(done);
|
1005 |
|
1006 | });
|
1007 |
|
1008 | it('remoteDestroy hook should allow the device to be destroyed when callback is called with true', function(done) {
|
1009 | var deviceKey = Object.keys(app.runtime._jsDevices)[0];
|
1010 | var device = app.runtime._jsDevices[deviceKey];
|
1011 |
|
1012 | var remoteDestroy = function(cb) {
|
1013 | cb(null, true);
|
1014 | }
|
1015 |
|
1016 | device._remoteDestroy = remoteDestroy.bind(device);
|
1017 |
|
1018 | request(getHttpServer(app))
|
1019 | .del(url)
|
1020 | .expect(getBody(function(res, body) {
|
1021 | assert.equal(res.statusCode, 204);
|
1022 | assert.equal(Object.keys(app.runtime._jsDevices).length, 0);
|
1023 | }))
|
1024 | .end(done);
|
1025 |
|
1026 | });
|
1027 |
|
1028 | it('should not overwrite monitor properties using PUT', function(done) {
|
1029 | request(getHttpServer(app))
|
1030 | .put(url)
|
1031 | .type('json')
|
1032 | .send({ foo: 1 })
|
1033 | .expect(getBody(function(res, body) {
|
1034 | assert.equal(res.statusCode, 200);
|
1035 | assert.equal(body.properties.foo, 0);
|
1036 | }))
|
1037 | .end(done);
|
1038 | });
|
1039 |
|
1040 | it('should return a 404 when updating a non-existent device', function(done) {
|
1041 | request(getHttpServer(app))
|
1042 | .put(url + '1234567890')
|
1043 | .type('json')
|
1044 | .send({ foo: 1, bar: 2, value: 3 })
|
1045 | .expect(function(res) {
|
1046 | assert.equal(res.statusCode, 404);
|
1047 | })
|
1048 | .end(done);
|
1049 | });
|
1050 |
|
1051 | it('should return a 400 when updating with a Content-Range header', function(done) {
|
1052 | request(getHttpServer(app))
|
1053 | .put(url)
|
1054 | .set('Content-Range', 'bytes 0-499/1234')
|
1055 | .type('json')
|
1056 | .send({ foo: 1, bar: 2, value: 3 })
|
1057 | .expect(function(res) {
|
1058 | assert.equal(res.statusCode, 400);
|
1059 | })
|
1060 | .end(done);
|
1061 | });
|
1062 |
|
1063 | it('should return a 400 when receiving invalid JSON input', function(done) {
|
1064 | request(getHttpServer(app))
|
1065 | .put(url)
|
1066 | .type('json')
|
1067 | .send('{"name":}')
|
1068 | .expect(function(res) {
|
1069 | assert.equal(res.statusCode, 400);
|
1070 | })
|
1071 | .end(done);
|
1072 | });
|
1073 |
|
1074 | it('should not include reserved fields on device updates', function(done) {
|
1075 | var input = { foo: 1, bar: 2, value: 3, id: 'abcdef',
|
1076 | _x: 4, type: 'h', state: 'yo', streams: 's' };
|
1077 |
|
1078 | request(getHttpServer(app))
|
1079 | .put(url)
|
1080 | .type('json')
|
1081 | .send(input)
|
1082 | .expect(getBody(function(res, body) {
|
1083 | assert.equal(res.statusCode, 200);
|
1084 | assert.notEqual(body.properties.id, 'abcdef');
|
1085 | assert.notEqual(body.properties._x, 4);
|
1086 | assert.notEqual(body.properties.streams, 's');
|
1087 | assert.notEqual(body.properties.state, 'yo');
|
1088 | assert.notEqual(body.properties.type, 'h');
|
1089 | }))
|
1090 | .end(done);
|
1091 | });
|
1092 | });
|
1093 |
|
1094 | describe('Proxied requests', function() {
|
1095 | var base = null;
|
1096 | var cloudUrl = null;
|
1097 | var cluster = null;
|
1098 |
|
1099 | beforeEach(function(done) {
|
1100 | cluster = zettacluster({ zetta: zetta })
|
1101 | .server('cloud')
|
1102 | .server('detroit', [Scout], ['cloud'])
|
1103 | .server('sanjose', [Scout], ['cloud'])
|
1104 | .on('ready', function(){
|
1105 | cloudUrl = 'localhost:' + cluster.servers['cloud']._testPort;
|
1106 | base = 'localhost:' + cluster.servers['cloud']._testPort + '/servers/' + cluster.servers['cloud'].locatePeer('detroit');
|
1107 | setTimeout(done, 300);
|
1108 | })
|
1109 | .run(function(err) {
|
1110 | console.log(err)
|
1111 | if (err) {
|
1112 | done(err);
|
1113 | }
|
1114 | });
|
1115 | });
|
1116 |
|
1117 | afterEach(function(done) {
|
1118 | cluster.stop();
|
1119 | setTimeout(done, 10);
|
1120 | });
|
1121 |
|
1122 | it('zetta should not crash when req to hub is pending and hub disconnects', function(done) {
|
1123 | http.get('http://' + base, function(res) {
|
1124 | assert.equal(res.statusCode, 502);
|
1125 | done();
|
1126 | }).on('socket', function(socket) {
|
1127 | socket.on('connect', function() {
|
1128 | cluster.servers['cloud'].httpServer.peers['detroit'].close();
|
1129 | });
|
1130 | })
|
1131 | })
|
1132 |
|
1133 | it('zetta should return 404 on non-existent peer', function(done) {
|
1134 | http.get('http://' + cloudUrl + '/servers/some-peer', function(res) {
|
1135 | assert.equal(res.statusCode, 404);
|
1136 | done();
|
1137 | })
|
1138 | })
|
1139 |
|
1140 | it('zetta should return 404 on disconnected peer', function(done) {
|
1141 | cluster.servers['detroit']._peerClients[0].close()
|
1142 | http.get('http://' + cloudUrl + '/servers/detroit', function(res) {
|
1143 | assert.equal(res.statusCode, 404);
|
1144 | done();
|
1145 | })
|
1146 | })
|
1147 |
|
1148 | it('device action should support extended characters throw a proxied connection', function(done) {
|
1149 |
|
1150 | var device = cluster.servers['detroit'].runtime._jsDevices[Object.keys(cluster.servers['detroit'].runtime._jsDevices)[0]];
|
1151 |
|
1152 | request(getHttpServer(cluster.servers['cloud']))
|
1153 | .post('/servers/detroit/devices/' + device.id)
|
1154 | .type('form')
|
1155 | .send({ action: 'test-text', value: "ππΈπ" })
|
1156 | .expect(getBody(function(res, body) {
|
1157 | assert.equal(body.properties.message, "ππΈπ");
|
1158 | }))
|
1159 | .end(done);
|
1160 | });
|
1161 |
|
1162 |
|
1163 | })
|
1164 |
|
1165 | describe('Server name issues', function() {
|
1166 | var cluster;
|
1167 | var hubName = 'hub 1';
|
1168 | var getServer = function(peerName) {
|
1169 | return cluster.servers[peerName].httpServer.server;
|
1170 | };
|
1171 | beforeEach(function(done) {
|
1172 | cluster = zettacluster({ zetta: zetta })
|
1173 | .server('cloud')
|
1174 | .server(hubName, [Scout], ['cloud'])
|
1175 | .on('ready', done)
|
1176 | .run(function(err) {
|
1177 | if (err) {
|
1178 | done(err);
|
1179 | }
|
1180 | });
|
1181 | });
|
1182 |
|
1183 | it('server name with space has correct path to root of server', function(done) {
|
1184 | request(getServer('cloud'))
|
1185 | .get('/')
|
1186 | .expect(getBody(function(res, body) {
|
1187 | var link = body.links.filter(function(link) { return link.title === hubName})[0];
|
1188 | var parsed = require('url').parse(link.href);
|
1189 | assert.equal(decodeURI(parsed.path), '/servers/' + hubName);
|
1190 | }))
|
1191 | .end(done);
|
1192 | })
|
1193 |
|
1194 | it('server name with space has correct path to device', function(done) {
|
1195 | request(getServer('cloud'))
|
1196 | .get('/servers/' + hubName)
|
1197 | .expect(getBody(function(res, body) {
|
1198 | body.entities.forEach(function(entity) {
|
1199 | entity.links.forEach(function(link) {
|
1200 | var parsed = require('url').parse(link.href);
|
1201 | assert.equal(decodeURI(parsed.path).indexOf('/servers/' + hubName), 0);
|
1202 | });
|
1203 | });
|
1204 | }))
|
1205 | .end(done);
|
1206 | })
|
1207 | })
|
1208 |
|
1209 |
|
1210 | });
|