├── .gitattributes ├── test ├── mocks │ ├── _nodata.get.json │ ├── _boolean.get.json │ ├── _404.get.json │ ├── _500.get.json │ ├── form │ │ ├── index.get.json │ │ └── index.post.json │ ├── index.get.json │ └── data.js ├── spec │ ├── parse.spec.js │ ├── timeout.spec.js │ ├── 204.spec.js │ ├── options.spec.js │ ├── prop.spec.js │ ├── middleware.spec.js │ ├── init.spec.js │ ├── data.spec.js │ └── actions.spec.js └── setup.js ├── NOTICE ├── .editorconfig ├── .travis.yml ├── CHANGELOG.md ├── .jshintrc ├── config └── prefs.ini ├── .gitignore ├── package.json ├── karma.conf.js ├── karma-ci.conf.js ├── CONTRIBUTING.md ├── README.md ├── gulpfile.js ├── lib ├── extend.js ├── urltemplate.js └── urlparse.js ├── demo └── hal.html ├── LICENSE └── src └── hyperGard.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /test/mocks/_nodata.get.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mocks/_boolean.get.json: -------------------------------------------------------------------------------- 1 | true 2 | -------------------------------------------------------------------------------- /test/mocks/_404.get.json: -------------------------------------------------------------------------------- 1 | //! statusCode: 404 2 | -------------------------------------------------------------------------------- /test/mocks/_500.get.json: -------------------------------------------------------------------------------- 1 | //! statusCode: 500 2 | -------------------------------------------------------------------------------- /test/mocks/form/index.get.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": true 3 | } 4 | -------------------------------------------------------------------------------- /test/mocks/form/index.post.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": true 3 | } 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | hypergard 2 | Copyright 2015-2018 Comcast Cable Communications Management, LLC 3 | 4 | This product includes software developed at Comcast (http://www.comcast.com/). 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '10' 5 | script: npm run build 6 | deploy: 7 | provider: npm 8 | skip_cleanup: true 9 | email: brendan_davies@comcast.com 10 | api_key: $NPM_TOKEN 11 | on: 12 | tags: true 13 | repo: Comcast/hypergard 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This should be updated on every major tag. 4 | 5 | Add your new version to the top and a brief summary. 6 | 7 | **This does not replace `tag` and `release` and semver best practices. It is a supplement.** 8 | 9 | --- 10 | 11 | ## v6.0.0 12 | 13 | Simplify build process, by no longer requiring developer to check in `dist` folder. 14 | Upgrade node from 6 > 10. (Node 12 would require additional changes to the build process) 15 | 16 | --- 17 | 18 | ## v5.1.0 19 | 20 | Initial publishing of open source version of `Hypergard` 21 | 22 | --- -------------------------------------------------------------------------------- /test/spec/parse.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paul.Bronshteyn 3 | * @comment Built by a geek loaded on caffeine ... 4 | */ 5 | describe('hyperGard', function() { 6 | beforeEach(function() { 7 | this.testHyperGard = new HyperGard(testEndpoint, testOptions); 8 | }); 9 | 10 | describe('Test hyperGard parse', function() { 11 | it('should return a parsed Resource', function() { 12 | expect(this.testHyperGard.parse({})).toEqual(jasmine.any(Object)); 13 | }); 14 | 15 | it('should return an empty parsed Resource', function() { 16 | expect(this.testHyperGard.parse()).toEqual(jasmine.any(Object)); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "jquery": true, 4 | "node": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "esversion": 6, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": false, 12 | "newcap": true, 13 | "noarg": true, 14 | "noempty": true, 15 | "quotmark": "single", 16 | "regexp": true, 17 | "nomen": true, 18 | "nonew": true, 19 | "undef": true, 20 | "unused": true, 21 | "strict": true, 22 | "validthis": true, 23 | "trailing": true, 24 | "smarttabs": true, 25 | "white": true, 26 | "expr": true, 27 | "predef": [ 28 | "urlparse", 29 | "urltemplate", 30 | "define", 31 | "Promise", 32 | "Response" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /config/prefs.ini: -------------------------------------------------------------------------------- 1 | Opera Preferences version 2.1 2 | 3 | [User Prefs] 4 | Show Default Browser Dialog = 0 5 | Startup Type = 2 6 | Home URL = about:blank 7 | Show Close All But Active Dialog = 0 8 | Show Close All Dialog = 0 9 | Show Crash Log Upload Dialog = 0 10 | Show Delete Mail Dialog = 0 11 | Show Download Manager Selection Dialog = 0 12 | Show Geolocation License Dialog = 0 13 | Show Mail Error Dialog = 0 14 | Show New Opera Dialog = 0 15 | Show Problem Dialog = 0 16 | Show Progress Dialog = 0 17 | Show Validation Dialog = 0 18 | Show Widget Debug Info Dialog = 0 19 | Show Startup Dialog = 0 20 | Show E-mail Client = 0 21 | Show Mail Header Toolbar = 0 22 | Show Setupdialog On Start = 0 23 | Ask For Usage Stats Percentage = 0 24 | Enable Usage Statistics = 0 25 | Disable Opera Package AutoUpdate = 1 26 | Browser JavaScript = 0 27 | 28 | [Install] 29 | Newest Used Version = 1.00.0000 30 | 31 | [State] 32 | Accept License = 1 33 | Run = 0 34 | -------------------------------------------------------------------------------- /test/spec/timeout.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paul.Bronshteyn 3 | * @comment Built by a geek loaded on caffeine ... 4 | */ 5 | describe('hyperGard', function() { 6 | beforeEach(function(done) { 7 | window.fetch.and.returnValue(new Promise(function() {})); 8 | this.testHyperGard = new HyperGard(testEndpoint, { 9 | preloadHomepage: false, 10 | xhr: { 11 | timeout: 1 12 | } 13 | }); 14 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 15 | }); 16 | 17 | describe('Test fetch timeout', function() { 18 | it('should not return data', function() { 19 | expect(this.data).toBe(null); 20 | }); 21 | 22 | it('should return proper error object', function() { 23 | expect(this.error).toEqual({ 24 | error: { 25 | code: '0011', 26 | msg: 'Fetch timeout', 27 | timeout: 1 28 | } 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/spec/204.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paul.Bronshteyn 3 | * @comment Built by a geek loaded on caffeine ... 4 | */ 5 | describe('hyperGard', function() { 6 | beforeEach(function(done) { 7 | window.fetch.and.returnValue(this.homepageData(homepage)); 8 | this.testHyperGard = new HyperGard(testEndpoint, testOptions); 9 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 10 | }); 11 | 12 | describe('Test 204 response', function() { 13 | beforeEach(function(done) { 14 | this.link = this.data.getFirstAction('twoParamsPath', { 15 | path1: 'path1', 16 | path2: 'path2' 17 | }); 18 | 19 | window.fetch.and.returnValue(this.homepageData('', 204)); 20 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 21 | }); 22 | 23 | it('should return empty data response', function() { 24 | expect(this.actionData).toEqual(''); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Transpiled files to support testing 61 | .test-build 62 | 63 | # Build folder 64 | dist/ 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hypergard", 3 | "version": "6.0.0", 4 | "repository": "git@github.com:Comcast/hypergard.git", 5 | "bugs": "https://github.com/Comcast/hypergard/issues", 6 | "homepage": "https://github.com/Comcast/hypergard", 7 | "main": "dist/hyperGard.js", 8 | "module": "dist/hyperGard.es.js", 9 | "devDependencies": { 10 | "canned": ">=0.3.7", 11 | "del": "^3.0.0", 12 | "es6-promise": "^3.2.2", 13 | "gulp": "~3.9.0", 14 | "gulp-better-rollup": "^2.0.0", 15 | "gulp-butternut": "^1.0.0", 16 | "gulp-concat": "~2.6.0", 17 | "gulp-jshint": "~2.0.0", 18 | "gulp-rename": "~1.2.2", 19 | "gulp-sourcemaps": "^2.6.0", 20 | "gulp-sync": "~0.1.4", 21 | "handlebars": "^4.0.11", 22 | "jasmine-core": "~2.4.1", 23 | "jshint": "~2.9.1-rc2", 24 | "jshint-stylish": "~2.1.0", 25 | "karma": "~0.13.19", 26 | "karma-chrome-launcher": "~0.2.2", 27 | "karma-coverage": "~0.5.3", 28 | "karma-firefox-launcher": "~0.1.7", 29 | "karma-jasmine": "~0.3.6", 30 | "karma-mocha": "~0.2.1", 31 | "karma-mocha-reporter": "~1.2.3", 32 | "karma-opera-launcher": "~0.3.0", 33 | "karma-phantomjs-launcher": "~1.0.0", 34 | "karma-safari-launcher": "~0.1.1", 35 | "lazypipe": "~1.0.1", 36 | "merge-stream": "^1.0.1", 37 | "phantomjs-prebuilt": "~2.1.4", 38 | "whatwg-fetch": "^2.0.3" 39 | }, 40 | "engines": { 41 | "node": ">=10.0.0", 42 | "npm": ">=6.0.0" 43 | }, 44 | "scripts": { 45 | "build": "gulp build:all", 46 | "setup": "npm install", 47 | "pretest": "gulp build-test-files", 48 | "test": "gulp test-ci" 49 | }, 50 | "directories": { 51 | "test": "test" 52 | }, 53 | "files": [ 54 | "src/**/*", 55 | "lib/**/*", 56 | "dist/**/*" 57 | ], 58 | "license": "Apache-2.0" 59 | } 60 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | 3 | module.exports = function(config) { 4 | 'use strict'; 5 | 6 | config.set({ 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | client: { 11 | captureConsole: true 12 | }, 13 | 14 | frameworks: ['jasmine'], 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | '.test-build/*.js', 19 | 'test/**/*.js' 20 | ], 21 | 22 | // list of files to exclude 23 | exclude: [], 24 | 25 | // test results reporter to use 26 | // possible values: dots || progress || growl 27 | reporters: ['progress'], 28 | 29 | // web server port 30 | port: 8080, 31 | 32 | // cli runner port 33 | runnerPort: 9100, 34 | 35 | // enable / disable colors in the output (reporters and logs) 36 | colors: true, 37 | 38 | // level of logging 39 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 40 | logLevel: config.LOG_INFO, 41 | 42 | // enable / disable watching file and executing tests whenever any file changes 43 | autoWatch: false, 44 | 45 | // Start these browsers, currently available: 46 | // - Chrome 47 | // - ChromeCanary 48 | // - Firefox 49 | // - Opera 50 | // - Safari (only Mac) 51 | // - PhantomJS 52 | // - IE (only Windows) 53 | browsers: ['Chrome'], 54 | 55 | // If browser does not capture in given timeout [ms], kill it 56 | captureTimeout: 5000, 57 | 58 | // Continuous Integration mode 59 | // if true, it capture browsers, run tests and exit 60 | singleRun: false 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /test/spec/options.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paul.Bronshteyn 3 | * @comment Built by a geek loaded on caffeine ... 4 | */ 5 | describe('hyperGard', function() { 6 | beforeEach(function() { 7 | this.testHyperGard = new HyperGard(testEndpoint, testOptions); 8 | }); 9 | 10 | describe('Test hyperGard options', function() { 11 | describe('getOptions', function() { 12 | it('should have getOptions api', function() { 13 | expect(this.testHyperGard.getOptions).toEqual(jasmine.any(Function)); 14 | }); 15 | 16 | it('it should get preloadHomepage option', function() { 17 | expect(this.testHyperGard.getOptions().preloadHomepage).toEqual(false); 18 | }); 19 | 20 | it('it should get cacheHomepage option', function() { 21 | expect(this.testHyperGard.getOptions().cacheHomepage).toEqual(false); 22 | }); 23 | 24 | it('it should get debug option', function() { 25 | expect(this.testHyperGard.getOptions().debug).toEqual(true); 26 | }); 27 | 28 | it('it should get fetch options', function() { 29 | expect(this.testHyperGard.getOptions().xhr.headers.auth).toEqual(testOptions.xhr.headers.auth); 30 | }); 31 | }); 32 | 33 | describe('setOptions', function() { 34 | it('should have setOptions api', function() { 35 | expect(this.testHyperGard.setOptions).toEqual(jasmine.any(Function)); 36 | }); 37 | 38 | it('it should set debug option', function() { 39 | this.testHyperGard.setOptions({ debug: false }); 40 | expect(this.testHyperGard.getOptions().debug).toEqual(false); 41 | }); 42 | 43 | it('it should not change debug option', function() { 44 | this.testHyperGard.setOptions(); 45 | expect(this.testHyperGard.getOptions().debug).toEqual(true); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/spec/prop.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paul.Bronshteyn 3 | * @comment Built by a geek loaded on caffeine ... 4 | */ 5 | describe('hyperGard', function() { 6 | beforeEach(function(done) { 7 | window.fetch.and.returnValue(this.homepageData(homepage)); 8 | this.testHyperGard = new HyperGard(testEndpoint, testOptions); 9 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 10 | }); 11 | 12 | describe('Test retrieval of properties', function() { 13 | describe('by using getProp function', function() { 14 | it('calls getProp with array', function() { 15 | expect(this.data.getProp([])).toBeUndefined(); 16 | }); 17 | 18 | describe('calls getProp with boolean', function() { 19 | it('calls getProp with true', function() { 20 | expect(this.data.getProp(true)).toBeUndefined(); 21 | }); 22 | 23 | it('calls getProp with false', function() { 24 | expect(this.data.getProp(false)).toBeUndefined(); 25 | }); 26 | }); 27 | 28 | it('calls getProp with null', function() { 29 | expect(this.data.getProp(null)).toBeUndefined(); 30 | }); 31 | 32 | it('calls getProp with number', function() { 33 | expect(this.data.getProp(1)).toBeUndefined(); 34 | }); 35 | 36 | it('calls getProp with string', function() { 37 | expect(this.data.getProp('string')).toBeUndefined(); 38 | }); 39 | 40 | it('calls getProp with object', function() { 41 | expect(this.data.getProp({})).toBeUndefined(); 42 | }); 43 | 44 | it('calls getProp with undefined', function() { 45 | expect(this.data.getProp(undefined)).toBeUndefined(); 46 | }); 47 | }); 48 | 49 | describe('by using getProps function', function() { 50 | it('calls getProps', function() { 51 | expect(this.data.getProps()).toEqual(jasmine.any(Object)); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /karma-ci.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | 3 | module.exports = function(config) { 4 | 'use strict'; 5 | 6 | config.set({ 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'node_modules/es6-promise/dist/es6-promise.js', 15 | 'node_modules/whatwg-fetch/fetch.js', 16 | '.test-build/*.js', 17 | 'test/**/*.js', 18 | ], 19 | 20 | // list of files to exclude 21 | exclude: [], 22 | 23 | preprocessors: { 24 | 'src/hyperGard.js': 'coverage' 25 | }, 26 | 27 | coverageReporter: { 28 | type: 'lcov', 29 | dir: 'coverage/' 30 | }, 31 | 32 | // test results reporter to use 33 | // possible values: dots || progress || growl 34 | reporters: ['coverage', 'mocha'], 35 | 36 | // web server port 37 | port: 8880, 38 | 39 | // cli runner port 40 | runnerPort: 9100, 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | // level of logging 46 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 47 | logLevel: config.LOG_INFO, 48 | 49 | // enable / disable watching file and executing tests whenever any file changes 50 | autoWatch: false, 51 | 52 | // Start these browsers, currently available: 53 | // - Chrome 54 | // - ChromeCanary 55 | // - Firefox 56 | // - Opera 57 | // - Safari (only Mac) 58 | // - PhantomJS 59 | // - IE (only Windows) 60 | browsers: ['PhantomJS'], 61 | 62 | // If browser does not capture in given timeout [ms], kill it 63 | captureTimeout: 5000, 64 | 65 | // Continuous Integration mode 66 | // if true, it capture browsers, run tests and exit 67 | singleRun: false 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | if (!Function.prototype.bind) { 2 | Function.prototype.bind = function(oThis) { 3 | if (typeof this !== 'function') { 4 | // closest thing possible to the ECMAScript 5 5 | // internal IsCallable function 6 | throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); 7 | } 8 | 9 | var aArgs = [].slice.call(arguments, 1), 10 | fToBind = this, 11 | fNOP = function() { 12 | }, 13 | fBound = function() { 14 | return fToBind.apply(this instanceof fNOP 15 | ? this 16 | : oThis, 17 | aArgs.concat([].slice.call(arguments))); 18 | }; 19 | 20 | fNOP.prototype = this.prototype; 21 | fBound.prototype = new fNOP(); 22 | 23 | return fBound; 24 | }; 25 | } 26 | 27 | var toString = {}.toString; 28 | 29 | var testEndpoint = '/endpoint/'; 30 | var testOptions = { 31 | preloadHomepage: false, 32 | cacheHomepage: false, 33 | debug: true, 34 | 35 | xhr: { 36 | headers: { 37 | Authorization: 'auth' 38 | } 39 | } 40 | }; 41 | 42 | beforeEach(function() { 43 | this.hyperGard = null; 44 | this.error = null; 45 | this.data = null; 46 | this.actionData = null; 47 | this.actionError = null; 48 | 49 | this.homepageData = function(responseData, status) { 50 | return new window.Response(JSON.stringify(responseData), { 51 | status: status || 200, 52 | headers: { 53 | 'Content-type': 'application/hal+json' 54 | } 55 | }); 56 | }; 57 | 58 | this.onError = function(error) { 59 | this.error = error; 60 | }.bind(this); 61 | 62 | this.onSuccess = function(response) { 63 | this.data = response.data; 64 | }.bind(this); 65 | 66 | this.onActionError = function(error) { 67 | this.actionError = error; 68 | }.bind(this); 69 | 70 | this.onActionSuccess = function(response) { 71 | this.actionData = response.data; 72 | }.bind(this); 73 | 74 | spyOn(window, 'fetch'); 75 | spyOn(this, 'onError').and.callThrough(); 76 | spyOn(this, 'onSuccess').and.callThrough(); 77 | spyOn(this, 'onActionError').and.callThrough(); 78 | spyOn(this, 'onActionSuccess').and.callThrough(); 79 | }); 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribution Guidelines 2 | ======================= 3 | 4 | We love to see contributions to the project and have tried to make it easy to do so. If you would like to contribute code to this project you can do so through GitHub by forking the repository and sending a pull request. 5 | 6 | Before Comcast merges your code into the project you must sign the [Comcast Contributor License Agreement (CLA)](https://gist.github.com/ComcastOSS/a7b8933dd8e368535378cda25c92d19a). 7 | 8 | If you haven't previously signed a Comcast CLA, you'll automatically be asked to when you open a pull request. Alternatively, we can send you a PDF that you can sign and scan back to us. Please create a new GitHub issue to request a PDF version of the CLA. 9 | 10 | For more details about contributing to GitHub projects see 11 | http://gun.io/blog/how-to-github-fork-branch-and-pull-request/ 12 | 13 | Documentation 14 | ------------- 15 | 16 | If you contribute anything that changes the behavior of the 17 | application, document it in the [README](https://github.com/Comcast/hypergard/blob/master/README.md) or [wiki](https://github.com/Comcast/hypergard/wiki)! This includes new features, additional variants of behavior and breaking changes. 18 | 19 | Testing 20 | ------- 21 | 22 | Tests are written in [Jasmine](http://jasmine.github.io/), run with [Karma](http://karma-runner.github.io/), and instrumented by [Istanbul](https://github.com/yahoo/istanbul) via [karma-coverage](https://github.com/karma-runner/karma-coverage). 23 | 24 | For a pull request to be accepted, it must have automated tests. If you're having trouble writing the tests, feel free to send your pull request and mention you need help testing it. 25 | 26 | Pull Requests 27 | ------------- 28 | 29 | * should be from a forked project with an appropriate branch name 30 | * should be narrowly focused with no more than 3 or 4 logical commits 31 | * when possible, address no more than one issue 32 | * should be reviewable in the GitHub code review tool 33 | * should be linked to any issues it relates to (i.e. issue number after 34 | (#) in commit messages or pull request message) 35 | 36 | Expect a thorough review process for any pull requests that add functionality or change the behavior of the application. We encourage you to sketch your 37 | approach in writing on a relevant issue (or creating such an issue if needed) 38 | before starting to code, in order to save time and frustration all around. 39 | -------------------------------------------------------------------------------- /test/spec/middleware.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Brendan.Davies 3 | * @comment Testing middleware stack 4 | */ 5 | describe('Middleware', function() { 6 | var customHeader1 = { 7 | 'X-CustomHeader1': true, 8 | }; 9 | var customHeader2 ={ 10 | 'X-CustomHeader2': true, 11 | }; 12 | var mockLogger = { 13 | log: function() {}, 14 | }; 15 | 16 | function headerMiddlewareOne(url, options, next) { 17 | var newOptions = deepExtend({}, options, { headers: customHeader1}); 18 | return next(url, newOptions); 19 | } 20 | 21 | function headerMiddlewareTwo(url, options, next) { 22 | var newOptions = deepExtend({}, options, { headers: customHeader2}); 23 | return next(url, newOptions); 24 | } 25 | 26 | function loggerMiddleware(url, options, next) { 27 | var result = next(url, options); 28 | 29 | mockLogger.log('headers', options.headers); 30 | result 31 | .then(function(response) { 32 | mockLogger.log('success', {status: response.status}); 33 | }) 34 | .catch(function(error) { 35 | mockLogger.log('failure', {error: error.status}); 36 | }) 37 | 38 | return result; 39 | } 40 | 41 | beforeEach(function() { 42 | window.fetch.and.returnValue(this.homepageData(homepage)); 43 | this.testHyperGard = new HyperGard(testEndpoint, testOptions); 44 | this.testHyperGard.applyMiddlewareStack([ 45 | headerMiddlewareOne, 46 | headerMiddlewareTwo, 47 | loggerMiddleware, 48 | ]); 49 | }); 50 | 51 | describe('applyMiddlewareStack, manipulating request prior to fetch', function() { 52 | beforeEach(function(done) { 53 | this.testHyperGard.fetch().then(done, done); 54 | }); 55 | 56 | it('Expect homepage endpoint to be called', function() { 57 | expect(window.fetch).toHaveBeenCalledWith(testEndpoint, jasmine.any(Object)); 58 | }); 59 | 60 | it('Expect each mock header to be set, before fetch', function() { 61 | var expectedHeaders = deepExtend({}, customHeader1, customHeader2); 62 | expect(window.fetch).toHaveBeenCalledWith( 63 | testEndpoint, 64 | { 65 | action: jasmine.any(String), 66 | headers: jasmine.objectContaining(expectedHeaders), 67 | method: jasmine.any(String), 68 | } 69 | ); 70 | }); 71 | }); 72 | 73 | describe('applyMiddlewareStack, responding to success', function() { 74 | beforeEach(function(done) { 75 | spyOn(mockLogger, 'log'); 76 | this.testHyperGard.fetch().then(done, done); 77 | }); 78 | 79 | it('Logger is called after both headers are set', function() { 80 | var expectedHeaders = deepExtend({}, customHeader1, customHeader2); 81 | expect(mockLogger.log).toHaveBeenCalledWith( 82 | 'headers', 83 | jasmine.objectContaining(expectedHeaders) 84 | ); 85 | }); 86 | 87 | it('To be called after fetch on success', function() { 88 | expect(mockLogger.log).toHaveBeenCalledWith('success', {status: 200}); 89 | }); 90 | }); 91 | 92 | describe('applyMiddlewareStack, responding to rejection', function() { 93 | beforeEach(function(done) { 94 | spyOn(mockLogger, 'log'); 95 | window.fetch.and.returnValue(this.homepageData('', 500)); 96 | this.testHyperGard.fetch().then(done, done); 97 | }); 98 | 99 | it('To be called after fetch on failure', function() { 100 | expect(mockLogger.log).toHaveBeenCalledWith('failure', {error: 500}); 101 | }); 102 | }); 103 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyperGard 2 | 3 | [![Build Status](https://travis-ci.org/Comcast/hypergard.svg?branch=master)](https://travis-ci.org/Comcast/hypergard) 4 | 5 | Javascript client for HAL APIs, with support for Hypermedia Forms 6 | 7 | 8 | ## Installation 9 | 10 | When using [npm](https://www.npmjs.com/) 11 | 12 | ```bash 13 | npm install --save hypergard 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### Initialize HyperGard instance 19 | ```javascript 20 | const HOMEPAGE_ENDPOINT = 'https://hypermedia-endpoint.com/' 21 | const HalApi = new HyperGard(HOMEPAGE_ENDPOINT, {}); 22 | ``` 23 | 24 | ### Fetch Homepage 25 | ```javascript 26 | const homepageResource = await HalApi.fetchHomepage(); 27 | ``` 28 | 29 | ### Options 30 | 31 | #### `cacheHomepage` 32 | 33 | Default value: `false` 34 | 35 | Determines whether to keep locally closed over reference to homepage response, since the Homepage resource should be highly cache-able by Hypermedia standards. 36 | 37 | #### `preloadHomepage` 38 | 39 | Default: `true` 40 | 41 | Auto-fetch homepage endpoint on initialization of HyperGard object. 42 | 43 | #### `xhr` 44 | 45 | Network request that will be passed along to [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). 46 | 47 | Additionally `hypergard` implements a timeout around any network fetch, which will default to `60000` milliseconds 48 | 49 | ```js 50 | { 51 | // Headers to be added to each network fetch 52 | headers: { 53 | ['X-Custom-Header']: "value", 54 | } 55 | // Timeout period in milliseconds 56 | timeout: 2000, 57 | } 58 | ``` 59 | 60 | #### `applyMiddlewareStack` 61 | 62 | Allows you to pass an array of middleware function to wrap around each fetch. 63 | 64 | Each middleware function should: 65 | * Have a method signature with arguments `url`, `options`, and `next` 66 | * Return the remaining stack `return next(url, options)` so the promise chain isn't broken 67 | 68 | #### Example of Middleware for accessing calls before fetch. (Replaces `beforeFetch` and `uniqueFetchHeaders` functionality) 69 | ``` 70 | function setCustomHeader(url, options, next) { 71 | var newOptions = Object.assign({}, options, { 72 | headers: { 73 | 'X-MoneyTrace': 'hey nowwwww', 74 | } 75 | }); 76 | 77 | // Call next piece of middleware 78 | return next(url, newOptions); 79 | } 80 | ``` 81 | 82 | #### Example of Middleware for accessing calls after fetch (Replaces `afterFetchSuccess` and `afterFetchFailure` functionality) 83 | ``` 84 | function loggerMiddleware(url, options, next) { 85 | // Call next piece of middleware 86 | var promiseChain = next(url, options); 87 | 88 | // Log any 401 89 | promiseChain.catch(function(error) { 90 | if (error.status === '401') { 91 | mockLogger.log('Unauthorized', {error: error}); 92 | } 93 | }) 94 | 95 | // Return un-caught promise chain 96 | return promiseChain; 97 | } 98 | ``` 99 | 100 | #### Example of Applying middleware stack 101 | 102 | Middleware can be applied to an initialized `HyperGard` object, and will be executed based on order of array. 103 | 104 | ``` 105 | HalApi.applyMiddlewareStack([ 106 | setCustomHeader, 107 | loggerMiddleware, 108 | ]); 109 | ``` 110 | 111 | ## Running Tests 112 | 113 | ### Install Dependencies 114 | 115 | ``` 116 | $ npm run setup 117 | ``` 118 | 119 | ### Running tests to manually validate a patchset 120 | 121 | ``` 122 | $ npm test 123 | ``` 124 | 125 | ### Running tests during development 126 | 127 | ``` 128 | $ gulp test 129 | ``` 130 | 131 | That `gulp test` command will load up Chrome. Click the "Debug" button and then open the JavaScript Console to see the test results. You can also use `console` methods to be able to debug your tests. 132 | 133 | Note that if you make a code change, you cannot simply reload http://localhost:8080/debug.html in Chrome. You have to stop the `gulp test` process with `Control+C` and then rerun the command (there's probably a better way to handle that, but it does the job for now). 134 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gulpsync = require('gulp-sync')(gulp); 3 | var jshint = require('gulp-jshint'); 4 | var rename = require("gulp-rename"); 5 | var sourcemaps = require('gulp-sourcemaps'); 6 | var lazypipe = require('lazypipe'); 7 | var karma = require('karma').Server; 8 | var rollup = require('gulp-better-rollup'); 9 | var butternut = require('gulp-butternut'); 10 | var mergeStream = require('merge-stream'); 11 | var del = require('del'); 12 | 13 | var testFiles = [ 14 | { 15 | src: './lib/extend.js', 16 | moduleName: 'deepExtend', 17 | }, 18 | { 19 | src: './lib/urlparse.js', 20 | moduleName: 'urlparse', 21 | }, 22 | { 23 | src: './lib/urltemplate.js', 24 | moduleName: 'urltemplate', 25 | }, 26 | { 27 | src: './src/hyperGard.js', 28 | moduleName: 'HyperGard', 29 | }, 30 | ]; 31 | 32 | var jshintFlow = lazypipe() 33 | .pipe(jshint) 34 | .pipe(jshint.reporter, 'jshint-stylish') 35 | .pipe(jshint.reporter, 'fail'); 36 | 37 | gulp.task('default', ['build-test-files', 'test-ci']); 38 | 39 | gulp.task('test', function(done) { 40 | new karma({ 41 | configFile: __dirname + '/karma.conf.js' 42 | }, done).start(); 43 | }); 44 | 45 | gulp.task('test-ci', function(done) { 46 | new karma({ 47 | configFile: __dirname + '/karma-ci.conf.js', 48 | singleRun: true 49 | }, done).start(); 50 | }); 51 | 52 | gulp.task('jshint', function() { 53 | return gulp.src('src/*.js') 54 | .pipe(jshintFlow()); 55 | }); 56 | 57 | gulp.task('compress', function() { 58 | return gulp.src(['dist/*.js']) 59 | .pipe(butternut()) 60 | .pipe(rename({suffix: '.min'})) 61 | .pipe(gulp.dest('dist')); 62 | }) 63 | 64 | gulp.task('clean:dist', function() { 65 | return del(['dist/**/*']); 66 | }) 67 | 68 | gulp.task('bundle', function() { 69 | var entryPoint = './src/hyperGard.js'; 70 | 71 | var umd = gulp.src(entryPoint) 72 | .pipe(sourcemaps.init()) 73 | .pipe(rollup({ 74 | format: 'umd', 75 | name: 'HyperGard', 76 | amd: { 77 | id: 'HyperGard', 78 | }, 79 | })); 80 | 81 | var es = gulp.src(entryPoint) 82 | .pipe(sourcemaps.init()) 83 | .pipe(rollup({ 84 | format: 'es', 85 | })) 86 | .pipe(rename(function(path) { 87 | path.basename += ".es"; 88 | })); 89 | 90 | return mergeStream([es, umd]) 91 | .pipe(sourcemaps.write('./', { addComment: false })) 92 | .pipe(gulp.dest('dist')); 93 | }) 94 | 95 | gulp.task('build-test-files', function() { 96 | var streams = testFiles.map(function(testFile){ 97 | return gulp.src(testFile.src) 98 | .pipe(rollup({ 99 | format: 'umd', 100 | name: testFile.moduleName, 101 | amd: { 102 | id: testFile.moduleName, 103 | }, 104 | })) 105 | }) 106 | 107 | return mergeStream(streams) 108 | .pipe(gulp.dest('.test-build')); 109 | 110 | }) 111 | 112 | gulp.task('build:all', gulpsync.sync(['jshint', 'build-test-files', 'test-ci', 'clean:dist', 'bundle', 'compress'])); 113 | 114 | gulp.task('canned', function() { 115 | var 116 | canned = require('canned'), 117 | http = require('http'), 118 | options = { 119 | port: '4444', 120 | src: './test/mocks', 121 | cors_headers: 'x-hypergard' 122 | }, 123 | 124 | cannedOptions = { 125 | cors: options.cors || true, 126 | logger: options.logger || process.stdout, 127 | cors_headers: options.cors_headers || false 128 | }, 129 | 130 | can = canned(options.src, cannedOptions), 131 | 132 | server = http.createServer(can).listen(options.port); 133 | 134 | console.log('Mock API server running at http://localhost:' + options.port + ', serving files from ' + options.src); 135 | }); 136 | -------------------------------------------------------------------------------- /lib/extend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Comcast Cable Communications Management, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | var typeObj = {}; 19 | var slice = [].slice; 20 | var toString = typeObj.toString; 21 | 22 | var getType = function(mixed) { 23 | if (mixed == null) { 24 | return mixed + ''; 25 | } 26 | 27 | return typeof mixed === 'object' || typeof mixed === 'function' ? 28 | typeObj[toString.call(mixed)] || 'object' : typeof mixed; 29 | }; 30 | 31 | var isSpecificValue = function(val) { 32 | return getType(val) === 'buffer' || getType(val) === 'date' || getType(val) === 'regexp'; 33 | }; 34 | 35 | var cloneSpecificValue = function(val) { 36 | var valType = getType(val); 37 | var x; 38 | 39 | if (valType === 'buffer') { 40 | x = new Buffer(val.length); 41 | val.copy(x); 42 | return x; 43 | } 44 | 45 | if (valType === 'date') { 46 | return new Date(val.getTime()); 47 | } 48 | 49 | if (valType === 'regexp') { 50 | return new RegExp(val); 51 | } 52 | 53 | throw new Error('Unexpected situation'); 54 | }; 55 | 56 | /** 57 | * Recursive cloning array. 58 | */ 59 | var deepCloneArray = function(arr) { 60 | var clone = []; 61 | 62 | arr.forEach(function(item, index) { 63 | if (getType(item) === 'object') { 64 | if (Array.isArray(item)) { 65 | clone[index] = deepCloneArray(item); 66 | } else if (isSpecificValue(item)) { 67 | clone[index] = cloneSpecificValue(item); 68 | } else { 69 | clone[index] = deepExtend({}, item); 70 | } 71 | } else { 72 | clone[index] = item; 73 | } 74 | }); 75 | 76 | return clone; 77 | }; 78 | 79 | /** 80 | * Extening object that entered in first argument. 81 | * 82 | * Returns extended object or false if have no target object or incorrect type. 83 | * 84 | * If you wish to clone source object (without modify it), just use empty new 85 | * object as first argument, like this: 86 | * deepExtend({}, yourObj_1, [yourObj_N]); 87 | */ 88 | var deepExtend = function(/*obj_1, [obj_2], [obj_N]*/) { 89 | if (arguments.length < 1 || getType(arguments[0]) !== 'object') { 90 | return false; 91 | } 92 | 93 | if (arguments.length < 2) { 94 | return arguments[0]; 95 | } 96 | 97 | var target = arguments[0]; 98 | var args = slice.call(arguments, 1); 99 | var val, src; 100 | 101 | args.forEach(function(obj) { 102 | // skip argument if it is array or isn't object 103 | if (getType(obj) !== 'object' || Array.isArray(obj)) { 104 | return; 105 | } 106 | 107 | Object.keys(obj).forEach(function(key) { 108 | src = target[key]; // source value 109 | val = obj[key]; // new value 110 | 111 | // recursion prevention 112 | if (val !== target) { 113 | if (getType(val) !== 'object' || val === null) { 114 | target[key] = val; 115 | } else if (Array.isArray(val)) { 116 | // just clone arrays (and recursive clone objects inside) 117 | target[key] = deepCloneArray(val); 118 | } else if (isSpecificValue(val)) { 119 | // custom cloning and overwrite for specific objects 120 | target[key] = cloneSpecificValue(val); 121 | } else if (getType(src) !== 'object' || src === null || Array.isArray(src)) { 122 | // overwrite by new value if source isn't object or array 123 | target[key] = deepExtend({}, val); 124 | } else { 125 | // source value and new value is objects both, extending... 126 | target[key] = deepExtend(src, val); 127 | } 128 | } 129 | }); 130 | }); 131 | 132 | return target; 133 | }; 134 | 135 | ['Boolean', 'Number', 'String', 'Function', 'Array', 'Date', 'RegExp', 'Object', 'Error'].forEach(function(name) { 136 | typeObj["[object " + name + "]"] = name.toLowerCase(); 137 | }); 138 | 139 | export default deepExtend; 140 | 141 | -------------------------------------------------------------------------------- /test/mocks/index.get.json: -------------------------------------------------------------------------------- 1 | { 2 | "_links": { 3 | "curies": [ 4 | { 5 | "name": "test", 6 | "href": "http://www.example.com/docs/{rel}", 7 | "templated": true 8 | }, 9 | { 10 | "href": "invalid curie" 11 | } 12 | ], 13 | "self": { 14 | "href": "./" 15 | }, 16 | "noHref": { 17 | "title": "No href, just title" 18 | }, 19 | "noTitle": { 20 | "href": "no/title" 21 | }, 22 | "noParams": { 23 | "href": "no/params/", 24 | "title": "Link with no params" 25 | }, 26 | "noParamsNotTemplated": { 27 | "href": "no/params/", 28 | "title": "Link with no params, templated false", 29 | "templated": false 30 | }, 31 | "noParamsTemplated": { 32 | "href": "no/params/", 33 | "title": "Link with no params, templated true", 34 | "templated": true 35 | }, 36 | "oneParamPath": { 37 | "href": "one/param/{path}/", 38 | "title": "One param in path", 39 | "templated": true 40 | }, 41 | "oneParamQuery": { 42 | "href": "one/param/{?query}", 43 | "title": "One param in query", 44 | "templated": true 45 | }, 46 | "twoParamsMixed": { 47 | "href": "two/params/{path}/{?query}", 48 | "title": "Two params, one in path, one in query", 49 | "templated": true 50 | }, 51 | "twoParamsPath": { 52 | "href": "two/params/{path1}/{path2}/", 53 | "title": "Two params in path", 54 | "templated": true 55 | }, 56 | "twoParamsQuery": { 57 | "href": "two/params/{?query1,query2}", 58 | "title": "Two params in query", 59 | "templated": true 60 | }, 61 | "rootRelativeUrl": { 62 | "href": "/root/relative/", 63 | "title": "Root relative url", 64 | "templated": false 65 | }, 66 | "pathRelativeUrl": { 67 | "href": "../root/relative/", 68 | "title": "Path relative url", 69 | "templated": false 70 | }, 71 | "absoluteUrl": { 72 | "href": "http://www.example.com", 73 | "title": "Absolute url", 74 | "templated": false 75 | }, 76 | "linkList": [ 77 | { 78 | "href": "first/link", 79 | "title": "first link" 80 | }, 81 | { 82 | "href": "second/link", 83 | "title": "second link" 84 | } 85 | ], 86 | "test:curie-test": { 87 | "href": "/curie/test/", 88 | "title": "Curie test link" 89 | } 90 | }, 91 | "_forms": { 92 | "formNoAction": { 93 | "method": "POST", 94 | "fields": {} 95 | }, 96 | "formNoMethod": { 97 | "action": "form/", 98 | "fields": {} 99 | }, 100 | "formGetNoFields": { 101 | "action": "form/", 102 | "method": "GET" 103 | }, 104 | "formGetEmptyFields": { 105 | "action": "form/", 106 | "method": "GET", 107 | "fields": {} 108 | }, 109 | "formGetOneField": { 110 | "action": "form/", 111 | "method": "GET", 112 | "fields": { 113 | "param": {} 114 | } 115 | }, 116 | "formGetTwoFields": { 117 | "action": "form/", 118 | "method": "GET", 119 | "fields": { 120 | "param1": {}, 121 | "param2": {} 122 | } 123 | }, 124 | "formGetTwoFieldsDefaultValue": { 125 | "action": "form/", 126 | "method": "GET", 127 | "fields": { 128 | "param1": { 129 | "default": "test" 130 | }, 131 | "param2": {} 132 | } 133 | }, 134 | "formPostNoFields": { 135 | "action": "form/", 136 | "method": "POST" 137 | }, 138 | "formPostEmptyFields": { 139 | "action": "form/", 140 | "method": "POST", 141 | "fields": {} 142 | }, 143 | "formPostOneField": { 144 | "action": "form/", 145 | "method": "POST", 146 | "fields": { 147 | "param": {} 148 | } 149 | }, 150 | "formPostTwoFields": { 151 | "action": "form/", 152 | "method": "POST", 153 | "fields": { 154 | "param1": {}, 155 | "param2": {} 156 | } 157 | }, 158 | "formPostTwoFieldsDefaultValue": { 159 | "action": "form/", 160 | "method": "POST", 161 | "fields": { 162 | "param1": { 163 | "default": "test" 164 | }, 165 | "param2": {} 166 | } 167 | }, 168 | "formPostWithTemplatedAction": { 169 | "action": "form/{param}/test", 170 | "method": "POST", 171 | "fields": { 172 | "param1": {}, 173 | "param2": {} 174 | }, 175 | "templated": true 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /demo/hal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 50 | 51 | 52 |
53 | 54 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /lib/urltemplate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Comcast Cable Communications Management, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @constructor 19 | */ 20 | function UrlTemplate() { 21 | var 22 | /** 23 | * Operators 24 | * @type {String[]} 25 | */ 26 | operators = ['+', '#', '.', '/', ';', '?', '&'], 27 | 28 | parseTemplate = /\{([^\{\}]+)\}|([^\{\}]+)/g, 29 | parseVariable = /([^:\*]*)(?::(\d+)|(\*))?/, 30 | 31 | /** 32 | * @private 33 | * @param {string} str 34 | * @return {string} 35 | */ 36 | encodeReserved = function(str) { 37 | return str.split(/(%[0-9A-Fa-f]{2})/g).map(function(part) { 38 | if (!/%[0-9A-Fa-f]/.test(part)) { 39 | part = encodeURI(part); 40 | } 41 | return part; 42 | }).join(''); 43 | }, 44 | 45 | /** 46 | * @private 47 | * @param {string} operator 48 | * @param {string} value 49 | * @param {string} key 50 | * @return {string} 51 | */ 52 | encodeValue = function(operator, value, key) { 53 | value = (operator === '+' || operator === '#') ? encodeReserved(value) : encodeURIComponent(value); 54 | return key ? encodeURIComponent(key) + '=' + value : value; 55 | }, 56 | 57 | /** 58 | * @private 59 | * @param {*} value 60 | * @return {boolean} 61 | */ 62 | isDefined = function(value) { 63 | return value !== undefined && value !== null; 64 | }, 65 | 66 | /** 67 | * @private 68 | * @param {string} operator 69 | * @return {boolean} 70 | */ 71 | isKeyOperator = function(operator) { 72 | return operator === ';' || operator === '&' || operator === '?'; 73 | }, 74 | 75 | /** 76 | * @private 77 | * @param {Object} context 78 | * @param {string} operator 79 | * @param {string} key 80 | * @param {string} modifier 81 | */ 82 | getValues = function(context, operator, key, modifier) { 83 | var 84 | value = context[key], 85 | result = []; 86 | 87 | if (typeof value !== 'undefined' && value !== null) { 88 | if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { 89 | value = value.toString(); 90 | 91 | if (modifier && modifier !== '*') { 92 | value = value.substring(0, parseInt(modifier, 10)); 93 | } 94 | 95 | result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null)); 96 | } else { 97 | if (modifier === '*') { 98 | if (Array.isArray(value)) { 99 | value.filter(isDefined).forEach(function(value) { 100 | result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null)); 101 | }); 102 | } else { 103 | Object.keys(value).forEach(function(k) { 104 | if (isDefined(value[k])) { 105 | result.push(encodeValue(operator, value[k], k)); 106 | } 107 | }); 108 | } 109 | } else { 110 | var tmp = []; 111 | 112 | if (Array.isArray(value)) { 113 | value.filter(isDefined).forEach(function(value) { 114 | tmp.push(encodeValue(operator, value, '')); 115 | }); 116 | } else { 117 | Object.keys(value).forEach(function(k) { 118 | if (isDefined(value[k])) { 119 | tmp.push(encodeURIComponent(k)); 120 | tmp.push(encodeValue(operator, value[k].toString(), '')); 121 | } 122 | }); 123 | } 124 | 125 | if (isKeyOperator(operator)) { 126 | result.push(encodeURIComponent(key) + '=' + tmp.join(',')); 127 | } else if (tmp.length !== 0) { 128 | result.push(tmp.join(',')); 129 | } 130 | } 131 | } 132 | } else { 133 | if (operator === ';') { 134 | result.push(encodeURIComponent(key)); 135 | } else if (value === '' && (operator === '&' || operator === '?')) { 136 | result.push(encodeURIComponent(key) + '='); 137 | } else if (value === '') { 138 | result.push(''); 139 | } 140 | } 141 | 142 | return result; 143 | }; 144 | 145 | /** 146 | * @param {String} url 147 | * @param {Object} params to be applied 148 | * @return {string} 149 | */ 150 | this.expand = function(url, params) { 151 | return url.replace(parseTemplate, function(_, expression, literal) { 152 | var 153 | result; 154 | 155 | if (expression) { 156 | var 157 | operator, 158 | values = []; 159 | 160 | if (operators.indexOf(expression.charAt(0)) !== -1) { 161 | operator = expression.charAt(0); 162 | expression = expression.substr(1); 163 | 164 | if ((url.match(/\?/g) || []).length > 1) { 165 | operator = '&'; 166 | } 167 | } 168 | 169 | expression.split(/,/g).forEach(function(variable) { 170 | var tmp = parseVariable.exec(variable); 171 | values.push.apply(values, getValues(params, operator, tmp[1], tmp[2] || tmp[3])); 172 | }); 173 | 174 | if (operator && operator !== '+') { 175 | var separator = ','; 176 | 177 | if (operator === '?') { 178 | separator = '&'; 179 | } else if (operator !== '#') { 180 | separator = operator; 181 | } 182 | 183 | result = (values.length ? operator : '') + values.join(separator); 184 | } else { 185 | result = values.join(','); 186 | } 187 | } else { 188 | result = encodeReserved(literal); 189 | } 190 | 191 | return result; 192 | }); 193 | }; 194 | 195 | this.extractParams = function(url) { 196 | var 197 | result = []; 198 | 199 | url.replace(parseTemplate, function(_, expression) { 200 | if (expression) { 201 | if (operators.indexOf(expression.charAt(0)) !== -1) { 202 | expression = expression.substr(1); 203 | } 204 | result = result.concat(expression.split(/,/g)); 205 | } 206 | }); 207 | 208 | return result; 209 | }; 210 | } 211 | 212 | export default new UrlTemplate(); 213 | -------------------------------------------------------------------------------- /lib/urlparse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Comcast Cable Communications Management, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @constructor 19 | */ 20 | function UrlParse() { 21 | var 22 | // scheme (optional), host, port 23 | fullurl = /^([A-Za-z]+)?(:?\/\/)([0-9.\-A-Za-z]*)(?::(\d+))?(.*)$/, 24 | // path, query, fragment 25 | parse_leftovers = /([^?#]*)?(?:\?([^#]*))?(?:#(.*))?$/; 26 | 27 | // Unlike to be useful standalone 28 | // 29 | // NORMALIZE PATH with "../" and "./" 30 | // http://en.wikipedia.org/wiki/URL_normalization 31 | // http://tools.ietf.org/html/rfc3986#section-5.2.3 32 | // 33 | this.normalizepath = function(path) { 34 | if (!path || path === '/') { 35 | return '/'; 36 | } 37 | 38 | var 39 | parts = path.split('/'), 40 | newparts = []; 41 | 42 | // make sure path always starts with '/' 43 | if (parts[0]) { 44 | newparts.push(''); 45 | } 46 | 47 | parts.forEach(function(part) { 48 | if (part === '..') { 49 | if (newparts.length > 1) { 50 | newparts.pop(); 51 | } else { 52 | newparts.push(part); 53 | } 54 | } else if (part !== '.') { 55 | newparts.push(part); 56 | } 57 | }); 58 | 59 | return newparts.join('/') || '/'; 60 | }; 61 | 62 | // 63 | // Does many of the normalizations that the stock 64 | // python urlsplit/urlunsplit/urljoin neglects 65 | // 66 | // Doesn't do hex-escape normalization on path or query 67 | // %7e -> %7E 68 | // Nor, '+' <--> %20 translation 69 | // 70 | this.urlnormalize = function(url) { 71 | var parts = this.urlsplit(url); 72 | 73 | switch (parts.scheme) { 74 | case 'file': 75 | // files can't have query strings 76 | // and we don't bother with fragments 77 | parts.query = ''; 78 | parts.fragment = ''; 79 | break; 80 | case 'http': 81 | case 'https': 82 | // remove default port 83 | if ((parts.scheme === 'http' && parts.port == 80) || 84 | (parts.scheme === 'https' && parts.port == 443)) { 85 | delete parts.port; 86 | // hostname is already lower case 87 | parts.netloc = parts.hostname; 88 | } 89 | break; 90 | default: 91 | // if we don't have specific normalizations for this 92 | // scheme, return the original url unmolested 93 | return url; 94 | } 95 | 96 | // for [file|http|https]. Not sure about other schemes 97 | parts.path = this.normalizepath(parts.path); 98 | 99 | return this.urlunsplit(parts); 100 | }; 101 | 102 | this.urldefrag = function(url) { 103 | var idx = url.indexOf('#'); 104 | return (idx === -1) ? [url, ''] : [url.substr(0, idx), url.substr(idx + 1)]; 105 | }; 106 | 107 | this.urlsplit = function(url, default_scheme, allow_fragments) { 108 | allow_fragments = allow_fragments !== false; 109 | 110 | var 111 | leftover, 112 | o = {}, 113 | parts = (url || '').match(fullurl); 114 | 115 | if (parts) { 116 | o.scheme = parts[1] || default_scheme || ''; 117 | o.hostname = parts[3].toLowerCase() || ''; 118 | o.port = parseInt(parts[4], 10) || ''; 119 | // Probably should grab the netloc from regexp 120 | // and then parse again for hostname/port 121 | 122 | o.netloc = parts[3]; 123 | 124 | if (parts[4]) { 125 | o.netloc += ':' + parts[4]; 126 | } 127 | 128 | leftover = parts[5]; 129 | } else { 130 | o.scheme = default_scheme || ''; 131 | o.netloc = ''; 132 | o.hostname = ''; 133 | leftover = url; 134 | } 135 | o.scheme = o.scheme.toLowerCase(); 136 | 137 | parts = leftover.match(parse_leftovers); 138 | 139 | o.path = parts[1] || ''; 140 | o.query = parts[2] || ''; 141 | 142 | o.fragment = allow_fragments ? (parts[3] || '') : ''; 143 | 144 | return o; 145 | }; 146 | 147 | this.urlunsplit = function(o) { 148 | var s = ''; 149 | 150 | if (o.scheme) { 151 | s += o.scheme + '://'; 152 | } 153 | 154 | if (o.netloc) { 155 | if (s === '') { 156 | s += '//'; 157 | } 158 | 159 | s += o.netloc; 160 | } else if (o.hostname) { 161 | // extension. Python only uses netloc 162 | if (s === '') { 163 | s += '//'; 164 | } 165 | 166 | s += o.hostname; 167 | 168 | if (o.port) { 169 | s += ':' + o.port; 170 | } 171 | } 172 | 173 | if (o.path) { 174 | s += o.path; 175 | } 176 | 177 | if (o.query) { 178 | s += '?' + o.query; 179 | } 180 | 181 | if (o.fragment) { 182 | s += '#' + o.fragment; 183 | } 184 | 185 | return s; 186 | }; 187 | 188 | this.urljoin = function(base, url, allow_fragments) { 189 | if (typeof allow_fragments === 'undefined') { 190 | allow_fragments = true; 191 | } 192 | 193 | var url_parts = this.urlsplit(url); 194 | 195 | // if url parts has a scheme (i.e. absolute) 196 | // then nothing to do 197 | if (url_parts.scheme) { 198 | return !allow_fragments ? url : this.urldefrag(url)[0]; 199 | } 200 | 201 | var base_parts = this.urlsplit(base); 202 | 203 | // copy base, only if not present 204 | if (!base_parts.scheme) { 205 | base_parts.scheme = url_parts.scheme; 206 | } 207 | 208 | // copy netloc, only if not present 209 | if (!base_parts.netloc || !base_parts.hostname) { 210 | base_parts.netloc = url_parts.netloc; 211 | base_parts.hostname = url_parts.hostname; 212 | base_parts.port = url_parts.port; 213 | } 214 | 215 | // paths 216 | if (url_parts.path.length > 0) { 217 | if (url_parts.path.charAt(0) === '/') { 218 | base_parts.path = url_parts.path; 219 | } else { 220 | // relative path.. get rid of "current filename" and 221 | // replace. Same as var parts = 222 | // base_parts.path.split('/'); parts[parts.length-1] = 223 | // url_parts.path; base_parts.path = parts.join('/'); 224 | var idx = base_parts.path.lastIndexOf('/'); 225 | if (idx === -1) { 226 | base_parts.path = url_parts.path; 227 | } else { 228 | base_parts.path = base_parts.path.substr(0, idx) + '/' + 229 | url_parts.path; 230 | } 231 | } 232 | } 233 | 234 | // clean up path 235 | base_parts.path = this.normalizepath(base_parts.path); 236 | 237 | // copy query string 238 | base_parts.query = url_parts.query; 239 | 240 | // copy fragments 241 | base_parts.fragment = allow_fragments ? url_parts.fragment : ''; 242 | 243 | return this.urlunsplit(base_parts); 244 | }; 245 | } 246 | 247 | export default new UrlParse(); 248 | -------------------------------------------------------------------------------- /test/mocks/data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paul.Bronshteyn 3 | * @comment Built by a geek loaded on caffeine ... 4 | */ 5 | var 6 | homepage = { 7 | "_links": { 8 | "curies": [ 9 | { 10 | "name": "test", 11 | "href": "http://www.example.com/docs/{rel}", 12 | "templated": true 13 | }, 14 | { 15 | "href": "invalid curie" 16 | } 17 | ], 18 | "self": { 19 | "href": "./" 20 | }, 21 | "noHref": { 22 | "title": "No href, just title" 23 | }, 24 | "noTitle": { 25 | "href": "no/title" 26 | }, 27 | "noParams": { 28 | "href": "no/params/", 29 | "title": "Link with no params" 30 | }, 31 | "noParamsNotTemplated": { 32 | "href": "no/params/", 33 | "title": "Link with no params, templated false", 34 | "templated": false 35 | }, 36 | "noParamsTemplated": { 37 | "href": "no/params/", 38 | "title": "Link with no params, templated true", 39 | "templated": true 40 | }, 41 | "oneParamPath": { 42 | "href": "one/param/{path}/", 43 | "title": "One param in path", 44 | "templated": true 45 | }, 46 | "oneParamQuery": { 47 | "href": "one/param/{?query}", 48 | "title": "One param in query", 49 | "templated": true 50 | }, 51 | "twoParamsMixed": { 52 | "href": "two/params/{path}/{?query}", 53 | "title": "Two params, one in path, one in query", 54 | "templated": true 55 | }, 56 | "twoParamsPath": { 57 | "href": "two/params/{path1}/{path2}/", 58 | "title": "Two params in path", 59 | "templated": true 60 | }, 61 | "twoParamsQuery": { 62 | "href": "two/params/{?query1,query2}", 63 | "title": "Two params in query", 64 | "templated": true 65 | }, 66 | "twoParamsQueryAndExisting": { 67 | "href": "two/params/?test=1{&query1,query2}", 68 | "title": "Two params in query with hard coded param", 69 | "templated": true 70 | }, 71 | "twoParamsQueryInvalidAndExisting": { 72 | "href": "two/params/?test=1{?query1,query2}", 73 | "title": "Two wrongly formatted params in query with hard coded param", 74 | "templated": true 75 | }, 76 | "rootRelativeUrl": { 77 | "href": "/root/relative/", 78 | "title": "Root relative url", 79 | "templated": false 80 | }, 81 | "pathRelativeUrl": { 82 | "href": "../root/relative/", 83 | "title": "Path relative url", 84 | "templated": false 85 | }, 86 | "absoluteUrl": { 87 | "href": "http://www.example.com", 88 | "title": "Absolute url", 89 | "templated": false 90 | }, 91 | "linkList": [ 92 | { 93 | "href": "first/link", 94 | "title": "first link" 95 | }, 96 | { 97 | "href": "second/link", 98 | "title": "second link" 99 | } 100 | ], 101 | "test:curie-test": { 102 | "href": "/curie/test/", 103 | "title": "Curie test link" 104 | } 105 | }, 106 | "_forms": { 107 | "formNoAction": { 108 | "method": "POST", 109 | "fields": {} 110 | }, 111 | "formNoMethod": { 112 | "action": "form/", 113 | "fields": {} 114 | }, 115 | "formGetNoFields": { 116 | "action": "form/", 117 | "method": "GET" 118 | }, 119 | "formGetEmptyFields": { 120 | "action": "form/", 121 | "method": "GET", 122 | "fields": {} 123 | }, 124 | "formGetOneField": { 125 | "action": "form/", 126 | "method": "GET", 127 | "fields": { 128 | "param": {} 129 | } 130 | }, 131 | "formGetTwoFields": { 132 | "action": "form/", 133 | "method": "GET", 134 | "fields": { 135 | "param1": {}, 136 | "param2": {} 137 | } 138 | }, 139 | "formGetTwoFieldsDefaultValue": { 140 | "action": "form/", 141 | "method": "GET", 142 | "fields": { 143 | "param1": { 144 | "default": "test" 145 | }, 146 | "param2": {} 147 | } 148 | }, 149 | "formPostNoFields": { 150 | "action": "form/", 151 | "method": "POST" 152 | }, 153 | "formPostEmptyFields": { 154 | "action": "form/", 155 | "method": "POST", 156 | "fields": {} 157 | }, 158 | "formPostOneField": { 159 | "action": "form/", 160 | "method": "POST", 161 | "fields": { 162 | "param": {} 163 | } 164 | }, 165 | "formPostTwoFields": { 166 | "action": "form/", 167 | "method": "POST", 168 | "fields": { 169 | "param1": {}, 170 | "param2": {} 171 | } 172 | }, 173 | "formPostTwoFieldsDefaultValue": { 174 | "action": "form/", 175 | "method": "POST", 176 | "fields": { 177 | "param1": { 178 | "default": "test" 179 | }, 180 | "param2": {} 181 | } 182 | }, 183 | "formPostWithTemplatedAction": { 184 | "action": "form/{param}/test", 185 | "method": "POST", 186 | "fields": { 187 | "param1": {}, 188 | "param2": {} 189 | }, 190 | "templated": true 191 | } 192 | } 193 | }, 194 | 195 | actionResponse = { 196 | "_links": { 197 | "self": { 198 | "href": "./" 199 | } 200 | }, 201 | "_embedded": { 202 | "one": { 203 | "id": "636363636363", 204 | "title": "One Title", 205 | "nestedProp": { 206 | "name": "Prop value" 207 | }, 208 | "_embedded": { 209 | "sub_one": null 210 | } 211 | }, 212 | "two": [{ 213 | "_links": { 214 | "self": { 215 | "href": "./" 216 | }, 217 | "linkedEmbedded": { 218 | "href": "../linked" 219 | } 220 | }, 221 | "_embedded": { 222 | "sub_two": { 223 | "_links": { 224 | "self": { 225 | "href": "../../../test/id/" 226 | } 227 | }, 228 | "_embedded": { 229 | "images": [{ 230 | "_links": { 231 | "imageUrl": { 232 | "href": "http://some.cdn/foo/bar.jpg" 233 | } 234 | }, 235 | "width": 50, 236 | "height": 30 237 | }], 238 | "company": [{ 239 | "_links": { 240 | "self": { 241 | "href": "../../../text/company/" 242 | }, 243 | "companyUrl": { 244 | "href": "http://companyUrl" 245 | } 246 | }, 247 | "_embedded": { 248 | "images": [] 249 | }, 250 | "id": "12345", 251 | "name": "Test Company", 252 | "title": "Title of company", 253 | "description": "Description of company" 254 | }] 255 | }, 256 | "id": "98756", 257 | "title": "Sub one title" 258 | } 259 | }, 260 | "id": "78423y08" 261 | }], 262 | "three": { 263 | "_links": { 264 | "self": "../linked" 265 | }, 266 | "id": "linked123" 267 | } 268 | } 269 | }; 270 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /test/spec/init.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paul.Bronshteyn 3 | * @comment Built by a geek loaded on caffeine ... 4 | */ 5 | describe('hyperGard', function() { 6 | beforeEach(function() { 7 | window.fetch.and.returnValue(this.homepageData(homepage)); 8 | }); 9 | 10 | describe('Test hyperGard Initialization', function() { 11 | describe('with no params', function() { 12 | beforeEach(function(done) { 13 | this.testHyperGard = new HyperGard(); 14 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 15 | }); 16 | 17 | it('should not make an fetch request', function() { 18 | expect(window.fetch).not.toHaveBeenCalled(); 19 | }); 20 | 21 | it('should not call resolve promise', function() { 22 | expect(this.onSuccess).not.toHaveBeenCalled(); 23 | }); 24 | 25 | it('should call reject promise with 1 param', function() { 26 | expect(this.onError.calls.mostRecent().args.length).toEqual(1); 27 | }); 28 | 29 | it('should call fail promise with error object', function() { 30 | expect(this.onError).toHaveBeenCalledWith({ 31 | error: { 32 | code: '0000', 33 | msg: 'API endpoint was not provided' 34 | } 35 | }); 36 | }); 37 | }); 38 | 39 | describe('with options param and no endpoint', function() { 40 | beforeEach(function(done) { 41 | this.testHyperGard = new HyperGard(null, testOptions); 42 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 43 | }); 44 | 45 | it('should not make an fetch request', function() { 46 | expect(window.fetch).not.toHaveBeenCalled(); 47 | }); 48 | 49 | it('should not call resolve promise', function() { 50 | expect(this.onSuccess).not.toHaveBeenCalled(); 51 | }); 52 | 53 | it('should call reject promise with 1 param', function() { 54 | expect(this.onError.calls.mostRecent().args.length).toEqual(1); 55 | }); 56 | 57 | it('should call fail promise with error object', function() { 58 | expect(this.onError).toHaveBeenCalledWith({ 59 | error: { 60 | code: '0000', 61 | msg: 'API endpoint was not provided' 62 | } 63 | }); 64 | }); 65 | }); 66 | 67 | describe('with endpoint param and no options', function() { 68 | beforeEach(function(done) { 69 | this.testHyperGard = new HyperGard(testEndpoint); 70 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 71 | }); 72 | 73 | it('should make an fetch request', function() { 74 | expect(window.fetch).toHaveBeenCalled(); 75 | }); 76 | 77 | it('should not call reject promise', function() { 78 | expect(this.onError).not.toHaveBeenCalled(); 79 | }); 80 | 81 | it('should call resolve promise', function() { 82 | expect(this.onSuccess).toHaveBeenCalled(); 83 | }); 84 | 85 | it('should call resolve promise with 1 param', function() { 86 | expect(this.onSuccess.calls.mostRecent().args.length).toEqual(1); 87 | }); 88 | 89 | it('should call resolve promise with result object', function() { 90 | expect(this.onSuccess.calls.mostRecent().args[0]).toEqual(jasmine.any(Object)); 91 | }); 92 | }); 93 | 94 | describe('with valid params', function() { 95 | beforeEach(function() { 96 | this.testHyperGard = new HyperGard(testEndpoint, testOptions); 97 | }); 98 | 99 | describe('fetch request params', function() { 100 | beforeEach(function(done) { 101 | this.testHyperGard.fetch().then(done); 102 | }); 103 | 104 | it('should call fetch with correct params', function() { 105 | expect(window.fetch).toHaveBeenCalledWith(testEndpoint, jasmine.any(Object)); 106 | }); 107 | 108 | it('should pass correct xhr options', function() { 109 | expect(window.fetch.calls.argsFor(0)[1].headers.Authorization).toEqual('auth'); 110 | }); 111 | }); 112 | 113 | describe('fetch request failed', function() { 114 | beforeEach(function(done) { 115 | this.xhr500 = this.homepageData('', 500); 116 | window.fetch.and.returnValue(this.xhr500); 117 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 118 | }); 119 | 120 | it('should make fetch request', function() { 121 | expect(window.fetch).toHaveBeenCalled(); 122 | }); 123 | 124 | it('should not call resolve promise', function() { 125 | expect(this.onSuccess).not.toHaveBeenCalled(); 126 | }); 127 | 128 | it('should call reject promise with 1 param', function() { 129 | expect(this.onError.calls.mostRecent().args.length).toEqual(1); 130 | }); 131 | 132 | it('should call reject promise with error object', function() { 133 | expect(this.onError).toHaveBeenCalledWith({ 134 | error: { 135 | code: '0001', 136 | msg: 'Failed to retrieve homepage' 137 | }, 138 | 139 | xhr: this.xhr500 140 | }); 141 | }); 142 | }); 143 | 144 | describe('two requests in succession', function() { 145 | beforeEach(function(done) { 146 | this.testHyperGard.fetch(); 147 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 148 | }); 149 | 150 | it('should make one fetch request', function() { 151 | expect(window.fetch.calls.count()).toEqual(1); 152 | }); 153 | }); 154 | 155 | describe('two requests in succession with cacheHomepage enabled', function() { 156 | beforeEach(function(done) { 157 | this.testHyperGard.setOptions({ cacheHomepage: true }); 158 | this.testHyperGard.fetch(); 159 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 160 | }); 161 | 162 | it('should make one fetch request', function() { 163 | expect(window.fetch.calls.count()).toEqual(1); 164 | }); 165 | }); 166 | 167 | describe('fetch succeeded with invalid responses', function() { 168 | describe('returned array', function() { 169 | beforeEach(function(done) { 170 | this.testResponse = this.homepageData([]); 171 | window.fetch.and.returnValue(this.testResponse); 172 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 173 | }); 174 | 175 | it('should make fetch request', function() { 176 | expect(window.fetch).toHaveBeenCalled(); 177 | }); 178 | 179 | it('should not call resolve promise', function() { 180 | expect(this.onSuccess).not.toHaveBeenCalled(); 181 | }); 182 | 183 | it('should call reject promise with 1 param', function() { 184 | expect(this.onError.calls.mostRecent().args.length).toEqual(1); 185 | }); 186 | 187 | it('should call reject promise with error object', function() { 188 | expect(this.onError).toHaveBeenCalledWith({ 189 | error: { 190 | code: '0002', 191 | msg: 'Could not parse homepage' 192 | }, 193 | 194 | xhr: this.testResponse 195 | }); 196 | }); 197 | }); 198 | 199 | describe('returned boolean - true', function() { 200 | beforeEach(function(done) { 201 | this.testResponse = this.homepageData(true); 202 | window.fetch.and.returnValue(this.testResponse); 203 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 204 | }); 205 | 206 | it('should make fetch request', function() { 207 | expect(window.fetch).toHaveBeenCalled(); 208 | }); 209 | 210 | it('should not call resolve promise', function() { 211 | expect(this.onSuccess).not.toHaveBeenCalled(); 212 | }); 213 | 214 | it('should call reject promise with 1 param', function() { 215 | expect(this.onError.calls.mostRecent().args.length).toEqual(1); 216 | }); 217 | 218 | it('should call reject promise with error object', function() { 219 | expect(this.onError).toHaveBeenCalledWith({ 220 | error: { 221 | code: '0002', 222 | msg: 'Could not parse homepage' 223 | }, 224 | 225 | xhr: this.testResponse 226 | }); 227 | }); 228 | }); 229 | 230 | describe('returned boolean - false', function() { 231 | beforeEach(function(done) { 232 | this.testResponse = this.homepageData(false); 233 | window.fetch.and.returnValue(this.testResponse); 234 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 235 | }); 236 | 237 | it('should make fetch request', function() { 238 | expect(window.fetch).toHaveBeenCalled(); 239 | }); 240 | 241 | it('should not call resolve promise', function() { 242 | expect(this.onSuccess).not.toHaveBeenCalled(); 243 | }); 244 | 245 | it('should call reject promise with 1 param', function() { 246 | expect(this.onError.calls.mostRecent().args.length).toEqual(1); 247 | }); 248 | 249 | it('should call reject promise with error object', function() { 250 | expect(this.onError).toHaveBeenCalledWith({ 251 | error: { 252 | code: '0002', 253 | msg: 'Could not parse homepage' 254 | }, 255 | 256 | xhr: this.testResponse 257 | }); 258 | }); 259 | }); 260 | 261 | describe('returned number', function() { 262 | beforeEach(function(done) { 263 | this.testResponse = this.homepageData(1); 264 | window.fetch.and.returnValue(this.testResponse); 265 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 266 | }); 267 | 268 | it('should make fetch request', function() { 269 | expect(window.fetch).toHaveBeenCalled(); 270 | }); 271 | 272 | it('should not call resolve promise', function() { 273 | expect(this.onSuccess).not.toHaveBeenCalled(); 274 | }); 275 | 276 | it('should call reject promise with 1 param', function() { 277 | expect(this.onError.calls.mostRecent().args.length).toEqual(1); 278 | }); 279 | 280 | it('should call reject promise with error object', function() { 281 | expect(this.onError).toHaveBeenCalledWith({ 282 | error: { 283 | code: '0002', 284 | msg: 'Could not parse homepage' 285 | }, 286 | 287 | xhr: this.testResponse 288 | }); 289 | }); 290 | }); 291 | 292 | describe('returned string', function() { 293 | beforeEach(function(done) { 294 | this.testResponse = this.homepageData('string'); 295 | window.fetch.and.returnValue(this.testResponse); 296 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 297 | }); 298 | 299 | it('should make fetch request', function() { 300 | expect(window.fetch).toHaveBeenCalled(); 301 | }); 302 | 303 | it('should not call resolve promise', function() { 304 | expect(this.onSuccess).not.toHaveBeenCalled(); 305 | }); 306 | 307 | it('should call reject promise with 1 param', function() { 308 | expect(this.onError.calls.mostRecent().args.length).toEqual(1); 309 | }); 310 | 311 | it('should call reject promise with error object', function() { 312 | expect(this.onError).toHaveBeenCalledWith({ 313 | error: { 314 | code: '0002', 315 | msg: 'Could not parse homepage' 316 | }, 317 | 318 | xhr: this.testResponse 319 | }); 320 | }); 321 | }); 322 | }); 323 | 324 | describe('fetch succeeded with valid response', function() { 325 | beforeEach(function(done) { 326 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 327 | }); 328 | 329 | describe('validate response', function() { 330 | it('should make fetch request', function() { 331 | expect(window.fetch).toHaveBeenCalled(); 332 | }); 333 | 334 | it('should not call reject promise', function() { 335 | expect(this.onError).not.toHaveBeenCalled(); 336 | }); 337 | 338 | it('should call resolve promise', function() { 339 | expect(this.onSuccess).toHaveBeenCalled(); 340 | }); 341 | 342 | it('should call resolve promise with 1 param', function() { 343 | expect(this.onSuccess.calls.mostRecent().args.length).toEqual(1); 344 | }); 345 | }); 346 | 347 | describe('validate fetch request', function() { 348 | it('passes correct params to fetch request', function() { 349 | expect(window.fetch).toHaveBeenCalledWith('/endpoint/', { 350 | action: jasmine.any(String), 351 | headers: jasmine.any(Object), 352 | method: jasmine.any(String), 353 | }); 354 | }); 355 | }); 356 | }); 357 | }); 358 | }); 359 | }); 360 | -------------------------------------------------------------------------------- /src/hyperGard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Comcast Cable Communications Management, LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | import deepExtend from '../lib/extend.js'; 19 | import urltemplate from '../lib/urltemplate.js'; 20 | import urlparse from '../lib/urlparse.js'; 21 | 22 | var 23 | version = '6.0.0', 24 | 25 | defaultOptions = { 26 | preloadHomepage: true, 27 | cacheHomepage: false, 28 | debug: false, 29 | 30 | xhr: { 31 | headers: { 32 | Accept: 'application/hal+json, application/json, */*; q=0.01', 33 | 'X-HyperGard': version 34 | } 35 | } 36 | }, 37 | 38 | excludedProps = /^(_embedded|_links|_forms)$/, 39 | excludeBody = /^(head|get)$/i, 40 | concat = [].concat, 41 | keys = Object.keys, 42 | 43 | isObject = function(value) { 44 | return !!value && {}.toString.call(value) === '[object Object]'; 45 | }, 46 | 47 | xhrStatus = function(response) { 48 | return (response.status >= 200 && response.status < 300) ? response : 49 | Promise.reject(response instanceof Response ? response : new Response('', { 50 | status: 503, 51 | statusText: 'Possible CORS error' 52 | })); 53 | }, 54 | 55 | xhrTimeout = function(timeout) { 56 | return new Promise(function(res, rej) { 57 | setTimeout(function() { 58 | rej({ 59 | error: { 60 | code: '0011', 61 | msg: 'Fetch timeout', 62 | timeout: timeout 63 | } 64 | }); 65 | }, timeout); 66 | }); 67 | }, 68 | 69 | load = function(url, fetchOptions) { 70 | var o = deepExtend({}, defaultOptions.xhr, fetchOptions); 71 | 72 | return Promise.race([ 73 | xhrTimeout(fetchOptions.timeout || 60000), 74 | fetch(url, o) 75 | ]).then(xhrStatus); 76 | }, 77 | 78 | urlSerialize = function(obj) { 79 | return Object.keys(obj).map(function(key) { 80 | return encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]); 81 | }).join('&'); 82 | }, 83 | 84 | HyperGard = function(endpoint, initOptions) { 85 | var 86 | homepage, 87 | homepageLoaded = false, 88 | options = deepExtend({}, defaultOptions, initOptions || {}), 89 | linkRefs = {}, 90 | 91 | parseCuries = function(curies) { 92 | var result = {}; 93 | 94 | (curies || []).forEach(function(curie) { 95 | if (curie.name) { 96 | result[curie.name] = curie; 97 | } 98 | }); 99 | 100 | return result; 101 | }, 102 | 103 | parseEmbedded = function(res, parent) { 104 | var 105 | result = {}; 106 | 107 | keys(res).forEach(function(key) { 108 | result[key] = Array.isArray(res[key]) ? res[key].map(function(item) { 109 | return new EmbeddedResource(item, parent); 110 | }) : new EmbeddedResource(res[key], parent); 111 | }); 112 | 113 | return result; 114 | }, 115 | 116 | sharedResourceAPI = function(obj, res, parent, base) { 117 | var 118 | props = {}, 119 | link = ((res._links || {}).self || {}).href || '', 120 | actions = [], 121 | embedded = parseEmbedded(res._embedded || {}, obj); 122 | 123 | keys(res).forEach(function(key) { 124 | if (!excludedProps.test(key)) { 125 | this[key] = res[key]; 126 | } 127 | }, props); 128 | 129 | /** 130 | * Get Action 131 | * @description Allow to follow actions (links, forms) on HAL resources and embedded resources 132 | * @param {String} name The name of the action to be taken 133 | * @param {Object} params Parameters to populate the action 134 | * @returns {Array} List of available actions 135 | */ 136 | obj.getAction = function(name, params) { 137 | if (!actions[name]) { 138 | actions[name] = []; 139 | 140 | var 141 | linkCuries = parseCuries((res._links || {}).curies), 142 | formCuries = parseCuries((res._forms || {}).curies), 143 | curieName = String(name).split(':')[0] || ''; 144 | 145 | concat.call([], (res._links || {})[name]).forEach(function(lnk) { 146 | if (lnk) { 147 | lnk.curie = linkCuries[curieName]; 148 | actions[name].push(new Action(this, name, lnk, params, 'link')); 149 | } 150 | }, this); 151 | 152 | concat.call([], (res._forms || {})[name]).forEach(function(frm) { 153 | if (frm) { 154 | frm.curie = formCuries[curieName]; 155 | actions[name].push(new Action(this, name, frm, params, 'form')); 156 | } 157 | }, this); 158 | } 159 | 160 | return actions[name].map(function(action) { 161 | return action.setParams(params); 162 | }); 163 | }; 164 | 165 | /** 166 | * Get base api path 167 | * @returns {String} 168 | */ 169 | obj.getBase = function() { 170 | return base || parent.getBase(); 171 | }; 172 | 173 | /** 174 | * Get embedded object 175 | * @param {String} name Name of the embedded object 176 | * @returns {Object} 177 | */ 178 | obj.getEmbedded = function(name) { 179 | return embedded[name]; 180 | }; 181 | 182 | /** 183 | * Fetch embedded 184 | * @description Get the object from embedded or follow a link with the same name 185 | * @param {String} name Name of the embedded object 186 | * @param {Object} [options] to provide to fetch for actions 187 | * @returns {Promise} 188 | */ 189 | obj.fetchEmbedded = function(name, options) { 190 | return this.hasEmbedded(name) ? Promise.resolve({ 191 | data: this.getEmbedded(name) 192 | }) : this.getFirstAction(name).fetch(options); 193 | }; 194 | 195 | /** 196 | * Get first action from getAction list 197 | * @description Convenience method to get the first action instead of using getAction()[0] 198 | * @param {String} name The name of the action to be taken 199 | * @param {Object} params Parameters to populate the action 200 | * @returns {Object} Get Action API 201 | */ 202 | obj.getFirstAction = function(name, params) { 203 | return this.getAction(name, params)[0] || new Action(this, name, {}, {}, 'none'); 204 | }; 205 | 206 | /** 207 | * Get parent resource object 208 | * @returns {Object} 209 | */ 210 | obj.getParent = function() { 211 | return parent; 212 | }; 213 | 214 | /** 215 | * Get property value 216 | * @returns {*} 217 | */ 218 | obj.getProp = function(prop) { 219 | return props[prop]; 220 | }; 221 | 222 | /** 223 | * Get all properties as an object 224 | * @returns {Object} 225 | */ 226 | obj.getProps = function() { 227 | return props; 228 | }; 229 | 230 | /** 231 | * Does an object have a given action? 232 | * @description Convenience method to find out if an object has a link or form 233 | * @param {String} name The name of the action 234 | * @returns {Boolean} 235 | */ 236 | obj.hasAction = function(name) { 237 | return obj.hasLink(name) || obj.hasForm(name); 238 | }; 239 | 240 | /** 241 | * Does a resource contain a given embedded obj? 242 | * @description Convenience method to find out if a resource contains an embedded object 243 | * @param {String} name The name of the embedded obj 244 | * @returns {Boolean} 245 | */ 246 | obj.hasEmbedded = function(name) { 247 | return !!embedded[name]; 248 | }; 249 | 250 | /** 251 | * Does an object have a given form? 252 | * @description Convenience method to find out if an object has a form 253 | * @param {String} name The name of the form 254 | * @returns {Boolean} 255 | */ 256 | obj.hasForm = function(name) { 257 | return obj.listActions().forms.indexOf(name) >= 0; 258 | }; 259 | 260 | /** 261 | * Does an object have a given link? 262 | * @description Convenience method to find out if an object has a link 263 | * @param {String} name The name of the link 264 | * @returns {Boolean} 265 | */ 266 | obj.hasLink = function(name) { 267 | return obj.listActions().links.indexOf(name) >= 0; 268 | }; 269 | 270 | /** 271 | * List of all available actions 272 | * @returns {Object} 273 | */ 274 | obj.listActions = function() { 275 | var 276 | filter = function(item) { 277 | return item !== 'curies'; 278 | }; 279 | 280 | return { 281 | links: keys(res._links || {}).filter(filter), 282 | forms: keys(res._forms || {}).filter(filter) 283 | }; 284 | }; 285 | 286 | /** 287 | * List of all available embedded objects 288 | * @returns {Array} 289 | */ 290 | obj.listEmbedded = function() { 291 | return keys(embedded); 292 | }; 293 | 294 | 295 | if (options.debug) { 296 | /** 297 | * original HAL resource 298 | */ 299 | obj.resource = res; 300 | } 301 | 302 | if (!base) { 303 | /** 304 | * Get root level resource 305 | */ 306 | obj.getRoot = function() { 307 | var root; 308 | 309 | do { 310 | root = this.getParent(); 311 | } while (!(root instanceof Resource)); 312 | 313 | return root; 314 | }; 315 | } 316 | 317 | if (link) { 318 | linkRefs[link] = obj; 319 | } 320 | }, 321 | 322 | /** 323 | * Action instance 324 | * @param {Object} parent Parent resource 325 | * @param {String} name Action name 326 | * @param {Object} action Action object 327 | * @param {Object} [params] Params to be used for templated action 328 | * @param {String} type Internal identification of action type 329 | * @constructor 330 | */ 331 | Action = function(parent, name, action, params, type) { 332 | var 333 | api = this, 334 | rawUrl = (action.href || action.action || '').replace(/^#/, ''), 335 | method = action.method || (type === 'form' ? 'POST' : 'GET'), 336 | actionUrl = '', 337 | payLoad = ''; 338 | 339 | /** 340 | * Get action name 341 | * @returns {String} 342 | */ 343 | api.getActionName = function() { 344 | return name; 345 | }; 346 | 347 | /** 348 | * Get action type 349 | * @returns {String} 350 | */ 351 | api.getActionType = function() { 352 | return type; 353 | }; 354 | 355 | /** 356 | * Get formatted url for the action 357 | * @returns {String} 358 | */ 359 | api.getActionUrl = function() { 360 | return actionUrl; 361 | }; 362 | 363 | /** 364 | * Get action method 365 | * @returns {String} 366 | */ 367 | api.getMethod = function() { 368 | return method; 369 | }; 370 | 371 | /** 372 | * Get params that need are required for the action 373 | * @returns {Array} 374 | */ 375 | api.getParams = function() { 376 | return urltemplate.extractParams(rawUrl); 377 | }; 378 | 379 | /** 380 | * Get un-formatted url for the action 381 | * @returns {String} 382 | */ 383 | api.getRawActionUrl = function() { 384 | return rawUrl; 385 | }; 386 | 387 | /** 388 | * Get action title 389 | * @returns {String} 390 | */ 391 | api.getTitle = function() { 392 | return action.title || ''; 393 | }; 394 | 395 | /** 396 | * Get action template flag 397 | * @returns {Boolean} 398 | */ 399 | api.isTemplated = function() { 400 | return action.templated || false; 401 | }; 402 | 403 | api.setParams = function(params) { 404 | actionUrl = action.templated ? urltemplate.expand(rawUrl, params || {}) : rawUrl; 405 | 406 | if (actionUrl) { 407 | actionUrl = urlparse.urljoin(action.curie ? action.curie.href : parent.getBase(), actionUrl); 408 | } 409 | 410 | if (type === 'form') { 411 | if (action.fields && isObject(params)) { 412 | payLoad = {}; 413 | keys(action.fields).forEach(function(field) { 414 | if (params.hasOwnProperty(field)) { 415 | payLoad[field] = params[field]; 416 | } else if (action.fields[field].hasOwnProperty('default')) { 417 | payLoad[field] = action.fields[field]['default']; 418 | } 419 | }); 420 | } else if (params) { 421 | payLoad = JSON.stringify(params); 422 | } 423 | 424 | if (excludeBody.test(method) && payLoad) { 425 | actionUrl = urlparse.urljoin(actionUrl, '?' + urlSerialize(payLoad)); 426 | payLoad = ''; 427 | } 428 | } 429 | 430 | return api; 431 | }; 432 | 433 | if (type === 'form') { 434 | /** 435 | * Get form fields for the form action 436 | * @returns {Object} 437 | */ 438 | api.getFields = function() { 439 | return action.fields || {}; 440 | }; 441 | 442 | /** 443 | * Get payload to be submitted for the form action 444 | * @returns {Object} 445 | */ 446 | api.getPayload = function() { 447 | return payLoad; 448 | }; 449 | } 450 | 451 | if (action.curie) { 452 | /** 453 | * Get curie documentation url 454 | * @returns {String} 455 | */ 456 | api.getDocUrl = function() { 457 | return urltemplate.expand(action.curie.href, { 458 | rel: name.split(':')[1] 459 | }); 460 | }; 461 | } 462 | 463 | api.setParams(params); 464 | }, 465 | 466 | /** 467 | * HAL Embedded resource 468 | * @param {Resource} res HAL resource 469 | * @param {Resource} parent Parent HAL resource 470 | * @constructor 471 | */ 472 | EmbeddedResource = function(res, parent) { 473 | return sharedResourceAPI(this, res || {}, parent); 474 | }, 475 | 476 | /** 477 | * HAL Resource 478 | * @param {String} base Base url for the resource 479 | * @param {Object} res Resource data 480 | * @constructor 481 | */ 482 | Resource = function(base, res) { 483 | return sharedResourceAPI(this, res, this, base); 484 | }; 485 | 486 | Action.prototype.fetch = function(fetchOptions) { 487 | fetchOptions || (fetchOptions = {}); 488 | 489 | var 490 | url = fetchOptions.url || this.getActionUrl(), 491 | rawUrl = this.getRawActionUrl(), 492 | name = this.getActionName(), 493 | payLoad = this.getPayload ? this.getPayload() : '', 494 | o = deepExtend({ 495 | method: this.getMethod(), 496 | action: name, 497 | }, options.xhr, fetchOptions), 498 | 499 | onSuccess = function(response) { 500 | if (response.status === 204) { 501 | return Promise.resolve({ 502 | action: name, 503 | data: '', 504 | xhr: response 505 | }); 506 | } 507 | 508 | /** 509 | * If flag to returnRawData is false & the object is a valid 510 | * use Resource constructor 511 | * @param {} data 512 | */ 513 | function formatData(data) { 514 | if (!o.returnRawData && isObject(data)) { 515 | return new Resource(url, data); 516 | } 517 | 518 | return data; 519 | } 520 | 521 | return response.json().then(function(data) { 522 | return { 523 | action: name, 524 | data: formatData(data), 525 | xhr: response 526 | }; 527 | }, function() { 528 | return { 529 | action: name, 530 | data: response.text(), 531 | xhr: response 532 | }; 533 | }); 534 | }, 535 | 536 | onError = function(response) { 537 | return Promise.reject(response instanceof Response ? { 538 | error: { 539 | action: name, 540 | code: '0021', 541 | msg: 'Failed to retrieve action' 542 | }, 543 | xhr: response 544 | } : response); 545 | }; 546 | 547 | if (!excludeBody.test(o.method) && isObject(payLoad)) { 548 | o.headers['Content-Type'] = 'application/x-www-form-urlencoded'; 549 | o.body = urlSerialize(payLoad); 550 | } 551 | 552 | if (!url) { 553 | return Promise.reject({ 554 | error: { 555 | action: name, 556 | code: '0020', 557 | msg: 'Url is not provided for this action' 558 | } 559 | }); 560 | } else if (o.method === 'GET' && !fetchOptions.force && linkRefs.hasOwnProperty(rawUrl)) { 561 | return Promise.resolve({ 562 | action: name, 563 | data: linkRefs[rawUrl] 564 | }); 565 | } 566 | 567 | return load(url, o).then(onSuccess, onError); 568 | }; 569 | 570 | /** 571 | * Fetch 572 | * @returns {Promise} 573 | */ 574 | this.fetch = function() { 575 | var o = deepExtend({ 576 | method: 'GET', 577 | action: 'homepage', 578 | }, options.xhr); 579 | var 580 | onSuccess = function(response) { 581 | if (!options.cacheHomepage) { 582 | homepageLoaded = false; 583 | } 584 | 585 | return response.json().then(function(data) { 586 | return isObject(data) ? { 587 | data: new Resource(endpoint, data), 588 | xhr: response 589 | } : Promise.reject(); 590 | })['catch'](function() { 591 | return Promise.reject({ 592 | error: { 593 | code: '0002', 594 | msg: 'Could not parse homepage' 595 | }, 596 | xhr: response 597 | }); 598 | }); 599 | }, 600 | 601 | onError = function(response) { 602 | homepageLoaded = false; 603 | 604 | return Promise.reject(response instanceof Response ? { 605 | error: { 606 | code: '0001', 607 | msg: 'Failed to retrieve homepage' 608 | }, 609 | xhr: response 610 | } : response); 611 | }; 612 | 613 | if (!endpoint) { 614 | return Promise.reject({ 615 | error: { 616 | code: '0000', 617 | msg: 'API endpoint was not provided' 618 | } 619 | }); 620 | } 621 | 622 | if (!homepageLoaded) { 623 | homepageLoaded = true; 624 | homepage = load(endpoint, o).then(onSuccess, onError); 625 | } 626 | 627 | return homepage; 628 | }; 629 | 630 | /** 631 | * Set global options 632 | * @param {Object} o Options object 633 | * @return {Object} HyperGard 634 | */ 635 | this.setOptions = function(o) { 636 | deepExtend(options, o || {}); 637 | return this; 638 | }; 639 | 640 | /** 641 | * Get global options 642 | * @returns {Object} 643 | */ 644 | this.getOptions = function() { 645 | return options; 646 | }; 647 | 648 | /** 649 | * Parse json into resource 650 | * @param {Object} json Data in json format to be parsed as resource 651 | * @returns {Resource} 652 | */ 653 | this.parse = function(json) { 654 | return new Resource(endpoint, json || {}); 655 | }; 656 | 657 | /** 658 | * Fetch homepage on initialization 659 | */ 660 | if (options.preloadHomepage) { 661 | this.fetch(); 662 | } 663 | }; 664 | 665 | HyperGard.prototype.version = version; 666 | 667 | /** 668 | * Will wrap any fetch performed in supplied middleware 669 | * This will allow custom logging headers to be set without 670 | * using before/after fetch events 671 | * @param {Function} middleware Function to wrap fetches in 672 | */ 673 | function applyMiddleware(middleware) { 674 | load = (function(stack) { 675 | return function(url, fetchOptions) { 676 | return middleware(url, fetchOptions, stack); 677 | }; 678 | })(load); 679 | } 680 | 681 | /** 682 | * Will apply an array of middleware functions around the load method 683 | * @param {Array} middlewareStack Array of functions to be wrapped around every load 684 | */ 685 | HyperGard.prototype.applyMiddlewareStack = function(middlewareStack) { 686 | var applicationOrder; 687 | if (middlewareStack && Array.isArray(middlewareStack) && middlewareStack.length) { 688 | // Apply in reverse order, for first entry is applied as inner most wrapper 689 | applicationOrder = middlewareStack.reverse(); 690 | applicationOrder.forEach(applyMiddleware); 691 | } 692 | }; 693 | 694 | 695 | export default HyperGard; 696 | 697 | -------------------------------------------------------------------------------- /test/spec/data.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paul.Bronshteyn 3 | * @comment Built by a geek loaded on caffeine ... 4 | */ 5 | describe('hyperGard', function() { 6 | beforeEach(function(done) { 7 | window.fetch.and.returnValue(this.homepageData(homepage)); 8 | this.testHyperGard = new HyperGard(testEndpoint, testOptions); 9 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 10 | }); 11 | 12 | describe('Test hyperGard data', function() { 13 | describe('response api', function() { 14 | it('should have getEmbedded function', function() { 15 | expect(this.data.getEmbedded).toEqual(jasmine.any(Function)); 16 | }); 17 | 18 | it('should have getAction function', function() { 19 | expect(this.data.getAction).toEqual(jasmine.any(Function)); 20 | }); 21 | 22 | it('should have getFirstAction function', function() { 23 | expect(this.data.getFirstAction).toEqual(jasmine.any(Function)); 24 | }); 25 | 26 | it('should have getBase function', function() { 27 | expect(this.data.getBase).toEqual(jasmine.any(Function)); 28 | }); 29 | 30 | it('should have getParent function', function() { 31 | expect(this.data.getParent).toEqual(jasmine.any(Function)); 32 | }); 33 | 34 | it('should have getProp function', function() { 35 | expect(this.data.getProp).toEqual(jasmine.any(Function)); 36 | }); 37 | 38 | it('should have getProps function', function() { 39 | expect(this.data.getProps).toEqual(jasmine.any(Function)); 40 | }); 41 | 42 | it('should have hasAction function', function() { 43 | expect(this.data.hasAction).toEqual(jasmine.any(Function)); 44 | }); 45 | 46 | it('should have hasEmbedded function', function() { 47 | expect(this.data.hasEmbedded).toEqual(jasmine.any(Function)); 48 | }); 49 | 50 | it('should have hasForm function', function() { 51 | expect(this.data.hasForm).toEqual(jasmine.any(Function)); 52 | }); 53 | 54 | it('should have hasLink function', function() { 55 | expect(this.data.hasLink).toEqual(jasmine.any(Function)); 56 | }); 57 | 58 | it('should have listActions function', function() { 59 | expect(this.data.listActions).toEqual(jasmine.any(Function)); 60 | }); 61 | 62 | it('should have listEmbedded function', function() { 63 | expect(this.data.listEmbedded).toEqual(jasmine.any(Function)); 64 | }); 65 | }); 66 | 67 | describe('homepage data', function() { 68 | it('should have base equal to /endpoint/', function() { 69 | expect(this.data.getBase()).toEqual('/endpoint/'); 70 | }); 71 | 72 | it('should return a parent of homepage data', function() { 73 | expect(this.data.getParent()).toEqual(this.data); 74 | }); 75 | 76 | it('should not have a property test', function() { 77 | expect(this.data.getProp('test')).toBeUndefined(); 78 | }); 79 | 80 | it('should have empty properties object', function() { 81 | expect(this.data.getProps()).toEqual(jasmine.any(Object)); 82 | expect(Object.keys(this.data.getProps()).length).toEqual(0); 83 | }); 84 | }); 85 | 86 | describe('retrieving a link', function() { 87 | describe('fetch link with no href', function() { 88 | beforeEach(function(done) { 89 | this.link = this.data.getFirstAction('noHref'); 90 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 91 | }); 92 | 93 | it('should make fetch request', function() { 94 | expect(window.fetch).toHaveBeenCalled(); 95 | }); 96 | 97 | it('should not call resolve promise', function() { 98 | expect(this.onActionSuccess).not.toHaveBeenCalled(); 99 | }); 100 | 101 | it('should call reject promise with 1 param', function() { 102 | expect(this.onActionError.calls.mostRecent().args.length).toEqual(1); 103 | }); 104 | 105 | it('should call reject promise with error object', function() { 106 | expect(this.onActionError).toHaveBeenCalledWith({ 107 | error: { 108 | action: 'noHref', 109 | code: '0020', 110 | msg: 'Url is not provided for this action' 111 | } 112 | }); 113 | }); 114 | }); 115 | 116 | describe('fetch request failed', function() { 117 | beforeEach(function(done) { 118 | this.link = this.data.getFirstAction('absoluteUrl'); 119 | this.xhr500 = this.homepageData('', 500); 120 | window.fetch.and.returnValue(this.xhr500); 121 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 122 | }); 123 | 124 | it('should make fetch request', function() { 125 | expect(window.fetch).toHaveBeenCalled(); 126 | }); 127 | 128 | it('should not call resolve promise', function() { 129 | expect(this.onActionSuccess).not.toHaveBeenCalled(); 130 | }); 131 | 132 | it('should call reject promise with 1 param', function() { 133 | expect(this.onActionError.calls.mostRecent().args.length).toEqual(1); 134 | }); 135 | 136 | it('should call reject promise with error object', function() { 137 | expect(this.onActionError).toHaveBeenCalledWith({ 138 | error: { 139 | action: 'absoluteUrl', 140 | code: '0021', 141 | msg: 'Failed to retrieve action' 142 | }, 143 | xhr: this.xhr500 144 | }); 145 | }); 146 | }); 147 | 148 | describe('fetch succeeded with invalid responses', function() { 149 | beforeEach(function() { 150 | this.link = this.data.getFirstAction('absoluteUrl'); 151 | }); 152 | 153 | describe('returned array', function() { 154 | beforeEach(function(done) { 155 | this.testResponse = this.homepageData([]); 156 | window.fetch.and.returnValue(this.testResponse); 157 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 158 | }); 159 | 160 | it('should make fetch request', function() { 161 | expect(window.fetch).toHaveBeenCalled(); 162 | }); 163 | 164 | it('should call resolve promise', function() { 165 | expect(this.onActionSuccess).toHaveBeenCalled(); 166 | }); 167 | 168 | it('should not call reject promise', function() { 169 | expect(this.onActionError).not.toHaveBeenCalled(); 170 | }); 171 | }); 172 | 173 | describe('returned boolean - true', function() { 174 | beforeEach(function(done) { 175 | this.testResponse = this.homepageData(true); 176 | window.fetch.and.returnValue(this.testResponse); 177 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 178 | }); 179 | 180 | it('should make fetch request', function() { 181 | expect(window.fetch).toHaveBeenCalled(); 182 | }); 183 | 184 | it('should call resolve promise', function() { 185 | expect(this.onActionSuccess).toHaveBeenCalled(); 186 | }); 187 | 188 | it('should not call reject promise', function() { 189 | expect(this.onActionError).not.toHaveBeenCalled(); 190 | }); 191 | }); 192 | 193 | describe('returned boolean - false', function() { 194 | beforeEach(function(done) { 195 | this.testResponse = this.homepageData(false); 196 | window.fetch.and.returnValue(this.testResponse); 197 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 198 | }); 199 | 200 | it('should make fetch request', function() { 201 | expect(window.fetch).toHaveBeenCalled(); 202 | }); 203 | 204 | it('should call resolve promise', function() { 205 | expect(this.onActionSuccess).toHaveBeenCalled(); 206 | }); 207 | 208 | it('should not call reject promise', function() { 209 | expect(this.onActionError).not.toHaveBeenCalled(); 210 | }); 211 | }); 212 | 213 | describe('returned number', function() { 214 | beforeEach(function(done) { 215 | this.testResponse = this.homepageData(1); 216 | window.fetch.and.returnValue(this.testResponse); 217 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 218 | }); 219 | 220 | it('should make fetch request', function() { 221 | expect(window.fetch).toHaveBeenCalled(); 222 | }); 223 | 224 | it('should call resolve promise', function() { 225 | expect(this.onActionSuccess).toHaveBeenCalled(); 226 | }); 227 | 228 | it('should not call reject promise', function() { 229 | expect(this.onActionError).not.toHaveBeenCalled(); 230 | }); 231 | }); 232 | 233 | describe('with returnRawResponse option', function() { 234 | var baseResponse = { a: 1 }; 235 | var processedResponse; 236 | 237 | beforeEach(function() { 238 | this.testResponse = this.homepageData(baseResponse); 239 | window.fetch.and.returnValue(this.testResponse); 240 | 241 | return this.link.fetch({ returnRawData: true }).then(function (result) { 242 | processedResponse = result.data; 243 | }); 244 | }); 245 | 246 | it('should make fetch request', function() { 247 | expect(window.fetch).toHaveBeenCalled(); 248 | }); 249 | 250 | it('it should return raw response', function() { 251 | expect(processedResponse).toEqual(baseResponse); 252 | }); 253 | }); 254 | }); 255 | 256 | describe('response api', function() { 257 | beforeEach(function(done) { 258 | this.link = this.data.getFirstAction('twoParamsPath', { 259 | path1: 'path1', 260 | path2: 'path2' 261 | }); 262 | 263 | window.fetch.and.returnValue(this.homepageData(actionResponse)); 264 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 265 | }); 266 | 267 | it('should have getEmbedded function', function() { 268 | expect(this.actionData.getEmbedded).toEqual(jasmine.any(Function)); 269 | }); 270 | 271 | it('should have getAction function', function() { 272 | expect(this.actionData.getAction).toEqual(jasmine.any(Function)); 273 | }); 274 | 275 | it('should have getFirstAction function', function() { 276 | expect(this.actionData.getFirstAction).toEqual(jasmine.any(Function)); 277 | }); 278 | 279 | it('should have getBase function', function() { 280 | expect(this.actionData.getBase).toEqual(jasmine.any(Function)); 281 | }); 282 | 283 | it('should have getParent function', function() { 284 | expect(this.actionData.getParent).toEqual(jasmine.any(Function)); 285 | }); 286 | 287 | it('should have getProp function', function() { 288 | expect(this.actionData.getProp).toEqual(jasmine.any(Function)); 289 | }); 290 | 291 | it('should have getProps function', function() { 292 | expect(this.actionData.getProps).toEqual(jasmine.any(Function)); 293 | }); 294 | 295 | it('should have hasAction function', function() { 296 | expect(this.actionData.hasAction).toEqual(jasmine.any(Function)); 297 | }); 298 | 299 | it('should have hasEmbedded function', function() { 300 | expect(this.actionData.hasEmbedded).toEqual(jasmine.any(Function)); 301 | }); 302 | 303 | it('should have hasForm function', function() { 304 | expect(this.actionData.hasForm).toEqual(jasmine.any(Function)); 305 | }); 306 | 307 | it('should have hasLink function', function() { 308 | expect(this.actionData.hasLink).toEqual(jasmine.any(Function)); 309 | }); 310 | 311 | it('should have listActions function', function() { 312 | expect(this.actionData.listActions).toEqual(jasmine.any(Function)); 313 | }); 314 | 315 | it('should have listEmbedded function', function() { 316 | expect(this.actionData.listEmbedded).toEqual(jasmine.any(Function)); 317 | }); 318 | }); 319 | 320 | describe('response data - link', function() { 321 | beforeEach(function(done) { 322 | this.link = this.data.getFirstAction('twoParamsPath', { 323 | path1: 'path1', 324 | path2: 'path2' 325 | }); 326 | 327 | window.fetch.and.returnValue(this.homepageData(actionResponse)); 328 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 329 | }); 330 | 331 | it('should have proper base endpoint', function() { 332 | expect(this.actionData.getBase()).toEqual('/endpoint/two/params/path1/path2/'); 333 | }); 334 | 335 | it('should have proper parent', function() { 336 | expect(this.actionData.getParent()).toEqual(this.actionData); 337 | }); 338 | 339 | describe('embedded data', function() { 340 | describe('getEmbedded', function() { 341 | it('should return an object for embedded `one`', function() { 342 | expect(this.actionData.getEmbedded('one')).toEqual(jasmine.any(Object)); 343 | }); 344 | 345 | it('should return an array for embedded `two`', function() { 346 | expect(this.actionData.getEmbedded('two')).toEqual(jasmine.any(Array)); 347 | }); 348 | }); 349 | 350 | describe('fetchEmbedded', function() { 351 | describe('it has the embedded object', function() { 352 | beforeEach(function(done) { 353 | this.embedded = this.actionData.getEmbedded('two')[0]; 354 | this.embedded.fetchEmbedded('sub_two').then(function(subData) { 355 | this.subEmbedded = subData.data; 356 | done(); 357 | }.bind(this)); 358 | }); 359 | 360 | it('should return a sub_two embedded object', function() { 361 | expect(this.subEmbedded).toEqual(this.actionData.getEmbedded('two')[0].getEmbedded('sub_two')); 362 | }); 363 | }); 364 | 365 | describe('it does not have the embedded object', function() { 366 | beforeEach(function(done) { 367 | this.embedded = this.actionData.getEmbedded('two')[0]; 368 | this.embedded.fetchEmbedded('linkedEmbedded').then(this.onActionSuccess, this.onActionError).then(done, done); 369 | }); 370 | 371 | it('should make fetch request', function() { 372 | expect(window.fetch).toHaveBeenCalled(); 373 | }); 374 | 375 | it('should call resolve promise', function() { 376 | expect(this.onActionSuccess).toHaveBeenCalled(); 377 | }); 378 | 379 | it('should not call reject promise', function() { 380 | expect(this.onActionError).not.toHaveBeenCalled(); 381 | }); 382 | }); 383 | }); 384 | 385 | describe('hasEmbedded', function() { 386 | it('should return true for embedded `one`', function() { 387 | expect(this.actionData.hasEmbedded('one')).toBe(true); 388 | }); 389 | 390 | it('should return true for embedded `two`', function() { 391 | expect(this.actionData.hasEmbedded('two')).toBe(true); 392 | }); 393 | 394 | it('should return false for embedded `noembedded`', function() { 395 | expect(this.actionData.hasEmbedded('noembedded')).toEqual(false); 396 | }); 397 | }); 398 | 399 | describe('listEmbedded', function() { 400 | it('should return an array', function() { 401 | expect(this.actionData.listEmbedded()).toEqual(jasmine.any(Array)); 402 | }); 403 | 404 | it('should return a list of 3', function() { 405 | expect(this.actionData.listEmbedded().length).toEqual(3); 406 | }); 407 | 408 | it('should return a list of embedded objects', function() { 409 | expect(this.actionData.listEmbedded()).toEqual(['one', 'two', 'three']); 410 | }); 411 | }); 412 | 413 | describe('embedded api', function() { 414 | beforeEach(function() { 415 | this.embedded = this.actionData.getEmbedded('one'); 416 | }); 417 | 418 | it('should have proper base', function() { 419 | expect(this.embedded.getBase()).toEqual(this.actionData.getBase()); 420 | }); 421 | 422 | it('should have proper parent', function() { 423 | expect(this.embedded.getParent()).toEqual(this.actionData); 424 | }); 425 | 426 | it('should have getEmbedded function', function() { 427 | expect(this.embedded.getEmbedded).toEqual(jasmine.any(Function)); 428 | }); 429 | 430 | it('should have getAction function', function() { 431 | expect(this.embedded.getAction).toEqual(jasmine.any(Function)); 432 | }); 433 | 434 | it('should have getFirstAction function', function() { 435 | expect(this.embedded.getFirstAction).toEqual(jasmine.any(Function)); 436 | }); 437 | 438 | it('should have getBase function', function() { 439 | expect(this.embedded.getBase).toEqual(jasmine.any(Function)); 440 | }); 441 | 442 | it('should have getProp function', function() { 443 | expect(this.embedded.getProp).toEqual(jasmine.any(Function)); 444 | }); 445 | 446 | it('should have getProps function', function() { 447 | expect(this.embedded.getProps).toEqual(jasmine.any(Function)); 448 | }); 449 | 450 | it('should have getRoot function', function() { 451 | expect(this.embedded.getRoot).toEqual(jasmine.any(Function)); 452 | }); 453 | 454 | it('should have hasAction function', function() { 455 | expect(this.embedded.hasAction).toEqual(jasmine.any(Function)); 456 | }); 457 | 458 | it('should have hasEmbedded function', function() { 459 | expect(this.embedded.hasEmbedded).toEqual(jasmine.any(Function)); 460 | }); 461 | 462 | it('should have hasForm function', function() { 463 | expect(this.embedded.hasForm).toEqual(jasmine.any(Function)); 464 | }); 465 | 466 | it('should have hasLink function', function() { 467 | expect(this.embedded.hasLink).toEqual(jasmine.any(Function)); 468 | }); 469 | 470 | it('should have listActions function', function() { 471 | expect(this.embedded.listActions).toEqual(jasmine.any(Function)); 472 | }); 473 | 474 | it('should have listEmbedded function', function() { 475 | expect(this.embedded.listEmbedded).toEqual(jasmine.any(Function)); 476 | }); 477 | 478 | describe('getRoot', function() { 479 | it('should return root level resource', function() { 480 | expect(this.embedded.getRoot()).toEqual(this.actionData); 481 | }); 482 | }); 483 | }); 484 | 485 | describe('cached link', function() { 486 | beforeEach(function(done) { 487 | window.fetch.calls.reset(); 488 | this.embedded = this.actionData.getEmbedded('two')[0]; 489 | this.link = this.embedded.getFirstAction('self'); 490 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 491 | }); 492 | 493 | it('should not make fetch request', function() { 494 | expect(window.fetch).not.toHaveBeenCalled(); 495 | }); 496 | 497 | it('should call resolve promise', function() { 498 | expect(this.onActionSuccess).toHaveBeenCalled(); 499 | }); 500 | 501 | it('should not call reject promise', function() { 502 | expect(this.onActionError).not.toHaveBeenCalled(); 503 | }); 504 | }); 505 | }); 506 | }); 507 | 508 | describe('response data - form', function() { 509 | beforeEach(function(done) { 510 | this.link = this.data.getFirstAction('formPostOneField', { 511 | param: 'param' 512 | }); 513 | 514 | window.fetch.and.returnValue(this.homepageData(actionResponse)); 515 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 516 | }); 517 | 518 | it('should have proper base endpoint', function() { 519 | expect(this.actionData.getBase()).toEqual('/endpoint/form/'); 520 | }); 521 | 522 | it('should have proper parent', function() { 523 | expect(this.actionData.getParent()).toEqual(this.actionData); 524 | }); 525 | 526 | describe('embedded data', function() { 527 | describe('getEmbedded', function() { 528 | it('should return an object for embedded `one`', function() { 529 | expect(this.actionData.getEmbedded('one')).toEqual(jasmine.any(Object)); 530 | }); 531 | 532 | it('should return an array for embedded `two`', function() { 533 | expect(this.actionData.getEmbedded('two')).toEqual(jasmine.any(Array)); 534 | }); 535 | }); 536 | 537 | describe('fetchEmbedded', function() { 538 | describe('it has the embedded object', function() { 539 | beforeEach(function(done) { 540 | this.embedded = this.actionData.getEmbedded('two')[0]; 541 | this.embedded.fetchEmbedded('sub_two').then(function(subData) { 542 | this.subEmbedded = subData.data; 543 | done(); 544 | }.bind(this)); 545 | }); 546 | 547 | it('should return a sub_two embedded object', function() { 548 | expect(this.subEmbedded).toEqual(this.actionData.getEmbedded('two')[0].getEmbedded('sub_two')); 549 | }); 550 | }); 551 | 552 | describe('it does not have the embedded object', function() { 553 | beforeEach(function(done) { 554 | this.embedded = this.actionData.getEmbedded('two')[0]; 555 | this.embedded.fetchEmbedded('linkedEmbedded').then(this.onActionSuccess, this.onActionError).then(done, done); 556 | }); 557 | 558 | it('should make fetch request', function() { 559 | expect(window.fetch).toHaveBeenCalled(); 560 | }); 561 | 562 | it('should call resolve promise', function() { 563 | expect(this.onActionSuccess).toHaveBeenCalled(); 564 | }); 565 | 566 | it('should not call reject promise', function() { 567 | expect(this.onActionError).not.toHaveBeenCalled(); 568 | }); 569 | }); 570 | }); 571 | 572 | describe('hasEmbedded', function() { 573 | it('should return true for embedded `one`', function() { 574 | expect(this.actionData.hasEmbedded('one')).toBe(true); 575 | }); 576 | 577 | it('should return true for embedded `two`', function() { 578 | expect(this.actionData.hasEmbedded('two')).toBe(true); 579 | }); 580 | 581 | it('should return false for embedded `noembedded`', function() { 582 | expect(this.actionData.hasEmbedded('noembedded')).toEqual(false); 583 | }); 584 | }); 585 | 586 | describe('listEmbedded', function() { 587 | it('should return an array', function() { 588 | expect(this.actionData.listEmbedded()).toEqual(jasmine.any(Array)); 589 | }); 590 | 591 | it('should return a list of 3', function() { 592 | expect(this.actionData.listEmbedded().length).toEqual(3); 593 | }); 594 | 595 | it('should return a list of embedded objects', function() { 596 | expect(this.actionData.listEmbedded()).toEqual(['one', 'two', 'three']); 597 | }); 598 | }); 599 | 600 | describe('embedded api', function() { 601 | beforeEach(function() { 602 | this.embedded = this.actionData.getEmbedded('one'); 603 | }); 604 | 605 | it('should have proper base', function() { 606 | expect(this.embedded.getBase()).toEqual(this.actionData.getBase()); 607 | }); 608 | 609 | it('should have proper parent', function() { 610 | expect(this.embedded.getParent()).toEqual(this.actionData); 611 | }); 612 | 613 | it('should have getEmbedded function', function() { 614 | expect(this.embedded.getEmbedded).toEqual(jasmine.any(Function)); 615 | }); 616 | 617 | it('should have getAction function', function() { 618 | expect(this.embedded.getAction).toEqual(jasmine.any(Function)); 619 | }); 620 | 621 | it('should have getFirstAction function', function() { 622 | expect(this.embedded.getFirstAction).toEqual(jasmine.any(Function)); 623 | }); 624 | 625 | it('should have getBase function', function() { 626 | expect(this.embedded.getBase).toEqual(jasmine.any(Function)); 627 | }); 628 | 629 | it('should have getProp function', function() { 630 | expect(this.embedded.getProp).toEqual(jasmine.any(Function)); 631 | }); 632 | 633 | it('should have getProps function', function() { 634 | expect(this.embedded.getProps).toEqual(jasmine.any(Function)); 635 | }); 636 | 637 | it('should have hasAction function', function() { 638 | expect(this.embedded.hasAction).toEqual(jasmine.any(Function)); 639 | }); 640 | 641 | it('should have hasEmbedded function', function() { 642 | expect(this.embedded.hasEmbedded).toEqual(jasmine.any(Function)); 643 | }); 644 | 645 | it('should have hasForm function', function() { 646 | expect(this.embedded.hasForm).toEqual(jasmine.any(Function)); 647 | }); 648 | 649 | it('should have hasLink function', function() { 650 | expect(this.embedded.hasLink).toEqual(jasmine.any(Function)); 651 | }); 652 | 653 | it('should have listActions function', function() { 654 | expect(this.embedded.listActions).toEqual(jasmine.any(Function)); 655 | }); 656 | 657 | it('should have listEmbedded function', function() { 658 | expect(this.embedded.listEmbedded).toEqual(jasmine.any(Function)); 659 | }); 660 | }); 661 | 662 | describe('cached link', function() { 663 | beforeEach(function(done) { 664 | window.fetch.calls.reset(); 665 | this.embedded = this.actionData.getEmbedded('two')[0]; 666 | this.link = this.embedded.getFirstAction('self'); 667 | this.link.fetch().then(this.onActionSuccess, this.onActionError).then(done, done); 668 | }); 669 | 670 | it('should not make fetch request', function() { 671 | expect(window.fetch).not.toHaveBeenCalled(); 672 | }); 673 | 674 | it('should call resolve promise', function() { 675 | expect(this.onActionSuccess).toHaveBeenCalled(); 676 | }); 677 | 678 | it('should not call reject promise', function() { 679 | expect(this.onActionError).not.toHaveBeenCalled(); 680 | }); 681 | }); 682 | }); 683 | }); 684 | }); 685 | }); 686 | }); 687 | -------------------------------------------------------------------------------- /test/spec/actions.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paul.Bronshteyn 3 | * @comment Built by a geek loaded on caffeine ... 4 | */ 5 | describe('hyperGard', function() { 6 | describe('Test hyperGard handling of actions', function() { 7 | beforeEach(function(done) { 8 | window.fetch.and.returnValue(this.homepageData(homepage)); 9 | this.testHyperGard = new HyperGard(testEndpoint, testOptions); 10 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 11 | }); 12 | 13 | describe('test action api', function() { 14 | describe('hasAction', function() { 15 | it('should have a hasAction api', function() { 16 | expect(this.data.hasAction).toEqual(jasmine.any(Function)); 17 | }); 18 | 19 | it('should return a boolean', function() { 20 | expect(this.data.hasAction()).toEqual(jasmine.any(Boolean)); 21 | }); 22 | 23 | it('should return true if the data has a form with the action name', function() { 24 | expect(this.data.hasAction('formNoAction')).toEqual(true); 25 | }); 26 | 27 | it('should return true if the data has a link with the action name', function() { 28 | expect(this.data.hasAction('self')).toEqual(true); 29 | }); 30 | 31 | it('should return false if the data does not have a link or a form with the action nam', function() { 32 | expect(this.data.hasAction('foo')).toEqual(false); 33 | }); 34 | }); 35 | 36 | describe('hasForm', function() { 37 | it('should have a hasForm api', function() { 38 | expect(this.data.hasForm).toEqual(jasmine.any(Function)); 39 | }); 40 | 41 | it('should return a boolean', function() { 42 | expect(this.data.hasForm()).toEqual(jasmine.any(Boolean)); 43 | }); 44 | 45 | it('should return true if the data has the form', function() { 46 | expect(this.data.hasForm('formNoAction')).toEqual(true); 47 | }); 48 | 49 | it('should return false if the data does not have the form', function() { 50 | expect(this.data.hasForm('foo')).toEqual(false); 51 | }); 52 | }); 53 | 54 | describe('hasLink', function() { 55 | it('should have a hasLink api', function() { 56 | expect(this.data.hasLink).toEqual(jasmine.any(Function)); 57 | }); 58 | 59 | it('should return a boolean', function() { 60 | expect(this.data.hasLink()).toEqual(jasmine.any(Boolean)); 61 | }); 62 | 63 | it('should return true if the data has the action', function() { 64 | expect(this.data.hasLink('self')).toEqual(true); 65 | }); 66 | 67 | it('should return false if the data does not hav the action', function() { 68 | expect(this.data.hasLink('foo')).toEqual(false); 69 | }); 70 | }); 71 | 72 | describe('listActions', function() { 73 | it('should have listAction api', function() { 74 | expect(this.data.listActions).toEqual(jasmine.any(Function)); 75 | }); 76 | 77 | it('should return an object', function() { 78 | expect(this.data.listActions()).toEqual(jasmine.any(Object)); 79 | }); 80 | 81 | it('should have key links', function() { 82 | expect(this.data.listActions().links).toBeDefined(); 83 | }); 84 | 85 | it('should have links defined as object', function() { 86 | expect(this.data.listActions().links).toEqual(jasmine.any(Array)); 87 | }); 88 | 89 | it('should have key forms', function() { 90 | expect(this.data.listActions().forms).toBeDefined(); 91 | }); 92 | 93 | it('should have forms defined as object', function() { 94 | expect(this.data.listActions().forms).toEqual(jasmine.any(Array)); 95 | }); 96 | }); 97 | 98 | describe('getAction', function() { 99 | it('should have getAction api', function() { 100 | expect(this.data.getAction).toEqual(jasmine.any(Function)); 101 | }); 102 | 103 | describe('getAction responses', function() { 104 | it('should return list of available actions', function() { 105 | expect(this.data.getAction()).toEqual(jasmine.any(Array)); 106 | }); 107 | 108 | it('getAction should return empty array', function() { 109 | expect(this.data.getAction().length).toEqual(0); 110 | }); 111 | }); 112 | }); 113 | 114 | describe('getFirstAction', function() { 115 | it('should have getFirstAction api', function() { 116 | expect(this.data.getFirstAction).toEqual(jasmine.any(Function)); 117 | }); 118 | 119 | describe('getFirstAction responses', function() { 120 | it('calls getFirstAction with no params', function() { 121 | expect(this.data.getFirstAction()).toEqual(jasmine.any(Object)); 122 | }); 123 | 124 | it('calls getFirstAction with array', function() { 125 | expect(this.data.getFirstAction([], [])).toEqual(jasmine.any(Object)); 126 | }); 127 | 128 | it('calls getFirstAction with boolean - true', function() { 129 | expect(this.data.getFirstAction(true, true)).toEqual(jasmine.any(Object)); 130 | }); 131 | 132 | it('calls getFirstAction with boolean - false', function() { 133 | expect(this.data.getFirstAction(false, false)).toEqual(jasmine.any(Object)) 134 | }); 135 | 136 | it('calls getFirstAction with null', function() { 137 | expect(this.data.getFirstAction(null, null)).toEqual(jasmine.any(Object)) 138 | }); 139 | 140 | it('calls getFirstAction with number', function() { 141 | expect(this.data.getFirstAction(1, 2)).toEqual(jasmine.any(Object)) 142 | }); 143 | 144 | it('calls getFirstAction with object', function() { 145 | expect(this.data.getFirstAction({}, {})).toEqual(jasmine.any(Object)) 146 | }); 147 | 148 | it('calls getFirstAction with undefined', function() { 149 | expect(this.data.getFirstAction(undefined, undefined)).toEqual(jasmine.any(Object)) 150 | }); 151 | }); 152 | }); 153 | }); 154 | 155 | describe('with valid homepage response', function() { 156 | describe('listActions', function() { 157 | it('should have 18 links', function() { 158 | expect(Object.keys(this.data.listActions().links).length).toEqual(18); 159 | }); 160 | 161 | it('should have 13 forms', function() { 162 | expect(Object.keys(this.data.listActions().forms).length).toEqual(13); 163 | }); 164 | }); 165 | 166 | describe('handling of links', function() { 167 | beforeEach(function() { 168 | this.params = {}; 169 | }); 170 | 171 | describe('link action api', function() { 172 | beforeEach(function() { 173 | this.link = this.data.getFirstAction('test:curie-test'); 174 | }); 175 | 176 | it('should have fetch api', function() { 177 | expect(this.link.fetch).toBeDefined(); 178 | }); 179 | 180 | it('should have getActionUrl api', function() { 181 | expect(this.link.getActionUrl).toBeDefined(); 182 | }); 183 | 184 | it('should have getRawActionUrl api', function() { 185 | expect(this.link.getRawActionUrl).toBeDefined(); 186 | }); 187 | 188 | it('should have getActionType api', function() { 189 | expect(this.link.getActionType).toBeDefined(); 190 | }); 191 | 192 | it('should return action type of "link"', function() { 193 | expect(this.link.getActionType()).toEqual('link'); 194 | }); 195 | 196 | it('should have getTitle api', function() { 197 | expect(this.link.getTitle).toBeDefined(); 198 | }); 199 | 200 | it('should have getActionName api', function() { 201 | expect(this.link.getActionName).toBeDefined(); 202 | }); 203 | 204 | it('should have getParams api', function() { 205 | expect(this.link.getParams).toBeDefined(); 206 | }); 207 | 208 | it('should have isTemplated api', function() { 209 | expect(this.link.isTemplated).toBeDefined(); 210 | }); 211 | 212 | it('should have setParams api', function() { 213 | expect(this.link.setParams).toBeDefined(); 214 | }); 215 | 216 | it('should have getDocUrl api', function() { 217 | expect(this.link.getDocUrl).toBeDefined(); 218 | }); 219 | 220 | it('should not have getFields api', function() { 221 | expect(this.link.getFields).not.toBeDefined(); 222 | }); 223 | 224 | it('should not have getPayload api', function() { 225 | expect(this.link.getPayload).not.toBeDefined(); 226 | }); 227 | }); 228 | 229 | describe('test link with no title', function() { 230 | beforeEach(function() { 231 | this.link = this.data.getFirstAction('noTitle'); 232 | }); 233 | 234 | it('getActionName', function() { 235 | expect(this.link.getActionName()).toBe('noTitle'); 236 | }); 237 | 238 | it('getActionUrl', function() { 239 | expect(this.link.getActionUrl()).toBe('/endpoint/no/title'); 240 | }); 241 | 242 | it('getRawActionUrl', function() { 243 | expect(this.link.getRawActionUrl()).toBe('no/title'); 244 | }); 245 | 246 | it('getTitle', function() { 247 | expect(this.link.getTitle()).toEqual(''); 248 | }); 249 | 250 | it('getParams', function() { 251 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 252 | }); 253 | 254 | it('isTemplated', function() { 255 | expect(this.link.isTemplated()).toEqual(false); 256 | }); 257 | }); 258 | 259 | describe('test link with no href', function() { 260 | beforeEach(function() { 261 | this.link = this.data.getFirstAction('noHref'); 262 | }); 263 | 264 | it('getActionUrl', function() { 265 | expect(this.link.getActionUrl()).toBe(''); 266 | }); 267 | 268 | it('getRawActionUrl', function() { 269 | expect(this.link.getRawActionUrl()).toBe(''); 270 | }); 271 | 272 | it('getTitle', function() { 273 | expect(this.link.getTitle()).toEqual('No href, just title'); 274 | }); 275 | 276 | it('getParams', function() { 277 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 278 | }); 279 | 280 | it('isTemplated', function() { 281 | expect(this.link.isTemplated()).toEqual(false); 282 | }); 283 | }); 284 | 285 | describe('test link with no params', function() { 286 | beforeEach(function() { 287 | this.link = this.data.getFirstAction('noParams'); 288 | }); 289 | 290 | it('getActionUrl', function() { 291 | expect(this.link.getActionUrl()).toBe('/endpoint/no/params/'); 292 | }); 293 | 294 | it('getRawActionUrl', function() { 295 | expect(this.link.getRawActionUrl()).toBe('no/params/'); 296 | }); 297 | 298 | it('getTitle', function() { 299 | expect(this.link.getTitle()).toEqual('Link with no params'); 300 | }); 301 | 302 | it('getParams', function() { 303 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 304 | }); 305 | 306 | it('isTemplated', function() { 307 | expect(this.link.isTemplated()).toEqual(false); 308 | }); 309 | }); 310 | 311 | describe('test link with no params, templated false', function() { 312 | beforeEach(function() { 313 | this.link = this.data.getFirstAction('noParamsNotTemplated'); 314 | }); 315 | 316 | it('getActionUrl', function() { 317 | expect(this.link.getActionUrl()).toBe('/endpoint/no/params/'); 318 | }); 319 | 320 | it('getRawActionUrl', function() { 321 | expect(this.link.getRawActionUrl()).toBe('no/params/'); 322 | }); 323 | 324 | it('getTitle', function() { 325 | expect(this.link.getTitle()).toEqual('Link with no params, templated false'); 326 | }); 327 | 328 | it('getParams', function() { 329 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 330 | }); 331 | 332 | it('isTemplated', function() { 333 | expect(this.link.isTemplated()).toEqual(false); 334 | }); 335 | }); 336 | 337 | describe('test link with no params, templated true', function() { 338 | beforeEach(function() { 339 | this.link = this.data.getFirstAction('noParamsTemplated'); 340 | }); 341 | 342 | it('getActionUrl', function() { 343 | expect(this.link.getActionUrl()).toBe('/endpoint/no/params/'); 344 | }); 345 | 346 | it('getRawActionUrl', function() { 347 | expect(this.link.getRawActionUrl()).toBe('no/params/'); 348 | }); 349 | 350 | it('getTitle', function() { 351 | expect(this.link.getTitle()).toEqual('Link with no params, templated true'); 352 | }); 353 | 354 | it('getParams', function() { 355 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 356 | }); 357 | 358 | it('isTemplated', function() { 359 | expect(this.link.isTemplated()).toEqual(true); 360 | }); 361 | }); 362 | 363 | describe('test link with one param in path', function() { 364 | beforeEach(function() { 365 | this.params = { 366 | path: 'path' 367 | }; 368 | 369 | this.link = this.data.getFirstAction('oneParamPath', this.params); 370 | }); 371 | 372 | it('getActionUrl', function() { 373 | expect(this.link.getActionUrl()).toBe('/endpoint/one/param/path/'); 374 | }); 375 | 376 | it('getRawActionUrl', function() { 377 | expect(this.link.getRawActionUrl()).toBe('one/param/{path}/'); 378 | }); 379 | 380 | it('getTitle', function() { 381 | expect(this.link.getTitle()).toEqual('One param in path'); 382 | }); 383 | 384 | it('getParams', function() { 385 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 386 | }); 387 | 388 | it('isTemplated', function() { 389 | expect(this.link.isTemplated()).toEqual(true); 390 | }); 391 | }); 392 | 393 | describe('test link with one param in query', function() { 394 | beforeEach(function() { 395 | this.params = { 396 | query: 'query' 397 | }; 398 | 399 | this.link = this.data.getFirstAction('oneParamQuery', this.params); 400 | }); 401 | 402 | it('getActionUrl', function() { 403 | expect(this.link.getActionUrl()).toBe('/endpoint/one/param/?query=query'); 404 | }); 405 | 406 | it('getRawActionUrl', function() { 407 | expect(this.link.getRawActionUrl()).toBe('one/param/{?query}'); 408 | }); 409 | 410 | it('getTitle', function() { 411 | expect(this.link.getTitle()).toEqual('One param in query'); 412 | }); 413 | 414 | it('getParams', function() { 415 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 416 | }); 417 | 418 | it('isTemplated', function() { 419 | expect(this.link.isTemplated()).toEqual(true); 420 | }); 421 | 422 | describe('when query param is a boolean false', function() { 423 | beforeEach(function() { 424 | this.params = { 425 | query: false 426 | }; 427 | 428 | this.link = this.data.getFirstAction('oneParamQuery', this.params); 429 | }); 430 | 431 | it('getActionUrl', function() { 432 | expect(this.link.getActionUrl()).toBe('/endpoint/one/param/?query=false'); 433 | }); 434 | 435 | it('getRawActionUrl', function() { 436 | expect(this.link.getRawActionUrl()).toBe('one/param/{?query}'); 437 | }); 438 | 439 | it('getTitle', function() { 440 | expect(this.link.getTitle()).toEqual('One param in query'); 441 | }); 442 | 443 | it('getParams', function() { 444 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 445 | }); 446 | 447 | it('isTemplated', function() { 448 | expect(this.link.isTemplated()).toEqual(true); 449 | }); 450 | }); 451 | 452 | describe('when query param is undefined', function() { 453 | beforeEach(function() { 454 | this.params = { 455 | query: undefined 456 | }; 457 | 458 | this.link = this.data.getFirstAction('oneParamQuery', this.params); 459 | }); 460 | 461 | it('getActionUrl', function() { 462 | expect(this.link.getActionUrl()).toBe('/endpoint/one/param/'); 463 | }); 464 | 465 | it('getRawActionUrl', function() { 466 | expect(this.link.getRawActionUrl()).toBe('one/param/{?query}'); 467 | }); 468 | 469 | it('getTitle', function() { 470 | expect(this.link.getTitle()).toEqual('One param in query'); 471 | }); 472 | 473 | it('getParams', function() { 474 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 475 | }); 476 | 477 | it('isTemplated', function() { 478 | expect(this.link.isTemplated()).toEqual(true); 479 | }); 480 | }); 481 | 482 | describe('when query param is null', function() { 483 | beforeEach(function() { 484 | this.params = { 485 | query: null 486 | }; 487 | 488 | this.link = this.data.getFirstAction('oneParamQuery', this.params); 489 | }); 490 | 491 | it('getActionUrl', function() { 492 | expect(this.link.getActionUrl()).toBe('/endpoint/one/param/'); 493 | }); 494 | 495 | it('getRawActionUrl', function() { 496 | expect(this.link.getRawActionUrl()).toBe('one/param/{?query}'); 497 | }); 498 | 499 | it('getTitle', function() { 500 | expect(this.link.getTitle()).toEqual('One param in query'); 501 | }); 502 | 503 | it('getParams', function() { 504 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 505 | }); 506 | 507 | it('isTemplated', function() { 508 | expect(this.link.isTemplated()).toEqual(true); 509 | }); 510 | }); 511 | }); 512 | 513 | describe('test link with two params, path and query', function() { 514 | beforeEach(function() { 515 | this.params = { 516 | path: 'path', 517 | query: 'query' 518 | }; 519 | 520 | this.link = this.data.getFirstAction('twoParamsMixed', this.params); 521 | }); 522 | 523 | it('getActionUrl', function() { 524 | expect(this.link.getActionUrl()).toBe('/endpoint/two/params/path/?query=query'); 525 | }); 526 | 527 | it('getRawActionUrl', function() { 528 | expect(this.link.getRawActionUrl()).toBe('two/params/{path}/{?query}'); 529 | }); 530 | 531 | it('getTitle', function() { 532 | expect(this.link.getTitle()).toEqual('Two params, one in path, one in query'); 533 | }); 534 | 535 | it('getParams', function() { 536 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 537 | }); 538 | 539 | it('isTemplated', function() { 540 | expect(this.link.isTemplated()).toEqual(true); 541 | }); 542 | }); 543 | 544 | describe('test link with two path params', function() { 545 | beforeEach(function() { 546 | this.params = { 547 | path1: 'path1', 548 | path2: 'path2' 549 | }; 550 | 551 | this.link = this.data.getFirstAction('twoParamsPath', this.params); 552 | }); 553 | 554 | it('getActionUrl', function() { 555 | expect(this.link.getActionUrl()).toBe('/endpoint/two/params/path1/path2/'); 556 | }); 557 | 558 | it('getRawActionUrl', function() { 559 | expect(this.link.getRawActionUrl()).toBe('two/params/{path1}/{path2}/'); 560 | }); 561 | 562 | it('getTitle', function() { 563 | expect(this.link.getTitle()).toEqual('Two params in path'); 564 | }); 565 | 566 | it('getParams', function() { 567 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 568 | }); 569 | 570 | it('isTemplated', function() { 571 | expect(this.link.isTemplated()).toEqual(true); 572 | }); 573 | }); 574 | 575 | describe('test link with two query params and hard coded param', function() { 576 | beforeEach(function() { 577 | this.params = { 578 | query1: 'query1', 579 | query2: 'query2' 580 | }; 581 | 582 | this.link = this.data.getFirstAction('twoParamsQueryAndExisting', this.params); 583 | }); 584 | 585 | it('getActionUrl', function() { 586 | expect(this.link.getActionUrl()).toBe('/endpoint/two/params/?test=1&query1=query1&query2=query2'); 587 | }); 588 | 589 | it('getRawActionUrl', function() { 590 | expect(this.link.getRawActionUrl()).toBe('two/params/?test=1{&query1,query2}'); 591 | }); 592 | 593 | it('getTitle', function() { 594 | expect(this.link.getTitle()).toEqual('Two params in query with hard coded param'); 595 | }); 596 | 597 | it('getParams', function() { 598 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 599 | }); 600 | 601 | it('isTemplated', function() { 602 | expect(this.link.isTemplated()).toEqual(true); 603 | }); 604 | }); 605 | 606 | describe('test link with two wrongly formatted query params and hard coded param', function() { 607 | beforeEach(function() { 608 | this.params = { 609 | query1: 'query1', 610 | query2: 'query2' 611 | }; 612 | 613 | this.link = this.data.getFirstAction('twoParamsQueryInvalidAndExisting', this.params); 614 | }); 615 | 616 | it('getActionUrl', function() { 617 | expect(this.link.getActionUrl()).toBe('/endpoint/two/params/?test=1&query1=query1&query2=query2'); 618 | }); 619 | 620 | it('getRawActionUrl', function() { 621 | expect(this.link.getRawActionUrl()).toBe('two/params/?test=1{?query1,query2}'); 622 | }); 623 | 624 | it('getTitle', function() { 625 | expect(this.link.getTitle()).toEqual('Two wrongly formatted params in query with hard coded param'); 626 | }); 627 | 628 | it('getParams', function() { 629 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 630 | }); 631 | 632 | it('isTemplated', function() { 633 | expect(this.link.isTemplated()).toEqual(true); 634 | }); 635 | }); 636 | 637 | describe('test link with root relative url', function() { 638 | beforeEach(function() { 639 | this.link = this.data.getFirstAction('rootRelativeUrl', this.params); 640 | }); 641 | 642 | it('getActionUrl', function() { 643 | expect(this.link.getActionUrl()).toBe('/root/relative/'); 644 | }); 645 | 646 | it('getRawActionUrl', function() { 647 | expect(this.link.getRawActionUrl()).toBe('/root/relative/'); 648 | }); 649 | 650 | it('getTitle', function() { 651 | expect(this.link.getTitle()).toEqual('Root relative url'); 652 | }); 653 | 654 | it('getParams', function() { 655 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 656 | }); 657 | 658 | it('isTemplated', function() { 659 | expect(this.link.isTemplated()).toEqual(false); 660 | }); 661 | }); 662 | 663 | describe('test link with path relative url', function() { 664 | beforeEach(function() { 665 | this.link = this.data.getFirstAction('pathRelativeUrl', this.params); 666 | }); 667 | 668 | it('getActionUrl', function() { 669 | expect(this.link.getActionUrl()).toBe('/root/relative/'); 670 | }); 671 | 672 | it('getRawActionUrl', function() { 673 | expect(this.link.getRawActionUrl()).toBe('../root/relative/'); 674 | }); 675 | 676 | it('getTitle', function() { 677 | expect(this.link.getTitle()).toEqual('Path relative url'); 678 | }); 679 | 680 | it('getParams', function() { 681 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 682 | }); 683 | 684 | it('isTemplated', function() { 685 | expect(this.link.isTemplated()).toEqual(false); 686 | }); 687 | }); 688 | 689 | describe('test link with absolute url', function() { 690 | beforeEach(function() { 691 | this.link = this.data.getFirstAction('absoluteUrl', this.params); 692 | }); 693 | 694 | it('getActionUrl', function() { 695 | expect(this.link.getActionUrl()).toBe('http://www.example.com'); 696 | }); 697 | 698 | it('getRawActionUrl', function() { 699 | expect(this.link.getRawActionUrl()).toBe('http://www.example.com'); 700 | }); 701 | 702 | it('getTitle', function() { 703 | expect(this.link.getTitle()).toEqual('Absolute url'); 704 | }); 705 | 706 | it('getParams', function() { 707 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 708 | }); 709 | 710 | it('isTemplated', function() { 711 | expect(this.link.isTemplated()).toEqual(false); 712 | }); 713 | }); 714 | 715 | describe('test link which has array of links', function() { 716 | beforeEach(function() { 717 | this.link = this.data.getAction('linkList', this.params); 718 | }); 719 | 720 | it('should return an array of links', function() { 721 | expect(this.link).toEqual(jasmine.any(Array)); 722 | }); 723 | 724 | it('should have 2 links', function() { 725 | expect(this.link.length).toEqual(2); 726 | }); 727 | 728 | it('should have getFirstAction equal to first link', function() { 729 | expect(this.data.getFirstAction('linkList', this.params)).toBe(this.link[0]); 730 | }); 731 | 732 | describe('test first link', function() { 733 | it('getRawActionUrl', function() { 734 | expect(this.link[0].getRawActionUrl()).toBe('first/link'); 735 | }); 736 | 737 | it('getTitle', function() { 738 | expect(this.link[0].getTitle()).toEqual('first link'); 739 | }); 740 | }); 741 | 742 | describe('test second link', function() { 743 | it('getRawActionUrl', function() { 744 | expect(this.link[1].getRawActionUrl()).toBe('second/link'); 745 | }); 746 | 747 | it('getTitle', function() { 748 | expect(this.link[1].getTitle()).toEqual('second link'); 749 | }); 750 | }); 751 | }); 752 | 753 | describe('test calling link twice with different params', function() { 754 | beforeEach(function() { 755 | this.params = { 756 | query1: 'query1', 757 | query2: 'query2' 758 | }; 759 | 760 | this.link = this.data.getFirstAction('twoParamsQuery', this.params); 761 | }); 762 | 763 | it('getActionUrl', function() { 764 | expect(this.link.getActionUrl()).toBe('/endpoint/two/params/?query1=query1&query2=query2'); 765 | }); 766 | 767 | it('getParams', function() { 768 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 769 | }); 770 | 771 | describe('second call', function() { 772 | beforeEach(function() { 773 | this.params = { 774 | query1: 'query2', 775 | query2: 'query1' 776 | }; 777 | 778 | this.link = this.data.getFirstAction('twoParamsQuery', this.params); 779 | }); 780 | 781 | it('getActionUrl', function() { 782 | expect(this.link.getActionUrl()).toBe('/endpoint/two/params/?query1=query2&query2=query1'); 783 | }); 784 | 785 | it('getParams', function() { 786 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 787 | }); 788 | }); 789 | }); 790 | 791 | describe('test link with curie url', function() { 792 | beforeEach(function() { 793 | this.link = this.data.getFirstAction('test:curie-test', this.params); 794 | }); 795 | 796 | it('getActionUrl', function() { 797 | expect(this.link.getActionUrl()).toBe('http://www.example.com/curie/test/'); 798 | }); 799 | 800 | it('getRawActionUrl', function() { 801 | expect(this.link.getRawActionUrl()).toBe('/curie/test/'); 802 | }); 803 | 804 | it('getDocUrl', function() { 805 | expect(this.link.getDocUrl()).toBe('http://www.example.com/docs/curie-test'); 806 | }); 807 | 808 | it('getTitle', function() { 809 | expect(this.link.getTitle()).toEqual('Curie test link'); 810 | }); 811 | 812 | it('getParams', function() { 813 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 814 | }); 815 | 816 | it('isTemplated', function() { 817 | expect(this.link.isTemplated()).toEqual(false); 818 | }); 819 | }); 820 | }); 821 | 822 | describe('handling of forms', function() { 823 | beforeEach(function() { 824 | this.params = {}; 825 | this.fields = {}; 826 | this.payload = ''; 827 | }); 828 | 829 | describe('form action api', function() { 830 | beforeEach(function() { 831 | this.link = this.data.getFirstAction('formNoMethod'); 832 | }); 833 | 834 | it('should have fetch api', function() { 835 | expect(this.link.fetch).toBeDefined(); 836 | }); 837 | 838 | it('should have getActionUrl api', function() { 839 | expect(this.link.getActionUrl).toBeDefined(); 840 | }); 841 | 842 | it('should have getRawActionUrl api', function() { 843 | expect(this.link.getRawActionUrl).toBeDefined(); 844 | }); 845 | 846 | it('should have getActionType api', function() { 847 | expect(this.link.getActionType).toBeDefined(); 848 | }); 849 | 850 | it('should return action type of "form"', function() { 851 | expect(this.link.getActionType()).toEqual('form'); 852 | }); 853 | 854 | it('should have getTitle api', function() { 855 | expect(this.link.getTitle).toBeDefined(); 856 | }); 857 | 858 | it('should have getParams api', function() { 859 | expect(this.link.getParams).toBeDefined(); 860 | }); 861 | 862 | it('should have isTemplated api', function() { 863 | expect(this.link.isTemplated).toBeDefined(); 864 | }); 865 | 866 | it('should have setParams api', function() { 867 | expect(this.link.setParams).toBeDefined(); 868 | }); 869 | 870 | it('should have getFields api', function() { 871 | expect(this.link.getFields).toBeDefined(); 872 | }); 873 | 874 | it('should have getPayload api', function() { 875 | expect(this.link.getPayload).toBeDefined(); 876 | }); 877 | }); 878 | 879 | describe('test form with no method', function() { 880 | beforeEach(function() { 881 | this.link = this.data.getFirstAction('formNoMethod'); 882 | }); 883 | 884 | it('getActionUrl', function() { 885 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 886 | }); 887 | 888 | it('getRawActionUrl', function() { 889 | expect(this.link.getRawActionUrl()).toBe('form/'); 890 | }); 891 | 892 | it('getParams', function() { 893 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 894 | }); 895 | 896 | it('isTemplated', function() { 897 | expect(this.link.isTemplated()).toEqual(false); 898 | }); 899 | 900 | it('getMethod', function() { 901 | expect(this.link.getMethod()).toEqual('POST'); 902 | }); 903 | 904 | it('getFields', function() { 905 | expect(this.link.getFields()).toEqual(this.fields); 906 | }); 907 | 908 | it('getPayload', function() { 909 | expect(this.link.getPayload()).toEqual(this.payload); 910 | }); 911 | }); 912 | 913 | describe('test form with no action', function() { 914 | beforeEach(function() { 915 | this.link = this.data.getFirstAction('formNoAction'); 916 | }); 917 | 918 | it('getActionUrl', function() { 919 | expect(this.link.getActionUrl()).toBe(''); 920 | }); 921 | 922 | it('getRawActionUrl', function() { 923 | expect(this.link.getRawActionUrl()).toBe(''); 924 | }); 925 | 926 | it('getActionType', function() { 927 | expect(this.link.getActionType()).toEqual('form'); 928 | }); 929 | 930 | it('getParams', function() { 931 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 932 | }); 933 | 934 | it('isTemplated', function() { 935 | expect(this.link.isTemplated()).toEqual(false); 936 | }); 937 | 938 | it('getMethod', function() { 939 | expect(this.link.getMethod()).toEqual('POST'); 940 | }); 941 | 942 | it('getFields', function() { 943 | expect(this.link.getFields()).toEqual(this.fields); 944 | }); 945 | 946 | it('getPayload', function() { 947 | expect(this.link.getPayload()).toEqual(this.payload); 948 | }); 949 | }); 950 | 951 | describe('forms with method GET', function() { 952 | describe('test get form with no fields', function() { 953 | beforeEach(function() { 954 | this.link = this.data.getFirstAction('formGetNoFields'); 955 | }); 956 | 957 | it('getActionUrl', function() { 958 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 959 | }); 960 | 961 | it('getRawActionUrl', function() { 962 | expect(this.link.getRawActionUrl()).toBe('form/'); 963 | }); 964 | 965 | it('getActionType', function() { 966 | expect(this.link.getActionType()).toEqual('form'); 967 | }); 968 | 969 | it('getParams', function() { 970 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 971 | }); 972 | 973 | it('isTemplated', function() { 974 | expect(this.link.isTemplated()).toEqual(false); 975 | }); 976 | 977 | it('getMethod', function() { 978 | expect(this.link.getMethod()).toEqual('GET'); 979 | }); 980 | 981 | it('getFields', function() { 982 | expect(this.link.getFields()).toEqual(this.fields); 983 | }); 984 | 985 | it('getPayload', function() { 986 | expect(this.link.getPayload()).toEqual(this.payload); 987 | }); 988 | }); 989 | 990 | describe('test get form with empty fields', function() { 991 | beforeEach(function() { 992 | this.link = this.data.getFirstAction('formGetEmptyFields'); 993 | }); 994 | 995 | it('getActionUrl', function() { 996 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 997 | }); 998 | 999 | it('getRawActionUrl', function() { 1000 | expect(this.link.getRawActionUrl()).toBe('form/'); 1001 | }); 1002 | 1003 | it('getParams', function() { 1004 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 1005 | }); 1006 | 1007 | it('isTemplated', function() { 1008 | expect(this.link.isTemplated()).toEqual(false); 1009 | }); 1010 | 1011 | it('getMethod', function() { 1012 | expect(this.link.getMethod()).toEqual('GET'); 1013 | }); 1014 | 1015 | it('getFields', function() { 1016 | expect(this.link.getFields()).toEqual(this.fields); 1017 | }); 1018 | 1019 | it('getPayload', function() { 1020 | expect(this.link.getPayload()).toEqual(this.payload); 1021 | }); 1022 | }); 1023 | 1024 | describe('test get form with one field', function() { 1025 | beforeEach(function() { 1026 | this.params = { 1027 | param: 1 1028 | }; 1029 | this.fields = { 1030 | param: {} 1031 | }; 1032 | this.link = this.data.getFirstAction('formGetOneField', this.params); 1033 | }); 1034 | 1035 | it('getActionUrl', function() { 1036 | expect(this.link.getActionUrl()).toBe('/endpoint/form/?param=1'); 1037 | }); 1038 | 1039 | it('getRawActionUrl', function() { 1040 | expect(this.link.getRawActionUrl()).toBe('form/'); 1041 | }); 1042 | 1043 | it('getParams', function() { 1044 | expect(this.link.getParams()).toEqual(jasmine.any(Array)); 1045 | }); 1046 | 1047 | it('isTemplated', function() { 1048 | expect(this.link.isTemplated()).toEqual(false); 1049 | }); 1050 | 1051 | it('getMethod', function() { 1052 | expect(this.link.getMethod()).toEqual('GET'); 1053 | }); 1054 | 1055 | it('getFields', function() { 1056 | expect(this.link.getFields()).toEqual(this.fields); 1057 | }); 1058 | 1059 | it('getPayload', function() { 1060 | expect(this.link.getPayload()).toEqual(this.payload); 1061 | }); 1062 | }); 1063 | 1064 | describe('test get form with one field, empty payload', function() { 1065 | beforeEach(function() { 1066 | this.fields = { 1067 | param: {} 1068 | }; 1069 | 1070 | this.link = this.data.getFirstAction('formGetOneField'); 1071 | }); 1072 | 1073 | it('getActionUrl', function() { 1074 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 1075 | }); 1076 | 1077 | it('getRawActionUrl', function() { 1078 | expect(this.link.getRawActionUrl()).toBe('form/'); 1079 | }); 1080 | 1081 | it('getParams', function() { 1082 | expect(this.link.getParams()).toEqual(jasmine.any(Array)); 1083 | }); 1084 | 1085 | it('isTemplated', function() { 1086 | expect(this.link.isTemplated()).toEqual(false); 1087 | }); 1088 | 1089 | it('getMethod', function() { 1090 | expect(this.link.getMethod()).toEqual('GET'); 1091 | }); 1092 | 1093 | it('getFields', function() { 1094 | expect(this.link.getFields()).toEqual(this.fields); 1095 | }); 1096 | 1097 | it('getPayload', function() { 1098 | expect(this.link.getPayload()).toEqual(this.payload); 1099 | }); 1100 | }); 1101 | }); 1102 | 1103 | describe('forms with method POST', function() { 1104 | describe('test post form with no fields', function() { 1105 | beforeEach(function() { 1106 | this.link = this.data.getFirstAction('formPostNoFields'); 1107 | }); 1108 | 1109 | it('getActionUrl', function() { 1110 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 1111 | }); 1112 | 1113 | it('getRawActionUrl', function() { 1114 | expect(this.link.getRawActionUrl()).toBe('form/'); 1115 | }); 1116 | 1117 | it('getActionType', function() { 1118 | expect(this.link.getActionType()).toEqual('form'); 1119 | }); 1120 | 1121 | it('getParams', function() { 1122 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 1123 | }); 1124 | 1125 | it('isTemplated', function() { 1126 | expect(this.link.isTemplated()).toEqual(false); 1127 | }); 1128 | 1129 | it('getMethod', function() { 1130 | expect(this.link.getMethod()).toEqual('POST'); 1131 | }); 1132 | 1133 | it('getFields', function() { 1134 | expect(this.link.getFields()).toEqual(this.fields); 1135 | }); 1136 | 1137 | it('getPayload', function() { 1138 | expect(this.link.getPayload()).toEqual(this.payload); 1139 | }); 1140 | }); 1141 | 1142 | describe('test get form with empty fields', function() { 1143 | beforeEach(function() { 1144 | this.link = this.data.getFirstAction('formPostEmptyFields'); 1145 | }); 1146 | 1147 | it('getActionUrl', function() { 1148 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 1149 | }); 1150 | 1151 | it('getRawActionUrl', function() { 1152 | expect(this.link.getRawActionUrl()).toBe('form/'); 1153 | }); 1154 | 1155 | it('getParams', function() { 1156 | expect(this.link.getParams()).toEqual(Object.keys(this.params)); 1157 | }); 1158 | 1159 | it('isTemplated', function() { 1160 | expect(this.link.isTemplated()).toEqual(false); 1161 | }); 1162 | 1163 | it('getMethod', function() { 1164 | expect(this.link.getMethod()).toEqual('POST'); 1165 | }); 1166 | 1167 | it('getFields', function() { 1168 | expect(this.link.getFields()).toEqual(this.fields); 1169 | }); 1170 | 1171 | it('getPayload', function() { 1172 | expect(this.link.getPayload()).toEqual(this.payload); 1173 | }); 1174 | }); 1175 | 1176 | describe('test post form with empty fields sending params', function() { 1177 | beforeEach(function() { 1178 | this.params = { 1179 | param1: 1, 1180 | param2: 2 1181 | }; 1182 | 1183 | this.link = this.data.getFirstAction('formPostNoFields', this.params); 1184 | }); 1185 | 1186 | it('getActionUrl', function() { 1187 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 1188 | }); 1189 | 1190 | it('getRawActionUrl', function() { 1191 | expect(this.link.getRawActionUrl()).toBe('form/'); 1192 | }); 1193 | 1194 | it('getParams', function() { 1195 | expect(this.link.getParams()).toEqual(jasmine.any(Array)); 1196 | }); 1197 | 1198 | it('isTemplated', function() { 1199 | expect(this.link.isTemplated()).toEqual(false); 1200 | }); 1201 | 1202 | it('getMethod', function() { 1203 | expect(this.link.getMethod()).toEqual('POST'); 1204 | }); 1205 | 1206 | it('getFields', function() { 1207 | expect(this.link.getFields()).toEqual(this.fields); 1208 | }); 1209 | 1210 | it('getPayload', function() { 1211 | expect(this.link.getPayload()).toEqual(JSON.stringify(this.params)); 1212 | }); 1213 | }); 1214 | 1215 | describe('test post form with two field', function() { 1216 | beforeEach(function() { 1217 | this.params = { 1218 | param1: 1, 1219 | param2: 2 1220 | }; 1221 | this.fields = { 1222 | param1: {}, 1223 | param2: {} 1224 | }; 1225 | 1226 | this.link = this.data.getFirstAction('formPostTwoFields', this.params); 1227 | }); 1228 | 1229 | it('getActionUrl', function() { 1230 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 1231 | }); 1232 | 1233 | it('getRawActionUrl', function() { 1234 | expect(this.link.getRawActionUrl()).toBe('form/'); 1235 | }); 1236 | 1237 | it('getParams', function() { 1238 | expect(this.link.getParams()).toEqual(jasmine.any(Array)); 1239 | }); 1240 | 1241 | it('isTemplated', function() { 1242 | expect(this.link.isTemplated()).toEqual(false); 1243 | }); 1244 | 1245 | it('getMethod', function() { 1246 | expect(this.link.getMethod()).toEqual('POST'); 1247 | }); 1248 | 1249 | it('getFields', function() { 1250 | expect(this.link.getFields()).toEqual(this.fields); 1251 | }); 1252 | 1253 | it('getPayload', function() { 1254 | expect(this.link.getPayload()).toEqual(this.params); 1255 | }); 1256 | }); 1257 | 1258 | describe('test post form with two field, default value', function() { 1259 | beforeEach(function() { 1260 | this.params = { 1261 | param2: 2 1262 | }; 1263 | this.payload = { 1264 | param1: 'test', 1265 | param2: 2 1266 | }; 1267 | this.fields = { 1268 | param1: { 1269 | 'default': 'test' 1270 | }, 1271 | param2: {} 1272 | }; 1273 | 1274 | this.link = this.data.getFirstAction('formPostTwoFieldsDefaultValue', this.params); 1275 | }); 1276 | 1277 | it('getActionUrl', function() { 1278 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 1279 | }); 1280 | 1281 | it('getRawActionUrl', function() { 1282 | expect(this.link.getRawActionUrl()).toBe('form/'); 1283 | }); 1284 | 1285 | it('getParams', function() { 1286 | expect(this.link.getParams()).toEqual(jasmine.any(Array)); 1287 | }); 1288 | 1289 | it('isTemplated', function() { 1290 | expect(this.link.isTemplated()).toEqual(false); 1291 | }); 1292 | 1293 | it('getMethod', function() { 1294 | expect(this.link.getMethod()).toEqual('POST'); 1295 | }); 1296 | 1297 | it('getFields', function() { 1298 | expect(this.link.getFields()).toEqual(this.fields); 1299 | }); 1300 | 1301 | it('getPayload', function() { 1302 | expect(this.link.getPayload()).toEqual(this.payload); 1303 | }); 1304 | }); 1305 | 1306 | describe('test form with templated action', function() { 1307 | beforeEach(function() { 1308 | this.params = ['param']; 1309 | this.payload = { 1310 | param1: 1, 1311 | param2: 2 1312 | }; 1313 | this.fields = { 1314 | param1: {}, 1315 | param2: {} 1316 | }; 1317 | 1318 | this.link = this.data.getFirstAction('formPostWithTemplatedAction', { 1319 | param: 'param', 1320 | param1: 1, 1321 | param2: 2 1322 | }); 1323 | }); 1324 | 1325 | it('getActionUrl', function() { 1326 | expect(this.link.getActionUrl()).toBe('/endpoint/form/param/test'); 1327 | }); 1328 | 1329 | it('getRawActionUrl', function() { 1330 | expect(this.link.getRawActionUrl()).toBe('form/{param}/test'); 1331 | }); 1332 | 1333 | it('getParams', function() { 1334 | expect(this.link.getParams()).toEqual(jasmine.any(Array)); 1335 | }); 1336 | 1337 | it('isTemplated', function() { 1338 | expect(this.link.isTemplated()).toEqual(true); 1339 | }); 1340 | 1341 | it('getMethod', function() { 1342 | expect(this.link.getMethod()).toEqual('POST'); 1343 | }); 1344 | 1345 | it('getFields', function() { 1346 | expect(this.link.getFields()).toEqual(this.fields); 1347 | }); 1348 | 1349 | it('getPayload', function() { 1350 | expect(this.link.getPayload()).toEqual(this.payload); 1351 | }); 1352 | }); 1353 | 1354 | describe('test form with one field and wrong param', function() { 1355 | beforeEach(function() { 1356 | this.params = { 1357 | wrong: 1 1358 | }; 1359 | this.fields = { 1360 | param: {} 1361 | }; 1362 | this.link = this.data.getFirstAction('formPostOneField', this.params); 1363 | }); 1364 | 1365 | it('getActionUrl', function() { 1366 | expect(this.link.getActionUrl()).toBe('/endpoint/form/'); 1367 | }); 1368 | 1369 | it('getRawActionUrl', function() { 1370 | expect(this.link.getRawActionUrl()).toBe('form/'); 1371 | }); 1372 | 1373 | it('getParams', function() { 1374 | expect(this.link.getParams()).toEqual(jasmine.any(Array)); 1375 | }); 1376 | 1377 | it('isTemplated', function() { 1378 | expect(this.link.isTemplated()).toEqual(false); 1379 | }); 1380 | 1381 | it('getMethod', function() { 1382 | expect(this.link.getMethod()).toEqual('POST'); 1383 | }); 1384 | 1385 | it('getFields', function() { 1386 | expect(this.link.getFields()).toEqual(this.fields); 1387 | }); 1388 | 1389 | it('getPayload', function() { 1390 | expect(this.link.getPayload()).toEqual({}); 1391 | }); 1392 | }); 1393 | }); 1394 | }); 1395 | }); 1396 | 1397 | describe('with empty homepage response', function() { 1398 | beforeEach(function(done) { 1399 | window.fetch.and.returnValue(this.homepageData({})); 1400 | this.testHyperGard = new HyperGard(testEndpoint, testOptions); 1401 | this.testHyperGard.fetch().then(this.onSuccess, this.onError).then(done, done); 1402 | }); 1403 | 1404 | describe('getAction', function() { 1405 | it('should have 0 actions', function() { 1406 | expect(this.data.getAction('noLink').length).toEqual(0); 1407 | }); 1408 | }); 1409 | 1410 | describe('getFirstAction', function() { 1411 | it('should return an action', function() { 1412 | expect(this.data.getFirstAction('noLink')).toEqual(jasmine.any(Object)); 1413 | }); 1414 | }); 1415 | 1416 | describe('listActions', function() { 1417 | it('should have 0 links', function() { 1418 | expect(Object.keys(this.data.listActions().links).length).toEqual(0); 1419 | }); 1420 | 1421 | it('should have 0 forms', function() { 1422 | expect(Object.keys(this.data.listActions().forms).length).toEqual(0); 1423 | }); 1424 | }); 1425 | }); 1426 | }); 1427 | }); 1428 | --------------------------------------------------------------------------------