├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── Aptfile ├── LICENSE ├── Procfile ├── README.md ├── bin └── index.js ├── package-lock.json ├── package.json ├── src ├── app.js ├── cache.js ├── image.js ├── server.js └── twitter.js └── test ├── JustinBieber.jpg ├── JustinBieber.txt ├── SenecaCollege.jpg ├── SenecaCollege.txt ├── Twitter.jpg ├── app.live.test.js ├── app.mocked.test.js ├── cache.test.js ├── image.test.js └── twitter.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | // We're running in a node.js environment 4 | node: true, 5 | // We're using the Jest testing library, and its global functions 6 | 'jest/globals': true 7 | }, 8 | extends: [ 9 | 'eslint:recommended' 10 | ], 11 | plugins: [ 12 | 'jest' 13 | ], 14 | rules: { 15 | // Custom eslint rules 16 | 'indent': ['error', 2], 17 | 'linebreak-style': ['error', 'unix'], 18 | 'quotes': ['error', 'single'], 19 | 'semi': ['error', 'always'], 20 | 'eqeqeq': ['error', 'always'], 21 | 22 | 'no-console': ['warn'] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | # We could enable testing on multiple versions, uncomment to add more. 5 | #- '6' 6 | #- '7' 7 | - '8' 8 | env: 9 | - CACHE_TIMEOUT_SECONDS=5 10 | cache: 11 | directories: 12 | - node_modules 13 | addons: 14 | apt: 15 | packages: 16 | - graphicsmagick 17 | services: 18 | - redis 19 | deploy: 20 | provider: heroku 21 | api_key: 22 | secure: oFXC5D8PCoxn+taV0f38tkjfvUo5ahp4q8JzpOxPrv1QNywIuYkuTEpHdxrLaDbCirYieA3icpCSpR4afSl1rbLLBkra1swZSIJHDa7HVYsX1LNjh6igVpTSnDQyiN3RtLyxll8ngkywVSW+aAFGBdCDNr8KIyVlTjZvOynOW8H/OBKC8xFVjwXHVvGRSVu9WIHynbo1EffMKA2iZ4x3HaXdW8j4lHkVa9Qj/j1TSEjkpYfmmSxmTLHoaMhee/aoOmWHFpk88ApYqq/UHATp2vrkQxOWaH3Fn19uZePGRsM+zPTLZx6obFqel8OnqwtxNkPeKBaCARxUZvOTs0tHL32KvPRIHkUc7Oc/xEB2gp0yqz1gaEnFc+2FHMb3JXCW/6Jbfhw9AReqyQTiwhR+VjNivNTJGiSVNPBH4n/7qCuoIxGRRz+tg02bSmIEQprDRqeXQRX0zdF1xil1t5LnhAvpMqzOp4DA7S+z1DdY5Y399VclFG2bJTiuausluf/Netmc1TpWSS4DgcAaWMzjV2h3cwIwVrCJs48DYz8QGqTtK3l2hg7NANz5zk6mFzash+lUEyDSli/IFXYQuZynlaUQVfr0Od4iv2nswnWt3orESZMWrSPBuO34bejHiBd79oEk9EPaP3kTkilK7Il0WNr5SnySzi/cQ1Zray7cpQg= 23 | app: learn-travis 24 | 25 | -------------------------------------------------------------------------------- /Aptfile: -------------------------------------------------------------------------------- 1 | graphicsmagick 2 | libpng-dev 3 | zlib1g-dev 4 | libjasper-dev 5 | libjasper1 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Humphrey 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node src/server.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/humphd/learn-travis.svg?branch=master)](https://travis-ci.org/humphd/learn-travis) 2 | 3 | # Learn Travis Now 4 | 5 | ## Introduction 6 | 7 | This is a small node.js example showing some practical ways to use [TravisCI](https://travis-ci.com/) for automation. 8 | 9 | Our goal will be to create a simple web service that combines the [image-to-ascii](https://github.com/IonicaBizau/image-to-ascii) node.js module with [Twitter's profile image URLs](https://stackoverflow.com/questions/18381710/building-twitter-profile-image-url-with-twitter-user-id). 10 | 11 | Using our web service, we should be able to use the following URL: 12 | 13 | http://localhost:7000/profile/Twitter 14 | 15 | And have our server download and turn this: 16 | 17 | ![@Twitter](test/Twitter.jpg) 18 | 19 | into this: 20 | 21 | ``` 22 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 23 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 24 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 25 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 26 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 27 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 28 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 29 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 30 | fffffffffffffffffffffffffffffffffffLLfftfftfffffffffffffffff 31 | ffffffffffffffffLCfffffffffffffLG88@@80GLLCCffffffffffffffff 32 | ffffffffffffffff0@0CftffffffffG@@@@@@@@@@@8GLfffffffffffffff 33 | ffffffffffffffff0@@@8GCffftftL@@@@@@@@@@@@0Cffffffffffffffff 34 | fffffffffffffffff0@@@@@@80GCCG@@@@@@@@@@@@ftffffffffffffffff 35 | ffffffffffffffffGG8@@@@@@@@@@@@@@@@@@@@@@8ffffffffffffffffff 36 | ffffffffffffffffC@@@@@@@@@@@@@@@@@@@@@@@@Gtfffffffffffffffff 37 | fffffffffffffffffL08@@@@@@@@@@@@@@@@@@@@8fffffffffffffffffff 38 | ffffffffffffffffftL08@@@@@@@@@@@@@@@@@@8ffffffffffffffffffff 39 | fffffffffffffffffffC8@@@@@@@@@@@@@@@@@0fffffffffffffffffffff 40 | fffffffffffffffffftttLC0@@@@@@@@@@@@8Cffffffffffffffffffffff 41 | ffffffffffffffffLCCGG08@@@@@@@@@@80Lffffffffffffffffffffffff 42 | ffffffffffffffffLG08@@@@@@@@880GLfffffffffffffffffffffffffff 43 | fffffffffffffffffttfffLLLLLfffftffffffffffffffffffffffffffff 44 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 45 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 46 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 47 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 48 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 49 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 50 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 51 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 52 | ``` 53 | 54 | You can try finished web service via this Heroku link, which is auto-deployed from `master` (we'll cover how below): 55 | 56 | * https://learn-travis.herokuapp.com/profile/Twitter 57 | * https://learn-travis.herokuapp.com/profile/SenecaCollege 58 | 59 | ## How we'll use TravisCI 60 | 61 | [TravisCI](https://travis-ci.com/) can perform many different build, testing, deployment, and other automated tasks. We'll use it to do a number of things: 62 | 63 | * Recreate our environment per-commit, and make sure everything can be installed. 64 | * Lint our code, looking for any problems in what we've written. 65 | * Run our test suite, making sure new code doesn't break old expectations. 66 | * Deploy our web service to a staging server. 67 | 68 | Our first step will be to authenticate on https://travis-ci.org/auth using our GitHub identity, and enable Travis to build our repository. The process is discussed at length in https://docs.travis-ci.com/user/getting-started/. 69 | 70 | Next, we need to tell [TravisCI](https://travis-ci.com/) what to do with our code once it clones our repository and checks out a particular commit. We'll define as much of our project as "source code" in our repository as possible. At first it might seem like it would be easier to just have lots of GUI settings in a web app to handle our configuration, tests, etc. However, by "coding" our environment and automation settings, it makes it easier for us to version them, compare new vs. old versions, and have more than one person author them (e.g., someone without full rights can still write them). 71 | 72 | [TravisCI](https://travis-ci.com/) will look for a special configuration file in our repository named `.travis.yml`. This is a [YAML file](https://en.wikipedia.org/wiki/YAML) with [confiuration information about our environment, language, scripts, etc](https://docs.travis-ci.com/user/customizing-the-build/). We'll be adding to it as we go. 73 | 74 | ## Dependencies 75 | 76 | ### `npm` and `package.json` 77 | 78 | Our first task will be to define and install our dependencies, both those we'll need for our code, and for our environment. We're going to need use all of the following node modules: 79 | 80 | * [express](https://www.npmjs.com/package/express) to create our web service 81 | * [image-to-ascii](https://www.npmjs.com/package/image-to-ascii) to convert images to ASCII text 82 | * [redis](https://www.npmjs.com/package/redis) to cache values and increase performance 83 | * [jest](https://www.npmjs.com/package/jest) to write unit tests 84 | * [supertest](https://www.npmjs.com/package/supertest) to help us write network tests against our web service 85 | * [nock](https://www.npmjs.com/package/nock) to create mock (i.e., simulated) network responses in our tests from the Twitter API 86 | * [eslint](https://www.npmjs.com/package/eslint) to lint our code 87 | * [eslint-plugin-jest](https://www.npmjs.com/package/eslint-plugin-jest) to integrate our linting with our jest tests. 88 | 89 | In node the common way to install these dependencies is to define them in a [`package.json`](https://docs.npmjs.com/files/package.json) file, each with an associated [version number or range](https://docs.npmjs.com/getting-started/semantic-versioning): 90 | 91 | ``` 92 | "dependencies": { 93 | "express": "^4.16.2", 94 | "image-to-ascii": "^3.0.9", 95 | ... 96 | } 97 | ``` 98 | 99 | We can further divide these up into dependencies you need in order to *run* our web service vs. those you need to *develop* it: 100 | 101 | ``` 102 | "dependencies": { 103 | "express": "^4.16.2", 104 | "image-to-ascii": "^3.0.9", 105 | ... 106 | }, 107 | "devDependencies": { 108 | "eslint": "^4.11.0", 109 | "eslint-plugin-jest": "^21.3.2", 110 | ... 111 | } 112 | ``` 113 | 114 | Once we've defined all of our module dependencies and their versions, we can install them using the `npm` command: 115 | 116 | ``` 117 | npm install 118 | ``` 119 | 120 | This will parse the `package.json` file and look for the `dependencies` and `devDependencies` sections, installing each module that it finds into a `node_modules/` directory. 121 | 122 | NOTE: you can also have `npm` automatically install *and* record your dependency information using the `--save` or `--save-dev` flags: 123 | 124 | ``` 125 | npm install --save express 126 | npm install --save-dev eslint 127 | ``` 128 | 129 | Doing so will cause your `package.json` file to get automatically updated with the proper module names and versions. 130 | 131 | Once we've done this, any developer who wants to recreate our development/production environment can use: 132 | 133 | ``` 134 | npm install 135 | ``` 136 | 137 | This will download and save all necessary modules and proper versions to a local `node_modules/` directory. 138 | 139 | ### `.travis.yml` 140 | 141 | Everything we've done so far with our dependency definition is going to be useful to devs, who won't have to manually install things, and also for automation, where we'll have a single command to recreate our environment. 142 | 143 | Because we're building a node.js JavaScript based project, we can tell Travis that this is a node project. Travis will look for a `.travis.yml` file in our project root, which defines how it should do a build: 144 | 145 | ```yml 146 | # 1. We don't need to use `sudo`, and can instead use containers for faster builds 147 | sudo: false 148 | # 2. This is a node.js JS project 149 | language: node_js 150 | # 3. These are the various versions of node.js we want to test against 151 | node_js: 152 | - "6" 153 | - "7" 154 | - "8" 155 | - "node" 156 | # 4. We'd like to cache our node_modules dir, because it won't change very often 157 | cache: 158 | directories: 159 | - "node_modules" 160 | ``` 161 | 162 | Our initial `.travis.yml` file defines a few things here: 163 | 164 | 1. We aren't going to need to do any commands in our build using `sudo` (i.e., as `root`), since everything can be done with stock installed or addon packages via containers. This will mean our builds start faster. See the [docs for more info](https://docs.travis-ci.com/user/reference/overview/#Virtualization-environments). 165 | 2. We specify that we're writing a `node_js` project, which means Travis will use a standard `npm` style workflow to do things like `npm install` our dependencies, `npm test` to run our tests, etc. 166 | 3. We specify every version of node.js that we want to test against. Maybe we're concerned with backward compatibility, and don't want to accidentally use code that isn't supported in an older version. Or maybe we want to make sure that some dependency doens't break in a newer version of node.js. See the [docs for more info](https://docs.travis-ci.com/user/languages/javascript-with-nodejs/#Specifying-Node.js-versions). 167 | 4. We specify any directories (or other aspects of our project) that we want to cache. In this case, we don't want to bother re-downloading all our `node_modules/` data for every build, since they won't change that often. See the [docs for more info](https://docs.travis-ci.com/user/languages/javascript-with-nodejs/#Caching-with-npm). 168 | 169 | With this info added, we can `git commit` our `package.json` and `.travis.yml` and `git push origin master` to have our code go to GitHub, and thus to TravisCI as well, for our first build. 170 | 171 | Your builds will be available at a URL of the following form: 172 | 173 | https://travis-ci.org/[GitHub-Username]/[GitHub-Repo-Name] 174 | 175 | For example, the URL for the repository you're looking at now is: 176 | 177 | https://travis-ci.org/humphd/learn-travis 178 | 179 | Within this, you can see a number of different historical and current views: 180 | 181 | * [The most recent build](https://travis-ci.org/humphd/learn-travis) 182 | * [All builds arranged in historical order](https://travis-ci.org/humphd/learn-travis/builds) 183 | 184 | There are also special views for different [branches](https://travis-ci.org/humphd/learn-travis/branches) and [pull requests](https://travis-ci.org/humphd/learn-travis/pull_requests), which we're not using at the moment. Each build will include a link for any/all node.js runtime versions you specify, in our case there's a link for each of `6, 7, 8, node`. Clicking on any of these will give a complete log of everything that happened during the build, and whether it is still running (yellow), finished and passed (green), or failed (red). 185 | 186 | For example, here's a [build that worked](https://travis-ci.org/humphd/learn-travis/jobs/308095672), and another that [failed](https://travis-ci.org/humphd/learn-travis/jobs/308095674) (due to timing issues). 187 | 188 | NOTE: below we'll switch to using a single node runtime, since we want to target a specific version for deployment. However, you've now seen how to support parallel runtimes in testing. 189 | 190 | ### Other dependencies for Travis 191 | 192 | Running our first automated build on Travis results in failure. Even though things worked on my local machine, the automation environment Travis is using isn't working. However, the [error we get gives us a clue](https://travis-ci.org/humphd/learn-travis/jobs/306476584#L579): 193 | 194 | ``` 195 | $ npm install 196 | 197 | > lwip2@1.0.12 install /home/travis/build/humphd/learn-travis/node_modules/lwip2 198 | > node lib/install.js 199 | 200 | Installing lwip. If this fails, just install GraphicsMagick (http://www.graphicsmagick.org/). 201 | ... 202 | In file included from ../src/lib/png/png.c:14:0: 203 | ../src/lib/png/pngpriv.h:805:4: error: #error ZLIB_VERNUM != PNG_ZLIB_VERNUM "-I (include path) error: see the notes in pngpriv.h" 204 | 205 | # error ZLIB_VERNUM != PNG_ZLIB_VERNUM \ 206 | ^ 207 | make: *** [Release/obj.target/lwip_decoder/src/lib/png/png.o] Error 1 208 | make: Leaving directory `/home/travis/build/humphd/learn-travis/node_modules/lwip2/node_modules/lwip/build' 209 | gyp ERR! build error 210 | ... 211 | ``` 212 | 213 | The module we're using to convert images to ASCII seems to include a binary component that's dependent on [GraphicsMagick](http://www.graphicsmagick.org/). My laptop (macOS) has it installed from a previous project, but Travis doesn't have it by default. We'll have to add it. 214 | 215 | In addition to defining our node modules in `package.json`, we also sometimes need to specify additional operating system packages, services, and configurations. We can do all of this via our `.travis.yml` file. In this case, we need to specify an [APT package](https://docs.travis-ci.com/user/installing-dependencies/#Installing-Packages-on-Container-Based-Infrastructure) to get installed when our build environment is created: 216 | 217 | ``` 218 | addons: 219 | apt: 220 | packages: 221 | - graphicsmagick 222 | ``` 223 | 224 | Now our `install` step works, but we have a [new error in our tests](https://travis-ci.org/humphd/learn-travis/jobs/308055447#L1403): 225 | 226 | ``` 227 | console.error src/cache.js:10 228 | Redis Client Error { Error: Redis connection to 127.0.0.1:6379 failed - connect ECONNREFUSED 127.0.0.1:6379 229 | at Object._errnoException (util.js:1024:11) 230 | at _exceptionWithHostPort (util.js:1046:20) 231 | at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1182:14) 232 | code: 'ECONNREFUSED', 233 | errno: 'ECONNREFUSED', 234 | syscall: 'connect', 235 | address: '127.0.0.1', 236 | port: 6379 } 237 | ``` 238 | 239 | Our code is trying to use [Redis](https://redis.io/) to cache ASCII images, and while I'm running it locally, our Travis environment isn't. Let's fix it by adding the [Redis service](https://docs.travis-ci.com/user/database-setup/#Redis): 240 | 241 | ``` 242 | services: 243 | - redis-server 244 | ``` 245 | 246 | Once again we've been able to automate our project's build/test cycle by using "code" vs. manual configuration steps. Before automation systems, someone would have had to manually installed and run these system level dependencies, and then make sure they stayed up to date. Now we can include them in the process of building and testing our code. The environment itself becomes another bit of "code" we maintain along with our source. 247 | 248 | ## Linting 249 | 250 | As we code, and as we invite our friends and colleagues to join us in writing code, it's common to want some help in making sure our coding style follows a set of standards. Beyond syntax errors, and various anti-patterns, there isn't really a proper way to write code. You might swear by using semicolons and 8-space indents, and I might play fast and loose with no-semicolons and 2-spaces. There's also a bunch of practices that we might want to enforce, from remembering to eliminate `console.log` in our production code to catching duplicate keys in our object definitions. 251 | 252 | [Linting](https://en.wikipedia.org/wiki/Lint_%28software%29) tools help us find and remove "lint," which are warnings, errors, annoyances, or inconsistencies in our code. There are lots of options one can use, but the most popular for node.js is [eslint](https://eslint.org/). 253 | 254 | > Question: I already use eslint in my editor, why do I need to have it defined in the project 255 | 256 | Remember how we said that we want to define as much of our project's practices and tools as "code" as we can? Well, we don't want to rely on the fact that *you* use a linter with your editor, but one of our new contributors doesn't. We want consistency, and the only way to achieve that is to define it at the *source* in our repository. 257 | 258 | We aren't looking at linting per se, so I'd suggest that you take some time to look at the learning materials on https://eslint.org/docs/user-guide/getting-started. Learning to use your linter well is a great investment. 259 | 260 | What I'm most interested in now is how to tell TravisCI to lint our code on every commit. This will be a three part process. 261 | 262 | 1. Define any dependencies we need for Travis to use eslint 263 | 2. Define a set of rules that we'll have eslint check against our code 264 | 3. Define a command that Travis can run to test our code for these rules 265 | 266 | We've already dealt with 1. above when we specified our dependencies. For 2. we'll create a file to hold our [eslint settings](https://eslint.org/docs/user-guide/configuring), named `.eslintrc.js`: 267 | 268 | ```js 269 | module.exports = { 270 | env: { 271 | // We're running in a node.js environment 272 | node: true, 273 | // We're using the Jest testing library, and its global functions 274 | 'jest/globals': true 275 | }, 276 | extends: [ 277 | // We'll begin with the standard set of rules, and tweak from there 278 | 'eslint:recommended' 279 | ], 280 | plugins: [ 281 | // Use the jest eslint plugin to help catch any test-specific lint issues 282 | 'jest' 283 | ], 284 | rules: { 285 | // Define a few custom eslint rules 286 | 'indent': ['error', 2], 287 | 'linebreak-style': ['error', 'unix'], 288 | 'quotes': ['error', 'single'], 289 | 'semi': ['error', 'always'], 290 | 'eqeqeq': ['error', 'always'], 291 | // Some rules can be warnings vs. errors 292 | 'no-console': ['warn'] 293 | } 294 | }; 295 | ``` 296 | 297 | Here we've done a few things: 298 | 299 | 1. Specified that we're going to be writing code in a node.js environment, with all the assumed globals that node.js provides (i.e., we don't want eslint erroring becuase it assumes some node.js global is actually an undefined variable we missed). 300 | 2. We're going to start with the [`eslint:recommended` rule set](https://eslint.org/docs/user-guide/configuring#using-eslintrecommended), and modify it as necessary. This way we don't have to think of everything, and can rely on some good default rules. 301 | 3. We're going to be writing tests using Jest, and want `eslint` and `jest` to play nicely together. See (eslint-plugin-jest)[https://www.npmjs.com/package/eslint-plugin-jest]. 302 | 4. We're going to override and/or define some of our own rules. For example, we want to use 2-space indents, and `error` vs. `warn` when someone uses something else. However, we'll just `warn` vs. `error` when someone forgets to remove a `console.log()`. The [complete list of rules is available here](https://eslint.org/docs/rules/). 303 | 304 | Finally, we need to automate `eslint` to run. There are lots of ways to do this, but we'll use the standard method of defining an [`npm script`](https://docs.npmjs.com/misc/scripts). We can define any script we want in our `package.json` and attach it to a name that can then be run with `npm run `: 305 | 306 | ``` 307 | "scripts": { 308 | "test": "npm run lint", 309 | "lint": "eslint src test" 310 | } 311 | ``` 312 | 313 | Here we've got two scripts defined: `test` and `lint`. Why two? Doesn't `test` just call `lint`? Why not just have one? The answer is flexibility. We might want to `lint` our code without running other tests. Also, we will eventually want to compose a number of scripts into a single `test` run for our whole test suite; `lint` will be the first part of this. 314 | 315 | Now when TravisCI runs `npm test` in a build, it will run the `lint` command which will in turn run `eslint` on our `src/` and `test/` directories. 316 | 317 | ## Testing 318 | 319 | ### First, why test? 320 | 321 | With our installation automated via `npm install`, and our code linting in place via `eslint`, it's time to move on to testing. 322 | 323 | Testing as such is too large of a topic for us to cover in detail now. Our goal is to get a set of automated tests running in Travis, and to have the result of those tests to get reported back to GitHub. 324 | 325 | Why is this important? First, we want to automate our test suite in order to guarantee that it gets run. If we leave it up to manual processes, in all likelihood the burden to run the tests will often mean we choose not to do so. We want to put the burden of running our tests on an automated process with infinite patience vs in the hands of people who are in a hurry. 326 | 327 | Second, we want to automate our tests so that we can better support parallel development on a project by a large number of individuals. We want to try and keep our source code in a working state (buildable, lint free, all tests pass). When our tests break, it's usually a sign that something in our code is broken (broken tests can sometimes be the fault of the tests, not our code), and that we should investigate. When we allow things to remain in a broken state, we waste everyone's time, because it becomes difficult to keep working on other things that may depend on the now broken code. 328 | 329 | Writing tests for new features as well as bug fixes is a good habit for software projects (open or closed). Having an easy to use and fast way to automatically run our tests and see the results helps to encourage this practice. 330 | 331 | ### Writing Some Tests 332 | 333 | For the purposes of this example, we're going to use a few common node.js testing modules: 334 | 335 | * [Jest](https://facebook.github.io/jest/) 336 | * [Supertest](https://github.com/visionmedia/supertest) 337 | * [Nock](https://github.com/node-nock/nock) 338 | 339 | We'll write a variety of tests, all of which you can see in the `tests/` directory. Our tests will attempt to push our code in every direction possible, and try to deal with both acceptable and unacceptable data alike. We'll also use a mix of real and simulated (i.e., mocked) API calls to twitter.com, to look at different ways of working with external resources under testing conditions. 340 | 341 | Jest will be [responsible for running all of our tests](https://facebook.github.io/jest/docs/en/cli.html#content), something it does very well without much guidance. However, as has been our way up to this point, we'll also provide an explicit set of configuration options in "code" that allow our testing environment to get versioned in git. 342 | 343 | Jest can be [configured via additions to our `package.json`](https://facebook.github.io/jest/docs/en/configuration.html#content) file. We'll provide a few configuration options to tell it what we'd like to happen when tests are run: 344 | 345 | ``` 346 | "jest": { 347 | "verbose": true, 348 | "collectCoverage": true, 349 | "forceExit": true 350 | } 351 | ``` 352 | 353 | Here, we've told Jest to be `verbose` with its output (i.e., we'd like to see as much info on what's happening as possible), that we want it to collect test coverage data (i.e., to determine which parts of our code aren't getting tested), and that we always want it to quit when it's done (i.e., always report back a pass/fail vs. hanging for some reason). 354 | 355 | Next we need to automate the running of Jest everytime we need our tests to run. Like we did with `eslint`, we'll do that via an `npm` script in `package.json`: 356 | 357 | ``` 358 | "scripts": { 359 | "lint": "eslint src test", 360 | "jest": "jest", 361 | "test": "npm run lint && npm run jest", 362 | ... 363 | ``` 364 | 365 | We've now got a series of `scripts` we can `run` via `npm`. If we only want to lint our code we can use `npm run lint`. If we only want to see our tests run, `npm run jest`. And finally, if we want to run our entire suite of tests, we'll use `npm test`, which in turn runs each of the other two one after the other. 366 | 367 | So what do we need to do in order to get Travis to run our `npm test` command? Nothing. Our choice of `test` for this all-encompassing command was deliberate: `npm`-based node projects [assume that `npm test` is how you run the test suite](https://docs.npmjs.com/cli/test). And by extension, this is what Travis will do when it runs a node.js project via automation. By writing our tests in terms of the defaults assumed by the language, we've created a simple way for our project to get tested within the context of a developer's machine, but also via automation. 368 | 369 | ## Automatic Deployment of a Staging Server 370 | 371 | There are lots of other things we could do automatically with Travis. One final task we'll add is deployment. It would be nice if we always had a working version of our `master` branch running on a public server, so we can test things. Having a live "staging" server is a common approach projects take to make it easy to test things without having to build and run the latest code locally. 372 | 373 | Lots of cloud providers will let you [deploy your code from Travis](https://docs.travis-ci.com/user/deployment), and most offer some kind of free version, which is perfect for testing out a staging server. We'll use [Heroku](https://heroku.com) and their [free tier](https://www.heroku.com/free) (i.e., we won't spend any money on this). 374 | 375 | After creating an account on [Heroku](https://heroku.com), and adding a new app named `learn-travis` via the [dashboard](https://devcenter.heroku.com/articles/heroku-dashboard), we'll [install the Heroku cli tool](https://devcenter.heroku.com/articles/heroku-cli) and the [the Travis cli tool](https://github.com/travis-ci/travis.rb#installation). Using these we can securely [create a deployment config for Heroku](https://docs.travis-ci.com/user/deployment/heroku/). 376 | 377 | The first step is to encrypt our authentication token, so that Travis can deploy on our behalf--we won't store our password in GitHub (for obvious reasons): 378 | 379 | ``` 380 | $ travis encrypt $(heroku auth:token) --add deploy.api_key 381 | ``` 382 | 383 | And now my `.travis.yml` file contains the following: 384 | 385 | ``` 386 | deploy: 387 | provider: heroku 388 | api_key: 389 | secure: oFXC5D8PCoxn...(truncated encrypted key) 390 | app: learn-travis 391 | ``` 392 | 393 | This is the info Travis needs to deploy my code after a successful build (i.e., all tests pass). 394 | 395 | Next I need to deal with the same binary dependencies for the `image-to-ascii` module (see [bug](https://github.com/mcollina/heroku-buildpack-graphicsmagick/issues/27)) on Heroku that I did on Travis (the `.travis.yml` file isn't enough for Heroku). The various libraries I need to to exist in the operating system will go in an `Aptfile`: 396 | 397 | ``` 398 | graphicsmagick 399 | libpng-dev 400 | zlib1g-dev 401 | libjasper-dev 402 | libjasper1 403 | ``` 404 | 405 | Next I'll tell Heroku to add a [buildpack](https://devcenter.heroku.com/articles/buildpacks) to install my OS `apt-get` dependencies: 406 | 407 | ``` 408 | $ heroku git:remote -a learn-travis 409 | $ heroku buildpacks:add --index 1 https://github.com/heroku/heroku-buildpack-apt 410 | ``` 411 | 412 | Finally, I'll include a [Procfile](https://devcenter.heroku.com/articles/procfile) to tell Heroku how to start our node server. 413 | 414 | ``` 415 | web: node src/server.js 416 | ``` 417 | 418 | I can now push everything to GitHub, and Travis will take over. My dependencies will get installed, my code linted, my tests run, and if all that works, my app will get deployed to Heroku and I'll be able to access it here: 419 | 420 | https://learn-travis.herokuapp.com/profile/Twitter 421 | 422 | NOTE: if that URL doesn't work right away, it probably means that my "free" server isn't started yet (Heroku stops free servers when not in use for 30 mins, and restarts them when someone makes a new request). 423 | 424 | You can see an example build that did everything I just mentioned here: 425 | 426 | https://travis-ci.org/humphd/learn-travis/builds/309704482 427 | 428 | ## Conclusion 429 | 430 | Now that our example web service is written, tested, and automated, let's step back and take a look at what it took to get here. Specifically, consider how much of our repository is devoted to each of **Source** vs. **Tests** vs. **Config**, where config includes things like our Travis, git, Heroku, npm, eslint, and other configurations (I haven't included this README file, since it's so long): 431 | 432 | |Type |By File Count|By LOC| 433 | |------|-------------|------| 434 | |Source|27% |36% | 435 | |Tests |45% |42% | 436 | |Config|27% |21% | 437 | 438 | Only about 1/3rd of the code in our repository is our project's source code! The rest is configuration information, automation, and tests. This was a simple project, but already we can see that the process of automating our environment, testing, and deployment means a lot more "code" to maintain. This should help show you how much value you can bring to a project without necessarily working on the core code. Maybe you want to help contribut to a game engine, a browser, a machine learning framework, but don't feel you have the skills to jump into the code (yet). Don't let that stop you! Begin with tests, automation, build and deployment scripts. Projects are full of "code" you can help develop. 439 | 440 | Travis is only one of many popular and useful automation systems, and it would be good to experiment with others. You could also try taking what I've done here and converting it to another language. While the tech will change, the concepts will remain similar. 441 | 442 | If you notice a mistake or have an improvement you can see to what I've done here, feel free to file an issue or make a pull request. 443 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | /* https://eslint.org/docs/rules/no-console#when-not-to-use-it */ 2 | /* eslint-disable no-console */ 3 | 4 | var image = require('../src/image'); 5 | 6 | // Expect a Twitter username as the second arg: 7 | // node index.js 8 | var args = process.argv.slice(2); 9 | var twitterName = args[0]; 10 | 11 | if(!twitterName) { 12 | console.error('Expected Twitter username as only argument.'); 13 | process.exit(1); 14 | } 15 | 16 | image.load(twitterName, function(err, ascii) { 17 | if(err) { 18 | console.error('Unable to load profile image:', twitterName); 19 | process.exit(1); 20 | } 21 | 22 | console.log(ascii); 23 | process.exit(0); 24 | }); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-travis", 3 | "version": "1.0.1", 4 | "engines": { 5 | "node": "8.9.x" 6 | }, 7 | "description": "An example to show how to get TravisCI integrated", 8 | "scripts": { 9 | "start": "node ./src/server.js", 10 | "lint": "eslint src test", 11 | "lint-fix": "eslint --fix src test", 12 | "jest": "jest", 13 | "test": "npm run lint && npm run jest" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/humphd/learn-travis.git" 18 | }, 19 | "jest": { 20 | "verbose": true, 21 | "collectCoverage": true, 22 | "forceExit": true 23 | }, 24 | "author": "humphd", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "eslint": "^4.11.0", 28 | "eslint-plugin-jest": "^21.3.2", 29 | "jest": "^21.2.1", 30 | "nock": "^9.1.3", 31 | "supertest": "^3.0.0" 32 | }, 33 | "dependencies": { 34 | "express": "^4.16.2", 35 | "image-to-ascii": "^3.0.9", 36 | "redis": "^2.8.0", 37 | "version-healthcheck": "^0.1.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | var image = require('./image'); 2 | var twitter = require('./twitter'); 3 | 4 | var version = require('version-healthcheck'); 5 | var express = require('express'); 6 | var app = express(); 7 | 8 | // Provide a basic healthcheck route, see https://www.npmjs.com/package/version-healthcheck 9 | app.get('/healthcheck', version); 10 | 11 | app.get('/profile/:twitterName', function(req, res) { 12 | // Get the Twitter profile name off the URL and validate it 13 | var twitterName = req.params.twitterName; 14 | if(!twitter.isValidName(twitterName)) { 15 | return res.status(400).send('Invalid profile name.'); 16 | } 17 | 18 | // Try loading the given Twitter user's profile image and converting 19 | // to ASCII. If we can't find this user, return a 404. Otherwise, 20 | // return plain text ASCII of the image, preferring cached content 21 | // if available. 22 | image.load(twitterName, function(err, ascii, cached) { 23 | if(err) { 24 | res.status(404).send('Unable to load profile image.'); 25 | return; 26 | } 27 | 28 | // Return plain text (vs. html, image, etc) 29 | res.set('Content-Type', 'text/plain'); 30 | // Indicate whether we got this out of cache or had to hit the network 31 | if(cached) { 32 | res.status(304); 33 | } else { 34 | res.status(200); 35 | } 36 | // Finally, send the body of the response: the image ASCII 37 | res.send(ascii); 38 | }); 39 | }); 40 | 41 | // Make it easier to test by separating our web app logic from the server code. 42 | module.exports = app; 43 | -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | var redis = require('redis'); 4 | var client = redis.createClient(); 5 | // Shim implementation of the redis client, in case we can't get one. 6 | var fakeRedisClient = (function() { 7 | /* eslint-disable no-unused-vars */ 8 | return { 9 | get: function(key, callback) { 10 | callback(null, null); 11 | }, 12 | setex: function(key, ttl, value) { 13 | return; 14 | }, 15 | keys: function(pattern, callback) { 16 | var emptyKeyList = []; 17 | callback(null, emptyKeyList); 18 | } 19 | }; 20 | }()); 21 | 22 | // Only use cache if redis is present 23 | client.on('error', function(err) { 24 | console.warn('[redis cleint error]', err); 25 | // Swap out for the fake shim client instead. 26 | client = fakeRedisClient; 27 | }); 28 | 29 | // Namespace for all redis keys in the cache 30 | var KEY_PREFIX = 'learntravis:'; 31 | // Set cache time-to-live value, using environment variable or default of 1 min. 32 | var ONE_MINUTE = 1 * 60; 33 | var CACHE_TIMEOUT_SECONDS = process.env.CACHE_TIMEOUT_SECONDS || ONE_MINUTE; 34 | 35 | // Rewrite keys to include our app's namespace for redis 36 | function prefixKey(key) { 37 | return KEY_PREFIX + key; 38 | } 39 | 40 | exports.set = function(key, val) { 41 | if(!client) { 42 | return; 43 | } 44 | 45 | key = prefixKey(key); 46 | client.setex(key, CACHE_TIMEOUT_SECONDS, val); 47 | }; 48 | 49 | exports.get = function(key, callback) { 50 | if(!client) { 51 | return callback(null, null); 52 | } 53 | 54 | key = prefixKey(key); 55 | client.get(key, callback); 56 | }; 57 | 58 | // For testing, a way to clear all keys from the cache 59 | exports._clear = function(callback) { 60 | if(!client) { 61 | return callback(null, null); 62 | } 63 | 64 | client.keys(KEY_PREFIX + '*', function(err, keys) { 65 | if(err) { 66 | return callback(err); 67 | } 68 | 69 | keys.forEach(function(key) { 70 | client.del(key); 71 | }); 72 | 73 | callback(null); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /src/image.js: -------------------------------------------------------------------------------- 1 | var imageToAscii = require('image-to-ascii'); 2 | var twitter = require('./twitter'); 3 | var cache = require('./cache'); 4 | 5 | /** 6 | * Convert an image path or url to a black-and-white ASCII image 30 high. 7 | * Values are managed in a redis cache based on filename/url 8 | * @param {String} pathOrUrl a filesystem path or URL 9 | * @param {Function} callback (err, ascii) 10 | */ 11 | function convert(pathOrUrl, callback) { 12 | // https://github.com/IonicaBizau/image-to-ascii#imagetoasciisource-options-callback 13 | var options = { 14 | // Don't use ANSI colour codes in output 15 | colored: false, 16 | size: { 17 | // Force a height vs. using size of terminal 18 | height: 30 19 | } 20 | }; 21 | 22 | // Start by looking in the cache for this URL 23 | cache.get(pathOrUrl, function(err, ascii) { 24 | if(err) { 25 | return callback(err); 26 | } 27 | 28 | // If there is a cache hit, return it 29 | if(ascii) { 30 | return callback(null, ascii, /*cached=*/ true); 31 | } 32 | 33 | // Cache missed, so go to network instead, then cache result. 34 | // NOTE: image-to-ascii can throw if pathOrUrl don't exist, wrap in try/catch 35 | try { 36 | imageToAscii(pathOrUrl, options, function(err, ascii) { 37 | if(err) { 38 | return callback(err); 39 | } 40 | 41 | // Store result in cache and return via callback. 42 | cache.set(pathOrUrl, ascii); 43 | callback(null, ascii); 44 | }); 45 | } catch(e) { 46 | // Deal with this sychronous error as though it happened async via callback. 47 | callback(e); 48 | } 49 | }); 50 | } 51 | 52 | /** 53 | * Load's the profile pic for a given Twitter handle and converts it to ASCII 54 | * @param {String} twitterName a Twitter handle 55 | * @param {Function} callback (err, ascii) 56 | */ 57 | function load(twitterName, callback) { 58 | var url = twitter.getProfileUrl(twitterName); 59 | convert(url, callback); 60 | } 61 | 62 | exports.load = load; 63 | exports.convert = convert; 64 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | var app = require('./app'); 3 | 4 | // Set the port of our application 5 | // process.env.PORT lets the port be set by Heroku 6 | var port = process.env.PORT || 8080; 7 | 8 | app.listen(port, function() { 9 | console.log('Server started on http://localhost:' + port); 10 | }); 11 | -------------------------------------------------------------------------------- /src/twitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * From https://support.twitter.com/articles/101299#error: 3 | * 4 | * Your username cannot be longer than 15 characters. 5 | * Your name can be longer (50 characters), but usernames are kept 6 | * shorter for the sake of ease. 7 | * 8 | * A username can only contain alphanumeric characters 9 | * (letters A-Z, numbers 0-9) with the exception of underscores, 10 | * as noted above. Check to make sure your desired username doesn't 11 | * contain any symbols, dashes, or spaces. 12 | * 13 | * Also support passing a leading @... since that's so common 14 | */ 15 | function validateName(twitterName) { 16 | if(!twitterName) { 17 | return null; 18 | } 19 | 20 | // Strip leading @ if present 21 | twitterName = twitterName.replace(/^@/, ''); 22 | 23 | if(/^[A-Za-z0-9_]{1,15}$/.test(twitterName)) { 24 | return twitterName; 25 | } 26 | } 27 | 28 | // See discussions: 29 | // * https://stackoverflow.com/questions/18381710/building-twitter-profile-image-url-with-twitter-user-id 30 | // * https://gist.github.com/jcsrb/1081548#gistcomment-1493078 31 | function getProfileUrl(twitterName) { 32 | twitterName = validateName(twitterName); 33 | if(!twitterName) { 34 | return null; 35 | } 36 | return 'https://twitter.com/' + twitterName + '/profile_image?size=original'; 37 | } 38 | 39 | exports.getProfileUrl = getProfileUrl; 40 | // Provide convenience function for checking whether a name is valid 41 | exports.isValidName = function(name) { return !!validateName(name); }; 42 | -------------------------------------------------------------------------------- /test/JustinBieber.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humphd/learn-travis/7cc1c928f824dd7b96ed0d749a2d66d3842109e1/test/JustinBieber.jpg -------------------------------------------------------------------------------- /test/JustinBieber.txt: -------------------------------------------------------------------------------- 1 | 888888888888888888888@@@@@@@@8@@@@@8888888888888888888888888 2 | 88888888888888888@@800GCLLLLLCffLCG08@@888888888888888888888 3 | 88888888888888@80CLt11iiii1t11t1iii1tfC088888888888888888888 4 | 888888888888880tii1tt111f11fftLCLftt;;111C888888888888888888 5 | 8888888888888Liii1fttffftt1tftttfttCLf1i;1tL8888888888888888 6 | 88888888888@Gi1LCLCGfLCffLf11ttt1ii;ttftiii;1C88888888888888 7 | 888888888888ft0000080C00000GGGCCLff11iitit;1iif0888888888888 8 | 888888888888GGG000000800GGCGGCLLLfft11iiii:;;:iC888888888888 9 | 8888888888880G00000000000GGGGCCLffft11t1ii;;;;;f000888888888 10 | 888888888880G0000888800GCCLLLLffffffLft1iii1ii;tLLLCG0088888 11 | 8888888888@8GGCG0000Gf1i:;;iiiiitfffLL1iiiii11;tLLfLLLCCG008 12 | 88888888888L1ii1L000C1;;:;11ttf11fffff1;ii;i1i;tLffLLLLLLLCG 13 | 88888888880LLt111C0Ct;;itft1iitfLLLfff1;;;iii1itfffffLLLLLLL 14 | 888888888880t1tfLGCf11fLG0GGCGGGGGCffft;:i1;iiitfffffLLLLLLL 15 | 888888888888GGGG0GLt1tLGG00000GGGCfffftiif1;:i;ifffffLLLLLLL 16 | 8888888888880000Gftftt1C800000GCLfffLffttf1,,;i1fffffffLLLLL 17 | 888888888880G00f11::;1;;G8000GCftttffffft1:;1;1ttffffffffLLL 18 | 888888888880GGGCfLff1i1fC000GCLftttffffft::1t1ttffffffffLLfL 19 | 888888888888GGGGGCGGCLLLffffLLft1tttffffti1ittttttfffffffffL 20 | 8888888888880CLfft1ftiiiiittLLtt11ttffffttt1ttttttffffffffLL 21 | 8888888888888CftfCCCftttffLLGLtt1tttfffftfttttttttftftffffLL 22 | 88888888888880GG0CftfLLLLLffLft1ttttfffttf1ttttttttftffffffL 23 | 888888888888880GGGCCG0GGLLfft11111ttfft1tf1ttttttttfffffffff 24 | 888888888888888CGGG0GCLtt111ii1tttttft11tfttttttttffffffffff 25 | 88888888888888@LfCCLLLti::::i1ttfttff1iitffttttttffffffffffL 26 | 8888888888888888Lft111i;;i1tttttttff1iii1tffftffffffffffffLf 27 | 8888888888888888@8GCGCffftt1ii;itfftiiii1tfffffffffffffffLLL 28 | 88888888888888888@8G000Gf1i;;;;i1fftiiii1ttLffffffffffLfLLLL 29 | 88888888888888888@0GGG000GLt1iii1tt1iiii1ttfLLLffLfffLLLLLLL 30 | 88888888888888@880GGGGG0000GCf1ii111ii1111t1fLLLLLLLLLLLLLLL -------------------------------------------------------------------------------- /test/SenecaCollege.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humphd/learn-travis/7cc1c928f824dd7b96ed0d749a2d66d3842109e1/test/SenecaCollege.jpg -------------------------------------------------------------------------------- /test/SenecaCollege.txt: -------------------------------------------------------------------------------- 1 | @@@8L1i;;;;;;;;ii;;;;ii;iiiiiiiii;;;iii;ii;;;;;;;;;;;i1L8@@@ 2 | @@t;itLLLLLLLLL1iLLLLt;Lt;Lf;LL;fLLLL;tLi1LLLLLLLLLLLLti;t@@ 3 | 8;;LLLLLLLLLffL1iLffLtiLf1LfiffifLfLL1fLi1LffLffLffLffLLL;;8 4 | ;:LLLLLLLLLL;iL;;Li:L1:LLLLt:ff:fL,1LLLL:iL;iL;:Li:L1:LLLL:; 5 | fLLLL;1LLLLiiLLLLLLLLLLLLLffffffLLLLLLLLLLi1LLLLLLLtiLLLLf 6 | fLLLLitLLLL11LLLLLLLLf1i;;;;;;;;;;i1fLLLLLi1LLLLLLLtiLLfLf 7 | fLLLLLLLLLLLLL;:LLLf;:;;iiiiiiiiii;;:;fLLL;;L;:LLLL1;Lt:Lf 8 | fLLLLitLLLLLLLLLLL1,;iiiiiiiiiiiiiiii;,1LLLLLLLL1iLtiLtiLf 9 | fLLLLitLLLLLLLLLLt,iiiiiiiiiiiiiiiiiiii,tLLLLLLL1iLtiLt;Lf 10 | fL:1LLLLLLLLLL;;L:;1iiiiiiiiiiiiiiiiii1;:L;iLLLLLLLLLLLLLf 11 | fLitL1tLLLLLLL1iL,;iiiiiiiiiiiiiiiiiiii;:Li1LLLLt1LLLLf1Lf 12 | fL;tL;1LLLLLLLiiL:;iiiiiiiiiiiiiiiiiiii;:LiiLLLL1;LLLLt;Lf 13 | fLLLLLLLLLLLLfLLL::i;iiiiiiiiiiiiiiiii1::LLLfLLLLLL1:Lt:Lf 14 | fLLLL1fLLLLf,:;;i:;1Cfii1ii1ii11ii1ii1i;:i;;:,fLLLLffLftLf 15 | fLLLL;1LLLLL1;;:;ii1G0L0GL0G0C0GL0LtC8Lii;;;;1LLLLLLLLLLLf 16 | fLLLL;1L:iL;iLLf1,i1tLtfftLtftLftLffLffi,1fLLi;LLLLLLLLLLf 17 | fLLLLtfLtfLttLLLt,iiiiiiiiiiiiiiiiiiiiii,tLLLttLLLLLLLLLLf 18 | fLLLLLLLLLLLLLLi,iiiiiiiiiiiiiiiiiiiiiiii,iLLLLLLLLLLLLLLf 19 | fLLLLLLL;1LLLt::iiiiiiiiiiiiiiiiiiiiiiiiii::tLLLLLL1;Lt;Lf 20 | fLLLLfLL11ti;:i1iiiiiiiiiiiiiiiiiiiiiiiiii1i:;itfLLt1Lf1Lf 21 | fLLLL:1t,::;i1iiiiiiiiiiiiiiiiiiiiiiiiiiiiii1i;::,1LLLLLLf 22 | fLLLLitLt1ii;:iiiiiiiiiiiiiiiiiiiiiiiiiiiiii:;;i1tLt;Lt;Lf 23 | fLLLLitLLLLLLi,;;;;;;;iiiiiiiiiiiiii;;;;;;;,iLLLLLLtiLtiLf 24 | fLLLL;1LLLL:;LtttfLtt1;:;iiiiiiii;:;ittfftttLLLLLLLLLL1:Lf 25 | fLLLLitL1tLLLLt1Lt1Lf1Lti;;;;;;;;itLLLLLLL1tL11Lt1LtiLLfLf 26 | fLLLL;tL;1LLLLiiL1;L1;LLLLLfffffLLLLLLLLLLiiLiiL1;L1;LLLLf 27 | ;:LLLLLLLLLL:;LLLL1;L1:LLLLt:fLLLLLLL:1LLLLLLLi;LLLLLLLLLL:; 28 | 8;;LLLLLLLLLffLLLL1iLffLLLLLfLf1fLLLLitL1tL1tLffLLLLLLLLL;;8 29 | @@t;itLLLLLLLLLLLL1iLLLLLLLLLLf;fLLLL;tL;1CiiLLLLLLLLLti;f@@ 30 | @@@8L1i;;;;;;;;;;;ii;;;;;;;;;;iii;;;iii;ii;ii;;;;;;;;i1L8@@@ -------------------------------------------------------------------------------- /test/Twitter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humphd/learn-travis/7cc1c928f824dd7b96ed0d749a2d66d3842109e1/test/Twitter.jpg -------------------------------------------------------------------------------- /test/app.live.test.js: -------------------------------------------------------------------------------- 1 | // Tests against live Twitter.com API 2 | 3 | var app = require('../src/app'); 4 | var path = require('path'); 5 | var image = require('../src/image'); 6 | var request = require('supertest'); 7 | var cache = require('../src/cache'); 8 | 9 | describe('App tests against live Twitter URL API', function () { 10 | 11 | beforeEach(function(done) { 12 | cache._clear(done); 13 | }); 14 | 15 | test('Server should provide ascii profile images', function(done) { 16 | var pathToTwitterJpg = path.join(__dirname, 'Twitter.jpg'); 17 | 18 | image.convert(pathToTwitterJpg, function(err, a) { 19 | expect(err).toBeFalsy(); 20 | expect(a).toBeDefined(); 21 | 22 | request(app).get('/profile/Twitter').end(function(err, res) { 23 | expect(err).toBeFalsy(); 24 | expect(res.status).toEqual(200); 25 | expect(res.text).toEqual(a); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | 31 | test('Server should respond with cached content on multiple hits', function(done) { 32 | var beebs = '/profile/@JustinBieber'; 33 | 34 | request(app).get(beebs).end(function(err, res) { 35 | expect(err).toBeFalsy(); 36 | expect(res.status).toEqual(200); 37 | 38 | request(app).get(beebs).end(function(err, res) { 39 | expect(err).toBeFalsy(); 40 | expect(res.status).toEqual(304); 41 | 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | test('Unknown profile name should result in 404', function(done) { 48 | var nobody = '/profile/CenecaSollege'; 49 | 50 | request(app).get(nobody).end(function(err, res) { 51 | expect(err).toBeFalsy(); 52 | expect(res.status).toEqual(404); 53 | done(); 54 | }); 55 | }); 56 | 57 | test('Invalid profile name should result in 400', function(done) { 58 | var nobody = '/profile/NameIsTooLongForTwitter'; 59 | 60 | request(app).get(nobody).end(function(err, res) { 61 | expect(err).toBeFalsy(); 62 | expect(res.status).toEqual(400); 63 | done(); 64 | }); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /test/app.mocked.test.js: -------------------------------------------------------------------------------- 1 | // Tests against mocked Twitter.com API via nock 2 | 3 | var nock = require('nock'); 4 | var path = require('path'); 5 | var app = require('../src/app'); 6 | var image = require('../src/image'); 7 | var request = require('supertest'); 8 | var cache = require('../src/cache'); 9 | 10 | describe('App tests mocked against Twitter URL API', function () { 11 | 12 | beforeEach(function(done) { 13 | cache._clear(done); 14 | }); 15 | 16 | test('Server should provide ascii profile images', function(done) { 17 | var pathToTwitterJpg = path.join(__dirname, 'Twitter.jpg'); 18 | 19 | // Mock a response for twitter.com/Twitter 20 | var twitter = nock('https://twitter.com') 21 | .get('/Twitter/profile_image?size=original') 22 | .replyWithFile( 23 | 200, 24 | path.join(__dirname, 'Twitter.jpg'), 25 | { 'Content-Type': 'image/jpeg' } 26 | ); 27 | 28 | image.convert(pathToTwitterJpg, function(err, a) { 29 | expect(err).toBeFalsy(); 30 | expect(a).toBeDefined(); 31 | 32 | request(app).get('/profile/Twitter').end(function(err, res) { 33 | expect(err).toBeFalsy(); 34 | expect(res.status).toEqual(200); 35 | expect(res.text).toEqual(a); 36 | 37 | twitter.done(); 38 | done(); 39 | }); 40 | }); 41 | }); 42 | 43 | test('Server should respond with cached content on multiple hits', function(done) { 44 | var beebs = '/profile/@JustinBieber'; 45 | 46 | // Mock a response for twitter.com/JustinBieber 47 | var twitter = nock('https://twitter.com') 48 | .get('/JustinBieber/profile_image?size=original') 49 | .replyWithFile( 50 | 200, 51 | path.join(__dirname, 'JustinBieber.jpg'), 52 | { 'Content-Type': 'image/jpeg' } 53 | ); 54 | 55 | request(app).get(beebs).end(function(err, res) { 56 | expect(err).toBeFalsy(); 57 | expect(res.status).toEqual(200); 58 | 59 | request(app).get(beebs).end(function(err, res) { 60 | expect(err).toBeFalsy(); 61 | expect(res.status).toEqual(304); 62 | 63 | twitter.done(); 64 | done(); 65 | }); 66 | }); 67 | }); 68 | 69 | test('Unknown profile name should result in 404', function(done) { 70 | var nobody = '/profile/CenecaSollege'; 71 | 72 | // Mock a made-up profile name to simulate a bad request 73 | var twitter = nock('https://twitter.com') 74 | .get('/CenecaSollege/profile_image?size=original') 75 | .reply(404); 76 | 77 | request(app).get(nobody).end(function(err, res) { 78 | expect(err).toBeFalsy(); 79 | expect(res.status).toEqual(404); 80 | 81 | twitter.done(); 82 | done(); 83 | }); 84 | }); 85 | 86 | test('Invalid profile name should result in 400', function(done) { 87 | var nobody = '/profile/NameIsTooLongForTwitter'; 88 | 89 | request(app).get(nobody).end(function(err, res) { 90 | expect(err).toBeFalsy(); 91 | expect(res.status).toEqual(400); 92 | done(); 93 | }); 94 | }); 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /test/cache.test.js: -------------------------------------------------------------------------------- 1 | var cache = require('../src/cache'); 2 | 3 | test('Can set and get values from cache', function(done) { 4 | var now = Date.now(); 5 | var key = 'key-' + now; 6 | var value = 'value-' + now; 7 | 8 | cache.set(key, value); 9 | cache.get(key, function(err, result) { 10 | expect(err).toBeFalsy(); 11 | expect(result).toEqual(value); 12 | 13 | done(); 14 | }); 15 | }); 16 | 17 | test('_clear should remove all keys from cache', function(done) { 18 | var now = Date.now(); 19 | var key = 'key2-' + now; 20 | var value = 'value2-' + now; 21 | 22 | cache.set(key, value); 23 | cache._clear(function(err) { 24 | expect(err).toBeFalsy(); 25 | expect(cache.get(key)).not.toBeDefined(); 26 | 27 | done(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/image.test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var nock = require('nock'); 3 | var image = require('../src/image'); 4 | var cache = require('../src/cache'); 5 | 6 | beforeEach(function(done) { 7 | cache._clear(done); 8 | }); 9 | 10 | test('@Twitter profile pic should match known version', function(done) { 11 | var pathToTwitterJpg = path.join(__dirname, 'Twitter.jpg'); 12 | 13 | image.convert(pathToTwitterJpg, function(err, a) { 14 | expect(err).toBeFalsy(); 15 | expect(a).toBeDefined(); 16 | 17 | image.load('Twitter', function(err, b) { 18 | expect(err).toBeFalsy(); 19 | expect(b).toEqual(a); 20 | 21 | done(); 22 | }); 23 | }); 24 | }); 25 | 26 | test('Multiple loads of a URL results in cached copy being returned', function(done) { 27 | var url = 'https://twitter.com/SenecaCollege/profile_image?size=original'; 28 | 29 | // Mock responses for twitter.com/SenecaCollege 30 | var senecaCollege = nock('https://twitter.com') 31 | .get('/SenecaCollege/profile_image?size=original') 32 | .replyWithFile( 33 | 200, 34 | path.join(__dirname, 'SenecaCollege.jpg'), 35 | { 'Content-Type': 'image/jpeg' } 36 | ); 37 | 38 | // First load, shouldn't be cached. 39 | image.convert(url, function(err, a, cached) { 40 | expect(err).toBeFalsy(); 41 | expect(a).toBeDefined(); 42 | expect(cached).toBeFalsy(); 43 | 44 | // Second load, should be cached. 45 | image.convert(url, function(err, a, cached) { 46 | expect(err).toBeFalsy(); 47 | expect(a).toBeDefined(); 48 | expect(cached).toBeTruthy(); 49 | 50 | senecaCollege.done(); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/twitter.test.js: -------------------------------------------------------------------------------- 1 | var twitter = require('../src/twitter'); 2 | 3 | test('Only allow names 1-15 charcters long', function() { 4 | var name, i; 5 | 6 | // Falsy values should return null 7 | expect(twitter.getProfileUrl(undefined)).toBeNull(); 8 | expect(twitter.getProfileUrl(null)).toBeNull(); 9 | expect(twitter.getProfileUrl('')).toBeNull(); 10 | 11 | // Try every length from 1-15 12 | for(i = 1; i < 16; i++) { 13 | name = 'a'.repeat(i); 14 | expect(twitter.getProfileUrl(name)).not.toBeNull(); 15 | } 16 | 17 | // Confirm lengths beyond 15 fail 18 | for(i = 16; i < 50; i++) { 19 | name = 'a'.repeat(i); 20 | expect(twitter.getProfileUrl(name)).toBeNull(); 21 | } 22 | }); 23 | 24 | test('Only allow A-Z, a-Z, and _ characters', function() { 25 | var alphabet = 'abcdefghijklmnopqrstuvwxyz'; 26 | var numbers = '0123456789'; 27 | var underscore = '_'; 28 | var valid = alphabet + alphabet.toUpperCase() + numbers + underscore; 29 | var invalid = '~ +-=[]{}!#$%^&*()?/,.'; 30 | 31 | valid.split('').forEach(function(char) { 32 | expect(twitter.getProfileUrl(char)).not.toBeNull(); 33 | }); 34 | 35 | invalid.split('').forEach(function(char) { 36 | expect(twitter.getProfileUrl(char)).toBeNull(); 37 | }); 38 | }); 39 | 40 | test('Passing a leading @ should be OK', function() { 41 | var nameWith = twitter.getProfileUrl('@name'); 42 | var nameWithout = twitter.getProfileUrl('name'); 43 | 44 | expect(nameWith).toEqual(nameWithout); 45 | }); 46 | 47 | test('URL should be correct format', function() { 48 | var name = 'Twitter'; 49 | var url = 'https://twitter.com/' + name + '/profile_image?size=original'; 50 | expect(twitter.getProfileUrl(name)).toEqual(url); 51 | }); 52 | --------------------------------------------------------------------------------