UNPKG

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