├── .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 | [](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 |
--------------------------------------------------------------------------------