UNPKG

25 kBJavaScriptView Raw
1var assert = require('assert');
2var http = require('http');
3var os = require('os');
4var request = require('supertest');
5var spdy = require('spdy');
6var zetta = require('../');
7var Query = require('calypso').Query;
8var rels = require('zetta-rels');
9var zettacluster = require('zetta-cluster');
10var Scout = require('./fixture/example_scout');
11var Driver = require('./fixture/example_driver');
12var HttpDriver = require('./fixture/example_http_driver');
13var Registry = require('./fixture/mem_registry');
14var PeerRegistry = require('./fixture/mem_peer_registry');
15
16function getHttpServer(app) {
17 return app.httpServer.server;
18}
19
20function 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
36function 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); // should not have actions on it
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
53function 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
76describe('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
86 describe('/servers/<peer id> ', function() {
87 var app = null;
88 var url = null;
89
90 beforeEach(function(done) {
91 app = zetta({ registry: reg, peerRegistry: peerRegistry })
92 .silent()
93 .properties({ custom: 123 })
94 .use(Scout)
95 .use(HttpDriver)
96 .name('local')
97 .expose('*')
98 ._run(done);
99
100 url = '/servers/'+app._name;
101 });
102
103 it('should have content type application/vnd.siren+json', function(done) {
104 request(getHttpServer(app))
105 .get(url)
106 .expect('Content-Type', 'application/vnd.siren+json', done);
107 });
108
109 it('should return status code 200', function(done) {
110 request(getHttpServer(app))
111 .get(url)
112 .expect(200, done);
113 });
114
115 it('should have class ["server"]', function(done) {
116 request(getHttpServer(app))
117 .get(url)
118 .expect(getBody(function(res, body) {
119 assert.deepEqual(body.class, ['server']);
120 }))
121 .end(done);
122 });
123
124 it('should have proper name and id property', function(done) {
125 request(getHttpServer(app))
126 .get(url)
127 .expect(getBody(function(res, body) {
128 assert.equal(body.properties.name, 'local');
129 }))
130 .end(done);
131 });
132
133 it('should have custom properties in resp', function(done) {
134 request(getHttpServer(app))
135 .get(url)
136 .expect(getBody(function(res, body) {
137 assert.equal(body.properties.name, 'local');
138 assert.equal(body.properties.custom, 123);
139 }))
140 .end(done);
141 });
142
143 it('should have self link and log link', function(done) {
144 request(getHttpServer(app))
145 .get(url)
146 .expect(getBody(function(res, body) {
147 assert(body.links);
148 hasLinkRel(body.links, 'self');
149 hasLinkRel(body.links, 'monitor');
150 }))
151 .end(done);
152 });
153
154 it('should have a metadata link', function(done) {
155 request(getHttpServer(app))
156 .get(url)
157 .expect(getBody(function(res, body) {
158 assert(body.links);
159 hasLinkRel(body.links, rels.metadata);
160 }))
161 .end(done);
162 });
163
164 it('should have monitor log link formatted correctly for HTTP requests', function(done) {
165 request(getHttpServer(app))
166 .get(url)
167 .expect(getBody(function(res, body) {
168 var link = body.links.filter(function(l) {
169 return l.rel.indexOf('monitor') > -1;
170 })[0];
171 var obj = require('url').parse(link.href, true);
172 assert.equal(obj.protocol, 'ws:');
173 assert(obj.query.topic);
174 }))
175 .end(done);
176 });
177
178 it('should have monitor log link formatted correctly for SPDY requests', function(done) {
179 var a = getHttpServer(app);
180
181 if (!a.address()) a.listen(0);
182
183 var agent = spdy.createAgent({
184 host: '127.0.0.1',
185 port: a.address().port,
186 spdy: {
187 plain: true,
188 ssl: false
189 }
190 });
191
192 var request = http.get({
193 host: '127.0.0.1',
194 port: a.address().port,
195 path: url,
196 agent: agent
197 }, function(response) {
198
199 var buffers = [];
200 response.on('readable', function() {
201 var data;
202 while ((data = response.read()) !== null) {
203 buffers.push(data);
204 }
205 });
206
207 response.on('end', function() {
208 var body = JSON.parse(Buffer.concat(buffers));
209 var link = body.links.filter(function(l) {
210 return l.rel.indexOf('monitor') > -1;
211 })[0];
212 var obj = require('url').parse(link.href, true);
213 assert.equal(obj.protocol, 'http:');
214 assert(obj.query.topic);
215 agent.close();
216 });
217
218 response.on('end', done);
219 }).end();
220 });
221
222 it('should have valid entities', function(done) {
223 request(getHttpServer(app))
224 .get(url)
225 .expect(getBody(function(res, body) {
226 assert(body.entities);
227 assert.equal(body.entities.length, 1);
228 checkDeviceOnRootUri(body.entities[0]);
229 }))
230 .end(done);
231 });
232
233 it('should have one action', function(done) {
234 request(getHttpServer(app))
235 .get(url)
236 .expect(getBody(function(res, body) {
237 assert(body.actions);
238 assert.equal(body.actions.length, 1);
239 }))
240 .end(done);
241 });
242
243 it('should accept remote devices of type testdriver', function(done) {
244 request(getHttpServer(app))
245 .post(url + '/devices')
246 .send('type=testdriver')
247 .end(function(err, res) {
248 getBody(function(res, body) {
249 assert.equal(res.statusCode, 201);
250 var query = Query.of('devices');
251 reg.find(query, function(err, machines) {
252 assert.equal(machines.length, 2);
253 assert.equal(machines[1].type, 'testdriver');
254 done();
255 });
256 })(res);
257 });
258 });
259
260 it('should not accept a remote device of type foo', function(done) {
261 request(getHttpServer(app))
262 .post(url + '/devices')
263 .send('type=foo')
264 .expect(getBody(function(res, body) {
265 assert.equal(res.statusCode, 404);
266 }))
267 .end(done);
268 });
269
270 it('should accept remote devices of type testdriver, and allow them to set their own id properties', function(done) {
271 request(getHttpServer(app))
272 .post(url + '/devices')
273 .send('type=testdriver&id=12345&name=test')
274 .end(function(err, res) {
275 getBody(function(res, body) {
276 assert.equal(res.statusCode, 201);
277 var query = Query.of('devices').where('id', { eq: '12345'});
278 reg.find(query, function(err, machines) {
279 assert.equal(machines.length, 1);
280 assert.equal(machines[0].type, 'testdriver');
281 assert.equal(machines[0].id, '12345');
282 done();
283 });
284 })(res);
285 });
286 });
287
288 it('query for device should respond with properly formated api response', function(done) {
289 request(getHttpServer(app))
290 .get(url+'?server=local&ql=where%20type="testdriver"')
291 .expect(getBody(function(res, body) {
292 assert(body.entities);
293 assert.equal(body.entities.length, 1);
294 checkDeviceOnRootUri(body.entities[0]);
295 }))
296 .end(done);
297 });
298
299
300
301 });
302
303 describe('/', function() {
304 var app = null;
305
306 beforeEach(function() {
307 app = zetta({ registry: reg, peerRegistry: peerRegistry })
308 .silent()
309 .use(Scout)
310 .name('local')
311 .expose('*')
312 ._run();
313 });
314
315 it('should have content type application/vnd.siren+json', function(done) {
316 request(getHttpServer(app))
317 .get('/')
318 .expect('Content-Type', 'application/vnd.siren+json', done);
319 });
320
321 it('should have status code 200', function(done) {
322 request(getHttpServer(app))
323 .get('/')
324 .expect(200, done);
325 });
326
327 it('body should contain class ["root"]', function(done) {
328 request(getHttpServer(app))
329 .get('/')
330 .expect(getBody(function(res, body) {
331 assert.deepEqual(body.class, ['root']);
332 }))
333 .end(done)
334 });
335
336
337 it('body should contain links property', function(done) {
338 request(getHttpServer(app))
339 .get('/')
340 .expect(getBody(function(res, body) {
341 assert.equal(body.links.length, 3);
342 hasLinkRel(body.links, 'self');
343 }))
344 .end(done)
345 });
346
347 it('links should contain rel to server', function(done) {
348 request(getHttpServer(app))
349 .get('/')
350 .expect(getBody(function(res, body) {
351 hasLinkRel(body.links, rels.server);
352 }))
353 .end(done)
354 });
355
356 it('should use a default server name if none has been provided', function(done) {
357 var app = zetta({ registry: reg, peerRegistry: peerRegistry }).silent()._run();
358
359 request(getHttpServer(app))
360 .get('/')
361 .expect(getBody(function(res, body) {
362 var self = body.links.filter(function(link) {
363 return link.rel.indexOf(rels.server) !== -1;
364 })[0];
365
366 assert.equal(self.title, os.hostname());
367 }))
368 .end(done);
369 });
370 });
371
372 describe('/peer-management', function() {
373 var app = null;
374
375 before(function(done) {
376 peerRegistry.save({
377 id: '12341234',
378 name: 'test-peer'
379 }, done);
380 });
381
382 beforeEach(function(done) {
383 app = zetta({ registry: reg, peerRegistry: peerRegistry })
384 .silent()
385 .use(Scout)
386 .name('local')
387 .expose('*')
388 ._run(done);
389 });
390
391 it('should have content type application/vnd.siren+json', function(done) {
392 request(getHttpServer(app))
393 .get('/peer-management')
394 .expect('Content-Type', 'application/vnd.siren+json', done);
395 });
396
397 it('should return status code 200', function(done) {
398 request(getHttpServer(app))
399 .get('/peer-management')
400 .expect(200, done);
401 });
402
403 it('should have class ["peer-management"]', function(done) {
404 request(getHttpServer(app))
405 .get('/peer-management')
406 .expect(getBody(function(err, body) {
407 assert.deepEqual(body.class, ['peer-management']);
408 }))
409 .end(done);
410 });
411
412 it('should list saved peers', function(done) {
413 peerRegistry.save({ id: '0' }, function() {
414 request(getHttpServer(app))
415 .get('/peer-management')
416 .expect(getBody(function(err, body) {
417 assert.equal(body.entities.length, 1);
418 }))
419 .end(done);
420 });
421 });
422
423 it('should allow the querying of peers with the ql parameter', function(done) {
424 peerRegistry.save({ id: '1', type: 'initiator'}, function() {
425 request(getHttpServer(app))
426 .get('/peer-management?ql=where%20type%3D%22initiator%22')
427 .expect(getBody(function(err, body) {
428 assert.equal(body.entities.length, 1);
429 var entity = body.entities[0];
430 assert.equal(entity.properties.id, '1');
431 }))
432 .end(done);
433 });
434 });
435
436 describe('#link', function() {
437 it('should return status code 202', function(done) {
438 request(getHttpServer(app))
439 .post('/peer-management')
440 .send('url=http://testurl')
441 .expect(202, done);
442 });
443
444 it('should return a Location header', function(done) {
445 request(getHttpServer(app))
446 .post('/peer-management')
447 .send('url=http://testurl')
448 .expect('Location', /^http.+/)
449 .end(done);
450 });
451 });
452
453 describe('#show', function() {
454 it('should return the peer item representation', function(done) {
455 var id = '1234-5678-9ABCD';
456 peerRegistry.save({ id: id }, function() {
457 request(getHttpServer(app))
458 .get('/peer-management/' + id)
459 .expect(200, done);
460 });
461 });
462 });
463 });
464
465 describe('/devices of server', function() {
466 var app = null;
467
468 beforeEach(function(done) {
469 app = zetta({ registry: reg, peerRegistry: peerRegistry })
470 .silent()
471 .use(Scout)
472 .name('local')
473 .expose('*')
474 ._run(done);
475 });
476
477 it('should have content type application/vnd.siren+json', function(done) {
478 request(getHttpServer(app))
479 .get('/devices')
480 .expect('Content-Type', 'application/vnd.siren+json', done);
481 });
482
483 it('should return status code 200', function(done) {
484 request(getHttpServer(app))
485 .get('/devices')
486 .expect(200, done);
487 });
488
489 it('should have class ["devices"]', function(done) {
490 request(getHttpServer(app))
491 .get('/devices')
492 .expect(getBody(function(res, body) {
493 assert.deepEqual(body.class, ['devices']);
494 }))
495 .end(done);
496 });
497
498 it('should have one valid entity', function(done) {
499 request(getHttpServer(app))
500 .get('/devices')
501 .expect(getBody(function(res, body) {
502 assert(body.entities);
503 assert.equal(body.entities.length, 1);
504 checkDeviceOnRootUri(body.entities[0]);
505 hasLinkRel(body.links, 'self');
506 }))
507 .end(done);
508 });
509 });
510
511
512
513
514 describe('/servers/:id/devices/:id', function() {
515 var app = null;
516 var url = null;
517 var device = null;
518
519 beforeEach(function(done) {
520 app = zetta({ registry: reg, peerRegistry: peerRegistry })
521 .silent()
522 .use(Scout)
523 .name('local')
524 .expose('*')
525 ._run(function() {
526 device = app.runtime._jsDevices[Object.keys(app.runtime._jsDevices)[0]];
527 url = '/servers/' + app._name + '/devices/' + device.id;
528 done();
529 });
530 });
531
532 it('should have content type application/vnd.siren+json', function(done) {
533 request(getHttpServer(app))
534 .get(url)
535 .expect('Content-Type', 'application/vnd.siren+json', done);
536 });
537
538 it('class should be ["device", ":type"]', function(done) {
539 request(getHttpServer(app))
540 .get(url)
541 .expect(getBody(function(res, body) {
542 assert(body.class.indexOf('device') >= 0);
543 assert(body.class.indexOf(body.properties.type) >= 0);
544 }))
545 .end(done);
546 });
547
548 /*
549 checkDeviceOnRootUri(body.entities[0]);
550 hasLinkRel(body.links, 'self');
551
552 */
553
554 it('properties should match expected', function(done) {
555 request(getHttpServer(app))
556 .get(url)
557 .expect(getBody(function(res, body) {
558 assert(body.properties);
559 assert.equal(body.properties.name, device.name);
560 assert.equal(body.properties.type, device.type);
561 assert.equal(body.properties.id, device.id);
562 assert.equal(body.properties.state, device.state);
563 }))
564 .end(done);
565 });
566
567 it('device should have action change', function(done) {
568 request(getHttpServer(app))
569 .get(url)
570 .expect(getBody(function(res, body) {
571 assert.equal(body.actions.length, 3);
572 var action = body.actions[0];
573 assert.equal(action.name, 'change');
574 assert.equal(action.method, 'POST');
575 assert(action.href);
576 assert.deepEqual(action.fields[0], { name: 'action', type: 'hidden', value: 'change' });
577 }))
578 .end(done);
579 });
580
581 it('device actions should have class "transition"', function(done) {
582 request(getHttpServer(app))
583 .get(url)
584 .expect(getBody(function(res, body) {
585 assert.equal(body.actions.length, 3);
586 body.actions.forEach(function(action) {
587 assert(action.class.indexOf('transition') >= 0);
588 })
589 }))
590 .end(done);
591 });
592
593
594 it('device should have self link', function(done) {
595 request(getHttpServer(app))
596 .get(url)
597 .expect(getBody(function(res, body) {
598 hasLinkRel(body.links, 'self');
599 }))
600 .end(done);
601 });
602
603 it('device should have edit link', function(done) {
604 request(getHttpServer(app))
605 .get(url)
606 .expect(getBody(function(res, body) {
607 hasLinkRel(body.links, 'edit');
608 }))
609 .end(done);
610 });
611
612 it('device should have up link to server', function(done) {
613 request(getHttpServer(app))
614 .get(url)
615 .expect(getBody(function(res, body) {
616 hasLinkRel(body.links, 'up', 'local');
617 }))
618 .end(done);
619 });
620
621 it('device should have monitor link for bar', function(done) {
622 request(getHttpServer(app))
623 .get(url)
624 .expect(getBody(function(res, body) {
625 hasLinkRel(body.links, 'monitor');
626 }))
627 .end(done);
628 });
629
630 it('device should have monitor link for bar formatted correctly for HTTP requests', function(done) {
631 request(getHttpServer(app))
632 .get(url)
633 .expect(getBody(function(res, body) {
634 var fooBar = body.links.filter(function(link) {
635 return link.title === 'foobar';
636 });
637
638 hasLinkRel(fooBar, rels.binaryStream);
639 var parsed = require('url').parse(fooBar[0].href);
640 assert.equal(parsed.protocol, 'ws:');
641 }))
642 .end(done);
643 });
644
645 it('should have a monitor link for bar formatted correctly for SPDY requests', function(done) {
646 var a = getHttpServer(app);
647
648 if (!a.address()) a.listen(0);
649
650 var agent = spdy.createAgent({
651 host: '127.0.0.1',
652 port: a.address().port,
653 spdy: {
654 plain: true,
655 ssl: false
656 }
657 });
658
659 var request = http.get({
660 host: '127.0.0.1',
661 port: a.address().port,
662 path: url,
663 agent: agent
664 }, function(response) {
665
666 var buffers = [];
667 response.on('readable', function() {
668 var data;
669 while ((data = response.read()) !== null) {
670 buffers.push(data);
671 }
672 });
673
674 response.on('end', function() {
675 var body = JSON.parse(Buffer.concat(buffers));
676 var fooBar = body.links.filter(function(link) {
677 return link.title === 'foobar';
678 });
679
680 hasLinkRel(fooBar, rels.binaryStream);
681 var parsed = require('url').parse(fooBar[0].href);
682 assert.equal(parsed.protocol, 'http:');
683 agent.close();
684 });
685
686 response.on('end', done);
687 }).end();
688 });
689
690 it('device action should return a 400 status code on a missing request body', function(done) {
691 request(getHttpServer(app))
692 .post(url)
693 .send()
694 .expect(getBody(function(res, body) {
695 assert.equal(res.statusCode, 400);
696 }))
697 .end(done);
698 });
699
700 it('device action should return a 400 status code on an invalid request body', function(done) {
701 request(getHttpServer(app))
702 .post(url)
703 .type('form')
704 .send('{ "what": "invalid" }')
705 .expect(getBody(function(res, body) {
706 assert.equal(res.statusCode, 400);
707 }))
708 .end(done);
709 });
710
711 it('device action should work', function(done) {
712 request(getHttpServer(app))
713 .post(url)
714 .type('form')
715 .send({ action: 'test', value: 123 })
716 .expect(getBody(function(res, body) {
717 assert.equal(body.properties.value, 123);
718 hasLinkRel(body.links, 'monitor');
719 }))
720 .end(done);
721 });
722
723 it('device action should return 400 when not available.', function(done) {
724 request(getHttpServer(app))
725 .post(url)
726 .type('form')
727 .send({ action: 'prepare' })
728 .expect(getBody(function(res, body) {
729 assert.equal(res.statusCode, 400);
730 }))
731 .end(done);
732 });
733
734 it('should return 500 when a error is passed in a callback of device driver', function(done) {
735 request(getHttpServer(app))
736 .post(url)
737 .type('form')
738 .send({ action: 'error', value: 'some error' })
739 .expect(getBody(function(res, body) {
740 assert.equal(res.statusCode, 500);
741 }))
742 .end(done);
743 });
744
745 it('should support device updates using PUT', function(done) {
746 request(getHttpServer(app))
747 .put(url)
748 .type('json')
749 .send({ bar: 2, value: 3 })
750 .expect(getBody(function(res, body) {
751 assert.equal(res.statusCode, 200);
752 assert.equal(body.properties.bar, 2);
753 assert.equal(body.properties.value, 3);
754 }))
755 .end(done);
756 });
757
758 it('should not overwrite monitor properties using PUT', function(done) {
759 request(getHttpServer(app))
760 .put(url)
761 .type('json')
762 .send({ foo: 1 })
763 .expect(getBody(function(res, body) {
764 assert.equal(res.statusCode, 200);
765 assert.equal(body.properties.foo, 0);
766 }))
767 .end(done);
768 });
769
770 it('should return a 404 when updating a non-existent device', function(done) {
771 request(getHttpServer(app))
772 .put(url + '1234567890')
773 .type('json')
774 .send({ foo: 1, bar: 2, value: 3 })
775 .expect(function(res) {
776 assert.equal(res.statusCode, 404);
777 })
778 .end(done);
779 });
780
781 it('should return a 400 when updating with a Content-Range header', function(done) {
782 request(getHttpServer(app))
783 .put(url)
784 .set('Content-Range', 'bytes 0-499/1234')
785 .type('json')
786 .send({ foo: 1, bar: 2, value: 3 })
787 .expect(function(res) {
788 assert.equal(res.statusCode, 400);
789 })
790 .end(done);
791 });
792
793 it('should return a 400 when receiving invalid JSON input', function(done) {
794 request(getHttpServer(app))
795 .put(url)
796 .type('json')
797 .send('{"name":}')
798 .expect(function(res) {
799 assert.equal(res.statusCode, 400);
800 })
801 .end(done);
802 });
803
804 it('should not include reserved fields on device updates', function(done) {
805 var input = { foo: 1, bar: 2, value: 3, id: 'abcdef',
806 _x: 4, type: 'h', state: 'yo', streams: 's' };
807
808 request(getHttpServer(app))
809 .put(url)
810 .type('json')
811 .send(input)
812 .expect(getBody(function(res, body) {
813 assert.equal(res.statusCode, 200);
814 assert.notEqual(body.properties.id, 'abcdef');
815 assert.notEqual(body.properties._x, 4);
816 assert.notEqual(body.properties.streams, 's');
817 assert.notEqual(body.properties.state, 'yo');
818 assert.notEqual(body.properties.type, 'h');
819 }))
820 .end(done);
821 });
822 });
823
824 describe('Proxied requests', function() {
825 var base = null;
826 var cluster = null;
827
828 beforeEach(function(done) {
829 cluster = zettacluster({ zetta: zetta })
830 .server('cloud')
831 .server('detroit', [Scout], ['cloud'])
832 .on('ready', function(){
833 base = 'localhost:' + cluster.servers['cloud']._testPort + '/servers/' + cluster.servers['cloud'].locatePeer('detroit');
834 setTimeout(done, 300);
835 })
836 .run(function(err) {
837 console.log(err)
838 if (err) {
839 done(err);
840 }
841 });
842 });
843
844 afterEach(function(done) {
845 cluster.stop();
846 setTimeout(done, 10); // fix issues with server not being closed before a new one starts
847 });
848
849 it('zetta should not crash when req to hub is pending and hub disconnects', function(done) {
850 http.get('http://' + base, function(res) {
851 assert.equal(res.statusCode, 502);
852 done();
853 }).on('socket', function(socket) {
854 socket.on('connect', function() {
855 cluster.servers['cloud'].httpServer.peers['detroit'].close();
856 });
857 })
858 })
859
860 })
861
862});