diff --git a/.gitignore b/.gitignore index 5bfc897..2d1a24e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ build/Release # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules -config.env +*.env dump.rdb npm-debug.log +data/ diff --git a/README.md b/README.md index 07cbdf7..8888cee 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,168 @@ # hits -What if there was a *simple+easy* way to see how many people have viewed your GitHub Repository? +A _simple & easy_ way to see how many people have _viewed_ your GitHub Repository. -[![Build Status](https://travis-ci.org/dwyl/hits.svg)](https://travis-ci.org/dwyl/hits) -[![HitCount](https://hitt.herokuapp.com/nelsonic/hits.svg)](https://github.com/nelsonic/hits) -[![Code Climate](https://codeclimate.com/github/dwyl/hits/badges/gpa.svg)](https://codeclimate.com/github/dwyl/hits) -[![codecov.io](http://codecov.io/github/dwyl/hits/coverage.svg?branch=master)](http://codecov.io/github/dwyl/hits?branch=master) -[![Dependency Status](https://david-dm.org/dwyl/hits.svg)](https://david-dm.org/dwyl/hits) -[![devDependency Status](https://david-dm.org/dwyl/hits/dev-status.svg)](https://david-dm.org/dwyl/hits#info=devDependencies) +[![Build Status](https://img.shields.io/travis/dwyl/hits.svg?style=flat-square)](https://travis-ci.org/dwyl/hits) +[![HitCount](http://hits.dwyl.io/dwyl/hits.svg)](https://github.com/dwyl/hits) +[![codecov.io](https://img.shields.io/codecov/c/github/dwyl/hits/master.svg?style=flat-square)](http://codecov.io/github/dwyl/hits?branch=master) +[![Dependency Status](https://img.shields.io/david/dwyl/hits.svg?style=flat-square)](https://david-dm.org/dwyl/hits) +[![devDependency Status](https://img.shields.io/david/dev/dwyl/hits.svg?style=flat-square)](https://david-dm.org/dwyl/hits#info=devDependencies) ## Why? -We have a few repos on GitHub ... but sadly, we have no idea how many people -are looking at the repos unless they star/watch them; GitHub does not share -any stats with people using their site. +We have a _few_ projects on GitHub ...
+_Sadly_, we ~~have~~ _had_ no idea how many people +are _reading/using_ the projects because GitHub only shares "[traffic](https://github.com/blog/1672-introducing-github-traffic-analytics)" stats +for the [_past 14 days_](https://github.com/dwyl/hits/issues/49) and **not** in "***real time***". +(_unless people star/watch the repo_) Also, _manually_ checking who has viewed a +project is _exceptionally_ tedious when you have more than a handful of projects. -We would like to *know* the popularity of each of our repos -to know where we need to be investing our time. +We want to *know* the popularity of _each_ of our repos +to know what people are finding _useful_ and help us +decide where we need to be investing our time. ## What? A simple way to add (*very basic*) analytics to your GitHub repos. -There are already *many* "Badges" available which people put in their repos: https://github.com/dwyl/repo-badges +There are already *many* "badges" that people use in their repos. +See: [github.com/dwyl/**repo-badges**](https://github.com/dwyl/repo-badges)
But we haven't seen one that gives a "***hit counter***" -of the number of times a page has been viewed ... +of the number of times a GitHub page has been viewed ...
+So, in today's mini project we're going to _create_ a _basic **Web Counter**_. -## How? - -Place a badge (*image*) in your repo `README.md` so others can -can see how popular the page is and you can track it. +https://en.wikipedia.org/wiki/Web_counter -### Implementation +### What Data to Capture/Store? -What is the ***minimum possible*** amount of data we can store? +The _first_ question we asked ourselves was: +What is the ***minimum possible*** amount of (_useful/unique_) +**info** we can store ***per visit*** (_to one of our projects_)? -+ **date+time** the person visited the site. +1. **date + time** (_timestamp_) ***when*** +the person visited the site/page.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now -+ **user-agent** the browser or crawler visiting the page + +2. **url** being visited. i.e. which project was viewed. + +3. **user-agent** the browser/device (_or "crawler"_) visiting the site/page https://en.wikipedia.org/wiki/User_agent -+ **referer** url of the page where the image is requested from? -https://en.wikipedia.org/wiki/HTTP_referer -Log entries are stored as a `String` which can be parsed and re-formatted into -any other format: +4. IP Address of the client. (_for checking uniqueness_) + +5. **Language** of the person's web browser. +_Note: While not "essential", we added **Browser Language** +as the **5th** piece of data (when it is set/sent by the browser/device) +because it's **insightful** to know what language people are using +so that we can determine if we should be **translating**/"**localising**" +our content._ + +### "Common Log Format" (CLF) ? + +We initially _considered_ using the "Common Log Format" (CLF) +because it's well-known/understood. +see: https://en.wikipedia.org/wiki/Common_Log_Format + +An example log entry: +``` +127.0.0.1 user-identifier frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 +``` + +Real example: +``` +84.91.136.21 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) 007 [05/Aug/2017:16:50:51 -0000] "GET github.com/dwyl/phase-two HTTP/1.0" 200 42247 +``` + +The data makes sense when viewed as a table: + +| IP Address of Client | User Identifier | User ID | Date+Imte of Request | Request "Verb" and URL of Request | HTTP Status Code | Size of Response | +| -------------|:-----------|:--|:------------:|:--------:|:--|--|--| +| 84.91.136.21 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) | 007 | [05/Aug/2017:16:50:51 -0000] | "GET github.com/dwyl/phase-two HTTP/1.0" | 200 | 42247 | + +On further reflection, we think the "Common Log Format" is _inneficient_ +as it contains a lot of _duplicate_ and some _useless_ data. + +We can do better. + +### Alternative Log Format ("ALF") + +From the CLF we can remove: + ++ **IP Address**, **User Identifier** and **User ID** can be condensed into a single hash (_see below_). ++ "**GET**"" - the word is implied by the service we are running (_we only accept GET requests_) ++ **Response size** is _irrelevant_ and will be the same for most requests. + +| Timestamp | URL | User Agent | IP Address | Language | Hit Count | +| ------------- |:------------|:------------|:------------:|:--------:| +| 1436570536950 | github.com/dwyl/the-book | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) | 84.91.136.21 | EN-GB | 42 | + + +In the log entry (_example_) described above the first 3 bits of data will +identify the "user" requesting the page/resource, so rather than duplicating the data in an inefficient string, we can _hash_ it! + +Any repeating user-identifying data should be concactenated + +Log entries are stored as a (_"pipe" delimited_) `String` +which can be parsed and re-formatted into any other format: + ```sh -1436570536950 x7uapo9 84.91.136.21 +1436570536950|github.com/dwyl/phase-two|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)|88.88.88.88|EN-US|42 ``` -| Timestamp | User Agent | IP Address | -| ------------- |:------------|:------------:| -| 1436570536950 | x7uapo9 | 84.91.136.21 | -We then have a user-agent hash where we can lookup the by id: +### Reducing Storage (_Costs_) + +If a person views _multiple_ pages, _three_ pieces of data are duplicated: +User Agent, IP Address and Language. +Rather than storing this data multiple times, we _hash_ the data +and store the hash as a lookup. + +#### Hash Long Repeating (Identical) Data + +If we run the following `Browser|IP|Language` `String`: +```sh +'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)|84.91.136.21|EN-US' +``` +through a **SHA** hash function we get: `8HKg3NB5Cf` (_always_)1. + +_Sample_ code: ```js -{ - "x7uapo9":"Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us) AppleWebKit/531.21.10", - "N03v1lz":"Googlebot/2.1 (+http://www.google.com/bot.html)" -} +var hash = require('./lib/hash.js'); +var user_agent_string = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)|88.88.88.88|EN-US'; +var agent_hash = hash(user_agent_string, 10); // 8HKg3NB5Cf +``` + +1Note: SHA hash is _always_ 40 characters, +but we _truncate_ it because 10 alphanumeric characters (_selected from a set of 26 letters + 10 digits_) +means there are 3610 = [3,656,158,440,062,976](http://www.wolframalpha.com/input/?i=36%5E10) +(_three and a half [**Quadrillion**](http://www.wolframalpha.com/input/?i=3,656,158,440,062,976+in+english)_) +possible strings which we consider "_enough_" entropy. +(_if you disagree, tell us why in an + [issue](https://github.com/dwyl/hits/issues)_!) + +#### Hit Data With Hash + ``` +1436570536950|github.com/dwyl/the-book|8HKg3NB5Cf|42 +``` + + +## How? + +Place a badge (*image*) in your repo `README.md` so others can +can see how popular the page is and you can track it. -### Fetch SVG from shields.io and serve it just-in-time -Given that shields.io has a badge creation service, -and it has acceptable latency, we are proxying the their service. -## Run it! +## _Run_ it Your_self_! Download (clone) the code to your local machine: + ```sh git clone https://github.com/dwyl/hits.git && cd hits ``` -> Note: you will need to have Redis running on your localhost, -> if you are new to Redis see: https://github.com/dwyl/learn-redis + +> Note: you will need to have Node.js running on your localhost. Install dependencies: ```sh @@ -85,6 +175,20 @@ npm run dev Visit: http://localhost:8000/any/url/count.svg +# Data Storage + +Recording the "hit" data is _essential_ +for this app to _work_ and be _useful_. + +We have built it to work with _two_ "data stores": +Filesystem and Redis
+> _**Note**: you only need **one** storage option to be available_. + +## Filesystem + + + + ## Research ### User Agents @@ -108,3 +212,12 @@ http://www.monitorware.com/en/logsamples/apache.php ### Node.js http module headers https://nodejs.org/api/http.html#http_message_rawheaders + +## Running the Test Suite locally + +The test suite includes tests for 3 databases +therefore running the tests on your `localhost` +requires all 3 to be running. + +Deploying and _using_ the app only requires _one_ +of the databases to be available. diff --git a/lib/client.js b/lib/client.js index c77deb0..964588d 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,9 +1,52 @@ -// connect to websocket server -$( document ).ready(function() { - console.log('Ready!', window.location.host); +var root = document.getElementById("hits"); +console.log('Ready!', window.location.host); + +setTimeout(function(){ var socket = io(window.location.host); socket.on('news', function (data) { console.log(data); - socket.emit('my other event', { my: 'data' }); + socket.emit('hello', { msg: 'Hi!' }); }); -}); + + socket.on('hit', function (data) { + var previous = root.childNodes[0]; + root.insertBefore(div(Date.now(), data.hit), previous); + }); + + // borrowed from: https://git.io/v536m + function div(divid, text) { + var div = document.createElement('div'); + div.id = divid; + div.className = divid; + if(text !== undefined) { // if text is passed in render it in a "Text Node" + var txt = document.createTextNode(text); + div.appendChild(txt); + } + return div; + } + document.getElementById("how").classList.remove('dn'); // show form if JS available (progressive enhancement) + document.getElementById("nojs").classList.add('dn'); // show form if JS available (progressive enhancement) + display_badge_markdown(); // render initial markdown template +}, 500); + +// Markdown Template +var mt = '[![HitCount](http://hits.dwyl.io/{user}/{repo}.svg)](http://hits.dwyl.io/{user}/{repo})'; + +function generate_markdown () { + var user = document.getElementById("username").value || '{username}'; + var repo = document.getElementById("repo").value || '{project}'; + // console.log('user: ', user, 'repo: ', repo); + return mt.replace(/{user}/g, user).replace(/{repo}/g, repo); +} + +function display_badge_markdown() { + var md = generate_markdown() + var pre = document.getElementById("badge").innerHTML = md; +} + +var get = document.getElementsByTagName('input'); + for (i = 0; i < get.length; i++) { + get[i].addEventListener('keyup', display_badge_markdown, false); + get[i].addEventListener('keyup', display_badge_markdown, false); + + } diff --git a/lib/climate.svg b/lib/climate.svg deleted file mode 100644 index 8e5f2fb..0000000 --- a/lib/climate.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - hits - hits - 4.0 - xyz - - diff --git a/lib/db_filesystem.js b/lib/db_filesystem.js new file mode 100644 index 0000000..b9b759d --- /dev/null +++ b/lib/db_filesystem.js @@ -0,0 +1,68 @@ +var fs = require('fs'); +var rl = require('readline'); +var path = require('path'); +var hash = require('./hash.js'); +var error_log = require('../lib/error_logger.js'); +var EOL = require('os').EOL; // https://stackoverflow.com/a/14063413/1148249 +var LOG_DIR = path.resolve(__dirname, '../logs'); + +if (!fs.existsSync(LOG_DIR)) { // ignored if already exists + fs.mkdirSync(LOG_DIR); +} + +var LOG_FILE = path.resolve(LOG_DIR, 'access.log'); +fs.appendFileSync(LOG_FILE, '- - - New Logs:'); + +var AGENTS_DIR = path.join(LOG_DIR, '/agents'); +if (!fs.existsSync(AGENTS_DIR)) { // ignored if already exists + fs.mkdirSync(AGENTS_DIR); +} + +/** + * add - adds an entry into the List for a given url + * @param {Object} hit - the hit we just received + * @param {Function} callback - call this once redis responds + */ +module.exports = function file_save_hit (hit, callback) { + var h = hit.split('|'); // See README.md#How secton for sample data + var url = h[1]; + var LOG_FILE = path.join(LOG_DIR, + url.split('/').join('_').replace(':', '') + '.log'); + var count = 1; // hit count starts at 1 + // save unique hash of browser data to avoid duplication + var unique_browser_string = [ h[2], h[3], h[4] ].join('|'); + var hashed_agent = hash(unique_browser_string, 10); + // save unique data in file to reduce duplication in logs + var agent_path = path.join(path.resolve(AGENTS_DIR, hashed_agent)) + fs.writeFile(agent_path, unique_browser_string, function (err, data) { + error_log(err, 'unable to save agent data: ' + agent_path); + lineReader = rl.createInterface({ + input: require('fs').createReadStream(LOG_FILE) + .on('error', function (err) { + console.log('Error!', err); + error_log(err, 'unable to save agent data: ' + LOG_FILE); + var entry = [h[0], h[1], hashed_agent, count].join('|') + EOL; + fs.appendFile(LOG_FILE, entry, function (err) { + error_log(err, 'unable to APPEND to file:' + LOG_FILE); + callback(err, count); + }); + }) + }); + var lines = []; + lineReader.on('line', function (line) { + lines.push(line); + }); + + lineReader.on('close', function() { + var last_line = lines[lines.length - 1]; + var parts = last_line.split('|'); // parse and incremnt count: + count = parseInt(parts[parts.length - 1], 10) + 1; + + var entry = [h[0], h[1], hashed_agent, count].join('|') + EOL; + fs.appendFile(LOG_FILE, entry, function (err) { + error_log(err, 'unable to APPEND to file:' + LOG_FILE); + callback(err, count); + }); // if slow, optimise: https://stackoverflow.com/questions/12453057 + }); + }); +} diff --git a/lib/db_redis.js b/lib/db_redis.js new file mode 100644 index 0000000..57d596d --- /dev/null +++ b/lib/db_redis.js @@ -0,0 +1,22 @@ +var redisClient = require('redis-connection')(); +var hash = require('./hash.js'); +/** + * add - adds an entry into the List for a given url + * @param {Object} hit - the hit we just received + * @param {Function} callback - call this once redis responds + */ +module.exports = function redis_save_hit (hit, callback) { + var h = hit.split('|'); // See README.md#How secton for sample data + var url = h[1]; + + // save unique hash of browser data to avoid duplication + var unique_browser_string = [ h[2], h[3], h[4] ].join('|'); + var hashed_agent = hash(unique_browser_string, 10); + redisClient.hset('agents', hashed_agent, unique_browser_string); + + // save hit data with hashed browser data: + var entry = h[0] + '|' + hashed_agent; + redisClient.rpush(url, entry, function (err, data) { + callback(err, data) + }); +} diff --git a/lib/error_logger.js b/lib/error_logger.js new file mode 100644 index 0000000..b590812 --- /dev/null +++ b/lib/error_logger.js @@ -0,0 +1,30 @@ +var fs = require('fs'); +var path = require('path'); +var EOL = require('os').EOL; // https://stackoverflow.com/a/14063413/1148249 +var LOG_DIR = path.resolve(__dirname, '../logs'); + +/** + * add - adds an entry into the List for a given url + * @param {Object|null} error - the error we just received + * @param {String} [message=none] - the user-defined error message + & @returns {Boolean} - returns false if not error and true if error logged + */ +module.exports = function error_logger (error, message) { + if (error) { + var _error; + var filepath = path.resolve(LOG_DIR, 'error.log'); + if (typeof error !== 'object') { + _error = { error: error }; + } else { + _error = error; + } + _error._message = message || 'none'; + console.log('- - - - - - - - - - - - Error: - - - - - - - - - - - - '); + console.error(_error); + console.log('- - - - - - - - - - - - - - - - - - - - - - - - - - - - '); + // we expect errors to be infrequent so Sync is "OK" here! + fs.writeFileSync(filepath, JSON.stringify(_error) + EOL); + return true; + } + return false; +} diff --git a/lib/extract_request_data.js b/lib/extract_request_data.js new file mode 100644 index 0000000..da7a29e --- /dev/null +++ b/lib/extract_request_data.js @@ -0,0 +1,29 @@ +/** + * This file/module's only job is to extract the request data from http headers + * @param {Object} request - the standard nodejs http request object. + * @returns {string} hit - see readme for format. + */ +module.exports = function extract (request) { + var h = request.headers || {}; // shortcut to headers reduces typing + var lang; // the browser language + + // get the user's IP addres from headers or connection object: + var ip = h['x-forwarded-for'] || + request.connection && request.connection.remoteAddress; + + // get url the client requested: + var url = request.url.replace('.svg', '') + .replace('.png', '') + .replace('https://github.com/', ''); // strip to save storage + + if(h['accept-language']) { // Language for: github.com/dwyl/hits/issues/43 + if (h['accept-language'].indexOf(',') > -1) { // e.g: en-GB,en;q=0.5 + lang = h['accept-language'].split(',')[0].toUpperCase(); // e.g: EN-GB + } else { + lang = h['accept-language'].toUpperCase(); + } + } + + return [ Math.floor(Date.now()/1000), + url, h['user-agent'], ip, lang ].join('|'); +} diff --git a/lib/format_hit.js b/lib/format_hit.js new file mode 100644 index 0000000..5b9855c --- /dev/null +++ b/lib/format_hit.js @@ -0,0 +1,23 @@ +var hash = require('./hash.js'); +/** + * This file/module's only job is to format the Hit data for human-friendly UI + * @param {String} hit - the standard nodejs http request object. + * @param {Number} count - the count for the given url + * @returns {string} hit - human-friendly hit data for display in UI + */ +module.exports = function format_hit_for_ui (hit, count) { + var h = hit.split('|'); // See README.md#How secton for sample data + var url = h[1]; + var date = format_date_time_from_timestamp(h[0]) + + // save unique hash of browser data to avoid duplication + var unique_browser_string = [ h[2], h[3], h[4] ].join('|'); + var hashed_agent = hash(unique_browser_string, 10); + return [date, url, count, hashed_agent].join(' '); +} + +function format_date_time_from_timestamp (timestamp) { + var date = new Date(timestamp * 1000).toJSON(); + var len = date.length; + return date.substring(0, len -5).replace('T', ' '); +} diff --git a/lib/hash.js b/lib/hash.js new file mode 100644 index 0000000..26c84a9 --- /dev/null +++ b/lib/hash.js @@ -0,0 +1,9 @@ +var crypto = require('crypto'); + +// generate a hash of a specific length +module.exports = function hash (str, length) { + return crypto.createHash('sha512') // crypto.stackexchange.com/questions/26336 + .update(str.toString()).digest('base64') + .replace('/','').replace(/[Il0oO=\/\+]/g,'') // remove ambiguous chars + .substring(0, length || 12); +} diff --git a/lib/headers.json b/lib/headers.json new file mode 100644 index 0000000..333e49f --- /dev/null +++ b/lib/headers.json @@ -0,0 +1,6 @@ +{ + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + "Content-Type":"image/svg+xml" +} diff --git a/lib/hits.js b/lib/hits.js index 7da52be..96a8d30 100644 --- a/lib/hits.js +++ b/lib/hits.js @@ -1,41 +1,13 @@ -var redisClient = require('redis-connection')(); -var uniki = require('uniki'); /** * add - adds an entry into the List for a given url - * @param {String} url - the url for the hit * @param {Object} hit - the hit we just received * @param {Function} callback - call this once redis responds */ -module.exports.add = function add (hit, callback) { - // console.log(hit); - var url = hit.url.replace('https://github.com', ''); // don't waste RAM! - var now = Date.now(); - var agent = uniki(hit['user-agent'],7); - if(hit['accept-language'] && hit['accept-language'].indexOf(',') > -1){ - hit.lang = hit['accept-language'].split(',')[0]; - } else { - hit.lang = ''; +module.exports = function add (hit, callback) { + if(process.env.REDISCLOUD_URL) { + return require('./db_redis.js')(hit, callback); + } + else { + return require('./db_filesystem.js')(hit, callback); } - // console.log('agent',agent); - redisClient.hset('agents', agent, hit['user-agent']); - var entry = now + ' ' + agent + ' ' +hit.lang + ' ' + hit.ip - redisClient.rpush(url, entry, function (err, data) { - callback(err, data) - }); -} - -/** - * count - counts the number of hits for a given url - * @param {String} url - the url for the hit - * @param {Function} callback - call this once redis responds - */ -module.exports.count = function count (url, callback) { - console.log(url); - url = url.replace('https://github.com', ''); // don't waste space in Redis - redisClient.llen(url, function(err, data){ - callback(err, data); - }); } - - -module.exports.redisClient = redisClient; diff --git a/lib/index.html b/lib/index.html index e00c1da..6563e33 100644 --- a/lib/index.html +++ b/lib/index.html @@ -1,20 +1,87 @@ - Stats + Hits! - - + + + - -

Stats!

- - - + +

+ Hits! + + Hit Count + +

+

+ The easy way to know how many people are + viewing your GitHub projects! +

+ +

How?

+
+ + + + + + + + + + + +
+ Input your GitHub Username + ( or org name): + + +
+ Input the GitHub Project/Repository + name: + + +
+ +

Your Badge Markdown:

+
+
+      [![HitCount](http://hits.dwyl.io/{username}/{repo}.svg)](http://hits.dwyl.io/{username}/{repo})
+    
+ +

+ Using the above markdown as a template,
+ Replace the {username} with your GitHub username
+ Replace the {repo} with the repo name. +

+ +

+ Copy the markdown snippet and Paste it into your + README.md file
+ to start tracking the view count on your GitHub project! +

+ +

Recently Viewed Projects (tracked by Hits)

+
+
Dummy Child Node for insertBefore to work
+
+ + + diff --git a/lib/lanip.js b/lib/lanip.js new file mode 100644 index 0000000..fc7c52e --- /dev/null +++ b/lib/lanip.js @@ -0,0 +1,16 @@ +/** + * If you want to now the IP Address on the Local Network you're in luck! + * see/credit: http://stackoverflow.com/questions/10750303 + */ +var os = require('os'); +var interfaces = os.networkInterfaces(); +var ip = []; +for (var k in interfaces) { + for (var k2 in interfaces[k]) { + var address = interfaces[k][k2]; + if (address.family === 'IPv4' && !address.internal) { + ip.push(address.address); + } + } +} +module.exports = ip[0]; diff --git a/lib/make_svg.js b/lib/make_svg.js new file mode 100644 index 0000000..91b92f9 --- /dev/null +++ b/lib/make_svg.js @@ -0,0 +1,7 @@ +var fs = require('fs'); +var path = require('path'); +var template = fs.readFileSync(path.resolve('./lib/template.svg'), 'utf8'); + +module.exports = function make (count) { + return template.replace('{count}', count); +} diff --git a/lib/style.css b/lib/style.css deleted file mode 100644 index e69de29..0000000 diff --git a/lib/template.svg b/lib/template.svg new file mode 100644 index 0000000..8171e35 --- /dev/null +++ b/lib/template.svg @@ -0,0 +1,13 @@ + + + + + + + + + hits + {count} + + diff --git a/package.json b/package.json index 1a90384..f3515fc 100644 --- a/package.json +++ b/package.json @@ -21,24 +21,24 @@ "hits", "hit counter" ], - "author": "this guy", + "author": "some random person on the internet...", "license": "GPL-2.0", "bugs": { "url": "https://github.com/dwyl/hits/issues" }, "homepage": "https://github.com/dwyl/hits#readme", "dependencies": { - "redis-connection": "^5.0.0", - "socket.io": "^1.4.8", - "uniki": "^1.0.3", - "wreck": "^8.0.0" + "redis-connection": "^5.4.0", + "socket.io": "^2.0.3" }, "devDependencies": { - "decache": "^4.0.0", + "decache": "^4.1.0", "istanbul": "^0.4.4", - "nodemon": "^1.9.2", - "pre-commit": "^1.1.3", - "tape": "^4.6.0" + "mkdirp": "^0.5.1", + "nodemon": "^1.11.0", + "pre-commit": "^1.2.2", + "rimraf": "^2.6.1", + "tape": "^4.8.0" }, "pre-commit": [ "coverage" diff --git a/server.js b/server.js index 95f22cd..9fb1823 100644 --- a/server.js +++ b/server.js @@ -1,80 +1,53 @@ -var http = require('http'); -var hits = require('./lib/hits'); var port = process.env.PORT || 8000; -var wreck = require('wreck'); -var fs = require('fs'); -var png = fs.readFileSync('./lib/1x1px.png'); +var fs = require('fs'); // so we can open the HTML & JS file +var hits = require('./lib/hits'); // our storage interface +var make_svg = require('./lib/make_svg.js'); +var extract = require('./lib/extract_request_data.js'); +var format = require('./lib/format_hit.js') -var HEADERS = { // headers see: http://stackoverflow.com/a/2068407/1148249 - "Cache-Control": "no-cache, no-store, must-revalidate", // HTTP 1.1 - "Pragma": "no-cache", // HTTP 1.0 - "Expires": "0", // Proxies - "Content-Type":"image/svg+xml" // default to svg -}; +var FAVICON = 'http://i.imgur.com/zBEQq4w.png'; // dwyl favicon +var HEAD = require('./lib/headers.json'); // stackoverflow.com/a/2068407/1148249 -var app = http.createServer(function handler(req, res) { - var url = req.url; - var r = req.headers; - r.ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - r.url = url.replace('.svg', '').replace('.png', ''); +// plain node.js http server (no fancy framework required!) +var app = require('http').createServer(handler) +var io = require('socket.io')(app); + +io.on('connection', function (socket) { + socket.emit('news', { hello: 'world (test message)' }); + socket.on('hello', function (data) { + console.log(data); + }); +}); - if (url.match(/svg/)) { - hits.add(r, function(err, count) { - console.log(r.url, ' >> ', count); - var newurl = 'https://img.shields.io/badge/hits-' + count +'-brightgreen.svg'; - wreck.get(newurl, function (error, response, raw) { - res.writeHead(200, Object.assign(HEADERS, {"Location": newurl})); - res.end(raw); - }); +function handler (req, res) { + var url = req.url; + var hit = extract(req); + console.log(hit); + if (url.match(/svg/)) { // only return a badge if SVG requested + hits(hit, function(err, count) { + io.sockets.emit('hit', { 'hit': format(hit, count) }); // broadcast + console.log(url, ' >> ', count); // log in dev + res.writeHead(200, HEAD); // status code and SVG headers + res.end(make_svg(count)); // serve the SVG with count }); } - else if (url.match(/png/)) { - hits.add(r, function(err, count) { - console.log(r.url, ' >> ', count); - res.writeHead(200, Object.assign(HEADERS, {"Content-Type": "image/png"})); - res.end(png); - }) - } else if(url === '/favicon.ico') { - var favicon = 'http://i.imgur.com/zBEQq4w.png'; // dwyl favicon - res.writeHead(301, { "Location": favicon }); + res.writeHead(301, { "Location": FAVICON }); // redirect to @dwyl Favicon res.end(); } - else if(url === '/stats') { - fs.readFile('./lib/index.html', 'utf8', function (err, data) { - res.writeHead(200, {"Content-Type": "text/html"}); - res.end(data); - }); - } - else if(url === '/client.js') { + else if(url === '/client.js') { // these can be cached in "Prod" ... fs.readFile('./lib/client.js', 'utf8', function (err, data) { res.writeHead(200, {"Content-Type": "application/javascript"}); res.end(data); }); } - else if(url === '/style.css') { - fs.readFile('./lib/style.css', 'utf8', function (err, data) { - res.writeHead(200, {"Content-Type": "text/css"}); + else { // echo the record without saving it + fs.readFile('./lib/index.html', 'utf8', function (err, data) { + res.writeHead(200, {"Content-Type": "text/html"}); res.end(data); }); } - else { // echo the record without saving it - console.log(" - - - - - - - - - - record:", r); - res.writeHead(200, {"Content-Type": "text/plain"}); - res.end(JSON.stringify(r, null, " ")); - } // pretty JSON in Browser see: http://stackoverflow.com/a/5523967/1148249 -}).listen(port); - -var io = require('socket.io')(app); - -io.on('connection', function (socket) { - console.log(' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - '); - console.log(socket.client.conn); - console.log(' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - '); - socket.emit('news', { msg: 'welcome to stats-ville!' }); - socket.on('my other event', function (data) { - console.log(data); - }); -}); +} -console.log('Visit http://localhost:' + port); +app.listen(port); +console.log('Visit ' + require('./lib/lanip') + ':'+ port); diff --git a/test/db_filesystem.test.js b/test/db_filesystem.test.js new file mode 100644 index 0000000..7facce9 --- /dev/null +++ b/test/db_filesystem.test.js @@ -0,0 +1,49 @@ +var dir = __dirname.split('/')[__dirname.split('/').length-1]; +var file = dir + __filename.replace(__dirname, '') + " > "; +var test = require('tape'); +var extract = require('../lib/extract_request_data.js'); + +test(file+'Add a hit to the list for that url', function (t) { + var db = require('../lib/db_filesystem.js'); + var req = { + 'url': '/my/awesome/url', + headers: { + 'accept-language': 'en-US,en;q=0.8,pt;q=0.6,es;', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)' + }, + connection: { + remoteAddress: '88.88.88.88' + } + } + var hit = extract(req); + db(hit, function (err, count) { + t.ok(count >= 0, '✓ URL ' +req.url +' has: ' + count) + db(hit, function (err, count2) { + t.ok(count === count2 - 1, '✓ URL ' +req.url +' has: ' + count2) + require('decache')('../lib/db_filesystem.js') + t.end(); + }) + }) +}); + +test(file + 'Add a hit for new url', function (t) { + var db = require('../lib/db_filesystem.js'); + var req = { + 'url': '/another/' + Math.floor(Math.random() * 100000), + headers: { + 'accept-language': 'en-US,en;q=0.8,pt;q=0.6,es;', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)' + }, + connection: { + remoteAddress: '88.88.88.88' + } + } + var hit = extract(req); + db(hit, function (err, count) { + t.equal(count, 1, '✓ URL ' +req.url +' has: ' + count) + db(hit, function (err, count2) { + t.ok(count === count2 - 1, '✓ URL ' +req.url +' has: ' + count2) + t.end(); + }) + }) +}); diff --git a/test/db_redis.test.js b/test/db_redis.test.js new file mode 100644 index 0000000..212e1d3 --- /dev/null +++ b/test/db_redis.test.js @@ -0,0 +1,35 @@ +var dir = __dirname.split('/')[__dirname.split('/').length-1]; +var file = dir + __filename.replace(__dirname, '') + " > "; +var test = require('tape'); +var db = require('../lib/db_redis.js'); +var extract = require('../lib/extract_request_data.js'); + +test(file+'Add a hit to the list for that url', function (t) { + var req = { + 'url': '/my/awesome/url', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)', + 'ip': '8.8.8.8', + 'accept-language': 'en-US,en;q=0.8,pt;q=0.6,es;', + } + var hit = extract(req); + db(hit, function (err, count) { + t.ok(count >= 0, '✓ URL ' +req.url +' has: ' + count) + t.end(); + }) +}); + +test(file+'Add a hit without language', function (t) { + var req = { + 'url': '/my/awesome/url', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)', + 'ip': '8.8.8.8' + } + var hit = extract(req); + db(hit, function (err, count1) { + t.ok(count1 >= 0, '✓ URl ' +req.url +' was added at a index: ' + count1) + db(hit, function (err, count2) { + t.ok(count2 > count1, '✓ URL ' +req.url +' count is: ' + count2); + t.end(); // shutdown redis con + }); + }); +}); diff --git a/test/error_logger.test.js b/test/error_logger.test.js new file mode 100644 index 0000000..5244a4f --- /dev/null +++ b/test/error_logger.test.js @@ -0,0 +1,30 @@ +var dir = __dirname.split('/')[__dirname.split('/').length-1]; +var file = dir + __filename.replace(__dirname, '') + " > "; +var test = require('tape'); +var error_log = require('../lib/error_logger.js'); + +test(file+'Log an error if not null', function (t) { + var error = { + Error: "ENOTDIR: not a directory, open '/dwyl/hits/.txt' at Error (native)", + errno: -20, + code: 'ENOTDIR', + syscall: 'open', + path: '/dwyl/hits/data/my/awesome/url/.txt' + } + var message = 'unable to open file'; + var res = error_log(error, message); + t.true(res === true); + t.end(); +}); + +test(file+'Handle Logging a non-object error', function (t) { + t.true(error_log(1, 0) === true); + t.end(); +}); + +test(file + 'No error -> No Log', function (t) { + var error = null; + var res = error_log(error); + t.true(res === false, 'No error logged'); + t.end(); +}); diff --git a/test/extract_request_data.test.js b/test/extract_request_data.test.js new file mode 100644 index 0000000..2f6c083 --- /dev/null +++ b/test/extract_request_data.test.js @@ -0,0 +1,54 @@ +var dir = __dirname.split('/')[__dirname.split('/').length-1]; +var file = dir + __filename.replace(__dirname, '') + " > "; +var test = require('tape'); +var extract = require('../lib/extract_request_data.js'); + +test(file + 'Extract "Hit" data from HTTP Request', function(t){ + var req = { + 'url': '/my/awesome/url', + headers: { + 'accept-language': 'en-US,en;q=0.8,pt;q=0.6,es;', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)' + }, + connection: { + remoteAddress: '88.88.88.88' + } + } + var hit = extract(req); + t.ok(hit.indexOf('/my/awesome/url|Mozilla/5.0') > -1, + '✓ HTTP request data extracted: ' + hit) + t.ok(hit.indexOf('EN-US') > -1, + '✓ extracted language: ' + hit.split('|')[4]) + t.end(); +}); + +test(file + 'fewer headers are set on request object', function(t){ + var req = { + 'url': '/my/awesome/url', + headers: { + 'accept-language': 'en-GB', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)', + 'x-forwarded-for': '88.88.88.88' + } + } + var hit = extract(req); + t.ok(hit.indexOf('/my/awesome/url|Mozilla/5.0') > -1, + '✓ Reduced request data extracted: ' + hit) + t.ok(hit.indexOf('EN-GB') > -1, + '✓ extracted language: ' + hit.split('|')[4]) + t.end(); +}); + +test(file + 'no language defined', function(t){ + var req = { + 'url': '/my/awesome/url', + headers: { + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)', + 'x-forwarded-for': '88.88.88.88' + } + } + var hit = extract(req); + t.ok(hit.split('|')[4] === '', + '✓ no language defined') + t.end(); +}); diff --git a/test/format_hit.test.js b/test/format_hit.test.js new file mode 100644 index 0000000..f9d1342 --- /dev/null +++ b/test/format_hit.test.js @@ -0,0 +1,14 @@ +var dir = __dirname.split('/')[__dirname.split('/').length-1]; +var file = dir + __filename.replace(__dirname, '') + " > "; +var test = require('tape'); +var format = require('../lib/format_hit.js') + +test("Create hash for url: 1234", function(t) { + var hit = '1503784599|/dwyl/hits|Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0|::1|EN-GB'; + var count = 42; + var formatted = format(hit, count); + var expected = '2017-08-26 21:56:39 /dwyl/hits 42 3wtuQ6JcHR' + t.equal(formatted, expected , + '✓ Hit: ' + hit + ' formatted as: ' + formatted); + t.end(); +}); diff --git a/test/hash.test.js b/test/hash.test.js new file mode 100644 index 0000000..0c092a3 --- /dev/null +++ b/test/hash.test.js @@ -0,0 +1,35 @@ +var uniki = require('../lib/hash.js'); +var test = require('tape'); + + +test("Create hash for url: 1234", function(t) { + var hash = uniki(1234); + t.equal(hash.length, 12, "Worked as expected " + hash); + console.log(hash) + t.equal(hash, '1ARVn2Auq2WA', "Hash is consistent. 1234 >> 1ARVn2Auq2WA") + t.end(); +}); + +test("Full Length Hash", function(t) { + var hash = uniki("RandomGobbledygook", 100); + t.true(hash.length === 78, "✓ Full Length is " + hash.length + ' chars'); + t.end(); +}); + +test("Browser Agent String Hash", function(t) { + var user_agent_string = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)|84.91.136.21|EN-US'; + var hash = uniki(user_agent_string, 10); + t.true(hash === '8HKg3NB5Cf', "Browser Data Hash: " + hash); + t.end(); +}); + +test("Consistenty check against 100 sample hashes", function(t) { + var fixture = require('./hash_fixtures.json'); + Object.keys(fixture).forEach(function(k) { + var expected = fixture[k]; + var actual = uniki(k, 10); + t.true(expected === actual, + '✓ hash(' + k + ') >> expected: ' + expected + ' === actual: ' + actual); + }) + t.end(); +}); diff --git a/test/hash_fixtures.json b/test/hash_fixtures.json new file mode 100644 index 0000000..8f37d51 --- /dev/null +++ b/test/hash_fixtures.json @@ -0,0 +1,102 @@ +{ + "3SsqzMWq": "eWVH8kBYSD", + "eWVH8kBY": "TeeyDAQ4CB", + "TeeyDAQ4": "mmM1jDFSVq", + "mmM1jDFS": "cK8BrR4Mey", + "cK8BrR4M": "m47txHn9Wt", + "m47txHn9": "UCSr4DRxvH", + "UCSr4DRx": "2ELG2YmSHS", + "2ELG2YmS": "gCvcTTskdw", + "gCvcTTsk": "8DzDfLnye1", + "8DzDfLny": "PH5VKFCPJB", + "PH5VKFCP": "p7RJFQNs7K", + "p7RJFQNs": "D8dF7YbQVU", + "D8dF7YbQ": "9VbiATGx2w", + "9VbiATGx": "KcbEH44Axm", + "KcbEH44A": "QD8JEpV3vy", + "QD8JEpV3": "ft4dj4dGbX", + "ft4dj4dG": "bvCks4hDyr", + "bvCks4hD": "xv3YEpuC8r", + "xv3YEpuC": "mQWF9urNvk", + "mQWF9urN": "MeekDZKvGs", + "MeekDZKv": "ei4dVyz2kk", + "ei4dVyz2": "eydUXW6Hfc", + "eydUXW6H": "RizKvQaMcE", + "RizKvQaM": "L5HayUsbvr", + "L5HayUsb": "rrkt5f9zw6", + "rrkt5f9z": "NCcZ7v18MP", + "NCcZ7v18": "PNUBzUf1Hs", + "PNUBzUf1": "2xWDBgiAAH", + "2xWDBgiA": "H4cdM8aJDF", + "H4cdM8aJ": "BEdY7paJpb", + "BEdY7paJ": "uiYxrfDQxq", + "uiYxrfDQ": "epiQY6FpnF", + "epiQY6Fp": "LbHqfPuimp", + "LbHqfPui": "9QUSAr963s", + "9QUSAr96": "tZiSfeGtjb", + "tZiSfeGt": "WcDxf8mTvG", + "WcDxf8mT": "yTvaTfJkrd", + "yTvaTfJk": "CALjmZEkbm", + "CALjmZEk": "1YrxJTUTFi", + "1YrxJTUT": "GDTq6j3P2B", + "GDTq6j3P": "xJAG6wt4aX", + "xJAG6wt4": "ATgJYgVnsZ", + "ATgJYgVn": "tuDMLhKdNC", + "tuDMLhKd": "KBFrbhJ87Q", + "KBFrbhJ8": "BcqhShkSYR", + "BcqhShkS": "RaFvZ9kCXM", + "RaFvZ9kC": "ABzXYuJYZm", + "ABzXYuJY": "ZWyaMHn2Nq", + "ZWyaMHn2": "QjPZ91YnpA", + "QjPZ91Yn": "Mhygcvg913", + "Mhygcvg9": "cnRyVdCwju", + "cnRyVdCw": "DD5LJKZpss", + "DD5LJKZp": "JnMhiaKZD5", + "JnMhiaKZ": "QbMzukQjfW", + "QbMzukQj": "yYHB1kr4eR", + "yYHB1kr4": "b8qfZgZ3fa", + "b8qfZgZ3": "BaDxwVxLZ1", + "BaDxwVxL": "wEggb44WNg", + "wEggb44W": "NRxWnLtPEh", + "NRxWnLtP": "jDMQ5z1b3k", + "jDMQ5z1b": "MCquGjRhjE", + "MCquGjRh": "DGNedLyQYH", + "DGNedLyQ": "RKfbFMAcDQ", + "RKfbFMAc": "9LH3NTKHCr", + "9LH3NTKH": "WwrBGcWBz8", + "WwrBGcWB": "A2XCx6JGpq", + "A2XCx6JG": "zXMQfKi8fH", + "zXMQfKi8": "4Rr5SQvn1w", + "4Rr5SQvn": "AA1561SCdD", + "AA1561SC": "6iPKRYcvBW", + "6iPKRYcv": "MjxYeytSr3", + "MjxYeytS": "ynsaauCSJs", + "ynsaauCS": "KLx9EkyhbB", + "KLx9Ekyh": "As2JsXUjv4", + "As2JsXUj": "gCAvDGgKAf", + "gCAvDGgK": "rgJ7W2NGKu", + "rgJ7W2NG": "wWP9wpieRa", + "wWP9wpie": "whg76M2Zn7", + "whg76M2Z": "TuXJCiWGn8", + "TuXJCiWG": "MQaaqVSats", + "MQaaqVSa": "dvEYDe67mg", + "dvEYDe67": "YPhWT43FYu", + "YPhWT43F": "aABrr4NZv6", + "aABrr4NZ": "hNFjpPBhPz", + "hNFjpPBh": "QyLANAvBdZ", + "QyLANAvB": "ZxYtVyXqxg", + "ZxYtVyXq": "PQT1xxTv8f", + "PQT1xxTv": "GGcXzYyRZj", + "GGcXzYyR": "Gbcjvs3HRm", + "Gbcjvs3H": "bB7G8nbQe6", + "bB7G8nbQ": "CS46921VBH", + "CS46921V": "paE6D9FxBm", + "paE6D9Fx": "4Dt85zHh2F", + "4Dt85zHh": "3JFU3udq3s", + "3JFU3udq": "xq9p2F74uu", + "xq9p2F74": "c88pLtc7y8", + "c88pLtc7": "nbK3ePuCkH", + "nbK3ePuC": "AtT14PZ9D2", + "AtT14PZ9": "2AJxk5F81D", + "2AJxk5F8": "H4GGnC8PdG" +} diff --git a/test/hits.test.js b/test/hits.test.js index 4aede85..879957a 100644 --- a/test/hits.test.js +++ b/test/hits.test.js @@ -1,45 +1,50 @@ var dir = __dirname.split('/')[__dirname.split('/').length-1]; var file = dir + __filename.replace(__dirname, '') + " > "; var test = require('tape'); -var hits = require('../lib/hits'); +var extract = require('../lib/extract_request_data.js'); -test(file+'Add a hit to the list for that url', function(t){ +test(file + 'REDIS add hit', function (t) { + process.env.REDISCLOUD_URL = 'redis://u:@127.0.0.1:6379'; + var hits = require('../lib/hits'); var req = { 'url': '/my/awesome/url', - 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)', - 'ip': '8.8.8.8', - 'accept-language': 'en-US,en;q=0.8,pt;q=0.6,es;', + headers: { + 'accept-language': 'en-US,en;q=0.8,pt;q=0.6,es;', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)' + }, + connection: { + remoteAddress: '88.8.88.8' + } } - hits.add(req, function(err, data) { - t.ok(data >= 0, '✓ REQ ' +req.url +' was added at a index: ' + data) - // hits.redisClient.end(); - t.end(); - }) -}); - -test(file+'Add a hit without language', function(t){ - var req = { - 'url': '/my/awesome/url', - 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)', - 'ip': '8.8.8.8' - } - hits.add(req, function(err, data) { - t.ok(data >= 0, '✓ REQ ' +req.url +' was added at a index: ' + data) - // hits.redisClient.end(); - t.end(); - }) + var hit = extract(req); + hits(hit, function (err, count) { + hits(hit, function (err, count2) { + // console.log('16 >>> ', err, count); + t.ok(count === count2 - 1, + '✓ URL ' +req.url +' was added at a index: ' + count); + require('redis-connection')().end(true) + require('decache')('../lib/hits'); + t.end(); + }); + }); }); -test(file+'Add a hit without language', function(t){ +test(file+'Filesystem add hit', function (t) { + delete process.env.REDISCLOUD_URL; // force use of Filesystem + var hits = require('../lib/hits'); var req = { 'url': '/my/awesome/url', - 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)', - 'ip': '8.8.8.8' + headers: { + 'accept-language': 'en-US,en;q=0.8,pt;q=0.6,es;', + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)' + }, + connection: { + remoteAddress: '88.8.88.8' + } } - hits.count(req.url, function(err, data) { - console.log(data); - t.ok(data >= 0, '✓ REQ ' +req.url +' was added at a index: ' + data) - hits.redisClient.end(); - t.end(); + var hit = extract(req); + hits(hit, function (err, count) { + t.ok(count >= 0, '✓ REQ ' +req.url +' was added at a index: ' + count); + t.end(require('redis-connection')().end(true)); // shutdown redis con }) }); diff --git a/test/make_svg.test.js b/test/make_svg.test.js new file mode 100644 index 0000000..8257669 --- /dev/null +++ b/test/make_svg.test.js @@ -0,0 +1,16 @@ +var dir = __dirname.split('/')[__dirname.split('/').length-1]; +var file = dir + __filename.replace(__dirname, '') + " > "; +var test = require('tape'); +var make_svg = require('../lib/make_svg.js'); + +test(file + 'Make SVG file from template & count', function(t){ + var count = 1337; + var svg = make_svg(count); + t.ok(svg.indexOf(count.toString()) > -1, + '✓ SVG created for count: ' + count) + t.end(); +}); + +test.onFinish(function () { + require('redis-connection')().end(true); // shutdown redis con +}) diff --git a/test/teardown.test.js b/test/teardown.test.js new file mode 100644 index 0000000..5eda93c --- /dev/null +++ b/test/teardown.test.js @@ -0,0 +1,17 @@ +var dir = __dirname.split('/')[__dirname.split('/').length-1]; +var file = dir + __filename.replace(__dirname, '') + " > "; +var test = require('tape'); +var fs = require('fs'); +var path = require('path'); +var rimraf = require('rimraf'); +var LOG_DIR = path.resolve(__dirname, '../logs'); + +test(file + 'Delete /logs directory to ensure tests are fresh', function (t) { + console.log('LOG_DIR: ', LOG_DIR); + rimraf(LOG_DIR, function () { + console.log('fs.existsSync(LOG_DIR):', fs.existsSync(LOG_DIR)); + t.equal(false, fs.existsSync(LOG_DIR), + LOG_DIR + ' does not exist ' + fs.existsSync(LOG_DIR)); + t.end(); + }); +});