input-rttc.test.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. /**
  2. * Module dependencies
  3. */
  4. var assert = require('assert');
  5. var util = require('util');
  6. var Stream = require('stream');
  7. var _ = require('@sailshq/lodash');
  8. var M = require('../');
  9. var runSuite = require('../node_modules/rttc/spec/helpers/run-suite');
  10. var TEST_SUITE = require('../node_modules/rttc/spec/validation.spec.js');
  11. var expandSuite = require('../node_modules/rttc/spec/helpers/expand-suite');
  12. var toRunTest = require('./util/test-input-validation.helper');
  13. describe('argin validation & "light coercion" (for inputs)', function (){
  14. describe('using exhaustive fixtures from RTTC', function (){
  15. // Take the array of tests and extend them with some derivative
  16. // tests automatically.
  17. TEST_SUITE = expandSuite(TEST_SUITE);
  18. // Remove the "empty string" test for string examples, since these tests set the
  19. // input as "required" and for Machine (but not RTTC) an empty string is not a
  20. // valid value for a required string input. Also remove tests that expect null
  21. // values to work, since null is never okay for required inputs in machines.
  22. TEST_SUITE = _.reject(TEST_SUITE, function(test) {
  23. return _.any([
  24. { example: 'foo', actual: '', result: '' },
  25. { example: '===', actual: null, result: null },
  26. { example: '===', actual: 'null', result: 'null' },
  27. { example: '*', actual: null, result: null },
  28. ], function(testToSkip) {
  29. return _.isEqual(test, testToSkip);
  30. });
  31. });
  32. // Lodash 3.0 deprecated prototypal cloning of things like Errors
  33. // (so we shim a quick version for our purposes)
  34. var customCloneDeep = function (val){
  35. return _.cloneDeep(val, function(_val) {
  36. // Don't worry about cloning most things that _.cloneDeep would
  37. // normally reject; instead just pass them straight through.
  38. if (_.isError(_val)) {
  39. return _val;
  40. }
  41. else if (_.isFunction(_val)) {
  42. return _val;
  43. }
  44. else if (_.isObject(_val) && _val instanceof Buffer) {
  45. return _val;
  46. }
  47. else if (_.isObject(_val) && _val instanceof Stream) {
  48. return _val;
  49. }
  50. // Otherwise allow vanilla _.cloneDeep() behavior:
  51. else { return undefined; }
  52. });
  53. };
  54. // Modify the test suite to also test `typeclass` alongside the comparable examples.
  55. var extraTypeclassTests = [];
  56. _.each(TEST_SUITE, function (test){
  57. // Inject extra test to try `example:{}` as `typeclass: 'dictionary'` (at the top-level)
  58. if (_.isEqual(test.example, {})) {
  59. extraTypeclassTests.push((function(newTest){
  60. _.extend(newTest, customCloneDeep(test));
  61. delete newTest.example;
  62. newTest.typeclass = 'dictionary';
  63. return newTest;
  64. })({}));
  65. }
  66. // Inject extra test to try `example:[]` as `typeclass: 'array'` (at the top-level)
  67. else if (_.isEqual(test.example, [])) {
  68. extraTypeclassTests.push((function(newTest){
  69. _.extend(newTest, customCloneDeep(test));
  70. delete newTest.example;
  71. newTest.typeclass = 'array';
  72. return newTest;
  73. })({}));
  74. }
  75. // Inject extra test to try `example: '==='` as `typeclass: '*'` (at the top-level)
  76. else if (_.isEqual(test.example, '===')) {
  77. extraTypeclassTests.push((function(newTest){
  78. _.extend(newTest, customCloneDeep(test));
  79. delete newTest.example;
  80. newTest.typeclass = '*';
  81. return newTest;
  82. })({}));
  83. }
  84. });
  85. TEST_SUITE = TEST_SUITE.concat(extraTypeclassTests);
  86. // Now run all of the tests
  87. runSuite(TEST_SUITE, toRunTest );
  88. });//</using exhaustive fixtures from RTTC>
  89. describe('additional test cases specific to the machine runner', function() {
  90. it('should coerce input data into proper types', function(done) {
  91. var _inputs;
  92. var machine = {
  93. inputs: {
  94. foo: {
  95. example: 100
  96. },
  97. bar: {
  98. example: 'foobar'
  99. }
  100. },
  101. exits: {
  102. success: {
  103. example: 'hello'
  104. },
  105. error: {
  106. example: 'world'
  107. }
  108. },
  109. fn: function (inputs, exits, deps) {
  110. _inputs = inputs;
  111. exits(null, 'foo');
  112. }
  113. };
  114. try {
  115. M.build(machine)
  116. .configure({
  117. foo: '20',
  118. bar: 20
  119. })
  120. .exec(function(err, result) {
  121. if(err) { return done(err); }
  122. assert.strictEqual(_inputs.foo,20);
  123. assert.strictEqual(_inputs.bar,'20');
  124. done();
  125. });
  126. }
  127. catch (e){
  128. assert(false, 'Should not throw');
  129. }
  130. });
  131. it('should work the same using `type` instead of `example`', function(done) {
  132. var _inputs;
  133. var machine = {
  134. inputs: {
  135. foo: {
  136. type: 'number',
  137. example: 100
  138. },
  139. bar: {
  140. type: 'string',
  141. example: 'foobar'
  142. }
  143. },
  144. exits: {
  145. success: {
  146. example: 'hello'
  147. },
  148. error: {
  149. example: 'world'
  150. }
  151. },
  152. fn: function (inputs, exits, deps) {
  153. _inputs = inputs;
  154. exits(null, 'foo');
  155. }
  156. };
  157. try {
  158. M.build(machine)
  159. .configure({
  160. foo: '20',
  161. bar: 20
  162. })
  163. .exec(function(err, result) {
  164. if(err) { return done(err); }
  165. assert.strictEqual(_inputs.foo,20);
  166. assert.strictEqual(_inputs.bar,'20');
  167. done();
  168. });
  169. }
  170. catch (e){
  171. assert(false, 'Should not throw');
  172. }
  173. });
  174. it('should error if an example or typeclass is not given for an input', function() {
  175. var machine = {
  176. inputs: {
  177. foo: {}
  178. },
  179. exits: {
  180. success: {
  181. example: 'hello'
  182. },
  183. error: {
  184. example: 'world'
  185. }
  186. },
  187. fn: function (inputs, exits, deps) {
  188. exits(null, 'foo');
  189. }
  190. };
  191. assert.throws(function() {
  192. M.build(machine);
  193. }, Error);
  194. });
  195. it('should error if an invalid type is given for an input', function() {
  196. var machine = {
  197. inputs: {
  198. foo: {
  199. type: 'foobar'
  200. }
  201. },
  202. exits: {
  203. success: {
  204. example: 'hello'
  205. },
  206. error: {
  207. example: 'world'
  208. }
  209. },
  210. fn: function (inputs, exits, deps) {
  211. exits(null, 'foo');
  212. }
  213. };
  214. try {
  215. M.build(machine);
  216. }
  217. catch (e) {
  218. switch (e.code) {
  219. case 'MACHINE_INPUT_INVALID': break; // ok
  220. default: throw new Error('Expected MACHINE_INPUT_INVALID error, but instead got `code: '+e.code+'`. Details: '+e.stack);
  221. }
  222. }
  223. });
  224. it('should error if an input has an incompatible type / example combo', function() {
  225. var machine = {
  226. inputs: {
  227. foo: {
  228. type: 'number',
  229. example: 'abc'
  230. }
  231. },
  232. exits: {
  233. success: {
  234. example: 'hello'
  235. },
  236. error: {
  237. example: 'world'
  238. }
  239. },
  240. fn: function (inputs, exits, deps) {
  241. exits(null, 'foo');
  242. }
  243. };
  244. assert.throws(function() {
  245. M.build(machine);
  246. }, Error);
  247. });
  248. it('should fail if input expects string, but empty object ({}) is provided', function(done) {
  249. testInputConfiguration({
  250. example: 'asdf',
  251. actual: {}
  252. }, function (err, result){
  253. if (!err) {
  254. return done(new Error('Expected `error` outcome- instead no error, and got result:'+util.inspect(result)));
  255. }
  256. return done();
  257. });
  258. });
  259. it('should fail if input expects string, but empty array ([]) is provided', function(done) {
  260. testInputConfiguration({
  261. example: 'asdf',
  262. actual: []
  263. }, function (err, result){
  264. if (!err) {
  265. return done(new Error('Expected `error` outcome- instead no error, and got result:'+util.inspect(result)));
  266. }
  267. return done();
  268. });
  269. });
  270. it('should fail if input expects string, but `{foo:"bar"}` is provided', function(done) {
  271. testInputConfiguration({
  272. example: 'asdf',
  273. actual: {foo: 'bar'}
  274. }, function (err, result){
  275. if (!err) {
  276. return done(new Error('Expected `error` outcome- instead no error, and got result:'+util.inspect(result)));
  277. }
  278. return done();
  279. });
  280. });
  281. it('should fail if input expects string, but `{foo:{bar: "baz"}}` is provided', function(done) {
  282. testInputConfiguration({
  283. example: 'asdf',
  284. actual: {foo:{bar: 'baz'}}
  285. }, function (err, result){
  286. if (!err) {
  287. return done(new Error('Expected `error` outcome- instead no error, and got result:'+util.inspect(result)));
  288. }
  289. return done();
  290. });
  291. });
  292. it('should fail if input expects number, but `{foo:{bar: "baz"}}` is provided', function(done) {
  293. testInputConfiguration({
  294. example: 1234,
  295. actual: {foo:{bar: 'baz'}}
  296. }, function (err, result){
  297. if (!err) {
  298. return done(new Error('Expected `error` outcome- instead no error, and got result:'+util.inspect(result)));
  299. }
  300. return done();
  301. });
  302. });
  303. });//</additional test cases specific to the machine runner>
  304. });
  305. ////////////////////////////////////////////////////////////////////////////
  306. ////////////////////////////////////////////////////////////////////////////
  307. ////////////////////////////////////////////////////////////////////////////
  308. ////////////////////////////////////////////////////////////////////////////
  309. ////////////////////////////////////////////////////////////////////////////
  310. //////////////////////////////// a private test util used above ////////////
  311. /**
  312. * testInputConfiguration()
  313. *
  314. * Private test util.
  315. *
  316. * > TODO: move into a separate file.
  317. *
  318. * @required {Ref} actual
  319. * @optional {Exemplar} example
  320. *
  321. * @callback
  322. * @param {Error} err
  323. * @param {Ref} result
  324. */
  325. function testInputConfiguration(options, cb){
  326. var _inputsInFn;
  327. var outputValue;
  328. var machine = M.build({
  329. inputs: {
  330. x: (function _determineInputDefinition(){
  331. var def = {};
  332. if (!_.isUndefined(options.example)) {
  333. def.example = options.example;
  334. }
  335. return def;
  336. })()
  337. },
  338. exits: {
  339. error: {},
  340. success: {}
  341. },
  342. fn: function (inputs, exits) {
  343. _inputsInFn = inputs;
  344. exits(null, outputValue);
  345. }
  346. })
  347. .configure({
  348. x: options.actual
  349. })
  350. .exec(function (err, result){
  351. return cb(err, result);
  352. });
  353. }