├── .gitignore ├── .jsbeautifyrc ├── .jshintrc ├── LICENSE ├── README.md ├── bower-README.md ├── bower.json ├── circle.yml ├── example ├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── .yo-rc.json ├── api-server │ └── birds │ │ └── index.html ├── bower.json ├── demo.config.json ├── gulp │ ├── build.js │ ├── e2e-tests.js │ ├── inject.js │ ├── ngdocs.js │ ├── proxy.js │ ├── scripts.js │ ├── server.js │ ├── styles.js │ ├── unit-tests.js │ └── watch.js ├── gulpfile.js ├── karma.conf.js ├── package.json └── src │ ├── app │ ├── api.service.js │ ├── app.js │ ├── app.scss │ ├── birds.service.js │ ├── main.controller.js │ ├── main.controller.spec.js │ ├── main.html │ ├── mock-data.js │ └── nav.controller.js │ ├── header.html │ ├── images │ └── rangle.jpg │ └── index.html ├── gulpfile.js ├── karma.conf.js ├── package.json └── src └── hackstack ├── hackstack-mock-service.js ├── hackstack-mock-service.test.js ├── hackstack-mockfh-service.js ├── hackstack-mockfh-service.test.js ├── hackstack-utils-service.js ├── hackstack-utils-service.test.js ├── hackstack-wrapper-service.js ├── hackstack-wrapper-service.test.js └── hackstack.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | 4 | # Runtime data 5 | pids 6 | *.pid 7 | *.seed 8 | 9 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 10 | .grunt 11 | 12 | # Sass cache folder 13 | .sass-cache 14 | 15 | # Users Environment Variables 16 | .lock-wscript 17 | .idea/ 18 | *~ 19 | \#*# 20 | .#* 21 | *.keystore 22 | *.sw* 23 | .DS_Store 24 | ._* 25 | Thumbs.db 26 | .cache 27 | *.sublime-project 28 | *.sublime-workspace 29 | *swp 30 | *swo 31 | *swn 32 | build 33 | dist 34 | temp 35 | node_modules 36 | bower_components 37 | coverage 38 | .settings 39 | bower-angular-hackstack 40 | 41 | keys.md 42 | .env 43 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "brace_style": "collapse", 3 | "break_chained_methods": false, 4 | "e4x": false, 5 | "eval_code": false, 6 | "indent_char": " ", 7 | "indent_level": 0, 8 | "indent_size": 2, 9 | "indent_with_tabs": false, 10 | "jslint_happy": true, 11 | "keep_array_indentation": false, 12 | "keep_function_indentation": false, 13 | "max_preserve_newlines": 3, 14 | "preserve_newlines": true, 15 | "space_before_conditional": true, 16 | "space_in_paren": false, 17 | "unescape_strings": false, 18 | "wrap_line_length": 80 19 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "expr": true, 22 | "globals": { 23 | "angular": true, 24 | "alert": true, 25 | "_": true, 26 | "R": true, 27 | "Q": true, 28 | "inject": true, 29 | "describe": true, 30 | "expect": true, 31 | "it": true, 32 | "xit": true, 33 | "beforeEach": true, 34 | "afterEach": true, 35 | "testUtils": true, 36 | "sinon": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rangle.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HackStack [![Build status](https://circleci.com/gh/rangle/hackstack.svg?style=svg&circle-token=4e9f2c3295779e2494abbf8fc84a8aa4f4da0c3f)](https://circleci.com/gh/rangle/hackstack) 2 | 3 | ## What is HackStack? 4 | 5 | **HackStack** is an Angular module that helps you work with backend APIs that 6 | are incomplete or altogether missing. 7 | 8 | In our experience working on numerous Angular projects, broken or delayed 9 | backend APIs are quite common and can present a major risk if the front end 10 | team cannot find a good way to react in this situation. After trying a number of 11 | approaches, we've found client-side mocking to the be most effective route. 12 | 13 | We've done it a few different ways on a number of projects and presented some of 14 | our observations in a 15 | [talk](http://yto.io/slides/Building-an-AngularJS-Hack-Stack-2015.pdf) at ngConf 16 | in 2015. We've got a lot of positive response, but also a question: "Why don't 17 | you make this a reusable library?" So we did. Enter HackStack.js, the library. 18 | 19 | ## Installing with Bower 20 | 21 | The easiest way to install HackStack is by using bower: 22 | 23 | ```bash 24 | bower install --save angular-hackstack 25 | ``` 26 | 27 | Alternatively, build HackStack using this repo. 28 | 29 | ## Using HackStack 30 | 31 | To create a new HackStack endpoint, call: 32 | 33 | ```js 34 | var mockEndpoint = hackstack.mock(data); 35 | ``` 36 | 37 | This creates a fully mocked endpoint which won't make any calls to the backend 38 | at all. Here, `data` can be either an array of items or a path to a json file. 39 | 40 | Alternatively, you can "wrap" an existing endpoint: HackStack will then get the 41 | data from the server and fill in the missing properties of each item based on a 42 | provided template object: 43 | 44 | ```js 45 | var wrappedEndpoint = hackstack.wrap(endpoint, templateObject); 46 | ``` 47 | 48 | ### Example 49 | 50 | ```js 51 | var mockEndpoint = hackstack.mock([ 52 | { 53 | 'name': 'Alice', 54 | 'id': 1 55 | }, 56 | { 57 | 'name': 'Bob', 58 | 'id': 2 59 | } 60 | ]); 61 | 62 | mockEndpoint.get(1) 63 | .then(function (response) { 64 | console.log(response.data); // logs {'name': 'Alice', 'id': 1} 65 | }); 66 | ``` 67 | 68 | A full example is available under the [example directory](./example) 69 | 70 | ### Controlling HackStack from the Browser Console 71 | 72 | While you're working with HackStack, you may want to force a particular error to 73 | happen on the next call to the endpoint. You can do this by exposing the mock 74 | endpoint object to the console and then calling `.forceError()` 75 | on it. Subsequent requests will then return that error. Call `.forceError(null)` 76 | to turn this off. 77 | 78 | ### Random Errors 79 | 80 | HackStack defaults to generating random errors in response to endpoint requests. 81 | You can turn this off using `.disableError(true)` on your mock endpoint object. 82 | You can turn it back on by calling the same method with `false`. 83 | 84 | ### Artificial Delay 85 | 86 | HackStack introduces randomized artificial delay on all requests. This helps you 87 | detect cases where your code makes optimistic assumptions about timing. 88 | 89 | ## Assumptions 90 | 91 | This library currently makes a couple of assumptions: 92 | 93 | * You're using AngularJS. It's designed using AngularJS services. 94 | 95 | * You're using an abstraction factory to wrap your end points. This service 96 | will provide you an object that has methods for getting all records, getting a 97 | single record, creating a record, etc. 98 | 99 | ## Architecture 100 | 101 | ### `hackstack.utils` 102 | 103 | This service provides methods that are used by both `hackstack.mock` and `hackstack.wrap` 104 | services. Those functions are: 105 | 106 | * `addErrorTrigger(errorFn, errorCode, method)`: Adds an "error trigger" that 107 | will fire if `errorFn(response)` returns true (where `response` is the 108 | response object that would otherwise be returned by HackStack)
109 | `errorFn` : {function} predicate that decides whether error should be returned
110 | `errorCode` : {integer} HTTP error code to return
111 | `method` : {string} Which HTTP method to check error trigger against (e.g. 'POST') 112 | * `disableRandomErrors(value)`: Disable random error generation.
113 | `value` : {boolean} 114 | * `forceError(errorCode)`: Reject with this error code in the next response. 115 | Reset error if `errorCode` is `null` 116 |
117 | `errorCode` : {integer} 118 | * `produceError(errorArray)`: Return either an error object or null depending 119 | on the probability distribution defined in the errorArray
120 | `errorArray` : {\[object]} (optional) an array of error objects 121 | * `randomError(errorArray)`: Return a random error from an array of errors 122 | (`errorArray` or the default error array if none provided)
123 | `errorArray` : {\[object]} (optional) an array of error objects 124 | * `getErrorByCode(errorCode)`: Returns an error object with error code matching 125 | `errorCode`.
126 | `errorCode` : {integer} 127 | * `randomInt()`: Returns a random integer.
128 | * `setOptions(newOptions)`: Updates the HackStack options list
129 | `newOptions` : {object} 130 | * `waitForTime()`: Returns a promise that resolves after some time. Used to 131 | mimic latency
132 | 133 | ### `hackstack.mock` 134 | 135 | `hackstack.mock` is a service that creates a mock backend from scratch. 136 | To create a HackStack instance, call `hackstack.mock(mockData, options)` where `mockData` 137 | is an array of objects and `options` is an optional argument of type `Object`. 138 | 139 | Alternatively, `mockData` can be the path to a JSON that is an array of objects 140 | 141 | A `hackstack.mock` object contains the following methods: 142 | 143 | * `getAll()`: Get all results (equivalent to requesting `API_BASE/endpoint/`) 144 | * `get(id)`: Get a single result (equivalent to requesting `API_BASE/endpoint/id`) 145 | * `query(queryObject)`: get the first result where for any key:value pair in 146 | `queryObject`, there's a matching key:value pair in the mock data object
147 | `queryObject` : {object} 148 | * `create(object, createIdFn)`: Create a new record
149 | `object` : {object}
150 | `createIdFn` : {() -> int} Function that returns an integer to be used as an id 151 | * `update(id, object)`: Update a record.
152 | `id` : the id of the record
153 | `object` : {object} the object to update 154 | * `save(object, createIdFn)`: a method that will call create or update 155 | depending on presence of an id.
156 | signature is identical to `create` 157 | 158 | ### `hackstack.wrap` 159 | 160 | `hackstack.wrap` is a service that wraps a real backend with a local mock object. 161 | It can be useful if the backend is buggy, returns incomplete data, or is yet to 162 | be fully implemented. 163 | 164 | To create a `hackstack.wrap` instance, call `hackstack.wrap(endpoint, mockObject, options)` 165 | where: 166 | 167 | * `endpoint` is a string that contains the location of the endpoint 168 | * `mockObject` is a single object used to complete responses from the backend 169 | * `options` is a an object (optional argument) 170 | 171 | Note that unlike `hackstack.mock`, you only pass a single object to `hackstack.wrap`. 172 | It will use that one object to complete all of the responses your backend 173 | returns by deep merging the response's properties with the objects 174 | 175 | `hackstack.wrap` also requires that you make `API_BASE` available through Angular's 176 | injector. `API_BASE` should be a string that contains the base URL for your 177 | API. 178 | 179 | the `hackstack.wrap` factory returns an object which contains the same methods as 180 | a `hackstack.mock` object. Keep in mind however, that `hackstack.wrap` will relay all 181 | requests to the backend, including `post` requests. 182 | -------------------------------------------------------------------------------- /bower-README.md: -------------------------------------------------------------------------------- 1 | # HackStack Bower Repository 2 | 3 | This is the bower hosting repository for the [HackStack](https://github.com/rangle/hackstack) project 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HackStack", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/rangle/hackstack", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/rangle/hackstack" 8 | }, 9 | "authors": [ 10 | "Brian Olynyk ", 11 | "Ahmed Al-Sudani " 12 | ], 13 | "description": "A method for building an app against a non-existent API", 14 | "dependencies": { 15 | "ramda": "0.14.0", 16 | "angular": "~1.3" 17 | }, 18 | "main": "dist/hackstack.js", 19 | "ignore": [ 20 | "/example", 21 | "/server", 22 | "/src/**/*.test.js" 23 | ], 24 | "license": "MIT", 25 | "devDependencies": { 26 | "angular-mocks": "~1.3.15" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | post: 3 | - cp -r ./coverage $CIRCLE_ARTIFACTS -------------------------------------------------------------------------------- /example/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /example/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | bower_components 4 | dist 5 | ngDocs 6 | .DS_Store 7 | .tmp 8 | gulpfile.tmp* 9 | .svn 10 | /.idea 11 | -------------------------------------------------------------------------------- /example/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "undef": true, 15 | "unused": true, 16 | "strict": false, 17 | "globals": { 18 | "angular": false, 19 | "jasmine": false, 20 | "describe": false, 21 | "xdescribe": false, 22 | "before": false, 23 | "beforeEach": false, 24 | "after": false, 25 | "afterEach": false, 26 | "it": false, 27 | "xit": false, 28 | "it": false, 29 | "inject": false, 30 | "expect": false, 31 | "spyOn": false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - gem install sass 6 | - bower install 7 | -------------------------------------------------------------------------------- /example/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-modular": { 3 | "name": "hackstack demo app", 4 | "uxFramework": "material" 5 | } 6 | } -------------------------------------------------------------------------------- /example/api-server/birds/index.html: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "name": "European robin", 5 | "age": "55-60 million years", 6 | "img": "images/birds/300px-Erithacus_rubecula_with_cocked_head.jpg" 7 | }, 8 | { 9 | "id": 1, 10 | "name": "Fawn-breasted bowerbird", 11 | "age": "55-60 million years", 12 | "img": "images/birds/Stavenn_Chlamydera_cerviniventris.jpg" 13 | 14 | }, 15 | { 16 | "id": 2, 17 | "name": "Green-backed kingfisher", 18 | "age": "~60 million years" 19 | }, 20 | { 21 | "id": 3, 22 | "name": "Vulturine guineafowl", 23 | "age": "~85 million years" 24 | }, 25 | { 26 | "id": 4, 27 | "name": "Atlantic puffin", 28 | "age": "~70 million years" 29 | }, 30 | { 31 | "id": 5, 32 | "name": "Australian brushturkey", 33 | "age": "~85 million years" 34 | }, 35 | { 36 | "id": 6, 37 | "name": "American golden plover", 38 | "age": "~70 million years" 39 | }, 40 | { 41 | "id": 7, 42 | "name": "Barn swallow", 43 | "age": "55-60 million years" 44 | }, 45 | { 46 | "id": 8, 47 | "name": "Bornean bristlehead", 48 | "age": "55-60 million years" 49 | } 50 | ] -------------------------------------------------------------------------------- /example/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackstack demo app", 3 | "version": "0.0.0", 4 | "ignore": [ 5 | "**/.*", 6 | "node_modules", 7 | "bower_components", 8 | "test", 9 | "tests" 10 | ], 11 | "devDependencies": { 12 | "angular-mocks": "~1.3" 13 | }, 14 | "dependencies": { 15 | "angular": "~1.3", 16 | "angular-material": "latest", 17 | "angular-route": "~1.3", 18 | "angular-hackstack": "0.1.0" 19 | }, 20 | "resolutions": { 21 | "angular": "~1.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "BASE_URL": "http://localhost:8000" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example/gulp/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | var $ = require('gulp-load-plugins')({ 8 | pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del'] 9 | }); 10 | 11 | gulp.task('partials', function () { 12 | return gulp.src([ 13 | '!' + paths.src + '/index.html', 14 | '!' + paths.tmp + '/serve/index.html', 15 | paths.src + '/**/*.html', 16 | paths.src + '/**/*.jade', 17 | paths.tmp + '/**/*.html' 18 | ]) 19 | .pipe($.if('/**/*.jade', $.jade())) 20 | .pipe($.minifyHtml({ 21 | empty: true, 22 | spare: true, 23 | quotes: true 24 | })) 25 | .pipe($.angularTemplatecache('templateCacheHtml.js', { 26 | module: 'hackstack demo app' 27 | })) 28 | .pipe(gulp.dest(paths.tmp + '/partials/')); 29 | }); 30 | 31 | gulp.task('html', ['clean', 'inject', 'partials'], function () { 32 | var partialsInjectFile = gulp.src(paths.tmp + '/partials/templateCacheHtml.js', { read: false }); 33 | var partialsInjectOptions = { 34 | starttag: '', 35 | ignorePath: paths.tmp + '/partials', 36 | addRootSlash: false 37 | }; 38 | 39 | var htmlFilter = $.filter('*.html'); 40 | var jsFilter = $.filter('**/*.js'); 41 | var cssFilter = $.filter('**/*.css'); 42 | var assets; 43 | 44 | return gulp.src(paths.tmp + '/serve/*.html') 45 | .pipe($.inject(partialsInjectFile, partialsInjectOptions)) 46 | .pipe(assets = $.useref.assets()) 47 | .pipe($.rev()) 48 | .pipe(jsFilter) 49 | .pipe($.ngAnnotate()) 50 | .pipe($.uglify({preserveComments: $.uglifySaveLicense})) 51 | .pipe(jsFilter.restore()) 52 | .pipe(cssFilter) 53 | .pipe($.replace('../bootstrap-sass-official/assets/fonts/bootstrap', 'fonts')) 54 | .pipe($.minifyCss()) 55 | .pipe(cssFilter.restore()) 56 | .pipe(assets.restore()) 57 | .pipe($.useref()) 58 | .pipe($.revReplace()) 59 | .pipe(htmlFilter) 60 | .pipe($.minifyHtml({ 61 | empty: true, 62 | spare: true, 63 | quotes: true 64 | })) 65 | .pipe(htmlFilter.restore()) 66 | .pipe(gulp.dest(paths.dist + '/')) 67 | .pipe($.size({ title: paths.dist + '/', showFiles: true })); 68 | }); 69 | 70 | gulp.task('images', ['clean'], function () { 71 | return gulp.src(paths.src + '/assets/images/**/*') 72 | .pipe(gulp.dest(paths.dist + '/assets/images/')); 73 | }); 74 | 75 | gulp.task('fonts', ['clean'], function () { 76 | return gulp.src($.mainBowerFiles()) 77 | .pipe($.filter('**/*.{eot,svg,ttf,woff}')) 78 | .pipe($.flatten()) 79 | .pipe(gulp.dest(paths.dist + '/fonts/')); 80 | }); 81 | 82 | gulp.task('misc', ['clean'], function () { 83 | return gulp.src(paths.src + '/**/*.ico') 84 | .pipe(gulp.dest(paths.dist + '/')); 85 | }); 86 | 87 | gulp.task('clean', function (done) { 88 | $.del([ 89 | paths.dist + '/', paths.tmp + '/**/*.html', 90 | '!' + paths.tmp + '**/vendor.css', 91 | paths.tmp + '**/*.js', 92 | paths.tmp + '**/*.css' 93 | ], done); 94 | }); 95 | 96 | gulp.task('build', ['html', 'images', 'fonts', 'misc']); 97 | -------------------------------------------------------------------------------- /example/gulp/e2e-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var $ = require('gulp-load-plugins')(); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var paths = gulp.paths; 10 | 11 | // Downloads the selenium webdriver 12 | gulp.task('webdriver-update', $.protractor.webdriver_update); 13 | 14 | gulp.task('webdriver-standalone', $.protractor.webdriver_standalone); 15 | 16 | function runProtractor (done) { 17 | 18 | gulp.src(paths.e2e + '/**/*.js') 19 | .pipe($.protractor.protractor({ 20 | configFile: 'protractor.conf.js', 21 | })) 22 | .on('error', function (err) { 23 | // Make sure failed tests cause gulp to exit non-zero 24 | throw err; 25 | }) 26 | .on('end', function () { 27 | // Close browser sync server 28 | browserSync.exit(); 29 | done(); 30 | }); 31 | } 32 | 33 | gulp.task('protractor', ['protractor:src']); 34 | gulp.task('protractor:src', ['serve:e2e', 'webdriver-update'], runProtractor); 35 | gulp.task('protractor:dist', ['serve:e2e-dist', 'webdriver-update'], runProtractor); 36 | -------------------------------------------------------------------------------- /example/gulp/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | var $ = require('gulp-load-plugins')(); 8 | 9 | var wiredep = require('wiredep').stream; 10 | 11 | gulp.task('inject', ['inject-css', 'scripts'], function () { 12 | 13 | var injectStyles = gulp.src([ 14 | paths.tmp + '/serve/**/*.css', 15 | '!' + paths.tmp + '/serve/module/vendor.css', 16 | '!' + paths.tmp + '/serve/app/vendor.css' 17 | ], { read: false }); 18 | 19 | var injectScripts = gulp.src([ 20 | paths.tmp + '/**/*.js', 21 | paths.src + '/**/*.js', 22 | '!' + paths.src + '/**/*.spec.js', 23 | '!' + paths.tmp + '/**/*.spec.js', 24 | '!' + paths.src + '/**/*.mock.js', 25 | '!' + paths.tmp + '/**/*.mock.js' 26 | ]).pipe($.angularFilesort()); 27 | 28 | var injectOptions = { 29 | ignorePath: [paths.src, paths.tmp + '/serve', paths.tmp + '/partials' ], 30 | addRootSlash: false 31 | }; 32 | 33 | var wiredepOptions = { 34 | directory: 'bower_components', 35 | exclude: [/bootstrap-sass-official/] 36 | }; 37 | 38 | return gulp.src(paths.src + '/*.html') 39 | .pipe($.inject(injectStyles, injectOptions)) 40 | .pipe($.inject(injectScripts, injectOptions)) 41 | .pipe(wiredep(wiredepOptions)) 42 | .pipe(gulp.dest(paths.tmp + '/serve')); 43 | 44 | }); 45 | 46 | gulp.task('inject-css', ['styles'], function () { 47 | 48 | var target = gulp.src('./.tmp/serve/index.html'); 49 | var sources = gulp.src(['./tmp/**/*.css'], {read: false}); 50 | target.pipe($.inject(sources, {ignorePath: 'src', addRootSlash: false })) 51 | .pipe($.file('app/vendor.css','/* */')) 52 | .pipe(gulp.dest('./.tmp/serve')); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /example/gulp/ngdocs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var $ = require('gulp-load-plugins')(); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | gulp.task('ngdocs-build', function () { 10 | var options = { 11 | //scripts: ['src/app.js'], 12 | html5Mode: true, 13 | startPage: '/api', 14 | title: 'hackstack demo app', 15 | image: "http://swiip.github.io/yeoman-angular/slides/img/yeoman-009.png", 16 | imageLink: "/api", 17 | titleLink: "http://localhost:3000" 18 | } 19 | return gulp.src(['src/**/*.js']) 20 | .pipe($.ngdocs.process(options)) 21 | .pipe(gulp.dest('./ngDocs')); 22 | }); 23 | 24 | gulp.task('serve:ngdocs', ['ngdocs-build'], function() { 25 | browserSync({ 26 | port: 4000, 27 | server: { 28 | baseDir: 'ngDocs'//, 29 | // middleware: [ historyApiFallback ] 30 | } 31 | }); 32 | // gulp.watch(['./**/*.html'], {cwd: 'ngDocs'}, reload); 33 | }); 34 | -------------------------------------------------------------------------------- /example/gulp/proxy.js: -------------------------------------------------------------------------------- 1 | /*jshint unused:false */ 2 | 3 | /*************** 4 | 5 | This file allow to configure a proxy system plugged into BrowserSync 6 | in order to redirect backend requests while still serving and watching 7 | files from the web project 8 | 9 | IMPORTANT: The proxy is disabled by default. 10 | 11 | If you want to enable it, watch at the configuration options and finally 12 | change the `module.exports` at the end of the file 13 | 14 | ***************/ 15 | 16 | 'use strict'; 17 | 18 | var httpProxy = require('http-proxy'); 19 | var chalk = require('chalk'); 20 | 21 | /* 22 | * Location of your backend server 23 | */ 24 | var proxyTarget = 'http://server/context/'; 25 | 26 | var proxy = httpProxy.createProxyServer({ 27 | target: proxyTarget 28 | }); 29 | 30 | proxy.on('error', function(error, req, res) { 31 | res.writeHead(500, { 32 | 'Content-Type': 'text/plain' 33 | }); 34 | 35 | console.error(chalk.red('[Proxy]'), error); 36 | }); 37 | 38 | /* 39 | * The proxy middleware is an Express middleware added to BrowserSync to 40 | * handle backend request and proxy them to your backend. 41 | */ 42 | function proxyMiddleware(req, res, next) { 43 | /* 44 | * This test is the switch of each request to determine if the request is 45 | * for a static file to be handled by BrowserSync or a backend request to proxy. 46 | * 47 | * The existing test is a standard check on the files extensions but it may fail 48 | * for your needs. If you can, you could also check on a context in the url which 49 | * may be more reliable but can't be generic. 50 | */ 51 | if (/\.(html|css|js|png|jpg|jpeg|gif|ico|xml|rss|txt|eot|svg|ttf|woff|cur)(\?((r|v|rel|rev)=[\-\.\w]*)?)?$/.test(req.url)) { 52 | next(); 53 | } else { 54 | proxy.web(req, res); 55 | } 56 | } 57 | 58 | /* 59 | * This is where you activate or not your proxy. 60 | * 61 | * The first line activate if and the second one ignored it 62 | */ 63 | 64 | //module.exports = [proxyMiddleware]; 65 | module.exports = []; 66 | -------------------------------------------------------------------------------- /example/gulp/scripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var gulpNgConfig = require('gulp-ng-config'); 5 | 6 | var paths = gulp.paths; 7 | 8 | var $ = require('gulp-load-plugins')({ 9 | pattern: ['gulp-*', 'del'] 10 | }); 11 | 12 | gulp.task('config', function () { 13 | return gulp.src('demo.config.json') 14 | .pipe(gulpNgConfig('demo.config')) 15 | .pipe(gulp.dest(paths.tmp + '/serve')); 16 | }) 17 | 18 | gulp.task('scripts', ['clean:tmp', 'config'], function () { 19 | return gulp.src(paths.src + '/**/*.ts') 20 | .pipe($.typescript()) 21 | .on('error', function handleError(err) { 22 | console.error(err.toString()); 23 | this.emit('end'); 24 | }) 25 | .pipe(gulp.dest(paths.tmp + '/serve/')) 26 | .pipe($.size()); 27 | }); 28 | 29 | gulp.task('clean:tmp', function (done) { 30 | $.del([ 31 | paths.tmp + '/serve/**/*.js' 32 | ], { force: true } , done ); 33 | }); 34 | -------------------------------------------------------------------------------- /example/gulp/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | var util = require('util'); 8 | 9 | var browserSync = require('browser-sync'); 10 | 11 | var middleware = require('./proxy'); //TODO: residue from huge refactor 12 | middleware.push(require('connect-history-api-fallback')); 13 | 14 | middleware.push(function(req, res, next) { 15 | res.setHeader('Access-Control-Allow-Origin', '*'); 16 | next(); 17 | }); 18 | 19 | function browserSyncInit(baseDir, files, browser) { 20 | browser = browser === undefined ? 'default' : browser; 21 | 22 | var routes = null; 23 | if(baseDir === paths.src || (util.isArray(baseDir) && baseDir.indexOf(paths.src) !== -1)) { 24 | routes = { 25 | '/bower_components': 'bower_components' 26 | }; 27 | } 28 | 29 | browserSync.instance = browserSync.init(files, { 30 | startPath: '/', 31 | server: { 32 | baseDir: baseDir, 33 | middleware: middleware, 34 | routes: routes 35 | }, 36 | browser: browser 37 | }); 38 | } 39 | 40 | gulp.task('serve', ['watch'], function () { 41 | browserSyncInit([ 42 | paths.tmp + '/serve', 43 | paths.tmp + '/partials', 44 | paths.src 45 | ], [ 46 | paths.tmp + '/serve/**/*.css', 47 | paths.tmp + '/partials/**/*.js', 48 | paths.src + '/**/*.js', 49 | paths.src + 'src/assets/images/**/*', 50 | paths.tmp + '/serve/*.html', 51 | paths.tmp + '/serve/**/*.js', 52 | paths.tmp + '/serve/**/*.html', 53 | paths.src + '/**/*.html' 54 | ]); 55 | }); 56 | 57 | gulp.task('serve:dist', ['build'], function () { 58 | browserSyncInit(paths.dist); 59 | }); 60 | 61 | gulp.task('serve:e2e', ['inject'], function () { 62 | browserSyncInit([paths.tmp + '/serve', paths.src], null, []); 63 | }); 64 | 65 | gulp.task('serve:e2e-dist', ['build'], function () { 66 | browserSyncInit(paths.dist, null, []); 67 | }); 68 | -------------------------------------------------------------------------------- /example/gulp/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | var $ = require('gulp-load-plugins')(); 8 | 9 | gulp.task('styles', function () { 10 | var src = [ 'src/**/*.scss', 'src/**/*.css' ]; 11 | return gulp.src(src) 12 | .pipe($.sass()) 13 | .pipe($.concat('css' + '.css')) 14 | .pipe(gulp.dest(paths.tmp + '/serve/app/')); 15 | }); 16 | -------------------------------------------------------------------------------- /example/gulp/unit-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var $ = require('gulp-load-plugins')(); 6 | 7 | var wiredep = require('wiredep'); 8 | 9 | var paths = gulp.paths; 10 | 11 | function runTests (singleRun, done) { 12 | var bowerDeps = wiredep({ 13 | directory: 'bower_components', 14 | exclude: ['bootstrap-sass-official'], 15 | dependencies: true, 16 | devDependencies: true 17 | }); 18 | 19 | var testFiles = bowerDeps.js.concat([ 20 | paths.src + '/**/*.js', 21 | paths.tmp + '/**/*.js', 22 | '!' + paths.src + '/demo/**/*' 23 | ]); 24 | 25 | gulp.src(testFiles) 26 | .pipe($.karma({ 27 | configFile: 'karma.conf.js', 28 | action: (singleRun)? 'run': 'watch' 29 | })) 30 | .on('error', function () { 31 | // Make sure failed tests DO NOT cause gulp to exit 32 | }); 33 | } 34 | 35 | gulp.task('test', [ 'partials', 'inject', 'scripts' ], function (done) { runTests(true /* singleRun */, done) }); 36 | gulp.task('test:auto', function (done) { runTests(false /* singleRun */, done) }); 37 | -------------------------------------------------------------------------------- /example/gulp/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | gulp.task('watch', ['inject'], function () { 8 | gulp.watch([ 9 | paths.src + '/**/*.html', 10 | paths.src + '/**/*.jade', 11 | paths.src + '/**/*.scss', 12 | paths.src + '/**/*.js', 13 | paths.src + '/**/*.ts', 14 | 'bower.json', 15 | 'demo.config.json' 16 | ], ['inject', 'partials', 'test']); 17 | }); 18 | -------------------------------------------------------------------------------- /example/gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | gulp.paths = { 6 | src: 'src', 7 | dist: 'dist', 8 | tmp: '.tmp', 9 | e2e: 'e2e' 10 | }; 11 | 12 | require('require-dir')('./gulp'); 13 | 14 | gulp.task('default', ['clean'], function () { 15 | gulp.start('build'); 16 | }); 17 | -------------------------------------------------------------------------------- /example/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: '', 8 | 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['jasmine'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | 18 | // gulp-inject:mainBowerFiles 19 | // gulp-inject:mainBowerFiles:end 20 | 21 | // gulp-inject:src 22 | // gulp-inject:src:end 23 | 24 | ], 25 | 26 | // list of files to exclude 27 | exclude: [ 28 | '**/*.swp' 29 | ], 30 | 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: { 35 | 36 | }, 37 | 38 | 39 | // test results reporter to use 40 | // possible values: 'dots', 'progress' 41 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 42 | reporters: ['progress'], 43 | 44 | 45 | // web server port 46 | port: 9876, 47 | 48 | 49 | // enable / disable colors in the output (reporters and logs) 50 | colors: true, 51 | 52 | 53 | // level of logging 54 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 55 | logLevel: config.LOG_INFO, 56 | 57 | 58 | // enable / disable watching file and executing tests whenever any file changes 59 | autoWatch: true, 60 | 61 | 62 | // start these browsers 63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 64 | browsers: ['PhantomJS'], 65 | 66 | 67 | // Continuous Integration mode 68 | // if true, Karma captures browsers, runs the tests and exits 69 | singleRun: true 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackstack_demo_app", 3 | "version": "0.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "./node_modules/karma/bin/karma start karma.conf.js" 7 | }, 8 | "dependencies": { 9 | "gulp-ng-annotate": "^1.0.0", 10 | "gulp-ng-config": "^1.2.1" 11 | }, 12 | "devDependencies": { 13 | "bower": "^1.3.12", 14 | "gulp": "^3.8.10", 15 | "gulp-ngdocs": "^0.2.10", 16 | "gulp-autoprefixer": "~2.0.0", 17 | "gulp-angular-templatecache": "~1.4.2", 18 | "del": "~0.1.3", 19 | "gulp-consolidate": "~0.1.2", 20 | "gulp-minify-css": "latest", 21 | "gulp-filter": "~1.0.2", 22 | "gulp-flatten": "~0.0.4", 23 | "gulp-jshint": "~1.9.0", 24 | "gulp-load-plugins": "~0.7.1", 25 | "gulp-size": "~1.1.0", 26 | "gulp-uglify": "~1.0.1", 27 | "gulp-useref": "~1.0.2", 28 | "gulp-ng-annotate": "~0.3.6", 29 | "gulp-replace": "~0.5.0", 30 | "gulp-rename": "~1.2.0", 31 | "gulp-rev": "~2.0.1", 32 | "gulp-rev-replace": "~0.3.1", 33 | "gulp-minify-html": "~0.1.7", 34 | "gulp-inject": "~1.0.2", 35 | "gulp-protractor": "~0.0.11", 36 | "gulp-karma": "~0.0.4", 37 | "gulp-sass": "~1.1.0", 38 | "gulp-if": "^1.2.5", 39 | "gulp-jade": "^1.0.0", 40 | "gulp-angular-filesort": "~1.0.4", 41 | "main-bower-files": "~2.4.0", 42 | "jshint-stylish": "~1.0.0", 43 | "wiredep": "~2.2.0", 44 | "karma-jasmine": "~0.3.1", 45 | "karma-phantomjs-launcher": "~0.1.4", 46 | "require-dir": "~0.1.0", 47 | "browser-sync": "~1.7.1", 48 | "http-proxy": "~1.7.0", 49 | "chalk": "^1.0.0", 50 | "protractor": "~1.4.0", 51 | "gulp-concat": "^2.4.3", 52 | "uglify-save-license": "~0.4.1", 53 | "connect-history-api-fallback": "0.0.5", 54 | "gulp-typescript": "~2.3.0", 55 | "gulp-file": "^0.2.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/src/app/api.service.js: -------------------------------------------------------------------------------- 1 | angular.module('hackstack demo app') 2 | 3 | .service('api', function($http, config, hackstack, mockData, mockDataOverrides, $window) { 4 | 5 | var birdsUrl = [config.BASE_URL, 'birds'].join('/'); 6 | 7 | var mockBirds = hackstack.mock(mockData.birds); 8 | var wrappedBirds = hackstack.wrap(birdsUrl, mockDataOverrides.birds); 9 | $window.mockBirds = mockBirds; 10 | $window.wrappedBirds = wrappedBirds; 11 | 12 | var liveBirds = { 13 | 'get': function(id) { 14 | return $http.get([birdsUrl, id].join('/')); 15 | }, 16 | 'getAll': function() { 17 | return $http.get(birdsUrl + '/'); 18 | } 19 | } 20 | 21 | var endpoints = { 22 | birds: function() { 23 | var backendType = config.backendType; 24 | if (backendType === 'mock') { 25 | return mockBirds; 26 | } else if (backendType === 'wrap') { 27 | return wrappedBirds; 28 | } else if (backendType === 'live') { 29 | return liveBirds; 30 | } else { 31 | throw new Error('Unrecognized backend type', backendType); 32 | } 33 | } 34 | }; 35 | 36 | function getEndpoint(endpointHandle) { 37 | var endpoint = endpoints[endpointHandle] 38 | if (!endpoint) { 39 | throw new Error('No such endpoint: ', endpoint.toString()); 40 | } 41 | return endpoint(); 42 | } 43 | 44 | this.getAll = function getAll(endpoint) { 45 | return getEndpoint(endpoint).getAll(); 46 | }; 47 | 48 | this.get = function get(endpoint) { 49 | return getEndpoint(endpoint).get(); 50 | }; 51 | }); -------------------------------------------------------------------------------- /example/src/app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc overview 3 | * @name hackstack demo app 4 | * 5 | * @description 6 | * Generated by Yo men 7 | * 8 | * **Note:** Describe what this module does 9 | * 10 | * @example 11 | http://localhost:8000 12 | */ 13 | 14 | angular.module('hackstack demo app', [ 15 | 'ngMaterial', 16 | 'ngRoute', 17 | 'hackstack', 18 | 'demo.config' 19 | ]) 20 | .config([ '$routeProvider', '$locationProvider', 21 | function($routeProvider,$locationProvider) { 22 | var main = { 23 | templateUrl: 'app/main.html', 24 | controller: 'main', 25 | controllerAs: 'vm' 26 | }; 27 | $routeProvider 28 | .when('/mock', main) 29 | .when('/wrap', main) 30 | .when('/live', main) 31 | .otherwise({ 32 | redirectTo: '/live' 33 | }); 34 | 35 | $locationProvider.html5Mode(true); 36 | 37 | }]) 38 | .run(function(config, hackstack, $location, $log, $window) { 39 | $window.hsUtils.disableRandomErrors(true); 40 | config.backendType = $location.path().slice(1) || 'live'; 41 | }) 42 | -------------------------------------------------------------------------------- /example/src/app/app.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: white; 3 | }.modular_content md-whiteframe img { 4 | display: block; 5 | margin-top: 10px; 6 | margin-left: auto; 7 | margin-right: auto; 8 | } 9 | 10 | .modular_content md-whiteframe { 11 | margin: 10px 10px 10px 10px; 12 | } 13 | 14 | .modular_content md-whiteframe p, h1, h2, h3, h4 { 15 | margin: 10px 10px 10px 10px; 16 | } 17 | 18 | .item { 19 | width: 350px; 20 | height: 350px; 21 | float: left; 22 | margin: 10px; 23 | .name { 24 | font-weight: bold; 25 | } 26 | li { 27 | margin: 10px; 28 | text-align: center; 29 | } 30 | } 31 | 32 | .error { 33 | color: red; 34 | font-size: 300%; 35 | } 36 | 37 | .fields { 38 | list-style-type: none; 39 | } 40 | 41 | .species-pic { 42 | width: 130px; 43 | height: 130px; 44 | object-fit: cover; 45 | } 46 | -------------------------------------------------------------------------------- /example/src/app/birds.service.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('hackstack demo app') 3 | 4 | .service('birds', function (api) { 5 | this.getAllBirds = function getAllBirds() { 6 | return api.getAll('birds'); 7 | }; 8 | 9 | this.getBird = function getBird(id) { 10 | return api.get('birds', id); 11 | }; 12 | }); 13 | -------------------------------------------------------------------------------- /example/src/app/main.controller.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('hackstack demo app').controller('main', 3 | function (hackstack, birds, config, $log) { 4 | var vm = this; 5 | 6 | $log.info('main controller loading, mode:', config.backendType); 7 | 8 | vm.loadingMessage = 'Loading...'; 9 | 10 | birds.getAllBirds() 11 | .then(function handleSuccess(result) { 12 | vm.loadingMessage = null; 13 | vm.birds = result.data; 14 | }) 15 | .then(null, function handleError(err) { 16 | $log.error(err); 17 | vm.loadingMessage = null; 18 | vm.errorMessage = err.data || 'Couldn\'t contact server'; 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /example/src/app/main.controller.spec.js: -------------------------------------------------------------------------------- 1 | describe('Controller: main', function() { 2 | var controller, $rootScope; 3 | 4 | 5 | beforeEach(module('hackstack demo app')); 6 | beforeEach(inject(function (_$controller_,_$rootScope_) { 7 | 8 | $controller = _$controller_; 9 | $rootScope = _$rootScope_; 10 | 11 | 12 | controller = $controller('main', { 13 | }); 14 | })); 15 | 16 | it('should get initialized', function() { 17 | expect(controller).not.toEqual(undefined); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /example/src/app/main.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ vm.loadingMessage }} 4 |
{{ vm.errorMessage }}
5 |
6 | 7 |
    8 |
  • {{ bird.name }}
  • 9 | Species picture 10 |
  • Scientific name: {{ bird.scientificName }}
  • 11 |
  • Order age: {{ bird.age }}
  • 12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /example/src/app/mock-data.js: -------------------------------------------------------------------------------- 1 | angular.module('hackstack demo app') 2 | 3 | .constant('mockData', { 4 | birds: [ 5 | { 6 | "id": 0, 7 | "name": "European robin", 8 | "scientificName": "Erithacus rubecula", 9 | "age": "55-60 million years", 10 | "img": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Erithacus_rubecula_with_cocked_head.jpg/300px-Erithacus_rubecula_with_cocked_head.jpg" 11 | }, 12 | { 13 | "id": 1, 14 | "name": "Fawn-breasted bowerbird", 15 | "scientificName": "Chlamydera cerviniventris", 16 | "age": "55-60 million years", 17 | "img": "https://upload.wikimedia.org/wikipedia/commons/e/e2/Stavenn_Chlamydera_cerviniventris.jpg" 18 | }, 19 | { 20 | "id": 2, 21 | "name": "Green-backed kingfisher", 22 | "scientificName": "Actenoides monachus", 23 | "age": "~60 million years", 24 | "img": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Green-backed_Kingfisher_%28Male%29_cropped.jpg/384px-Green-backed_Kingfisher_%28Male%29_cropped.jpg" 25 | }, 26 | { 27 | "id": 3, 28 | "name": "vulturine guineafowl", 29 | "scientificName": "Acryllium vulturinum", 30 | "age": "~85 million years", 31 | "img": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f8/Acryllium_vulturinum_-Tsavo_East_National_Park%2C_Kenya-8.jpg/350px-Acryllium_vulturinum_-Tsavo_East_National_Park%2C_Kenya-8.jpg" 32 | }, 33 | { 34 | "id": 4, 35 | "name": "Atlantic puffin", 36 | "scientificName": "Fratercula arctica", 37 | "age": "~70 million years", 38 | "img": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c4/Atlantic_puffin_062.jpg/320px-Atlantic_puffin_062.jpg" 39 | }, 40 | { 41 | "id": 5, 42 | "name": "Australian brushturkey", 43 | "scientificName": "Alectura lathami", 44 | "age": "~85 million years", 45 | "img": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Alectura_lathami_-_Centenary_Lakes.jpg/300px-Alectura_lathami_-_Centenary_Lakes.jpg" 46 | }, 47 | { 48 | "id": 6, 49 | "name": "American golden plover", 50 | "scientificName": "Pluvialis dominica", 51 | "age": "~70 million years", 52 | "img": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Pluvialis_dominica1.jpg/320px-Pluvialis_dominica1.jpg" 53 | }, 54 | { 55 | "id": 7, 56 | "name": "Barn swallow", 57 | "scientificName": "Hirundo rustica", 58 | "age": "55-60 million years", 59 | "img": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Barn_swallow_%28Hirundo_rustica_rustica%29_singing.jpg/320px-Barn_swallow_%28Hirundo_rustica_rustica%29_singing.jpg" 60 | }, 61 | { 62 | "id": 8, 63 | "name": "Bornean bristlehead", 64 | "scientificName": "Pityriasis gymnocephala", 65 | "age": "55-60 million years", 66 | "img": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/Barite_chauve.JPG/320px-Barite_chauve.JPG" 67 | } 68 | ] 69 | }) 70 | 71 | .constant('mockDataOverrides', { 72 | birds: { 73 | "name": "Lorem chicken", 74 | "scientificName": "Loremius ipsuma", 75 | "age": "~40 quadrillion years", 76 | "img": "images/rangle.jpg" 77 | } 78 | }); -------------------------------------------------------------------------------- /example/src/app/nav.controller.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('hackstack demo app').controller('nav', 3 | function (config, $location, $route) { 4 | var vm = this; 5 | vm.types = [ 6 | {name: 'live', href: 'live'}, 7 | {name: 'mock', href: 'mock'}, 8 | {name: 'wrap', href: 'wrap'} 9 | ]; 10 | vm.isActive = function isActive(href) { 11 | return href === $location.path() || 12 | href === $location.absUrl(); 13 | } 14 | vm.click = function click(event) { 15 | if(vm.isActive(event.currentTarget.href)){ 16 | $route.reload(); 17 | } 18 | window.fn = vm.isActive; 19 | window.$loc = $location; 20 | config.backendType = event.currentTarget.type.slice(); 21 | } 22 | vm.setBackend = function setBackend(backendType) { 23 | config.backendType = backendType; 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /example/src/header.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 10 | {{type.name}} 11 | 12 | 13 |   14 |

15 |
16 | -------------------------------------------------------------------------------- /example/src/images/rangle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rangle/hackstack/1a4715dbcfbcbbd264404613f1e6262bd4354c52/example/src/images/rangle.jpg -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hackstack demo app 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var gulp = require('gulp'); 3 | var bumpVersion = require('gulp-bump'); 4 | var concat = require('gulp-concat'); 5 | var rename = require('gulp-rename'); 6 | var uglify = require('gulp-uglify'); 7 | var beautify = require('gulp-jsbeautifier'); 8 | var inject = require('gulp-inject'); 9 | var gulpFilter = require('gulp-filter'); 10 | var jshint = require('gulp-jshint'); 11 | var map = require('map-stream'); 12 | var ngAnnotate = require('gulp-ng-annotate'); 13 | var del = require('del'); 14 | var nodemon = require('gulp-nodemon'); 15 | var sequence = require('gulp-sequence'); 16 | 17 | var moduleFiles = ['./src/**/*.js', '!**/*test.js']; 18 | var allJsFiles = [ 19 | './src/**/*.js', 20 | './example/client/**/*.js', 21 | './karma.conf.js', 22 | './gulpfile.js' 23 | ]; 24 | var distributionFiles = './dist/app/**/*.js'; 25 | var indexTmpl = './dist/app/index.html'; 26 | var buildDir = './dist/'; 27 | var cleanDirs = ['./dist/*']; 28 | var bowerBuildDir = './bower-angular-hackstack/'; 29 | 30 | var jsHintErrorReporter = function () { 31 | return map(function (file, cb) { 32 | if (!file.jshint.success) { 33 | process.exit(1); 34 | } 35 | cb(null, file); 36 | }); 37 | }; 38 | 39 | gulp.task('clean', function () { 40 | del(cleanDirs); 41 | }); 42 | 43 | gulp.task('copy', function () { 44 | gulp.src('./example/client/**/*') 45 | .pipe(gulp.dest('./dist')); 46 | 47 | gulp.src('./example/bower_components/**/*') 48 | .pipe(gulp.dest('./dist/bower_components')); 49 | 50 | gulp.src('./src/**/*.js') 51 | .pipe(gulp.dest('./dist/app')); 52 | }); 53 | 54 | gulp.task('index-dev', ['copy'], function () { 55 | 56 | var filter = gulpFilter(function (file) { 57 | return !/\.test\.js$/.test(file.path); 58 | }); 59 | var target = gulp.src(indexTmpl); 60 | var sources = gulp.src(distributionFiles, { 61 | read: false 62 | }).pipe(filter); 63 | 64 | return target.pipe(inject(sources, { 65 | ignorePath: '/dist', 66 | addRootSlash: false 67 | })) 68 | .pipe(gulp.dest('./dist')); 69 | }); 70 | 71 | gulp.task('beautify', function () { 72 | return gulp.src(allJsFiles, { 73 | base: '.' 74 | }) 75 | .pipe(beautify({ 76 | config: '.jsbeautifyrc' 77 | })) 78 | .pipe(gulp.dest('.')); 79 | }); 80 | 81 | gulp.task('lint', function () { 82 | return gulp.src(allJsFiles) 83 | // '.jshintrc' was a parameter, which lead to jshint not working locally 84 | // for CD; so removing and seeing if the sky falls for anyone else. 85 | .pipe(jshint()) 86 | .pipe(jshint.reporter('jshint-stylish')) 87 | .pipe(jsHintErrorReporter()); 88 | }); 89 | 90 | gulp.task('dev', function () { 91 | nodemon({ 92 | script: 'server/app.js', 93 | ext: 'html js', 94 | tasks: ['lint', 'index-dev'] 95 | }) 96 | .on('restart', function () { 97 | console.log('restarted!'); 98 | }); 99 | }); 100 | 101 | gulp.task('build', function () { 102 | var normal = gulp.src(moduleFiles) 103 | .pipe(ngAnnotate()) 104 | .pipe(concat('hackstack.js')) 105 | .pipe(gulp.dest(buildDir)); 106 | 107 | var min = gulp.src(moduleFiles) 108 | .pipe(ngAnnotate()) 109 | .pipe(concat('hackstack.min.js')) 110 | .pipe(uglify()) 111 | .pipe(gulp.dest(buildDir)); 112 | }); 113 | 114 | gulp.task('bump-version', function () { 115 | gulp.src(['./package.json', './bower.json']) 116 | .pipe(bumpVersion()) 117 | .pipe(gulp.dest('./')); 118 | }); 119 | 120 | gulp.task('bower:copy-json', function () { 121 | gulp.src('./bower.json') 122 | .pipe(gulp.dest(bowerBuildDir)); 123 | }); 124 | 125 | gulp.task('bower:copy-docs', function () { 126 | gulp.src(['./bower-README.md', 'LICENSE']) 127 | .pipe(rename(function (path) { 128 | if (path.basename === 'bower-README') { 129 | path.basename = 'README'; 130 | } 131 | })) 132 | .pipe(gulp.dest(bowerBuildDir)); 133 | }); 134 | 135 | gulp.task('bower:copy-build', function () { 136 | gulp.src(buildDir + '*.js') 137 | .pipe(gulp.dest(bowerBuildDir + 'dist/')); 138 | }); 139 | 140 | gulp.task('bower', sequence( 141 | 'build', 142 | 'bower:copy-json', 143 | 'bower:copy-docs', 144 | 'bower:copy-build')); 145 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: '', 8 | 9 | // frameworks to use 10 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ['mocha', 'chai', 'chai-as-promised', 'sinon', 12 | 'sinon-chai' 13 | ], 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | './bower_components/angular/angular.min.js', 18 | './bower_components/angular-mocks/angular-mocks.js', 19 | './bower_components/ramda/dist/ramda.js', 20 | 'src/**/*.js' 21 | ], 22 | 23 | // preprocess matching files before serving them to the browser 24 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 25 | preprocessors: { 26 | 'example/client/app/**/*.html': 'ng-html2js', 27 | 'example/client/app/**/*.json': 'ng-json2js', 28 | 'src/**/!(*.test).js': ['coverage'], 29 | 'src/**/*.test.js': ['wrap'] 30 | }, 31 | 32 | wrapPreprocessor: { 33 | template: '(function () { <%= contents %> })()' 34 | }, 35 | 36 | coverageReporter: { 37 | reporters: [{ 38 | type: 'json' 39 | }, { 40 | type: 'html' 41 | }, { 42 | type: 'text-summary' 43 | }], 44 | dir: './coverage/' 45 | }, 46 | 47 | ngHtml2JsPreprocessor: { 48 | stripPrefix: 'client/', 49 | moduleName: 'htmlTemplates' 50 | }, 51 | 52 | ngJson2JsPreprocessor: { 53 | stripPrefix: 'client', 54 | prependPrefix: 'served' 55 | }, 56 | 57 | // list of files to exclude 58 | exclude: [], 59 | 60 | 61 | // test results reporter to use 62 | // possible values: 'dots', 'progress' 63 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 64 | reporters: ['story', 'coverage'], 65 | 66 | 67 | // web server port 68 | port: 9876, 69 | 70 | 71 | // enable / disable colors in the output (reporters and logs) 72 | colors: true, 73 | 74 | 75 | // level of logging 76 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 77 | logLevel: config.LOG_INFO, 78 | 79 | 80 | // enable / disable watching file and executing tests whenever any file changes 81 | autoWatch: false, 82 | 83 | 84 | // start these browsers 85 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 86 | browsers: ['PhantomJS'], 87 | 88 | 89 | // Continuous Integration mode 90 | // if true, Karma captures browsers, runs the tests and exits 91 | singleRun: true 92 | }); 93 | }; 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HackStack", 3 | "version": "0.1.0", 4 | "description": "The way you build app that don't have a backend", 5 | "main": "server/app.js", 6 | "scripts": { 7 | "test": "karma start", 8 | "postinstall": "bower install" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rangle/hackstack" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/rangle/hackstack/issues" 18 | }, 19 | "homepage": "https://github.com/rangle/hackstack", 20 | "dependencies": { 21 | "angular": "^1.3.14", 22 | "angular-ui-router": "^0.2.13", 23 | "body-parser": "^1.12.3", 24 | "bower": "^1.4.1", 25 | "del": "^1.1.1", 26 | "express": "^4.12.2", 27 | "gulp": "^3.8.11", 28 | "gulp-bump": "^0.3.1", 29 | "gulp-concat": "^2.5.2", 30 | "gulp-filter": "^2.0.2", 31 | "gulp-inject": "^1.2.0", 32 | "gulp-jsbeautifier": "0.0.8", 33 | "gulp-jshint": "^1.10.0", 34 | "gulp-ng-annotate": "^0.5.2", 35 | "gulp-rename": "^1.2.2", 36 | "gulp-sass": "^1.3.3", 37 | "gulp-sequence": "^0.3.2", 38 | "gulp-uglify": "^1.2.0", 39 | "jshint-stylish": "^1.0.1", 40 | "node-sass": "^2.1.1" 41 | }, 42 | "devDependencies": { 43 | "angular-mocks": "^1.4.1", 44 | "chai": "^2.2.0", 45 | "chai-as-promised": "^5.0.0", 46 | "gulp-nodemon": "^2.0.2", 47 | "jsdoc": "^3.3.0-beta3", 48 | "karma": "^0.12.31", 49 | "karma-chai": "^0.1.0", 50 | "karma-chai-plugins": "^0.5.0", 51 | "karma-chrome-launcher": "^0.1.8", 52 | "karma-cli": "0.0.4", 53 | "karma-coverage": "^0.3.1", 54 | "karma-fixture": "^0.2.3", 55 | "karma-mocha": "^0.1.10", 56 | "karma-ng-html2js-preprocessor": "^0.1.2", 57 | "karma-ng-json2js-preprocessor": "^1.0.0", 58 | "karma-phantomjs-launcher": "^0.1.4", 59 | "karma-sinon": "^1.0.4", 60 | "karma-story-reporter": "^0.3.1", 61 | "karma-wrap-preprocessor": "^0.1.0", 62 | "map-stream": "0.0.5", 63 | "mocha": "^2.2.4", 64 | "sinon": "^1.14.1", 65 | "sinon-chai": "^2.7.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/hackstack/hackstack-mock-service.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | /* global R */ 3 | 'use strict'; 4 | angular.module('hackstack.mock', [ 5 | 'hackstack.utils' 6 | ]) 7 | .factory('mock', function ($http, $q, utils) { 8 | /** 9 | * Create a mock endpoint to use in your app. 10 | * 11 | * @param mockData If this is a string, will be treated as a path for $http 12 | * to use to get a json file. If it's an array, it will be used as the 13 | * mock data. 14 | * 15 | * @param options an object of options for specifics about errors to be thrown 16 | * timeouts, etc. 17 | */ 18 | function createMock(mockData, options) { 19 | 20 | var responseObj; 21 | var makeCompareFn; 22 | 23 | if (options) { 24 | utils.setOptions(options); 25 | makeCompareFn = options.makeCompareFn; 26 | } 27 | 28 | makeCompareFn = makeCompareFn || function makeCompareFn( 29 | requestData) { 30 | return function (targetData) { 31 | return targetData.id === requestData; 32 | }; 33 | }; 34 | 35 | var disableRandomErrors = utils.disableRandomErrors; 36 | var produceError = utils.produceError; 37 | var getErrorByCode = utils.getErrorByCode; 38 | var waitForTime = utils.waitForTime; 39 | 40 | function findItem(requestData) { 41 | return R.find(makeCompareFn(requestData))(mockData); 42 | } 43 | 44 | function setGoodGET(response, data) { 45 | var defaultResponse = { 46 | status: 200, 47 | statusText: 'OK', 48 | data: data 49 | }; 50 | 51 | if (response) { 52 | return response; 53 | } 54 | return defaultResponse; 55 | } 56 | 57 | function goodPOST() { 58 | return { 59 | status: 201, 60 | statusText: 'Created', 61 | data: '' 62 | }; 63 | } 64 | 65 | if (Array.isArray(mockData)) { 66 | responseObj = setGoodGET(null, mockData); 67 | } else if (mockData && mockData.indexOf && mockData.indexOf('.json') !== 68 | -1) { 69 | $http.get(mockData) 70 | .then(function (response) { 71 | responseObj = setGoodGET(response); 72 | }) 73 | .then(null, function (error) { 74 | throw new Error(error); 75 | }); 76 | } else { 77 | throw new Error('mockData required to be an array or .json path'); 78 | } 79 | 80 | function processGETAll() { 81 | var error = produceError(null, 'get'); 82 | if (error !== null) { 83 | return $q.reject(error); 84 | } 85 | return angular.copy(responseObj); 86 | } 87 | 88 | function processGET(requestData) { 89 | var error = produceError(requestData, 'get'); 90 | 91 | if (null !== error) { 92 | return $q.reject(error); 93 | } 94 | 95 | /*jshint eqeqeq:false */ 96 | var foundItem = findItem(requestData); 97 | /*jshint eqeqeq:true */ 98 | if (foundItem !== undefined) { 99 | return { 100 | status: 200, 101 | statusText: 'OK', 102 | data: angular.copy(foundItem) 103 | }; 104 | } else { 105 | // return 404 106 | error = utils.getErrorByCode(404); 107 | return $q.reject(error); 108 | } 109 | } 110 | 111 | /** 112 | * Return first matching object from mockData. See query function for 113 | * what the definition of a match is 114 | * 115 | * @param queryObject 116 | * @returns {*} 117 | */ 118 | function processQuery(queryObject) { 119 | var keys = R.keys(queryObject); 120 | // From mockData, create new objects that only include properties 121 | // also present in queryObject 122 | var comparisonData = R.map(R.pick(keys))(mockData); 123 | // Find first object in comparisonData that matches our queryObject 124 | var foundIndex = R.findIndex(R.eqDeep(queryObject))( 125 | comparisonData); 126 | var foundItem = mockData[foundIndex]; 127 | if (foundIndex !== -1) { 128 | return { 129 | status: 200, 130 | statusText: 'OK', 131 | data: angular.copy(foundItem) 132 | }; 133 | } else { 134 | var error = utils.getErrorByCode(404); 135 | return $q.reject(error); 136 | } 137 | } 138 | 139 | /** 140 | * Randomly generate an error on create. If no error is generated 141 | * it will add the new item to the mock data array. 142 | * 143 | * @param data The new data item to be created. 144 | * @param createIdFn A function that contains logic to provide a new id. 145 | * This is done in case the ids are alphanumberic or not straight forward 146 | * to increment. 147 | * 148 | * @returns {*} An error or null. 149 | */ 150 | function processCreate(data, createIdFn) { 151 | var error = produceError(data, 'post'); 152 | 153 | if (null !== error) { 154 | return $q.reject(error); 155 | } 156 | 157 | if (createIdFn) { 158 | data.id = createIdFn(); 159 | } 160 | /** 161 | * TODO: Add a location header with the new id. 162 | * Though that would be weird because HackStack assumes you're using an 163 | * abstraction that makes requests to your backend 164 | */ 165 | mockData.push(data); 166 | setGoodGET(null, mockData); 167 | return goodPOST(); 168 | } 169 | 170 | function processUpdate(requestData, data) { 171 | var error = produceError(data, 'post'); 172 | 173 | if (null !== error) { 174 | return $q.reject(error); 175 | } 176 | 177 | var index = R.findIndex(makeCompareFn(requestData))(mockData); 178 | 179 | if (index > -1) { 180 | mockData[index] = data; 181 | return { 182 | status: 200, 183 | statusText: 'OK', 184 | data: '' 185 | }; 186 | 187 | } else { 188 | return $q.reject(getErrorByCode(404)); 189 | } 190 | } 191 | 192 | function getAll() { 193 | return waitForTime().then(function () { 194 | return $q.when(processGETAll()); 195 | }); 196 | } 197 | 198 | function get(requestData) { 199 | return waitForTime().then(function () { 200 | return $q.when(processGET(requestData)); 201 | }); 202 | } 203 | 204 | /** 205 | * Query mock data with a query object. 206 | * 207 | * A data object matches the query object if all the properties in the 208 | * query object (queryObject) have matching properties in the data object. 209 | * 210 | * For example, if {1:1} is our query object, it matches {1:1} and 211 | * {1:1, 2:2} but NOT {1:2} or {2:2} 212 | * 213 | * @param queryObject 214 | * @returns {*} 215 | */ 216 | function query(queryObject) { 217 | return waitForTime().then(function () { 218 | return $q.when(processQuery(queryObject)); 219 | }); 220 | } 221 | 222 | function update(requestData, data) { 223 | return waitForTime().then(function () { 224 | return $q.when(processUpdate(requestData, data)); 225 | }); 226 | } 227 | 228 | function getNextId() { 229 | return R.max(R.pluck('id', mockData)) + 1; 230 | } 231 | 232 | function create(data, createIdFn) { 233 | return waitForTime().then(function () { 234 | return $q.when(processCreate(data, createIdFn)); 235 | }); 236 | } 237 | 238 | function save(data, createIdFn) { 239 | createIdFn = createIdFn || getNextId; 240 | if (data.id) { 241 | return update(data.id, data); 242 | } else { 243 | return create(data, createIdFn); 244 | } 245 | } 246 | 247 | return { 248 | create: create, 249 | disableRandomErrors: disableRandomErrors, 250 | forceError: utils.forceError, 251 | get: get, 252 | getAll: getAll, 253 | query: query, 254 | save: save, 255 | update: update 256 | }; 257 | } 258 | 259 | return createMock; 260 | }); 261 | -------------------------------------------------------------------------------- /src/hackstack/hackstack-mock-service.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $timeout; 4 | var hs; 5 | var $q; 6 | var $http; 7 | var $httpBackend; 8 | var mockObject1 = { 9 | id: 1, 10 | title: 'My Mock Task', 11 | description: 'The description', 12 | tags: ['urgent'] 13 | }; 14 | var mockObject2 = { 15 | id: 7, 16 | title: 'my own task', 17 | description: 'this is the test', 18 | tags: ['easy'] 19 | }; 20 | 21 | describe('Hack Stack Mock tests', function () { 22 | beforeEach(function () { 23 | module('hackstack'); 24 | }); 25 | beforeEach(inject(function (_hackstack_, _$timeout_, _$q_, _$httpBackend_, 26 | _$http_) { 27 | hs = _hackstack_.mock; 28 | $timeout = _$timeout_; 29 | $q = _$q_; 30 | $http = _$http_; 31 | $httpBackend = _$httpBackend_; 32 | $httpBackend.whenGET('mock.json').respond( 33 | [mockObject1] 34 | ); 35 | })); 36 | 37 | afterEach(function () { 38 | $httpBackend.verifyNoOutstandingExpectation(); 39 | $httpBackend.verifyNoOutstandingRequest(); 40 | }); 41 | 42 | describe('GET tests', function () { 43 | it('Should throw an error if created with no data', function () { 44 | expect(function () { 45 | hs(); 46 | }).to.throw(); 47 | }); 48 | 49 | it('should throw an error when created with an object', function () { 50 | expect(function () { 51 | hs({ 52 | id: 1 53 | }); 54 | }).to.throw(); 55 | }); 56 | 57 | it('should return an object with a getAll function when ' + 58 | 'created with an array', 59 | function () { 60 | expect(hs([{ 61 | id: 1 62 | }]).getAll).to.be.an.instanceOf(Function); 63 | }); 64 | 65 | it('should return an object with a getAll function when ' + 66 | 'created with a json path', 67 | function () { 68 | expect(hs('mock.json').getAll).to.be.an.instanceOf(Function); 69 | $httpBackend.flush(); 70 | $timeout.flush(); 71 | }); 72 | 73 | it('should return the mock data it is created with', function () { 74 | var expectedResults = { 75 | status: 200, 76 | statusText: 'OK', 77 | data: [{ 78 | id: 1, 79 | title: 'mock', 80 | description: 'description' 81 | }] 82 | }; 83 | var hack = hs(expectedResults.data); 84 | hack.disableRandomErrors(true); 85 | 86 | expect(hack.getAll()) 87 | .to.eventually.be.deep.equal(expectedResults); 88 | $timeout.flush(); 89 | }); 90 | 91 | it('should call $http.get when you pass a json filename', function () { 92 | var expectedResults = { 93 | status: 200, 94 | statusText: 'OK', 95 | data: [mockObject1] 96 | }; 97 | $httpBackend.expectGET('mock.json'); 98 | var hack = hs('mock.json'); 99 | hack.disableRandomErrors(true); 100 | $httpBackend.flush(); 101 | var result = hack.getAll(); 102 | result.then(function (response) { 103 | expect(response.status).to.equal(200); 104 | expect(response.data).to.deep.equal(expectedResults.data); 105 | }); 106 | $timeout.flush(); 107 | }); 108 | 109 | it('Should return an error when forceError is set', function () { 110 | var expectedResults = { 111 | status: 404, 112 | statusText: 'Not found', 113 | data: 'Not found -- generated by HackStack' 114 | }; 115 | var hack = hs('mock.json'); 116 | hack.forceError(404); 117 | $httpBackend.flush(); 118 | var result = hack.getAll(); 119 | result.then(function () { 120 | throw ('Promise should reject'); 121 | }, function (response) { 122 | expect(response.status).to.equal(404); 123 | expect(response.data).to.equal(expectedResults.data); 124 | }); 125 | $timeout.flush(); 126 | }); 127 | 128 | it('Should have independent results if multiple objects created', 129 | function () { 130 | var expectedData1 = [mockObject1]; 131 | var expectedData2 = [mockObject2]; 132 | var hack1 = hs([mockObject1]); 133 | var hack2 = hs([mockObject2]); 134 | var result1 = hack1.getAll(); 135 | var result2 = hack2.getAll(); 136 | $timeout.flush(); 137 | 138 | expect(result1).to.eventually 139 | .have.property('data') 140 | .and.deep.equal(expectedData1); 141 | expect(result2).to.eventually 142 | .have.property('data') 143 | .and.deep.equal(expectedData2); 144 | }); 145 | 146 | it('should return a single result when calling get(id)', function () { 147 | var expectedResults = [mockObject2, mockObject1]; 148 | 149 | var hack = hs(expectedResults); 150 | hack.disableRandomErrors(true); 151 | var result = hack.getAll(); 152 | var singleResult = hack.get(1); 153 | 154 | expect(result).to.eventually.have.property('data').and.deep.equal( 155 | expectedResults); 156 | expect(singleResult).to.eventually.have.property('data').and.deep 157 | .equal(expectedResults[1]); 158 | $timeout.flush(); 159 | }); 160 | 161 | it( 162 | 'should return response{status: error, ...} if forceError(error) called', 163 | function () { 164 | var expectedResults = { 165 | status: 404, 166 | statusText: 'Not found', 167 | data: 'Not found -- generated by HackStack' 168 | }; 169 | var hack = hs('mock.json'); 170 | $httpBackend.flush(); 171 | hack.forceError(404); 172 | 173 | var result = hack.get(1); 174 | result.then(null, function (response) { 175 | expect(response.status).to.equal(404); 176 | expect(response.data).to.equal(expectedResults.data); 177 | }); 178 | $timeout.flush(); 179 | }); 180 | 181 | it('should return 404 if id not in mockData', function () { 182 | var mockData = [{ 183 | 'id': 0, 184 | 'data': 0 185 | }, { 186 | 'id': 1, 187 | 'data': 1 188 | }]; 189 | var hack = hs(mockData); 190 | hack.disableRandomErrors(true); 191 | var response = hack.get(2); 192 | response.then(function (i) { 193 | throw new Error('should have rejected promise'); 194 | }, function (response) { 195 | expect(response.status).to.equal(404); 196 | }); 197 | $timeout.flush(); 198 | }); 199 | 200 | it( 201 | 'should return the right object if options.makeCompareFn is defined', 202 | function () { 203 | var tag = mockObject1.tags[0]; 204 | var hack = hs([mockObject1, mockObject2], { 205 | makeCompareFn: function (requestData) { 206 | return R.compose(R.contains(requestData), function ( 207 | obj) { 208 | return obj.tags; 209 | }); 210 | } 211 | }); 212 | hack.disableRandomErrors(true); 213 | hack.get(tag).then(function (result) { 214 | expect(result).to.have.property('data').and.deep.equal( 215 | mockObject1); 216 | }, function (i) { 217 | console.log(i); 218 | throw new Error('Expected promise to resolve'); 219 | }); 220 | $timeout.flush(); 221 | }); 222 | }); 223 | 224 | describe('CREATE tests', function () { 225 | it('should return a successful creation', function () { 226 | var hack = hs( 227 | [mockObject1]); 228 | var newTask = mockObject2; 229 | var expected = { 230 | status: 201, 231 | statusText: 'Created', 232 | data: '' 233 | }; 234 | 235 | hack.disableRandomErrors(true); 236 | var result = hack.create(newTask); 237 | expect(result).to.eventually.deep.equal(expected); 238 | expect(result).to.eventually.have.property('status').and.equal( 239 | 201); 240 | $timeout.flush(); 241 | 242 | //getAll should return the new item. 243 | var getResult = hack.getAll(); 244 | getResult.then(function (response) { 245 | expect(response.status).to.equal(200); 246 | expect(response.data.length).to.equal(2); 247 | expect(response.data[1]).to.deep.equal(newTask); 248 | }); 249 | $timeout.flush(); 250 | }); 251 | 252 | it('should create an id when provided a function', function () { 253 | var hack = hs( 254 | [{ 255 | id: 1, 256 | title: 'task', 257 | description: 'mock task' 258 | }]); 259 | var newTask = { 260 | title: 'add task', 261 | description: 'to be added' 262 | }; 263 | var expected = { 264 | status: 201, 265 | statusText: 'Created', 266 | data: '' 267 | }; 268 | 269 | hack.disableRandomErrors(true); 270 | var result = hack.create(newTask, function () { 271 | return 2; 272 | }); 273 | expect(result).to.eventually.deep.equal(expected); 274 | expect(result).to.eventually.have.property('status').and.equal( 275 | 201); 276 | $timeout.flush(); 277 | 278 | //getAll should return the new item. 279 | var getResult = hack.getAll(); 280 | getResult.then(function (response) { 281 | expect(response.status).to.equal(200); 282 | expect(response.data.length).to.equal(2); 283 | expect(response.data[1].id).to.equal(2); 284 | }); 285 | $timeout.flush(); 286 | }); 287 | }); 288 | 289 | describe('Update tests', function () { 290 | it('should return a 200 if the data is in the mock data array' + 291 | 'and update the entry', 292 | function () { 293 | var hack = hs( 294 | [{ 295 | id: 1, 296 | title: 'task', 297 | description: 'mock task' 298 | }]); 299 | var expected = { 300 | status: 200, 301 | statusText: 'OK', 302 | data: '' 303 | }; 304 | hack.disableRandomErrors(true); 305 | var result = hack.update(1, { 306 | id: 1, 307 | title: 'updated task', 308 | description: 'an update' 309 | }); 310 | result.then(function (response) { 311 | expect(response.status).to.equal(200); 312 | }); 313 | $timeout.flush(); 314 | 315 | var getResult = hack.getAll(); 316 | getResult.then(function (response) { 317 | expect(response.status).to.equal(200); 318 | expect(response.data.length).to.equal(1); 319 | expect(response.data[0].title).to.equal('updated task'); 320 | }); 321 | $timeout.flush(); 322 | }); 323 | 324 | it('should return a 404 if item is not found in mockData', function () { 325 | var hack = hs( 326 | [{ 327 | id: 1, 328 | title: 'task', 329 | description: 'mock task' 330 | }]); 331 | var expected = { 332 | status: 404, 333 | statusText: 'Not Found', 334 | data: 'Forced error by hackStack' 335 | }; 336 | hack.disableRandomErrors(true); 337 | var result = hack.update(2, { 338 | id: 2, 339 | title: 'updated task', 340 | description: 'an update' 341 | }); 342 | result.then(null, function (error) { 343 | expect(error.status).to.equal(404); 344 | }); 345 | $timeout.flush(); 346 | }); 347 | }); 348 | 349 | describe('query tests', function () { 350 | var simon1 = { 351 | 'id': 1, 352 | 'name': 'Simon', 353 | 'age': 20 354 | }; 355 | 356 | var simon2 = angular.copy(simon1); 357 | simon2.id = 2; 358 | simon2.age = 2000; 359 | 360 | it('should return the right object', function () { 361 | var hack = hs([simon1, simon2]); 362 | hack.query({ 363 | 'name': simon1.name, 364 | 'age': simon1.age 365 | }) 366 | .then(function (response) { 367 | expect(response.data).to.deep.equal(simon1); 368 | }); 369 | $timeout.flush(); 370 | }); 371 | 372 | it('should return 404 if object not in data', function () { 373 | var hack = hs([simon1, simon2]); 374 | hack.query({ 375 | 'name': 'bob', 376 | 'age': simon1.age 377 | }) 378 | .then(function () { 379 | throw new Error('Expected promise to reject'); 380 | }, 381 | function (response) { 382 | expect(response.status).to.equal(404); 383 | expect(response.statusText).to.equal('Not found'); 384 | expect(response.data).to.equal( 385 | 'Not found -- generated by HackStack'); 386 | }); 387 | $timeout.flush(); 388 | }); 389 | }); 390 | 391 | }); 392 | -------------------------------------------------------------------------------- /src/hackstack/hackstack-mockfh-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Angular claims services are singletons, but mockEndpoints is not preserved 3 | // across separate mockFH instances. 4 | var mockEndpoints = {}, 5 | loggingEnabled = false; 6 | 7 | angular.module('hackstack.feedhenry', [ 8 | 'hackstack.mock' 9 | ]) 10 | .factory('mockFH', function (mock, $window, $log) { 11 | var mockFHObject = Object.create($window.$fh || null); 12 | 13 | function logger() { 14 | if (loggingEnabled) { 15 | $log.info.apply(null, arguments); 16 | } 17 | } 18 | 19 | /** 20 | * Takes a response object and wraps it so it looks like a response from 21 | * feedhenry 22 | * 23 | * @param {Object} res 24 | * @returns {{status: number, statusText: string, data: {status: string, data: *}}} 25 | */ 26 | function wrapBackendResponse(res) { 27 | var data; 28 | if (res.status === 200) { 29 | data = res.data; 30 | } else { 31 | data = res; 32 | } 33 | 34 | return { 35 | 'status': 200, 36 | 'statusText': 'OK', 37 | // top level data (feedhenry's response) 38 | 'data': { 39 | 'status': 'Ok', 40 | // backend data 41 | 'data': data 42 | } 43 | }; 44 | } 45 | 46 | /** 47 | * add a mock endpoint 48 | * 49 | * @param {String} endpoint path to this endpoint 50 | * @param {Array} mockData array of objects from which a response is 51 | * created. See hackstack.mock documentation 52 | */ 53 | mockFHObject.addMockEndpoint = function addMockEndpoint(endpoint, 54 | mockData, options) { 55 | if (mockData.length === 0 || !Array.isArray(mockData)) { 56 | throw new Error('Expected mockData to be a non-empty array'); 57 | } else if (mockEndpoints[endpoint] !== undefined) { 58 | throw new Error('Endpoint already defined: '.concat(endpoint)); 59 | } 60 | options = options || {}; 61 | options.makeCompareFn = options.makeCompareFn || function ( 62 | requestData) { 63 | return function (targetData) { 64 | return true; 65 | }; 66 | }; 67 | var hs = mock(mockData, options); 68 | hs.disableRandomErrors(true); 69 | mockEndpoints[endpoint] = hs; 70 | }; 71 | 72 | /** 73 | * Throw if invalid or missing options 74 | * 75 | * @param options 76 | */ 77 | function validateFHOptions(options) { 78 | if (options.method !== 'GET' && options.method !== 'POST') { 79 | throw new Error('Unsupported method or method wasn\'t provided'); 80 | } 81 | if (options.data === undefined) { 82 | throw new Error('options.data not provided'); 83 | } 84 | } 85 | 86 | /** 87 | * Mimic a request to our mock backend 88 | * 89 | * @param {Object} options See feedhenry v3 documentation 90 | * @param {Function} successCallback will be called with the response data 91 | * object as the first and only argument 92 | * @param {Function} failureCallback currently not used 93 | */ 94 | function mockCloudFn(options, successCallback, failureCallback) { 95 | var hs; 96 | var backendResponse; 97 | 98 | validateFHOptions(options); 99 | 100 | hs = mockEndpoints[options.path]; 101 | 102 | if (options.method === 'POST') { 103 | hs.save(options.data); 104 | } 105 | 106 | backendResponse = hs.get(options.data).then(function (result) { 107 | logger('Request: ', options, '\nResponse: ', result); 108 | return result; 109 | }); 110 | 111 | // wrap the backend response so it looks like a $fh res object 112 | backendResponse.then( 113 | function (result) { 114 | successCallback(wrapBackendResponse(result).data); 115 | }, 116 | // Because feedhenry returns 200 even if the backend fails, we'll call 117 | // the success feedback if the promise rejects 118 | function (result) { 119 | successCallback(wrapBackendResponse(result).data); 120 | }); 121 | } 122 | 123 | /** 124 | * cloud calls mockCloudFn or $window.$fh.cloud based on whether 125 | * an endpoint is defined (don't use mock if endpoint is not defined) 126 | * 127 | * @param {Object} options 128 | * @param {Function} successCallback 129 | * @param {Function} failureCallback 130 | * @returns {*} 131 | */ 132 | mockFHObject.cloud = function cloud(options, successCallback, 133 | failureCallback) { 134 | // if endpoint is not defined, don't use mock 135 | if (mockEndpoints[options.path] === undefined) { 136 | return $window.$fh.cloud(options, successCallback, failureCallback); 137 | } else { 138 | return mockCloudFn(options, successCallback, failureCallback); 139 | } 140 | }; 141 | 142 | /** 143 | * Returns a new mockFH object whose backend will return the error 144 | * specified in errorCode 145 | * 146 | * @param errorCode 147 | * @returns {mockFHObject} 148 | */ 149 | mockFHObject.forceBackendError = function (errorCode) { 150 | var newMockFH = Object.create(mockFHObject); 151 | newMockFH.cloud = function cloudForceError(options, successCallback, 152 | failureCallback) { 153 | var hs = mockEndpoints[options.path]; 154 | 155 | /** 156 | * Don't ask hackstack.mock to return an error if endpoint is not 157 | * defined as that means we will not use the mock for this call. 158 | * Without this, the next call to the mock might inadvertently return 159 | * an error even if it's not supposed to 160 | */ 161 | if (hs !== undefined) { 162 | validateFHOptions(options); 163 | hs.forceError(errorCode); 164 | } 165 | return mockFHObject.cloud.apply(mockFHObject, arguments); 166 | }; 167 | return newMockFH; 168 | }; 169 | 170 | mockFHObject.enableLogging = function enableLogging() { 171 | loggingEnabled = true; 172 | }; 173 | 174 | mockFHObject.disableLogging = function disableLogging() { 175 | loggingEnabled = false; 176 | }; 177 | 178 | mockFHObject.listEndpoints = function listEndpoints() { 179 | return R.keys(mockEndpoints); 180 | }; 181 | 182 | $window.hsUtils.mockFH = { 183 | 'listEndpoints': mockFHObject.listEndpoints, 184 | 'enableLogging': mockFHObject.enableLogging, 185 | 'disableLogging': mockFHObject.disableLogging 186 | }; 187 | 188 | return mockFHObject; 189 | }); 190 | -------------------------------------------------------------------------------- /src/hackstack/hackstack-mockfh-service.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function identityFn(x) { 4 | return x; 5 | } 6 | 7 | var $timeout, fh, $q, $httpBackend, $window, path, cloudSpy, defaultOptions; 8 | var mockData1 = { 9 | 'title': 'mock title 1', 10 | 'id': 1 11 | }; 12 | var mockData2 = { 13 | 'title': 'mock title 2', 14 | 'id': 2 15 | }; 16 | var endpoint = 'endpoint'; 17 | var incompleteObject = { 18 | 'id': 0, 19 | 'description': 'mock description' 20 | }; 21 | // Origin takes precedence 22 | var mockObject1 = R.merge(mockData1, incompleteObject); 23 | var mockObject2 = R.merge(mockData2, incompleteObject); 24 | 25 | describe('Hack Stack FeedHenry tests', function () { 26 | var pathCounter = 0; 27 | 28 | function generateNewPath() { 29 | pathCounter += 1; 30 | return '/path'.concat(pathCounter).concat('/endpoint'); 31 | } 32 | 33 | beforeEach(function () { 34 | module('hackstack.feedhenry'); 35 | }); 36 | 37 | beforeEach(inject(function (_mockFH_, _$timeout_, _$q_, _$window_, 38 | _$httpBackend_) { 39 | fh = _mockFH_; 40 | $timeout = _$timeout_; 41 | $q = _$q_; 42 | $window = _$window_; 43 | $httpBackend = _$httpBackend_; 44 | })); 45 | 46 | beforeEach(inject(function (_mockFH_) { 47 | cloudSpy = sinon.spy(); 48 | path = generateNewPath(); 49 | 50 | fh.addMockEndpoint(path, [mockObject1, mockObject2]); 51 | $window.$fh = { 52 | 'cloud': cloudSpy 53 | }; 54 | })); 55 | 56 | afterEach(function () { 57 | $httpBackend.verifyNoOutstandingExpectation(); 58 | $httpBackend.verifyNoOutstandingRequest(); 59 | $timeout.verifyNoPendingTasks(); 60 | }); 61 | 62 | describe('GET tests', function () { 63 | 64 | beforeEach(function () { 65 | defaultOptions = { 66 | 'path': path, 67 | 'method': 'GET', 68 | 'data': {} 69 | }; 70 | }); 71 | 72 | it('should return mock data in response', function () { 73 | var response = fh.cloud(defaultOptions, function (res) { 74 | expect(res.data).to.deep.equal(mockObject1); 75 | }); 76 | $timeout.flush(); 77 | }); 78 | 79 | it('should return data(status=Ok)', function () { 80 | var response = fh.cloud(defaultOptions, function (res) { 81 | expect(res.status).to.equal('Ok'); 82 | }); 83 | $timeout.flush(); 84 | }); 85 | 86 | it('should call $fh.cloud if endpoint not defined', function () { 87 | var options = { 88 | 'method': 'POST', 89 | 'path': 'undefined/endpoint', 90 | 'data': 'test' 91 | }; 92 | var callback = function () {}; 93 | fh.cloud(options, callback); 94 | expect(cloudSpy).to.have.been.calledWith(options, callback); 95 | }); 96 | 97 | it('should return a backend error if mockFH.forceBackendError()', 98 | function () { 99 | var errorCode = 404; 100 | var mockFHError = fh.forceBackendError(errorCode); 101 | mockFHError.cloud(defaultOptions, function (res) { 102 | expect(res.status).to.equal('Ok'); 103 | expect(res.data.status).to.equal(errorCode); 104 | }); 105 | $timeout.flush(); 106 | }); 107 | }); 108 | 109 | describe('sanity checks', function () { 110 | it('should throw if endpoint defined twice', function () { 111 | var options = { 112 | 'path': path, 113 | 'method': 'GET', 114 | 'data': {} 115 | }; 116 | expect(function () { 117 | fh.addMockEndpoint(options.path, [{}]); 118 | }).to.throw(); 119 | }); 120 | 121 | it('should only accept GET or POST', function () { 122 | var getOptions, postOptions, headOptions; 123 | getOptions = { 124 | 'path': path, 125 | 'method': 'GET', 126 | 'data': {} 127 | }; 128 | postOptions = angular.copy(getOptions); 129 | postOptions.method = 'POST'; 130 | headOptions = angular.copy(getOptions); 131 | headOptions.method = 'HEAD'; 132 | 133 | expect(function () { 134 | fh.cloud(getOptions, function () {}); 135 | }).to.not.throw(); 136 | expect(function () { 137 | fh.cloud(postOptions, function () {}); 138 | }).to.not.throw(); 139 | expect(function () { 140 | fh.cloud(headOptions, function () {}); 141 | }).to.throw(); 142 | 143 | $timeout.flush(); 144 | }); 145 | 146 | it('should throw if options.data === undefined', function () { 147 | var options = { 148 | 'path': path, 149 | 'method': 'GET' 150 | }; 151 | expect(function () { 152 | fh.cloud(options, identityFn); 153 | }).to.throw(); 154 | }); 155 | 156 | it('should not set an error if endpoint not defined', function () { 157 | var options = { 158 | 'path': 'undefined/endpoint', 159 | 'method': 'GET', 160 | 'data': {} 161 | }; 162 | fh.forceBackendError(404).cloud(options, identityFn, 163 | identityFn); 164 | options.path = path; 165 | fh.cloud(options, function (res) { 166 | expect(res.status).to.equal('Ok'); 167 | }); 168 | $timeout.flush(); 169 | }); 170 | 171 | it('should throw if given an empty array', function () { 172 | expect(function () { 173 | fh.addMockEndpoint(generateNewPath(), []); 174 | }).to.throw(); 175 | }); 176 | 177 | it('should throw if not given an array', function () { 178 | expect(function () { 179 | fh.addMockEndpoint(generateNewPath(), 1); 180 | }).to.throw(); 181 | 182 | expect(function () { 183 | fh.addMockEndpoint(generateNewPath(), 'string'); 184 | }).to.throw(); 185 | }); 186 | 187 | it('should set property $window.hsUtils.mockFH.listEndpoints,' + 188 | 'which lists mock endpoints', 189 | function () { 190 | var newPath = generateNewPath(); 191 | var listEndpoints = $window.hsUtils.mockFH.listEndpoints; 192 | var oldCount = listEndpoints().length; 193 | expect(R.contains(newPath)(listEndpoints())).to.be.false; 194 | fh.addMockEndpoint(newPath, ['data']); 195 | expect(R.contains(newPath)(listEndpoints())).to.be.true; 196 | expect(listEndpoints().length).to.equal(oldCount + 1); 197 | }); 198 | 199 | it('should use options.makeCompareFn if provided', function () { 200 | path = generateNewPath(); 201 | var options = angular.copy(defaultOptions); 202 | options.path = path; 203 | options.data = mockObject2.title; 204 | fh.addMockEndpoint(path, [mockObject1, mockObject2], { 205 | 'makeCompareFn': function (requestData) { 206 | return function (targetData) { 207 | return targetData.title === requestData; 208 | }; 209 | } 210 | }); 211 | fh.cloud(options, function (res) { 212 | expect(res.data).to.deep.equal(mockObject2); 213 | }); 214 | $timeout.flush(); 215 | }); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /src/hackstack/hackstack-utils-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('hackstack.utils', []) 3 | 4 | .factory('utils', function ($timeout, $window) { 5 | /** 6 | * A random number will be generated between 0 and MAX_ERROR_DISTRIBUTION. 7 | * The number generated will be used to determine which error will be produced. 8 | * Note when defining distributions of errors that you need to leave room 9 | * for a clean return. So try not to make your distributions add up to this 10 | * number. 11 | * 12 | * 100 - sum_of_distributions = the chance of a clean return. 13 | */ 14 | var MAX_ERROR_DISTRIBUTION = 100; 15 | /** 16 | * Set a specific error to be returned. Pass in the HTTP error code. 17 | * 18 | * @type {Number} The HTTP Error code to return. 19 | */ 20 | var nextError = null; 21 | 22 | /** 23 | * Set to ensure you get a 200 return from the the API. This will 24 | * bypass the random error generation. 25 | * 26 | * @type {boolean} False to produce errors, true to prevent errors. 27 | */ 28 | var errorsDisabled = false; 29 | 30 | /** 31 | * The list of error trigger functions that will be used to compare against 32 | * incoming or outgoing data. 33 | */ 34 | var errorTriggers = []; 35 | 36 | /** 37 | * This function is only for testing purposes. You SHOULD NOT USE IT! 38 | */ 39 | function _getErrorTriggers() { 40 | return errorTriggers; 41 | } 42 | 43 | var defaults = { 44 | maxTime: 2000, 45 | minTime: 0, 46 | absoluteTime: null 47 | }; 48 | 49 | var options = defaults; 50 | 51 | /** 52 | * The default list of errors to be randomly produced. 53 | * Contains three properties: 54 | * status: The HTTP status code of the error. 55 | * statusText: The status text associated with the error. 56 | * distribution: The chance out of 100 that the error will occure. 57 | * i.e. a 5 means the error will be produced 5 percent of the time. 58 | * 59 | * @type {*[]} An array of error objects. 60 | */ 61 | var errors = [{ 62 | status: 0, //Dropped connection 63 | statusText: '', 64 | distribution: 5 65 | }, { 66 | status: 400, 67 | statusText: 'Bad request', 68 | distribution: 1 69 | }, { 70 | status: 401, 71 | statusText: 'Not authorized', 72 | distribution: 3 73 | }, { 74 | status: 403, 75 | statusText: 'Forbidden', 76 | distribution: 3 77 | }, { 78 | status: 404, 79 | statusText: 'Not found', 80 | distribution: 6 81 | }, { 82 | status: 405, 83 | statusText: 'Method not allowed', 84 | distribution: 2 85 | }, { 86 | status: 406, 87 | statusText: 'Not acceptable', 88 | distribution: 2 89 | }, { 90 | status: 407, 91 | statusText: 'Proxy Authentication Required', 92 | distribution: 0 93 | }, { 94 | status: 408, 95 | statusText: 'Request timeout', 96 | distribution: 2 97 | }, { 98 | status: 409, 99 | statusText: 'Conflict', 100 | distribution: 1 101 | }, { 102 | status: 410, 103 | statusText: 'Gone', 104 | distribution: 1 105 | }, { 106 | status: 411, 107 | statusText: 'Length required', 108 | distribution: 1 109 | }, { 110 | status: 412, 111 | statusText: 'Precondition Failed', 112 | distribution: 1 113 | }, { 114 | status: 413, 115 | statusText: 'Request entity too large', 116 | distribution: 1 117 | }, { 118 | status: 414, 119 | statusText: 'Request-URI too long', 120 | distribution: 1 121 | }, { 122 | status: 415, 123 | statusText: 'Unsupported media type', 124 | distribution: 1 125 | }, { 126 | status: 416, 127 | statusText: 'Requested range not satisfiable', 128 | distribution: 1 129 | }, { 130 | status: 417, 131 | statusText: 'Expectation failed', 132 | distribution: 1 133 | }, { 134 | status: 500, 135 | statusText: 'Internal server error', 136 | distribution: 5 137 | }, { 138 | status: 501, 139 | statusText: 'Not implemented', 140 | distribution: 1 141 | }, { 142 | status: 502, 143 | statusText: 'Bad gateway', 144 | distribution: 0 145 | }, { 146 | status: 503, 147 | statusText: 'Service unavailable', 148 | distribution: 1 149 | }, { 150 | status: 504, 151 | statusText: 'Gateway timeout', 152 | distribution: 0 153 | }, { 154 | status: 505, 155 | statusText: 'HTTP version not supported', 156 | distribution: 0 157 | }]; 158 | 159 | /** 160 | * Set whether or not HackStack should randomly produce server errors 161 | * 162 | * @param {boolean} disabled true to disable errors, false (default) 163 | * otherwise. 164 | * @returns {boolean} If called without a parameter, acts as a getter. 165 | */ 166 | function disableRandomErrors(disabled) { 167 | if (disabled || disabled === false) { 168 | errorsDisabled = disabled; 169 | } else { 170 | return errorsDisabled; 171 | } 172 | } 173 | 174 | /** 175 | * Retrieve the full error object from the errors array. 176 | * 177 | * @param errorCode The HTTP error code to be retrieved. 178 | * @returns {*} The object as is appears in the errors Array. 179 | */ 180 | function getErrorByCode(errorCode) { 181 | if (typeof errorCode !== 'number') { 182 | throw new Error('Must provide an integer error code'); 183 | } 184 | 185 | var error = R.filter(function (errorItem) { 186 | return errorItem.status === errorCode; 187 | }, errors); 188 | 189 | if (error.length === 0 || error.length > 1) { 190 | return false; 191 | } 192 | return cleanError(error[0]); 193 | } 194 | 195 | /** 196 | * Set the error code to the desired HTTP error. 197 | * 198 | * @param errorCode 199 | */ 200 | function forceError(errorCode) { 201 | if (errorCode === null) { 202 | nextError = null; 203 | } else if (getErrorByCode(errorCode) !== false) { 204 | nextError = errorCode; 205 | } else { 206 | throw new Error('Unsupported HTTP Code'); 207 | } 208 | } 209 | 210 | /** 211 | * Cleans the entry in the error array to the actual return doesn't 212 | * contain unwanted information. 213 | * 214 | * @param error The error from the error aray. 215 | * @returns {{status: *, statusText: *, data: string}} The mock $HTTP 216 | * return object. 217 | * 218 | */ 219 | function cleanError(error) { 220 | return { 221 | status: error.status, 222 | statusText: error.statusText, 223 | data: error.statusText.concat(' -- generated by HackStack') 224 | }; 225 | } 226 | 227 | /** 228 | * Generate a random integer. 229 | * 230 | * @param min The minimum number you want to see. 231 | * @param max The highest number you want to see. 232 | * 233 | * @returns {*} A random integer within the specified range. 234 | */ 235 | function randomInt(min, max) { 236 | return Math.floor(Math.random() * (max - min)) + min; 237 | } 238 | 239 | /** 240 | * Produce a random HTTP error from the 400 or 500 series errors. The 241 | * errors come from the internal list of possible errors by default and have 242 | * weights assigned to them that indicate the relative frequency that 243 | * the error should occur. 244 | * 245 | * @param errorArray The list of possible errors to choose from. Defaults 246 | * to the internal list of errors. 247 | * 248 | * @returns {*} A object representing an HTTP error or null if there is no 249 | * error. 250 | */ 251 | function randomError(errorArray) { 252 | errorArray = errorArray || errors; 253 | 254 | var totalWeight = R.reduce(function (acc, value) { 255 | return acc + value.distribution; 256 | }, 0, errorArray); 257 | 258 | if (totalWeight > MAX_ERROR_DISTRIBUTION) { 259 | throw new Error( 260 | 'Sum of distributions is greater than defined max'); 261 | } 262 | 263 | var randomNumber = randomInt(0, MAX_ERROR_DISTRIBUTION); 264 | var error = null; 265 | var weightedSum = 0; 266 | 267 | if (nextError === null) { 268 | if (errorsDisabled === false) { 269 | R.forEach(function (item) { 270 | weightedSum += item.distribution; 271 | if (randomNumber <= weightedSum && error === null) { 272 | error = cleanError(item); 273 | } 274 | }, errorArray); 275 | } 276 | } else { 277 | return cleanError(getErrorByCode(nextError)); 278 | } 279 | 280 | return error; 281 | } 282 | 283 | /** 284 | * This function will go through the list of error triggers and run 285 | * each of the functions against incoming data. It will return the 286 | * first error found. 287 | * 288 | * @param {*} data The data to be tested. 289 | * @param {String} method The HTTP method used. Determines which 290 | * triggers are used to compare against. 291 | * 292 | * @return {*} An HTTP error object or null if no trigger matches. 293 | */ 294 | function evaluateTriggers(data, method) { 295 | var error = null; 296 | 297 | // catches null and undefined 298 | /*jshint -W116 */ 299 | if (data == null) { 300 | /*jshint +W116 */ 301 | return null; 302 | } 303 | 304 | R.forEach(function (trigger) { 305 | if (trigger.fn(data) === true) { 306 | error = cleanError(getErrorByCode(trigger.errorCode)); 307 | } 308 | }, R.filter(R.propEq('method', method.toLowerCase()), 309 | errorTriggers)); 310 | 311 | return error; 312 | } 313 | 314 | /** 315 | * Helper function that HackStack uses to determine if an error is to 316 | * be returned to the user. 317 | * 318 | * It first checks provided error triggers and if none are fired, 319 | * then it will call a random error generator. 320 | * 321 | * If neither the provided error triggers nor the random error generators 322 | * return an error, it returns null. 323 | * 324 | * @param {*} data The data object to be used in trigger comparisons. 325 | * @param {String} method The HTTP method used to determine which triggers 326 | * are relevant. 327 | * @param {Array} errorArray An array of HTTP error codes that will be used 328 | * to produce random errors. Defaults to the default list of HTTP Errors. 329 | * 330 | * @return {*} And HTTP error object or null. 331 | */ 332 | function produceError(data, method, errorArray) { 333 | var error = evaluateTriggers(data, method); 334 | if (null === error) { 335 | error = randomError(errorArray); 336 | } 337 | return error; 338 | } 339 | 340 | /** 341 | * Users can register error triggers that will compare against data 342 | * being sent to or returned from the server. If the trigger function 343 | * returns true, then the specified error will be returned. 344 | * 345 | * @param {Function} errorFn The error function that must return true 346 | * if the error condition is met. It will be passed one parameter, data. 347 | * @example 348 | * The user posts an object to the server of the form: 349 | * { 350 | * id: 5, 351 | * title: 'bad ticket' 352 | * description: 'something useful' 353 | * } 354 | * The error function looks for objects with bad in the title and returns 355 | * a 404 if it's found. 356 | * function errorTrigger(data) { 357 | * if(data.title && data.title.indexOf('bad') !== -1) { 358 | * return true; 359 | * } 360 | * return false; 361 | * } 362 | * @param errorCode The HTTP error code to return if the trigger is fired. 363 | * @param method The HTTP method when this trigger should be used. valid values 364 | * are: 'get', 'post', 'all'. 365 | * @return {Function} Returns a function that can be used to remove the trigger. 366 | * @example: 367 | * var myTrigger = addErrorTrigger(errorTrigger, 404, 'get'); //Add the trigger. 368 | * myTrigger(); //Remove the trigger. 369 | */ 370 | function addErrorTrigger(errorFn, errorCode, method) { 371 | var validMethods = [ 372 | 'get', 373 | 'post', 374 | 'all' 375 | ]; 376 | var validErrorCodes = R.pluck('status')(errors); 377 | 378 | if (!errorFn || typeof errorFn !== 'function') { 379 | throw new Error('generateError function requires a function' + 380 | ' as its first parameter'); 381 | } 382 | if (!errorCode || R.indexOf(errorCode, validErrorCodes) === -1) { 383 | throw new Error('error code must be one of: ' + 384 | validErrorCodes.toString()); 385 | } 386 | method = method || 'all'; 387 | if (R.indexOf(method.toLowerCase(), validMethods) === -1) { 388 | throw new Error('method must be one of: ' + validMethods.toString()); 389 | } 390 | 391 | var errorTriggerId = 0; 392 | if (errorTriggers.length > 0) { 393 | errorTriggerId = R.max(R.pluck('id', errorTriggers)) + 1; 394 | } 395 | 396 | errorTriggers.push({ 397 | id: errorTriggerId, 398 | fn: errorFn, 399 | errorCode: errorCode, 400 | method: method.toLowerCase() 401 | }); 402 | 403 | return function removeTrigger() { 404 | var myerrorTriggerId = errorTriggerId; 405 | var removeIndex = R.findIndex(R.propEq('id', myerrorTriggerId))( 406 | errorTriggers); 407 | errorTriggers.splice(removeIndex, 1); 408 | }; 409 | 410 | } 411 | 412 | /** 413 | * Add a false latency to any requests made. 414 | * 415 | * @returns {*} True, always. 416 | */ 417 | function waitForTime() { 418 | var time; 419 | if (options.absoluteTime !== null) { 420 | time = options.absoluteTime; 421 | } else { 422 | time = randomInt(options.minTime, options.maxTime); 423 | } 424 | 425 | return $timeout(function () { 426 | return true; 427 | }, time); 428 | } 429 | 430 | function setOptions(newOptions) { 431 | options = R.merge(options, newOptions); 432 | } 433 | 434 | $window.hsUtils = { 435 | forceError: forceError, 436 | disableRandomErrors: disableRandomErrors 437 | }; 438 | 439 | return { 440 | addErrorTrigger: addErrorTrigger, 441 | disableRandomErrors: disableRandomErrors, 442 | forceError: forceError, 443 | getErrorByCode: getErrorByCode, 444 | produceError: produceError, 445 | randomError: randomError, 446 | randomInt: randomInt, 447 | setOptions: setOptions, 448 | waitForTime: waitForTime, 449 | _getErrorTriggers: _getErrorTriggers 450 | }; 451 | }); 452 | -------------------------------------------------------------------------------- /src/hackstack/hackstack-utils-service.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $timeout; 4 | var hsu; 5 | var $window; 6 | var mockData = { 7 | id: 1, 8 | title: 'fire!' 9 | }; 10 | 11 | describe('HackStack common tests', function () { 12 | beforeEach(function () { 13 | module('hackstack'); 14 | }); 15 | beforeEach(inject(function (_hackstack_, _$timeout_, _$window_) { 16 | hsu = _hackstack_.utils; 17 | $timeout = _$timeout_; 18 | $window = _$window_; 19 | })); 20 | 21 | describe('functional tests', function () { 22 | it('should register an error trigger function', function () { 23 | hsu.addErrorTrigger(function (data) { 24 | return true; 25 | }, 404); 26 | expect(hsu._getErrorTriggers().length).to.equal(1); 27 | }); 28 | 29 | it( 30 | 'should remove an error trigger when the removal method is called', 31 | function () { 32 | var removeTrigger = hsu.addErrorTrigger(function (data) { 33 | return true; 34 | }, 404); 35 | expect(hsu._getErrorTriggers().length).to.equal(1); 36 | removeTrigger(); 37 | expect(hsu._getErrorTriggers().length).to.equal(0); 38 | }); 39 | 40 | it('should remove an error from the middle of the trigger list', 41 | function () { 42 | var mockFn = function (data) { 43 | return true; 44 | }; 45 | var rmTrg1 = hsu.addErrorTrigger(mockFn, 404); 46 | var rmTrg2 = hsu.addErrorTrigger(mockFn, 404); 47 | var rmTrg3 = hsu.addErrorTrigger(mockFn, 404); 48 | expect(hsu._getErrorTriggers().length).to.equal(3); 49 | rmTrg2(); 50 | expect(hsu._getErrorTriggers().length).to.equal(2); 51 | 52 | expect(hsu._getErrorTriggers()[0].id).to.equal(0); 53 | expect(hsu._getErrorTriggers()[1].id).to.equal(2); 54 | }); 55 | 56 | it('should produce an error when the function returns true', 57 | function () { 58 | hsu.disableRandomErrors(true); 59 | hsu.addErrorTrigger(function (data) { 60 | if (data.title === 'fire!') { 61 | return true; 62 | } 63 | return false; 64 | }, 404, 'get'); 65 | expect(hsu.produceError(mockData, 'get')) 66 | .to.have.property('status') 67 | .and.equal(404); 68 | }); 69 | 70 | it( 71 | 'should not produce an error if called for different method types', 72 | function () { 73 | hsu.disableRandomErrors(true); 74 | hsu.addErrorTrigger(function (data) { 75 | if (data.title === 'fire!') { 76 | return true; 77 | } 78 | return false; 79 | }, 404, 'get'); 80 | expect(hsu.produceError(mockData, 'post')) 81 | .to.equal(null); 82 | }); 83 | 84 | it( 85 | 'should override the disable error setting if you supply a trigger', 86 | function () { 87 | hsu.disableRandomErrors(true); 88 | hsu.addErrorTrigger(function (data) { 89 | if (data.title === 'fire!') { 90 | return true; 91 | } 92 | return false; 93 | }, 404, 'get'); 94 | expect(hsu.produceError(mockData, 'get')) 95 | .to.have.property('status') 96 | .and.equal(404); 97 | }); 98 | 99 | it('should force an error if random errors are disabled', function () { 100 | hsu.disableRandomErrors(true); 101 | hsu.forceError(405); 102 | var error = hsu.produceError(); 103 | expect(error).to.have.property('status').and.equal(405); 104 | }); 105 | }); 106 | 107 | describe('parameter tests', function () { 108 | it('should throw an error when you do not have a function as' + 109 | ' the first parameter', 110 | function () { 111 | expect(function () { 112 | hsu.addErrorTrigger('fail'); 113 | }).to.throw('generateError function requires a function' + 114 | ' as its first parameter'); 115 | }); 116 | 117 | it('should require an error code parameter', function () { 118 | expect(function () { 119 | hsu.addErrorTrigger(function () { 120 | return true; 121 | }); 122 | }).to.throw(); // I don't compare text because this is a long message. 123 | }); 124 | 125 | it('should require an error code in the known list', function () { 126 | expect(function () { 127 | hsu.addErrorTrigger(function () { 128 | return true; 129 | }, 5); 130 | }).to.throw(); // I don't compare text because this is a long message. 131 | }); 132 | }); 133 | 134 | it('should have a valid method (get, post, or all currently)', function () { 135 | expect(function () { 136 | hsu.addErrorTrigger(function () { 137 | return true; 138 | }, 404, 'bad'); 139 | }).to.throw(); // I don't compare text because this is a long message. 140 | }); 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /src/hackstack/hackstack-wrapper-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('hackstack.wrap', [ 3 | 'hackstack.utils' 4 | ]) 5 | /** 6 | * The hack wrapper wraps an end point with the HackStack error generation 7 | * methods so you get the random errors and mock latency while still talking 8 | * to a real server. 9 | * 10 | * When you get data with the hack wrapper you can merge it will a mock data 11 | * object to fill in either missing or incorrect data from the server. 12 | * 13 | * When you post the data back to the server you can pass in a transform 14 | * function that will change the data to the form expected by the server. 15 | */ 16 | .factory('wrap', function ($q, $http, utils) { 17 | function createWrapper(endpoint, mockData, options) { 18 | if (endpoint === undefined) { 19 | throw new Error('wrapper must be provided with an endpoint'); 20 | } 21 | 22 | if (mockData === undefined) { 23 | throw new Error('wrapper must be provided with mock data'); 24 | } 25 | 26 | if ('object' !== typeof (mockData)) { 27 | throw new Error('mock data must be an object'); 28 | } else if (R.isArrayLike(mockData) === true) { 29 | throw new Error('mock data must be an object, not an array'); 30 | } 31 | 32 | options = options || {}; 33 | options.priorityMock = options.priorityMock || false; 34 | utils.setOptions(options); 35 | var baseEndpoint = endpoint; 36 | 37 | var disableRandomErrors = utils.disableRandomErrors; 38 | var produceError = utils.produceError; 39 | var waitForTime = utils.waitForTime; 40 | 41 | /** 42 | * Merges two objects with the first object passed in taking priority. 43 | * This function will deeply merge the two objects. 44 | * @example 45 | * var obj1 = { 46 | * id: 2, 47 | * name: "brian" 48 | * }; 49 | * var obj2 = { 50 | * id: 1, 51 | * address: { 52 | * street: "John" 53 | * } 54 | * } 55 | * var merged = deepMerge(obj2, obj1); 56 | * //merged will be: 57 | * { 58 | * id: 1, 59 | * name: "brian", 60 | * address: { 61 | * street: "John" 62 | * } 63 | * } 64 | * @param priorityObj 65 | * @param mergingObj 66 | * @returns {*} 67 | */ 68 | function deepMerge(mergingObj, priorityObj) { 69 | // Make copies so we don't hold references 70 | var priObj = angular.copy(priorityObj); 71 | var newObj = angular.copy(mergingObj); 72 | 73 | for (var prop in priObj) { 74 | /** 75 | * Using void 0 to return undefined in case window.undefined is 76 | * modified. 77 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void 78 | */ 79 | if (priObj[prop] !== void 0 && typeof priObj[prop] === 'object' && 80 | !Array.isArray(priObj[prop])) { 81 | newObj[prop] = deepMerge(newObj[prop], priObj[prop]); 82 | } else { 83 | newObj[prop] = priObj[prop]; 84 | } 85 | } 86 | return newObj; 87 | } 88 | 89 | /** 90 | * A helper method to wrap the options check. 91 | * 92 | * @param responseData The data retrieved from the server. 93 | * @param mockData The mockData object. 94 | * 95 | * @returns {*} The merged object 96 | */ 97 | function mergeResponse(responseData, mockData) { 98 | if (options.priorityMock && options.priorityMock === true) { 99 | return deepMerge(responseData, mockData); 100 | } 101 | return deepMerge(mockData, responseData); 102 | } 103 | 104 | /** 105 | * Return a list of objects from the server from a specified end point. 106 | * The mock object used to instantiate this service will be merged 107 | * with each individual result. 108 | * 109 | * @returns {*} A promise with the merged result from the server and mock. 110 | */ 111 | function processGetAll() { 112 | return $http.get(baseEndpoint) 113 | .then(function (response) { 114 | var newData = R.map(function (item) { 115 | return mergeResponse(item, mockData); 116 | }, response.data); 117 | response.data = newData; 118 | return response; 119 | }); 120 | } 121 | 122 | /** 123 | * Perform a get request to the desired end-point and ID. 124 | * This process with merge the returned results with the mock object 125 | * the service was instantiated with. 126 | * 127 | * @param id The id to get. 128 | * @returns {*} A promise with the merged result from the server and mock. 129 | */ 130 | function processGet(id) { 131 | return $http.get([baseEndpoint, id].join('/')) 132 | .then(function (response) { 133 | response.data = mergeResponse(response.data, mockData); 134 | return response; 135 | }); 136 | } 137 | 138 | /** 139 | * Will create a new entity on the server. If given a transform function 140 | * the function will be applied to the data before it is sent to the 141 | * server. 142 | * 143 | * @param data The data to be created on the server. 144 | * @param transformFn The transformation to apply to the data before 145 | * sending it to the server. 146 | * 147 | * @returns {*} A promise with the server response. 148 | */ 149 | function create(data, transformFn) { 150 | if (transformFn) { 151 | data = transformFn(data); 152 | } 153 | var error = produceError(data, 'post'); 154 | 155 | if (null !== error) { 156 | return $q.reject(error); 157 | } 158 | 159 | return $http.post(baseEndpoint, data) 160 | .then(function (response) { 161 | var id; 162 | /* 163 | * Return a pointer to the resource if the origin API returns 164 | * a location header. Otherwise return the response we receive 165 | */ 166 | if (response.headers('location')) { 167 | id = R.last(response.headers().location.split('/')); 168 | // Wrap the request to the new object we just created by using 169 | // hackWrap.get rather than $http.get 170 | return get(id); 171 | } 172 | return response; 173 | }); 174 | } 175 | 176 | /** 177 | * Update an entity on the server. If given a transform function it will 178 | * apply the function to the data object before it is sent to the server. 179 | * 180 | * @param id The id to update. 181 | * @param data The data to update the data with. 182 | * @param transformFn The transformation function to apply to the data 183 | * before sending it to the server. 184 | * 185 | * @returns {*} A promise with the server response. 186 | */ 187 | function update(id, data, transformFn) { 188 | if (transformFn) { 189 | data = transformFn(data); 190 | } 191 | 192 | var error = produceError(data, 'post'); 193 | 194 | if (null !== error) { 195 | return $q.reject(error); 196 | } 197 | 198 | return $http.post([baseEndpoint, id].join('/'), data); 199 | } 200 | 201 | /** 202 | * A convenience method that will decide whether to use update or create 203 | * based on the presence of an id property in the data object. 204 | * 205 | * @param data The data to be saved to the server. 206 | * @param transformFn A data transformation function that will be applied 207 | * to the data before it is sent to the server. 208 | * 209 | * @returns {*} A promise with the server response. 210 | */ 211 | function save(data, transformFn) { 212 | if (data.id) { 213 | return update(data.id, data, transformFn); 214 | } else { 215 | return create(data, transformFn); 216 | } 217 | } 218 | 219 | /** 220 | * The getAll function wraps the processGetAll function and adds in the 221 | * random errors and mock latency. 222 | * 223 | * @returns {*} A promise with the server response. 224 | */ 225 | function getAll() { 226 | var error = produceError(); 227 | 228 | if (null !== error) { 229 | return $q.reject(error); 230 | } 231 | 232 | return waitForTime().then(function () { 233 | return $q.when(processGetAll()); 234 | }); 235 | } 236 | 237 | /** 238 | * A wrapper for the processGet function that adds in random errors and 239 | * mock latency. 240 | * 241 | * @param id The id of the entity to retrieve 242 | * @returns {*} A promise with the server response. 243 | */ 244 | function get(id) { 245 | var error = produceError(id, 'get'); 246 | 247 | if (null !== error) { 248 | return $q.reject(error); 249 | } 250 | 251 | return waitForTime().then(function () { 252 | return $q.when(processGet(id)); 253 | }); 254 | } 255 | 256 | return { 257 | create: create, 258 | disableRandomErrors: disableRandomErrors, 259 | forceError: utils.forceError, 260 | getAll: getAll, 261 | get: get, 262 | query: getAll, 263 | save: save, 264 | update: update 265 | }; 266 | } 267 | 268 | return createWrapper; 269 | }); 270 | -------------------------------------------------------------------------------- /src/hackstack/hackstack-wrapper-service.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $timeout; 4 | var hw; 5 | var $q; 6 | var $httpBackend; 7 | var mockData1 = { 8 | 'title': 'mock title 1', 9 | 'id': 1 10 | }; 11 | var mockData2 = { 12 | 'title': 'mock title 2', 13 | 'id': 2 14 | }; 15 | var endpoint = 'endpoint'; 16 | var incompleteObject = { 17 | 'id': 0, 18 | 'description': 'mock description' 19 | }; 20 | // Origin takes precedence 21 | var mockObject1 = R.merge(mockData1, incompleteObject); 22 | var mockObject2 = R.merge(mockData2, incompleteObject); 23 | 24 | describe('Hack Stack Wrapper tests', function () { 25 | beforeEach(function () { 26 | module('hackstack'); 27 | }); 28 | beforeEach(inject(function (_hackstack_, _$timeout_, _$q_, _$httpBackend_, 29 | _$http_) { 30 | hw = _hackstack_.wrap; 31 | $timeout = _$timeout_; 32 | $q = _$q_; 33 | $httpBackend = _$httpBackend_; 34 | $httpBackend.whenGET([endpoint].join('/')) 35 | .respond([incompleteObject, incompleteObject]); 36 | $httpBackend.whenGET(new RegExp([endpoint, '.*'].join('/'))) 37 | .respond(incompleteObject); 38 | })); 39 | 40 | 41 | afterEach(function () { 42 | $httpBackend.verifyNoOutstandingExpectation(); 43 | $httpBackend.verifyNoOutstandingRequest(); 44 | }); 45 | 46 | describe('sanity checks', function () { 47 | it('should throw an error if created with no endpoint/data', 48 | function () { 49 | expect(function () { 50 | hw(); // No endpoint, no data 51 | }).to.throw('wrapper must be provided with an endpoint'); 52 | 53 | expect(function () { 54 | hw(endpoint); // No data 55 | }).to.throw('wrapper must be provided with mock data'); 56 | 57 | expect(function () { 58 | hw(undefined, { 59 | 'one': 1 60 | }); // No endpoint 61 | }).to.throw('wrapper must be provided with an endpoint'); 62 | }); 63 | 64 | it('should throw an error if mock data is not an object', function () { 65 | expect(function () { 66 | hw(endpoint, 'test'); 67 | }).to.throw(); 68 | expect(function () { 69 | hw(endpoint, [1, 2, 3]); 70 | }).to.throw(); 71 | }); 72 | }); 73 | 74 | describe('GET tests', function () { 75 | it('should return mock data in response', function () { 76 | var wrapper = hw(endpoint, mockData1); 77 | wrapper.disableRandomErrors(true); 78 | var response = wrapper.get(1); 79 | expect(response).to.eventually.have.property('data') 80 | .deep.equal(mockObject1); 81 | $timeout.flush(); 82 | $httpBackend.flush(); 83 | }); 84 | 85 | it('should do a deep merge, preserving all sub-object properties', 86 | function () { 87 | var originObject = { 88 | 'level1': { 89 | 1: 1, 90 | 'level2': { 91 | 1: 1, 92 | 'data': 'lives' 93 | }, 94 | 'data': 'lives' 95 | } 96 | }; 97 | var mockData = { 98 | 'level1': { 99 | 2: 2, 100 | 'level2': { 101 | 2: 2, 102 | 'data': 'dies' 103 | }, 104 | 'data': 'dies' 105 | } 106 | }; 107 | var expected = { 108 | 'level1': { 109 | 1: 1, 110 | 2: 2, 111 | 'level2': { 112 | 1: 1, 113 | 2: 2, 114 | 'data': 'lives' 115 | }, 116 | 'data': 'lives' 117 | } 118 | }; 119 | var wrapper = hw(endpoint, mockData); 120 | wrapper.disableRandomErrors(true); 121 | $httpBackend.expectGET(/.*/).respond(200, originObject); 122 | expect(wrapper.get(1)).to.eventually.have.property('data') 123 | .deep.equal(expected); 124 | $timeout.flush(); 125 | $httpBackend.flush(); 126 | }); 127 | 128 | it('should prioritize mock data if asked to', function () { 129 | var expected = R.merge(incompleteObject, mockData1); 130 | var wrapper = hw(endpoint, mockData1, { 131 | 'priorityMock': true 132 | }); 133 | wrapper.disableRandomErrors(true); 134 | expect(wrapper.get(1)).to.eventually.have.property('data') 135 | .deep.equal(expected); 136 | expect(wrapper.getAll()).to.eventually.have.property('data') 137 | .deep.equal([expected, expected]); 138 | $timeout.flush(); 139 | $httpBackend.flush(); 140 | }); 141 | 142 | it('should modify all objects if getAll if called', function () { 143 | var wrapper = hw(endpoint, mockData1); 144 | wrapper.disableRandomErrors(true); 145 | var response = wrapper.getAll(); 146 | expect(response).to.eventually.have.property('data') 147 | .deep.equal([mockObject1, mockObject1]); 148 | $timeout.flush(); 149 | $httpBackend.flush(); 150 | }); 151 | 152 | it( 153 | 'should call $http.get(endpoint/id) when wrapper.get(id) is called', 154 | function () { 155 | var id = 1; 156 | var wrapper = hw(endpoint, mockData1); 157 | wrapper.disableRandomErrors(true); 158 | $httpBackend.expectGET([endpoint, id].join('/')); 159 | var response = wrapper.get(id); 160 | $timeout.flush(); 161 | $httpBackend.flush(); 162 | }); 163 | 164 | it( 165 | 'should call $http.get(endpoint) when wrapper.getAll() is called', 166 | function () { 167 | var wrapper = hw(endpoint, mockData1); 168 | wrapper.disableRandomErrors(true); 169 | $httpBackend.expectGET([endpoint].join('/')); 170 | var response = wrapper.getAll(); 171 | $timeout.flush(); 172 | $httpBackend.flush(); 173 | }); 174 | 175 | it( 176 | 'should return {status:error, ...} when forceError(error) is set', 177 | function () { 178 | var errorCode = 401; 179 | var wrapper = hw(endpoint, mockData1); 180 | wrapper.forceError(errorCode); 181 | var response = wrapper.get(1); 182 | response.then(function () { 183 | throw new Error('Promise should reject'); 184 | }, function (result) { 185 | expect(result.status).to.equal(errorCode); 186 | }); 187 | $timeout.flush(); 188 | }); 189 | 190 | it('should have independent results when multiple objects created', 191 | function () { 192 | var wrapper1 = hw(endpoint, mockData1); 193 | var wrapper2 = hw(endpoint, mockData2); 194 | wrapper1.disableRandomErrors(true); 195 | wrapper2.disableRandomErrors(true); 196 | expect(wrapper1.get(1)).to.eventually.have.property('data') 197 | .deep.equal(mockObject1); 198 | expect(wrapper2.get(1)).to.eventually.have.property('data') 199 | .deep.equal(mockObject2); 200 | $timeout.flush(); 201 | $httpBackend.flush(); 202 | }); 203 | 204 | it('should return a single result when calling get(id)', function () { 205 | var wrapper = hw(endpoint, mockData1); 206 | wrapper.disableRandomErrors(true); 207 | var response = wrapper.get(1); 208 | response.then(function (result) { 209 | expect(result.data).to.not.be.instanceOf(Array); 210 | }); 211 | $timeout.flush(); 212 | $httpBackend.flush(); 213 | }); 214 | 215 | it('should forward errors from origin', function () { 216 | var id = 1; 217 | var errorCode = 404; 218 | var wrapper = hw(endpoint, mockData1); 219 | wrapper.disableRandomErrors(true); 220 | $httpBackend.expectGET([endpoint, id].join('/')) 221 | .respond(errorCode, { 222 | 'statusText': 'Not found', 223 | 'data': 'Forced error by hackStack wrapper tests' 224 | }); 225 | var request = wrapper.get(id); 226 | request.then(function () { 227 | throw new Error('Promise should reject'); 228 | }, function (result) { 229 | expect(result.status).to.equal(errorCode); 230 | }); 231 | $timeout.flush(); 232 | $httpBackend.flush(); 233 | }); 234 | }); 235 | 236 | describe('CREATE tests', function () { 237 | it('should forward create requests', function () { 238 | var id = mockObject1.id; 239 | var wrapper = hw(endpoint, mockData1); 240 | wrapper.disableRandomErrors(true); 241 | $httpBackend.expectPOST([endpoint].join('/')) 242 | .respond(200, 'ok', { 243 | 'location': [endpoint, id].join('/') 244 | }); 245 | wrapper.create(mockObject1); 246 | $httpBackend.flush(); 247 | }); 248 | 249 | it('should create an id when provided a function', function () { 250 | var id = 1; 251 | var objectSansId = angular.copy(mockObject1); 252 | objectSansId.id = undefined; 253 | var wrapper = hw(endpoint, mockData1); 254 | wrapper.disableRandomErrors(true); 255 | $httpBackend.expectPOST([endpoint].join('/')) 256 | .respond(200, 'ok', { 257 | 'location': [endpoint, id].join('/') 258 | }); 259 | wrapper.create(objectSansId, function (data) { 260 | data.id = id; 261 | }); 262 | $timeout.flush(); 263 | $httpBackend.flush(); 264 | }); 265 | 266 | it( 267 | 'should return the object created in the response if no location header', 268 | function () { 269 | var wrapper = hw(endpoint, mockData1); 270 | wrapper.disableRandomErrors(true); 271 | $httpBackend.expectPOST([endpoint].join('/')) 272 | .respond(200, mockObject1); 273 | var response = wrapper.create(mockObject1); 274 | expect(response).to.eventually.have.property('data') 275 | .deep.equal(mockObject1); 276 | $httpBackend.flush(); 277 | }); 278 | 279 | it('should request and return object if location header present', 280 | function () { 281 | var location = [endpoint, 1].join('/'); 282 | var wrapper = hw(endpoint, mockData1); 283 | wrapper.disableRandomErrors(true); 284 | $httpBackend.expectPOST([endpoint].join('/')) 285 | .respond(200, mockObject1, { 286 | 'location': location 287 | }); 288 | var response = wrapper.create(mockObject1); 289 | expect(response).to.eventually.have.property('data') 290 | .deep.equal(mockObject1); 291 | $httpBackend.flush(); 292 | 293 | }); 294 | }); 295 | 296 | describe('Update tests', function () { 297 | it('should issue an update request to origin', function () { 298 | var id = mockObject1.id; 299 | var wrapper = hw(endpoint, mockData1); 300 | wrapper.disableRandomErrors(true); 301 | $httpBackend.expectPOST([endpoint, id].join('/')) 302 | .respond(200, 'ok'); 303 | wrapper.update(id, mockObject1); 304 | $httpBackend.flush(); 305 | }); 306 | 307 | it('should return a 200 if the origin returns a 200', function () { 308 | var id = mockObject1.id; 309 | var wrapper = hw(endpoint, mockData1); 310 | wrapper.disableRandomErrors(true); 311 | $httpBackend.expectPOST([endpoint, id].join('/')) 312 | .respond(200, 'ok'); 313 | var response = wrapper.update(id, mockObject1); 314 | expect(response).to.eventually.have.property('status').equal( 315 | 200); 316 | $httpBackend.flush(); 317 | }); 318 | 319 | it('should forward errors from origin', function () { 320 | var id = mockObject1.id; 321 | var errorCode = 403; 322 | var wrapper = hw(endpoint, mockData1); 323 | wrapper.disableRandomErrors(true); 324 | $httpBackend.expectPOST([endpoint, id].join('/')) 325 | .respond(errorCode, 'Forbidden'); 326 | var response = wrapper.update(id, mockObject1); 327 | // We want to assert on either success or failure 328 | response.then(function () { 329 | throw new Error('Promise should reject'); 330 | }, function (result) { 331 | expect(result.status).to.equal(errorCode); 332 | }); 333 | $httpBackend.flush(); 334 | }); 335 | }); 336 | }); 337 | -------------------------------------------------------------------------------- /src/hackstack/hackstack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by brian on 15-04-21. 3 | */ 4 | 'use strict'; 5 | angular.module('hackstack', [ 6 | 'hackstack.mock', 7 | 'hackstack.wrap', 8 | 'hackstack.utils', 9 | 'hackstack.feedhenry' 10 | ]) 11 | .factory('hackstack', function (mock, wrap, utils) { 12 | return { 13 | 'mock': mock, 14 | 'wrap': wrap, 15 | 'utils': utils 16 | }; 17 | }); 18 | --------------------------------------------------------------------------------