From fa5cf319838f58a3fb82f958f54ab5f673485f99 Mon Sep 17 00:00:00 2001 From: Klemek Date: Tue, 30 Mar 2021 15:20:22 +0200 Subject: [PATCH 1/5] comma dangle --- .eslintrc.js | 24 ++++++++++++++---------- src/app.js | 8 ++++---- src/file_walker.js | 4 ++-- src/renderer.js | 10 +++++----- test/app.test.js | 36 ++++++++++++++++++------------------ test/file_walker.test.js | 26 +++++++++++++------------- test/renderer.test.js | 20 ++++++++++---------- 7 files changed, 66 insertions(+), 62 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 5004e26..85f98ec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,38 +4,42 @@ module.exports = { 'commonjs': true, 'es2021': true, 'node': true, - 'jest/globals': true + 'jest/globals': true, }, 'extends': ['eslint:recommended', 'plugin:jest/recommended'], 'parserOptions': { - 'ecmaVersion': 12 + 'ecmaVersion': 12, }, 'rules': { 'indent': [ 'error', - 4 + 4, ], 'linebreak-style': [ 'error', - 'unix' + 'unix', ], 'quotes': [ 'error', - 'single' + 'single', ], 'semi': [ 'error', - 'always' + 'always', ], 'curly': [ 'error', - 'all' + 'all', ], 'brace-style': [ 'error', - '1tbs' + '1tbs', ], 'jest/no-done-callback': 'off', - 'jest/expect-expect': 'off' - } + 'jest/expect-expect': 'off', + 'comma-dangle': [ + 'error', + 'always-multiline', + ], + }, }; diff --git a/src/app.js b/src/app.js index 6ae48e6..0459650 100644 --- a/src/app.js +++ b/src/app.js @@ -93,7 +93,7 @@ module.exports = (config) => { host: host, version: pjson.version, request: req, - config: config + config: config, }; res.render(vPath, data, (err, html) => { if (err && vPath !== path.join(config['data_dir'], config['home']['error'])) { @@ -130,7 +130,7 @@ module.exports = (config) => { //rate limit for safer server const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes - max: config['rate_limit'] + max: config['rate_limit'], }); app.use(limiter); @@ -174,7 +174,7 @@ module.exports = (config) => { 'title': config['rss']['title'], 'description': config['rss']['description'], 'feed_url': host + req.url, - 'site_url': host + 'site_url': host, }); Object.values(articles) .slice(0, config['rss']['length']) @@ -182,7 +182,7 @@ module.exports = (config) => { feed.item({ title: article.title, url: host + article.url, - date: article.date + date: article.date, }); }); lastRSS = feed.xml(); diff --git a/src/file_walker.js b/src/file_walker.js index 22feb2f..4a397ca 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -92,7 +92,7 @@ module.exports = (config) => { realPath: path.join(config['data_dir'], p[0], p[1], p[2], p[3]), year: parseInt(p[0]), month: parseInt(p[1]), - day: parseInt(p[2]) + day: parseInt(p[2]), }; article.date = new Date(article.year, article.month, article.day); article.date.setUTCHours(0); @@ -116,6 +116,6 @@ module.exports = (config) => { }); }); - } + }, }; }; \ No newline at end of file diff --git a/src/renderer.js b/src/renderer.js index c059204..bd197cc 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -112,9 +112,9 @@ module.exports = (config) => { MathJax: { tex2jax: { inlineMath: [['$', '$']], - displayMath: [['$$', '$$']] - } - } + displayMath: [['$$', '$$']], + }, + }, }); } @@ -131,7 +131,7 @@ module.exports = (config) => { const mjConf = { math: eq, format: format, - speakText: config['mathjax']['speak_text'] + speakText: config['mathjax']['speak_text'], }; mjConf[output] = true; mjAPI.typeset(mjConf, (res) => { @@ -222,7 +222,7 @@ module.exports = (config) => { }); }); }); - } + }, }; }; diff --git a/test/app.test.js b/test/app.test.js index ec5599b..590172b 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -163,7 +163,7 @@ describe('Test root path', () => { utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05'), path.join(dataDir, '2018', '05', '05'), - path.join(dataDir, '2017', '05', '05') + path.join(dataDir, '2017', '05', '05'), ]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'draft.md'), @@ -219,11 +219,11 @@ describe('Test RSS feed', () => { test('200 2 rss items', (done, fail) => { utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05'), - path.join(dataDir, '2018', '05', '05') + path.join(dataDir, '2018', '05', '05'), ]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), - path.join(dataDir, '2018', '05', '05', 'index.md') + path.join(dataDir, '2018', '05', '05', 'index.md'), ]); app.reload(() => { request(app).get('/rsstest').then((response) => { @@ -238,12 +238,12 @@ describe('Test RSS feed', () => { utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05'), path.join(dataDir, '2018', '05', '05'), - path.join(dataDir, '2017', '05', '05') + path.join(dataDir, '2017', '05', '05'), ]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), path.join(dataDir, '2018', '05', '05', 'index.md'), - path.join(dataDir, '2017', '05', '05', 'index.md') + path.join(dataDir, '2017', '05', '05', 'index.md'), ]); app.reload(() => { request(app).get('/rsstest').then((response) => { @@ -265,10 +265,10 @@ describe('Test webhook', () => { }); }); test('200 no secret', (done) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), - path.join(dataDir, testTemplate) + path.join(dataDir, testTemplate), ]); config['webhook']['pull_command'] = 'git --help'; request(app).post('/webhooktest').then((response) => { @@ -280,10 +280,10 @@ describe('Test webhook', () => { }); }); test('500 command failed', (done) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), - path.join(dataDir, testTemplate) + path.join(dataDir, testTemplate), ]); config['webhook']['pull_command'] = 'qzgfqgqz'; request(app).post('/webhooktest').then((response) => { @@ -322,7 +322,7 @@ describe('Test articles rendering', () => { }); test('500 fail to render', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello'); fs.writeFileSync(path.join(dataDir, testTemplate), '<%- articl.content %><%- `reload` %>'); app.reload(() => { @@ -334,7 +334,7 @@ describe('Test articles rendering', () => { }); test('500 no template', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello'); app.reload(() => { request(app).get('/2019/05/05/hello/').then((response) => { @@ -345,7 +345,7 @@ describe('Test articles rendering', () => { }); test('200 rendered article', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello'); fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `reload` %>'); app.reload(() => { @@ -358,7 +358,7 @@ describe('Test articles rendering', () => { }); test('200 rendered draft', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'draft.md'), '# Hello'); fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `reload` %>'); app.reload(() => { @@ -371,10 +371,10 @@ describe('Test articles rendering', () => { }); test('200 other url', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), - path.join(dataDir, testTemplate) + path.join(dataDir, testTemplate), ]); app.reload(() => { request(app).get('/2019/05/05/anything/').then((response) => { @@ -385,10 +385,10 @@ describe('Test articles rendering', () => { }); test('200 other url 2', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), - path.join(dataDir, testTemplate) + path.join(dataDir, testTemplate), ]); app.reload(() => { request(app).get('/2019/05/05/').then((response) => { @@ -441,7 +441,7 @@ describe('Test static files', () => { }); }); test('200 valid resource of article', (done) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'somefile.txt'), 'filecontent'); request(app).get('/2019/05/05/title/somefile.txt').then((response) => { expect(response.statusCode).toBe(200); diff --git a/test/file_walker.test.js b/test/file_walker.test.js index 109de43..bd9c244 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -15,8 +15,8 @@ const config = { 'draft': 'draft.md', 'default_title': 'Untitled', 'default_thumbnail': 'default.png', - 'thumbnail_tag': 'thumbnail' - } + 'thumbnail_tag': 'thumbnail', + }, }; const fw = require('../src/file_walker')(config); @@ -46,7 +46,7 @@ describe('Test function fileTree', () => { utils.createEmptyDirs([ path.join(dataDir, 'test', 'test'), path.join(dataDir, 'test', 'test2'), - path.join(dataDir, 'test2') + path.join(dataDir, 'test2'), ]); fw.fileTree(dataDir, (err, list) => { expect(err).toBeNull(); @@ -58,7 +58,7 @@ describe('Test function fileTree', () => { test('simple files', (done) => { const fileList = [ path.join(dataDir, 'f1.txt'), - path.join(dataDir, 'f2.txt') + path.join(dataDir, 'f2.txt'), ]; utils.createEmptyFiles(fileList); fw.fileTree(dataDir, (err, list) => { @@ -72,13 +72,13 @@ describe('Test function fileTree', () => { test('nested files', (done) => { utils.createEmptyDirs([ path.join(dataDir, 'test', 'test'), - path.join(dataDir, 'test2') + path.join(dataDir, 'test2'), ]); const fileList = [ path.join(dataDir, 'f1.txt'), path.join(dataDir, 'test', 'f2.txt'), path.join(dataDir, 'test', 'test', 'f3.txt'), - path.join(dataDir, 'test2', 'f4.txt') + path.join(dataDir, 'test2', 'f4.txt'), ]; utils.createEmptyFiles(fileList); fw.fileTree(dataDir, (err, list) => { @@ -120,7 +120,7 @@ describe('Test index article reading', () => { expect(err).toBeNull(); expect(info).toEqual({ title: 'This is an awesome title !?¤', - thumbnail: './thumbnail.jpg' + thumbnail: './thumbnail.jpg', }); done(); }); @@ -136,7 +136,7 @@ describe('Test index article reading', () => { expect(err).toBeNull(); expect(info).toEqual({ title: undefined, - thumbnail: './thumbnail.jpg' + thumbnail: './thumbnail.jpg', }); done(); }); @@ -148,7 +148,7 @@ describe('Test index article reading', () => { expect(err).toBeNull(); expect(info).toEqual({ title: 'title', - thumbnail: undefined + thumbnail: undefined, }); done(); }); @@ -164,7 +164,7 @@ describe('Test index article reading', () => { expect(err).toBeNull(); expect(info).toEqual({ title: 'This is an awesome title !?¤', - thumbnail: undefined + thumbnail: undefined, }); done(); }); @@ -181,7 +181,7 @@ describe('Test index article reading', () => { expect(err).toBeNull(); expect(info).toEqual({ title: 'This is an awesome title !?¤', - thumbnail: './thumbnail.jpg' + thumbnail: './thumbnail.jpg', }); done(); }); @@ -208,12 +208,12 @@ describe('Test article fetching', () => { test('misplaced index file', (done) => { utils.createEmptyDirs([ path.join(dataDir, 'test', 'test'), - path.join(dataDir, '2019', '05', '05') + path.join(dataDir, '2019', '05', '05'), ]); utils.createEmptyFiles([ path.join(dataDir, testIndex), path.join(dataDir, 'test', 'test', testIndex), - path.join(dataDir, '2019', '05', testIndex) + path.join(dataDir, '2019', '05', testIndex), ]); fw.fetchArticles((err, dict) => { expect(err).toBeNull(); diff --git a/test/renderer.test.js b/test/renderer.test.js index ea629c1..9cd8711 100644 --- a/test/renderer.test.js +++ b/test/renderer.test.js @@ -15,15 +15,15 @@ const config = { }, 'showdown': { 'simplifiedAutoLink': true, - 'smartIndentationFix': true + 'smartIndentationFix': true, }, 'mathjax': { 'output_format': 'html', - 'speak_text': false + 'speak_text': false, }, 'plantuml': { - 'output_format': 'svg' - } + 'output_format': 'svg', + }, }; const renderer = require('../src/renderer')(config); @@ -48,7 +48,7 @@ describe('get parts', () => { const data = 'Hello\nthere\ngeneral\nkenobi'; const parts = renderer.getParts(data); expect(parts.map(p => p.text)).toEqual([ - 'Hello\nthere\ngeneral\nkenobi' + 'Hello\nthere\ngeneral\nkenobi', ]); }); test('lot of stuff', () => { @@ -58,27 +58,27 @@ describe('get parts', () => { { index: 0, end: 12, - text: 'Hello\nthere\n' + text: 'Hello\nthere\n', }, { index: 22, end: 30, - text: '\ngeneral' + text: '\ngeneral', }, { index: 53, end: 54, - text: '\n' + text: '\n', }, { index: 78, end: 79, - text: '\n' + text: '\n', }, { index: 109, end: 115, - text: 'kenobi' + text: 'kenobi', }, ]); }); From b0ba52e1409d7a163b82aaa85ca726fa80c354d6 Mon Sep 17 00:00:00 2001 From: Klemek Date: Tue, 30 Mar 2021 15:30:54 +0200 Subject: [PATCH 2/5] work in progress hit_counter --- src/app.js | 20 ++++++++++++++++++-- src/hit_counter.js | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/hit_counter.js diff --git a/src/app.js b/src/app.js index 0459650..55133c7 100644 --- a/src/app.js +++ b/src/app.js @@ -51,6 +51,7 @@ module.exports = (config) => { let showError; const fw = require('./file_walker')(config); const renderer = require('./renderer')(config); + const hc = require('./hit_counter')(config); // set view engine from configuration app.set('view engine', config['view_engine']); @@ -157,14 +158,22 @@ module.exports = (config) => { if (err) { showError(req, res, 404); } else { + hc.count(req, '/'); render(req, res, homePath, { articles: Object.values(articles) - .filter(d => !d.draft).sort((a, b) => ('' + b.path).localeCompare(a.path)) + .filter(d => !d.draft).sort((a, b) => ('' + b.path).localeCompare(a.path)), }); } }); }); + app.get('/stats', (req, res) => { + const data = hc.read('/'); + res.json({ + hits: data.hits, + visitors: data.visitors, + }); + }); //RSS endpoint app.get(config['rss']['endpoint'], (req, res) => { @@ -229,12 +238,19 @@ module.exports = (config) => { // catch all article urls and render them app.get('*', (req, res, next) => { - if (/^\/\d{4}\/\d{2}\/\d{2}\/$/.test(req.path)) { + if (/^\/\d{4}\/\d{2}\/\d{2}\/(stats)?$/.test(req.path)) { const articlePath = req.path.substr(1, 10); const article = articles[articlePath]; if (!article) { showError(req, res, 404); + } else if (req.path.endsWith('stats')) { + const data = hc.read(articlePath); + res.json({ + hits: data.hits, + visitors: data.visitors, + }); } else { + hc.count(req, articlePath); renderer.render(article.realPath, (err, html) => { if (err) { console.log(cons.error, `failed to render article ${req.path} : ${err}`); diff --git a/src/hit_counter.js b/src/hit_counter.js new file mode 100644 index 0000000..52c8141 --- /dev/null +++ b/src/hit_counter.js @@ -0,0 +1,17 @@ +module.exports = (config) => { + const count = (req, path) => { + + }; + + const read = (path) => { + return { + hits: 0, + visitors: 0, + }; + }; + + return { + count: count, + read: read, + }; +}; \ No newline at end of file From 88cb7ce30f50d6a58467de15a24035d3bc4d4514 Mon Sep 17 00:00:00 2001 From: Klemek Date: Tue, 30 Mar 2021 16:29:20 +0200 Subject: [PATCH 3/5] eslint update --- .eslintrc.js | 73 +++++- src/app.js | 122 +++++----- src/config.js | 4 +- src/file_walker.js | 161 ++++++------- src/hit_counter.js | 2 +- src/postinstall.js | 4 +- src/renderer.js | 218 ++++++++++-------- src/script_loader.js | 2 +- src/server.js | 2 +- test/app.test.js | 459 ++++++++++++++++++++----------------- test/config.test.js | 16 +- test/file_walker.test.js | 31 +-- test/renderer.test.js | 26 +-- test/script_loader.test.js | 2 +- test/test_utils.js | 6 +- 15 files changed, 637 insertions(+), 491 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 85f98ec..c157a97 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,16 +1,19 @@ module.exports = { - 'plugins': ['jest'], - 'env': { + plugins: [ 'jest' ], + env: { 'commonjs': true, 'es2021': true, 'node': true, 'jest/globals': true, }, - 'extends': ['eslint:recommended', 'plugin:jest/recommended'], - 'parserOptions': { - 'ecmaVersion': 12, + extends: [ + 'eslint:recommended', + 'plugin:jest/recommended', + ], + parserOptions: { + ecmaVersion: 12, }, - 'rules': { + rules: { 'indent': [ 'error', 4, @@ -41,5 +44,63 @@ module.exports = { 'error', 'always-multiline', ], + 'complexity': 'error', + 'consistent-return': 'error', + 'dot-location': [ + 'error', + 'property', + ], + 'eqeqeq': [ + 'error', + 'always', + { null: 'ignore' }, + ], + 'no-empty-function': 'error', + 'no-floating-decimal': 'error', + 'no-multi-spaces': 'error', + 'camelcase': [ + 'error', + { properties: 'never' }, + ], + 'comma-spacing': [ + 'error', + { before: false, after: true }, + ], + 'array-bracket-newline': [ + 'error', + { multiline: true }, + ], + 'array-element-newline': [ + 'error', + { multiline: true, minItems: 2 }, + ], + 'array-bracket-spacing': [ + 'error', + 'always', + ], + 'object-curly-spacing': [ + 'error', + 'always', + ], + 'comma-style': 'error', + 'computed-property-spacing': 'error', + 'eol-last': 'error', + 'func-call-spacing': 'error', + 'key-spacing': 'error', + 'keyword-spacing': 'error', + 'multiline-comment-style': 'error', + 'newline-per-chained-call': 'error', + 'no-lonely-if': 'error', + 'no-multiple-empty-lines': 'error', + 'no-trailing-spaces': 'error', + 'no-unneeded-ternary': 'error', + 'no-whitespace-before-property': 'error', + 'operator-assignment': 'error', + 'quote-props': [ + 'error', + 'consistent-as-needed', + ], + 'space-before-blocks': 'error', + 'space-infix-ops': 'error', }, }; diff --git a/src/app.js b/src/app.js index 55133c7..341128d 100644 --- a/src/app.js +++ b/src/app.js @@ -28,26 +28,26 @@ const cons = { module.exports = (config) => { /** - * Fetch articles from the data folder and send success as a response - * @param success - * @param error - */ + * Fetch articles from the data folder and send success as a response + * @param success + * @param error + */ let reload; /** - * Render the page with the view engine and catch errors - * @param req - * @param res - * @param vPath - path of the view - * @param data - data to pass to the view - * @param code - code to send along the page - */ + * Render the page with the view engine and catch errors + * @param req + * @param res + * @param vPath - path of the view + * @param data - data to pass to the view + * @param code - code to send along the page + */ let render; /** - * Show an error with the correct page - * @param req - * @param res - * @param code - error code - */ + * Show an error with the correct page + * @param req + * @param res + * @param code - error code + */ let showError; const fw = require('./file_walker')(config); const renderer = require('./renderer')(config); @@ -66,21 +66,22 @@ module.exports = (config) => { fw.fetchArticles((err, dict) => { if (err) { console.error(cons.error, 'error loading articles : ' + err); - return error ? error() : null; - } - Object.keys(articles).forEach((key) => delete articles[key]); - Object.keys(dict).forEach((key) => articles[key] = dict[key]); - const nb = Object.keys(articles).length; - const dnb = Object.values(articles).filter(a => a.draft).length; - if (nb > 0) { - console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''} (${dnb} drafted)`); + error(); } else { - console.log(cons.warn, 'no articles loaded, check your configuration'); + Object.keys(articles).forEach((key) => delete articles[key]); + Object.keys(dict).forEach((key) => articles[key] = dict[key]); + const nb = Object.keys(articles).length; + const dnb = Object.values(articles).filter(a => a.draft).length; + if (nb > 0) { + console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''} (${dnb} drafted)`); + } else { + console.log(cons.warn, 'no articles loaded, check your configuration'); + } + + lastRSS = ''; + + success(); } - - lastRSS = ''; - - success(); }); }; if (config['test']) { @@ -115,7 +116,7 @@ module.exports = (config) => { if (err) { res.sendStatus(code); } else { - render(req, res, errorPath, {error: code}, code); + render(req, res, errorPath, { error: code }, code); } }); }; @@ -142,7 +143,7 @@ module.exports = (config) => { res.end = (chunk, encoding) => { fs.appendFile(config['access_log'], `${res.statusCode} ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n`, - {encoding: 'UTF-8'}, () => { + { encoding: 'UTF-8' }, () => { res.end = end; res.end(chunk, encoding); }); @@ -162,7 +163,8 @@ module.exports = (config) => { render(req, res, homePath, { articles: Object.values(articles) - .filter(d => !d.draft).sort((a, b) => ('' + b.path).localeCompare(a.path)), + .filter(d => !d.draft) + .sort((a, b) => ('' + b.path).localeCompare(a.path)), }); } }); @@ -180,10 +182,10 @@ module.exports = (config) => { if (config['modules']['rss']) { if (!lastRSS) { const feed = new Rss({ - 'title': config['rss']['title'], - 'description': config['rss']['description'], - 'feed_url': host + req.url, - 'site_url': host, + title: config['rss']['title'], + description: config['rss']['description'], + feed_url: host + req.url, + site_url: host, }); Object.values(articles) .slice(0, config['rss']['length']) @@ -205,24 +207,29 @@ module.exports = (config) => { //webhook endpoint app.post(config['webhook']['endpoint'], (req, res) => { if (config['modules']['webhook']) { + let valid = true; if (config['webhook']['signature_header'] && config['webhook']['secret']) { const payload = JSON.stringify(req.body) || ''; const hmac = crypto.createHmac('sha1', config['webhook']['secret']); const digest = 'sha1=' + hmac.update(payload).digest('hex'); const checksum = req.headers[config['webhook']['signature_header']]; if (!checksum || !digest || checksum !== digest) { - return res.sendStatus(403); + res.sendStatus(403); + valid = false; } } - cp.exec(config['webhook']['pull_command'], {cwd: path.join(__dirname, '..', config['data_dir'])}, (err) => { - if (err) { - console.log(cons.error, `command '${config['webhook']['pull_command']}' failed : ${err}`); - return res.sendStatus(500); - } - reload(() => { - res.sendStatus(200); + if (valid) { + cp.exec(config['webhook']['pull_command'], { cwd: path.join(__dirname, '..', config['data_dir']) }, (err) => { + if (err) { + console.log(cons.error, `command '${config['webhook']['pull_command']}' failed : ${err}`); + res.sendStatus(500); + } else { + reload(() => { + res.sendStatus(200); + }); + } }); - }); + } } else { res.sendStatus(400); } @@ -254,18 +261,19 @@ module.exports = (config) => { renderer.render(article.realPath, (err, html) => { if (err) { console.log(cons.error, `failed to render article ${req.path} : ${err}`); - return showError(req, res, 500); + showError(req, res, 500); + } else { + article.content = html; + const templatePath = path.join(config['data_dir'], config['article']['template']); + fs.access(templatePath, fs.constants.R_OK, (err) => { + if (err) { + console.log(cons.error, `no template found at ${templatePath}`); + showError(req, res, 500); + } else { + render(req, res, templatePath, { article: article }); + } + }); } - article.content = html; - const templatePath = path.join(config['data_dir'], config['article']['template']); - fs.access(templatePath, fs.constants.R_OK, (err) => { - if (err) { - console.log(cons.error, `no template found at ${templatePath}`); - showError(req, res, 500); - } else { - render(req, res, templatePath, {article: article}); - } - }); }); } } else { @@ -300,7 +308,7 @@ module.exports = (config) => { } fs.appendFile(config['error_log'], `500 ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n${err.stack}\n`, - {encoding: 'UTF-8'}, () => { + { encoding: 'UTF-8' }, () => { next(err); }); }); diff --git a/src/config.js b/src/config.js index 58c373d..c0276a2 100644 --- a/src/config.js +++ b/src/config.js @@ -25,11 +25,11 @@ const merge = (ref, src) => { module.exports = () => { try { - let configData = fs.readFileSync('config.json', {encoding: 'UTF-8'}); + let configData = fs.readFileSync('config.json', { encoding: 'UTF-8' }); let config = JSON.parse(configData); return merge(refConfig, config); } catch (error) { console.log('\x1b[33m⚠\x1b[0m %s', 'Failed to load config.json : ' + error); return refConfig; } -}; \ No newline at end of file +}; diff --git a/src/file_walker.js b/src/file_walker.js index 4a397ca..96b40bd 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -11,29 +11,31 @@ const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/'); const getFileTree = (dir, cb) => { let list = []; let remaining = 0; - fs.readdir(dir, {withFileTypes: true}, (err, items) => { + fs.readdir(dir, { withFileTypes: true }, (err, items) => { if (err) { - return cb(err); - } - items.forEach((item) => { - if (item.isDirectory()) { - remaining++; - getFileTree(path.join(dir, item.name), (err, out) => { - if (err) { - return cb(err); - } - list.push(...out); - remaining--; - if (remaining === 0) { - cb(null, list); - } - }); - } else { - list.push(path.join(dir, item.name)); + cb(err); + } else { + items.forEach((item) => { + if (item.isDirectory()) { + remaining++; + getFileTree(path.join(dir, item.name), (err, out) => { + if (err) { + cb(err); + } else { + list.push(...out); + remaining--; + if (remaining === 0) { + cb(null, list); + } + } + }); + } else { + list.push(path.join(dir, item.name)); + } + }); + if (remaining === 0) { + cb(null, list); } - }); - if (remaining === 0) { - cb(null, list); } }); }; @@ -45,21 +47,21 @@ const getFileTree = (dir, cb) => { * @param cb */ const readIndexFile = (path, thumbnailTag, cb) => { - fs.readFile(path, {encoding: 'UTF-8'}, (err, data) => { + fs.readFile(path, { encoding: 'UTF-8' }, (err, data) => { if (err) { - return cb(err); + cb(err); + } else { + let info = {}; + + const regRes1 = data.match(/(^|[^#])#([^#\r\n]*)\r?\n?$/m); + info.title = regRes1 ? regRes1[2].trim() : undefined; + + const thumbnailRegEx = new RegExp(`!\\[${thumbnailTag}]\\(([^)]*)\\)`, 'i'); + const regRes2 = data.match(thumbnailRegEx); + info.thumbnail = regRes2 ? regRes2[1].trim() : undefined; + + cb(null, info); } - - let info = {}; - - const regRes1 = data.match(/(^|[^#])#([^#\r\n]*)\r?\n?$/m); - info.title = regRes1 ? regRes1[2].trim() : undefined; - - const thumbnailRegEx = new RegExp(`!\\[${thumbnailTag}]\\(([^)]*)\\)`, 'i'); - const regRes2 = data.match(thumbnailRegEx); - info.thumbnail = regRes2 ? regRes2[1].trim() : undefined; - - cb(null, info); }); }; @@ -68,54 +70,57 @@ module.exports = (config) => { fileTree: config['test'] ? getFileTree : undefined, readIndexFile: config['test'] ? readIndexFile : undefined, /** - * find and read all articles inside the data directory - * @param cb - */ + * find and read all articles inside the data directory + * @param cb + */ fetchArticles: (cb) => { getFileTree(config['data_dir'], (err, fileList) => { if (err) { - return cb(err); - } - const paths = fileList - .map((p) => p.substr(config['data_dir'].length + 1).split(path.sep)) - .filter((p) => p.length === 4 && (p[3] === config['article']['index'] || p[3] === config['article']['draft']) && - /^\d{4}$/.test(p[0]) && /^\d{2}$/.test(p[1]) && /^\d{2}$/.test(p[2])); - if (paths.length === 0) { - cb(null, {}); - } - const articles = {}; - let remaining = 0; - paths.forEach((p) => { - const article = { - path: joinUrl(p[0], p[1], p[2]), - draft: p[3] === config['article']['draft'], - realPath: path.join(config['data_dir'], p[0], p[1], p[2], p[3]), - year: parseInt(p[0]), - month: parseInt(p[1]), - day: parseInt(p[2]), - }; - article.date = new Date(article.year, article.month, article.day); - article.date.setUTCHours(0); - remaining++; - readIndexFile(article.realPath, config['article']['thumbnail_tag'], (err, info) => { - if (err) { - return cb(err); - } - article.title = info.title || config['article']['default_title']; - article.thumbnail = info.thumbnail ? joinUrl(article.path, info.thumbnail) : config['article']['default_thumbnail']; - article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_'); - article.url = '/' + joinUrl(article.path, article.escapedTitle) + '/'; - if (!articles[article.path] || !article.draft) { - articles[article.path] = article; - } - remaining--; - if (remaining === 0) { - cb(null, articles); - } + cb(err); + } else { + const paths = fileList + .map((p) => p.substr(config['data_dir'].length + 1).split(path.sep)) + .filter((p) => p.length === 4 && (p[3] === config['article']['index'] || p[3] === config['article']['draft']) && + /^\d{4}$/.test(p[0]) && /^\d{2}$/.test(p[1]) && /^\d{2}$/.test(p[2])); + if (paths.length === 0) { + cb(null, {}); + } + const articles = {}; + let remaining = 0; + paths.forEach((p) => { + const article = { + path: joinUrl(p[0], p[1], p[2]), + draft: p[3] === config['article']['draft'], + realPath: path.join(config['data_dir'], p[0], p[1], p[2], p[3]), + year: parseInt(p[0]), + month: parseInt(p[1]), + day: parseInt(p[2]), + }; + article.date = new Date(article.year, article.month, article.day); + article.date.setUTCHours(0); + remaining++; + readIndexFile(article.realPath, config['article']['thumbnail_tag'], (err, info) => { + if (err) { + cb(err); + } else { + article.title = info.title || config['article']['default_title']; + article.thumbnail = info.thumbnail ? joinUrl(article.path, info.thumbnail) : config['article']['default_thumbnail']; + article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ') + .trim() + .replace(/ /gm, '_'); + article.url = '/' + joinUrl(article.path, article.escapedTitle) + '/'; + if (!articles[article.path] || !article.draft) { + articles[article.path] = article; + } + remaining--; + if (remaining === 0) { + cb(null, articles); + } + } + }); }); - }); - + } }); }, }; -}; \ No newline at end of file +}; diff --git a/src/hit_counter.js b/src/hit_counter.js index 52c8141..df922f7 100644 --- a/src/hit_counter.js +++ b/src/hit_counter.js @@ -14,4 +14,4 @@ module.exports = (config) => { count: count, read: read, }; -}; \ No newline at end of file +}; diff --git a/src/postinstall.js b/src/postinstall.js index 285ea78..31bcad1 100644 --- a/src/postinstall.js +++ b/src/postinstall.js @@ -25,8 +25,8 @@ if (!fs.existsSync('data')) { const dir = path.join('data', datetime.getFullYear().toString(), pad0(datetime.getMonth() + 1), pad0(datetime.getDate())); if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, {recursive: true}); + fs.mkdirSync(dir, { recursive: true }); } copy(path.join('sample_data', 'article'), dir); -} \ No newline at end of file +} diff --git a/src/renderer.js b/src/renderer.js index bd197cc..d89769b 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -6,10 +6,10 @@ module.exports = (config) => { const converter = new showdown.Converter(config['showdown']); /** - * get parts outside of codes/scripts - * @param {string} data - * @returns {{index:number, end:number, text:string}[]} parts - */ + * get parts outside of codes/scripts + * @param {string} data + * @returns {{index:number, end:number, text:string}[]} parts + */ const getParts = (data) => { let parts = []; let match; @@ -67,17 +67,18 @@ module.exports = (config) => { const renderPrism = (data, cb) => { if (!config['modules']['prism']) { - return cb(data); + cb(data); + } else { + const codeRegex = /```([\w-]+)\r?\n((?:(?!```)[\s\S])*)\r?\n```/m; + let match; + while ((match = codeRegex.exec(data))) { + const lang = match[1].trim(); + const code = match[2].trim(); + const block = Prism.highlight(code, Prism.languages[lang] || Prism.languages.autoit, lang); + data = data.slice(0, match.index) + `
` + block + '
' + data.slice(match.index + match[0].length); + } + cb(data); } - const codeRegex = /```([\w-]+)\r?\n((?:(?!```)[\s\S])*)\r?\n```/m; - let match; - while ((match = codeRegex.exec(data))) { - const lang = match[1].trim(); - const code = match[2].trim(); - const block = Prism.highlight(code, Prism.languages[lang] || Prism.languages.autoit, lang); - data = data.slice(0, match.index) + `
` + block + '
' + data.slice(match.index + match[0].length); - } - cb(data); }; if (config['modules']['plantuml']) { @@ -87,22 +88,23 @@ module.exports = (config) => { const renderPlantUML = (data, cb) => { /* global encode64 */ if (!config['modules']['plantuml']) { - return cb(data); + cb(data); + } else { + const parts = getParts(data); + const umlRegex = /@startuml\r?\n((?:(?!@enduml)[\s\S])*)\r?\n@enduml/m; + let match; + parts.forEach(part => { + while ((match = umlRegex.exec(part.text))) { + const code = match[1].trim(); + const s = unescape(encodeURIComponent(code)); + const compressed = global['zip_deflate'](s); + const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`; + part.text = part.text.slice(0, match.index) + `generated PlantUML diagram` + part.text.slice(match.index + match[0].length); + } + data = data.slice(0, part.index) + part.text + data.slice(part.end); + }); + cb(data); } - const parts = getParts(data); - const umlRegex = /@startuml\r?\n((?:(?!@enduml)[\s\S])*)\r?\n@enduml/m; - let match; - parts.forEach(part => { - while ((match = umlRegex.exec(part.text))) { - const code = match[1].trim(); - const s = unescape(encodeURIComponent(code)); - const compressed = global['zip_deflate'](s); - const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`; - part.text = part.text.slice(0, match.index) + `generated PlantUML diagram` + part.text.slice(match.index + match[0].length); - } - data = data.slice(0, part.index) + part.text + data.slice(part.end); - }); - cb(data); }; let mjAPI; @@ -111,8 +113,18 @@ module.exports = (config) => { mjAPI.config({ MathJax: { tex2jax: { - inlineMath: [['$', '$']], - displayMath: [['$$', '$$']], + inlineMath: [ + [ + '$', + '$', + ], + ], + displayMath: [ + [ + '$$', + '$$', + ], + ], }, }, }); @@ -120,40 +132,47 @@ module.exports = (config) => { const renderMathJax = (data, cb) => { if (!config['modules']['mathjax']) { - return cb(data); - } + cb(data); + } else { + const parts = getParts(data); - const parts = getParts(data); - - const doMJ = (match, format, i) => { - const eq = match[1].trim(); - const output = config['mathjax']['output_format']; - const mjConf = { - math: eq, - format: format, - speakText: config['mathjax']['speak_text'], - }; - mjConf[output] = true; - mjAPI.typeset(mjConf, (res) => { - data = data.slice(0, parts[i].index + match.index) + res[output] + data.slice(parts[i].index + match.index + match[0].length); - renderMathJax(data, (data2) => { - cb(data2); + const doMJ = (match, format, i) => { + const eq = match[1].trim(); + const output = config['mathjax']['output_format']; + const mjConf = { + math: eq, + format: format, + speakText: config['mathjax']['speak_text'], + }; + mjConf[output] = true; + mjAPI.typeset(mjConf, (res) => { + data = data.slice(0, parts[i].index + match.index) + res[output] + data.slice(parts[i].index + match.index + match[0].length); + renderMathJax(data, (data2) => { + cb(data2); + }); }); - }); - }; + }; - const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m; - const inlineEqRegex = /\$([^$\n]*)\$/; + const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m; + const inlineEqRegex = /\$([^$\n]*)\$/; - for (let i = 0; i < parts.length; i++) { - let match; - if ((match = eqRegex.exec(parts[i].text))) { - return doMJ(match, 'TeX', i); - } else if ((match = inlineEqRegex.exec(parts[i].text))) { - return doMJ(match, 'inline-TeX', i); + let found = false; + for (let i = 0; i < parts.length; i++) { + let match; + if ((match = eqRegex.exec(parts[i].text))) { + doMJ(match, 'TeX', i); + found = true; + break; + } else if ((match = inlineEqRegex.exec(parts[i].text))) { + doMJ(match, 'inline-TeX', i); + found = true; + break; + } + } + if (!found) { + cb(data); } } - cb(data); }; let faDiagrams; @@ -165,36 +184,37 @@ module.exports = (config) => { const renderFaDiagrams = (data, cb) => { if (!config['modules']['fa-diagrams']) { - return cb(data); - } - const parts = getParts(data); - const diagramsRegex = /@startfad\r?\n((?:(?!@endfad)[\s\S])*)\r?\n@endfad/m; - let match; - parts.forEach(part => { - while ((match = diagramsRegex.exec(part.text))) { - const code = match[1].trim(); - let output; - try { - const diagData = toml.parse(code); - const findLineBreaks = (data) => { - Object.keys(data).forEach(key => { - if (typeof data[key] === 'object') { - findLineBreaks(data[key]); - } else if (typeof data[key] === 'string') { - data[key] = data[key].replace(/\\n/gm, '\n'); - } - }); - }; - findLineBreaks(diagData); - output = faDiagrams.compute(diagData); - } catch (err) { - output = `${err.toString()}`; + cb(data); + } else { + const parts = getParts(data); + const diagramsRegex = /@startfad\r?\n((?:(?!@endfad)[\s\S])*)\r?\n@endfad/m; + let match; + parts.forEach(part => { + while ((match = diagramsRegex.exec(part.text))) { + const code = match[1].trim(); + let output; + try { + const diagData = toml.parse(code); + const findLineBreaks = (data) => { + Object.keys(data).forEach(key => { + if (typeof data[key] === 'object') { + findLineBreaks(data[key]); + } else if (typeof data[key] === 'string') { + data[key] = data[key].replace(/\\n/gm, '\n'); + } + }); + }; + findLineBreaks(diagData); + output = faDiagrams.compute(diagData); + } catch (err) { + output = `${err.toString()}`; + } + part.text = part.text.slice(0, match.index) + output + part.text.slice(match.index + match[0].length); } - part.text = part.text.slice(0, match.index) + output + part.text.slice(match.index + match[0].length); - } - data = data.slice(0, part.index) + part.text + data.slice(part.end); - }); - cb(data); + data = data.slice(0, part.index) + part.text + data.slice(part.end); + }); + cb(data); + } }; return { @@ -205,22 +225,22 @@ module.exports = (config) => { renderMathJax: config['test'] ? renderMathJax : undefined, renderFaDiagrams: config['test'] ? renderFaDiagrams : undefined, render: (file, cb) => { - fs.readFile(file, {encoding: 'UTF-8'}, (err, data) => { + fs.readFile(file, { encoding: 'UTF-8' }, (err, data) => { if (err) { - return cb(err); - } - - renderPlantUML(data, (data) => { - renderFaDiagrams(data, (data) => { - renderMathJax(data, (data) => { - renderPrism(data, (data) => { - renderShowDown(data, (html) => { - cb(null, html); + cb(err); + } else { + renderPlantUML(data, (data) => { + renderFaDiagrams(data, (data) => { + renderMathJax(data, (data) => { + renderPrism(data, (data) => { + renderShowDown(data, (html) => { + cb(null, html); + }); }); }); }); }); - }); + } }); }, }; diff --git a/src/script_loader.js b/src/script_loader.js index 11ebbc0..d2bc6ac 100644 --- a/src/script_loader.js +++ b/src/script_loader.js @@ -5,6 +5,6 @@ const fs = require('fs'); * @param scriptPath */ module.exports = (scriptPath) => { - eval.call(global, fs.readFileSync(scriptPath, {encoding: 'UTF-8'})); + eval.call(global, fs.readFileSync(scriptPath, { encoding: 'UTF-8' })); }; diff --git a/src/server.js b/src/server.js index 3a9e0ef..dc8b4fe 100644 --- a/src/server.js +++ b/src/server.js @@ -1,4 +1,4 @@ const config = require('./config')(); const app = require('./app')(config); -app.start(); \ No newline at end of file +app.start(); diff --git a/test/app.test.js b/test/app.test.js index 590172b..96a24ec 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -49,115 +49,129 @@ describe('Test reload', () => { describe('Test request logging', () => { test('no log', (done) => { - request(app).get('/rsstest').then(() => { - expect(fs.existsSync(path.join(dataDir, 'access.log'))).toBe(false); - done(); - }); + request(app).get('/rsstest') + .then(() => { + expect(fs.existsSync(path.join(dataDir, 'access.log'))).toBe(false); + done(); + }); }); test('get 200', (done) => { config['access_log'] = path.join(dataDir, 'access.log'); - request(app).get('/rsstest').then(() => { - fs.readFile(path.join(dataDir, 'access.log'), {encoding: 'UTF-8'}, (err, data) => { - expect(err).toBeNull(); - expect(data).toBe('200 GET /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n'); - done(); - }); - }); - }); - test('post 400', (done) => { - config['access_log'] = path.join(dataDir, 'access.log'); - request(app).post('/rsstest').then(() => { - fs.readFile(path.join(dataDir, 'access.log'), {encoding: 'UTF-8'}, (err, data) => { - expect(err).toBeNull(); - expect(data).toBe('400 POST /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n'); - done(); - }); - }); - }); - test('2 requests', (done) => { - config['access_log'] = path.join(dataDir, 'access.log'); - request(app).get('/rss').then(() => { - request(app).post('/rsstest').then(() => { - fs.readFile(path.join(dataDir, 'access.log'), {encoding: 'UTF-8'}, (err, data) => { + request(app).get('/rsstest') + .then(() => { + fs.readFile(path.join(dataDir, 'access.log'), { encoding: 'UTF-8' }, (err, data) => { expect(err).toBeNull(); - expect(data).toBe('404 GET /rss ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n' + - '400 POST /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n'); + expect(data).toBe('200 GET /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n'); done(); }); }); - }); + }); + test('post 400', (done) => { + config['access_log'] = path.join(dataDir, 'access.log'); + request(app).post('/rsstest') + .then(() => { + fs.readFile(path.join(dataDir, 'access.log'), { encoding: 'UTF-8' }, (err, data) => { + expect(err).toBeNull(); + expect(data).toBe('400 POST /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n'); + done(); + }); + }); + }); + test('2 requests', (done) => { + config['access_log'] = path.join(dataDir, 'access.log'); + request(app).get('/rss') + .then(() => { + request(app).post('/rsstest') + .then(() => { + fs.readFile(path.join(dataDir, 'access.log'), { encoding: 'UTF-8' }, (err, data) => { + expect(err).toBeNull(); + expect(data).toBe('404 GET /rss ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n' + + '400 POST /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n'); + done(); + }); + }); + }); }); }); describe('Test error logging', () => { test('no log', (done) => { config['home']['index'] = null; - request(app).get('/').then(() => { - expect(fs.existsSync(path.join(dataDir, 'error.log'))).toBe(false); - done(); - }); + request(app).get('/') + .then(() => { + expect(fs.existsSync(path.join(dataDir, 'error.log'))).toBe(false); + done(); + }); }); test('null error', (done) => { config['home']['index'] = null; config['error_log'] = path.join(dataDir, 'error.log'); - request(app).get('/').then(() => { - fs.readFile(path.join(dataDir, 'error.log'), {encoding: 'UTF-8'}, (err, data) => { - expect(err).toBeNull(); - const start = data.split('\n').slice(0, 2).join('\n'); - const expected = '500 GET / ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\nTypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string.'; - expect(start.indexOf(expected)).toBe(0); - done(); + request(app).get('/') + .then(() => { + fs.readFile(path.join(dataDir, 'error.log'), { encoding: 'UTF-8' }, (err, data) => { + expect(err).toBeNull(); + const start = data.split('\n').slice(0, 2) + .join('\n'); + const expected = '500 GET / ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\nTypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string.'; + expect(start.indexOf(expected)).toBe(0); + done(); + }); }); - }); }); }); describe('Test root path', () => { test('404 no index no error', (done) => { - request(app).get('/').then((response) => { - expect(response.statusCode).toBe(404); - done(); - }); + request(app).get('/') + .then((response) => { + expect(response.statusCode).toBe(404); + done(); + }); }); test('404 no index but error page', (done) => { fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>'); - request(app).get('/').then((response) => { - expect(response.statusCode).toBe(404); - expect(response.text).toBe('error 404'); - done(); - }); + request(app).get('/') + .then((response) => { + expect(response.statusCode).toBe(404); + expect(response.text).toBe('error 404'); + done(); + }); }); test('500 render error', (done) => { fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>'); - request(app).get('/').then((response) => { - expect(response.statusCode).toBe(500); - done(); - }); + request(app).get('/') + .then((response) => { + expect(response.statusCode).toBe(500); + done(); + }); }); test('500 render error with page', (done) => { fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>'); fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>'); - request(app).get('/').then((response) => { - expect(response.statusCode).toBe(500); - expect(response.text).toBe('error 500'); - done(); - }); + request(app).get('/') + .then((response) => { + expect(response.statusCode).toBe(500); + expect(response.text).toBe('error 500'); + done(); + }); }); test('500 render error with failing page', (done) => { fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>'); fs.writeFileSync(path.join(dataDir, testError), 'error <%= null.error %>'); - request(app).get('/').then((response) => { - expect(response.statusCode).toBe(500); - done(); - }); + request(app).get('/') + .then((response) => { + expect(response.statusCode).toBe(500); + done(); + }); }); test('200 no articles', (done) => { fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>'); - request(app).get('/').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.text).toBe('articles 0'); - done(); - }); + request(app).get('/') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text).toBe('articles 0'); + done(); + }); }); test('200 2 articles 1 drafted', (done, fail) => { utils.createEmptyDirs([ @@ -173,11 +187,12 @@ describe('Test root path', () => { ]); fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>'); app.reload(() => { - request(app).get('/').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.text).toBe('articles 2'); - done(); - }); + request(app).get('/') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text).toBe('articles 2'); + done(); + }); }, fail); }); }); @@ -185,36 +200,42 @@ describe('Test root path', () => { describe('Test RSS feed', () => { test('404 rss deactivated', (done) => { config['modules']['rss'] = false; - request(app).get('/rsstest').then((response) => { - expect(response.statusCode).toBe(404); - done(); - }); + request(app).get('/rsstest') + .then((response) => { + expect(response.statusCode).toBe(404); + done(); + }); }); test('200 empty rss', (done) => { - request(app).get('/rsstest').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.type).toBe('application/rss+xml'); - expect(response.text.length).toBeGreaterThan(0); - expect(response.text.split('').length).toBe(1); - done(); - }); - }); - test('200 Mozilla fix', (done) => { - request(app).get('/rsstest').set('user-agent', 'Mozilla Firefox 64.0').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.type).toBe('text/xml'); - done(); - }); - }); - test('200 rss cache', (done) => { - request(app).get('/rsstest').then(() => { - request(app).get('/rsstest').then((response) => { + request(app).get('/rsstest') + .then((response) => { expect(response.statusCode).toBe(200); + expect(response.type).toBe('application/rss+xml'); expect(response.text.length).toBeGreaterThan(0); expect(response.text.split('').length).toBe(1); done(); }); - }); + }); + test('200 Mozilla fix', (done) => { + request(app).get('/rsstest') + .set('user-agent', 'Mozilla Firefox 64.0') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.type).toBe('text/xml'); + done(); + }); + }); + test('200 rss cache', (done) => { + request(app).get('/rsstest') + .then(() => { + request(app).get('/rsstest') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text.length).toBeGreaterThan(0); + expect(response.text.split('').length).toBe(1); + done(); + }); + }); }); test('200 2 rss items', (done, fail) => { utils.createEmptyDirs([ @@ -226,12 +247,13 @@ describe('Test RSS feed', () => { path.join(dataDir, '2018', '05', '05', 'index.md'), ]); app.reload(() => { - request(app).get('/rsstest').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.text.length).toBeGreaterThan(0); - expect(response.text.split('').length).toBe(3); - done(); - }); + request(app).get('/rsstest') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text.length).toBeGreaterThan(0); + expect(response.text.split('').length).toBe(3); + done(); + }); }, fail); }); test('200 max rss items', (done, fail) => { @@ -246,12 +268,13 @@ describe('Test RSS feed', () => { path.join(dataDir, '2017', '05', '05', 'index.md'), ]); app.reload(() => { - request(app).get('/rsstest').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.text.length).toBeGreaterThan(0); - expect(response.text.split('').length).toBe(3); - done(); - }); + request(app).get('/rsstest') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text.length).toBeGreaterThan(0); + expect(response.text.split('').length).toBe(3); + done(); + }); }, fail); }); }); @@ -259,45 +282,51 @@ describe('Test RSS feed', () => { describe('Test webhook', () => { test('400 webhook deactivated', (done) => { config['modules']['webhook'] = false; - request(app).post('/webhooktest').then((response) => { - expect(response.statusCode).toBe(400); - done(); - }); + request(app).post('/webhooktest') + .then((response) => { + expect(response.statusCode).toBe(400); + done(); + }); }); test('200 no secret', (done) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), path.join(dataDir, testTemplate), ]); config['webhook']['pull_command'] = 'git --help'; - request(app).post('/webhooktest').then((response) => { - expect(response.statusCode).toBe(200); - request(app).get('/2019/05/05/').then((response) => { + request(app).post('/webhooktest') + .then((response) => { expect(response.statusCode).toBe(200); - done(); + request(app).get('/2019/05/05/') + .then((response) => { + expect(response.statusCode).toBe(200); + done(); + }); }); - }); }); test('500 command failed', (done) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), path.join(dataDir, testTemplate), ]); config['webhook']['pull_command'] = 'qzgfqgqz'; - request(app).post('/webhooktest').then((response) => { - expect(response.statusCode).toBe(500); - done(); - }); + request(app).post('/webhooktest') + .then((response) => { + expect(response.statusCode).toBe(500); + done(); + }); }); test('403 wrong secret', (done) => { config['webhook']['signature_header'] = 'testheader'; config['webhook']['secret'] = 'testvalue'; - request(app).post('/webhooktest').set('testheader', 'sha1=invalid').then((response) => { - expect(response.statusCode).toBe(403); - done(); - }); + request(app).post('/webhooktest') + .set('testheader', 'sha1=invalid') + .then((response) => { + expect(response.statusCode).toBe(403); + done(); + }); }); test('200 valid secret', (done) => { config['webhook']['signature_header'] = 'testheader'; @@ -315,86 +344,93 @@ describe('Test webhook', () => { describe('Test articles rendering', () => { test('404 article not found', (done) => { - request(app).get('/2019/05/06/untitled/').then((response) => { - expect(response.statusCode).toBe(404); - done(); - }); + request(app).get('/2019/05/06/untitled/') + .then((response) => { + expect(response.statusCode).toBe(404); + done(); + }); }); test('500 fail to render', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello'); fs.writeFileSync(path.join(dataDir, testTemplate), '<%- articl.content %><%- `reload` %>'); app.reload(() => { - request(app).get('/2019/05/05/hello/').then((response) => { - expect(response.statusCode).toBe(500); - done(); - }); + request(app).get('/2019/05/05/hello/') + .then((response) => { + expect(response.statusCode).toBe(500); + done(); + }); }, fail); }); test('500 no template', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello'); app.reload(() => { - request(app).get('/2019/05/05/hello/').then((response) => { - expect(response.statusCode).toBe(500); - done(); - }); + request(app).get('/2019/05/05/hello/') + .then((response) => { + expect(response.statusCode).toBe(500); + done(); + }); }, fail); }); test('200 rendered article', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello'); fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `reload` %>'); app.reload(() => { - request(app).get('/2019/05/05/hello/').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.text).toBe('

Hello

reload'); - done(); - }); + request(app).get('/2019/05/05/hello/') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text).toBe('

Hello

reload'); + done(); + }); }, fail); }); test('200 rendered draft', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'draft.md'), '# Hello'); fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `reload` %>'); app.reload(() => { - request(app).get('/2019/05/05/hello/').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.text).toBe('

Hello

reload'); - done(); - }); + request(app).get('/2019/05/05/hello/') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text).toBe('

Hello

reload'); + done(); + }); }, fail); }); test('200 other url', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), path.join(dataDir, testTemplate), ]); app.reload(() => { - request(app).get('/2019/05/05/anything/').then((response) => { - expect(response.statusCode).toBe(200); - done(); - }); + request(app).get('/2019/05/05/anything/') + .then((response) => { + expect(response.statusCode).toBe(200); + done(); + }); }, fail); }); test('200 other url 2', (done, fail) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); utils.createEmptyFiles([ path.join(dataDir, '2019', '05', '05', 'index.md'), path.join(dataDir, testTemplate), ]); app.reload(() => { - request(app).get('/2019/05/05/').then((response) => { - expect(response.statusCode).toBe(200); - done(); - }); + request(app).get('/2019/05/05/') + .then((response) => { + expect(response.statusCode).toBe(200); + done(); + }); }, fail); }); }); @@ -402,72 +438,81 @@ describe('Test articles rendering', () => { describe('Test static files', () => { test('404 invalid file no error page', (done) => { - request(app).get('/somefile.txt').then((response) => { - expect(response.statusCode).toBe(404); - done(); - }); + request(app).get('/somefile.txt') + .then((response) => { + expect(response.statusCode).toBe(404); + done(); + }); }); test('404 invalid file but error page', (done) => { fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>'); - request(app).get('/somefile.txt').then((response) => { - expect(response.statusCode).toBe(404); - expect(response.text).toBe('error 404'); - done(); - }); + request(app).get('/somefile.txt') + .then((response) => { + expect(response.statusCode).toBe(404); + expect(response.text).toBe('error 404'); + done(); + }); }); test('404 hidden file', (done) => { - utils.createEmptyDirs([path.join(dataDir, 'tmp')]); + utils.createEmptyDirs([ path.join(dataDir, 'tmp') ]); fs.writeFileSync(path.join(dataDir, 'tmp', 'somefile.ejs'), ''); - request(app).get('/tmp/somefile.ejs').then((response) => { - expect(response.statusCode).toBe(404); - done(); - }); + request(app).get('/tmp/somefile.ejs') + .then((response) => { + expect(response.statusCode).toBe(404); + done(); + }); }); test('404 hidden folder', (done) => { - utils.createEmptyDirs([path.join(dataDir, '.git')]); + utils.createEmptyDirs([ path.join(dataDir, '.git') ]); fs.writeFileSync(path.join(dataDir, '.git', 'file.txt'), ''); - request(app).get('/.git/file.txt').then((response) => { - expect(response.statusCode).toBe(404); - done(); - }); + request(app).get('/.git/file.txt') + .then((response) => { + expect(response.statusCode).toBe(404); + done(); + }); }); test('200 valid file', (done) => { fs.writeFileSync(path.join(dataDir, 'somefile.css'), 'filecontent'); - request(app).get('/somefile.css').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.type).toBe('text/css'); - expect(response.text).toBe('filecontent'); - done(); - }); + request(app).get('/somefile.css') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.type).toBe('text/css'); + expect(response.text).toBe('filecontent'); + done(); + }); }); test('200 valid resource of article', (done) => { - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05')]); + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'somefile.txt'), 'filecontent'); - request(app).get('/2019/05/05/title/somefile.txt').then((response) => { - expect(response.statusCode).toBe(200); - expect(response.text).toBe('filecontent'); - done(); - }); + request(app).get('/2019/05/05/title/somefile.txt') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text).toBe('filecontent'); + done(); + }); }); }); describe('Test other requests', () => { test('400 POST', (done) => { - request(app).post('/').then((response) => { - expect(response.statusCode).toBe(400); - done(); - }); + request(app).post('/') + .then((response) => { + expect(response.statusCode).toBe(400); + done(); + }); }); test('400 PUT', (done) => { - request(app).put('/').then((response) => { - expect(response.statusCode).toBe(400); - done(); - }); + request(app).put('/') + .then((response) => { + expect(response.statusCode).toBe(400); + done(); + }); }); test('400 DELETE', (done) => { - request(app).delete('/').then((response) => { - expect(response.statusCode).toBe(400); - done(); - }); + request(app).delete('/') + .then((response) => { + expect(response.statusCode).toBe(400); + done(); + }); }); }); diff --git a/test/config.test.js b/test/config.test.js index 3fa029c..bbcbcf7 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -34,8 +34,8 @@ test('example config', () => { fs.unlinkSync(configFile); } fs.copyFileSync(path.join('src', 'config.default.json'), configFile); - const data = fs.readFileSync(configFile, {encoding: 'UTF-8'}); - fs.writeFileSync(configFile, data.replace('3000', '3333'), {encoding: 'UTF-8'}); + const data = fs.readFileSync(configFile, { encoding: 'UTF-8' }); + fs.writeFileSync(configFile, data.replace('3000', '3333'), { encoding: 'UTF-8' }); const config = require('../src/config')(); expect(config).toBeDefined(); expect(config['node_port']).toBe(3333); @@ -70,12 +70,18 @@ test('array parsing', () => { fs.writeFileSync(configFile, '{"home":{"hidden":["item1","item2"]}}'); const config = require('../src/config')(); expect(config).toBeDefined(); - expect(config['home']['hidden']).toEqual(['item1', 'item2']); + expect(config['home']['hidden']).toEqual([ + 'item1', + 'item2', + ]); }); test('array fix', () => { fs.writeFileSync(configFile, '{"home":{"hidden":{}}}'); const config = require('../src/config')(); expect(config).toBeDefined(); - expect(config['home']['hidden']).toEqual(['*.ejs', '/.git*']); -}); \ No newline at end of file + expect(config['home']['hidden']).toEqual([ + '*.ejs', + '/.git*', + ]); +}); diff --git a/test/file_walker.test.js b/test/file_walker.test.js index bd9c244..9f4ace6 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -8,14 +8,14 @@ const testIndex = 'testindex.md'; const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/'); const config = { - 'test': true, - 'data_dir': dataDir, - 'article': { - 'index': testIndex, - 'draft': 'draft.md', - 'default_title': 'Untitled', - 'default_thumbnail': 'default.png', - 'thumbnail_tag': 'thumbnail', + test: true, + data_dir: dataDir, + article: { + index: testIndex, + draft: 'draft.md', + default_title: 'Untitled', + default_thumbnail: 'default.png', + thumbnail_tag: 'thumbnail', }, }; @@ -225,8 +225,8 @@ describe('Test article fetching', () => { test('empty index file', (done) => { const dir = path.join(dataDir, '2019', '05', '05'); const file = path.join(dir, testIndex); - utils.createEmptyDirs([dir]); - utils.createEmptyFiles([file]); + utils.createEmptyDirs([ dir ]); + utils.createEmptyFiles([ file ]); const date = new Date(2019, 5, 5); date.setUTCHours(0); fw.fetchArticles((err, dict) => { @@ -252,7 +252,7 @@ describe('Test article fetching', () => { test('correct index file', (done) => { const dir = path.join(dataDir, '2019', '05', '05'); const file = path.join(dir, testIndex); - utils.createEmptyDirs([dir]); + utils.createEmptyDirs([ dir ]); fs.writeFileSync(file, ` # Title with : info ! ![thumbnail](./thumbnail.jpg) @@ -283,7 +283,7 @@ describe('Test article fetching', () => { test('correct draft file', (done) => { const dir = path.join(dataDir, '2019', '05', '05'); const file = path.join(dir, 'draft.md'); - utils.createEmptyDirs([dir]); + utils.createEmptyDirs([ dir ]); fs.writeFileSync(file, ` # Title with : info ! ![thumbnail](./thumbnail.jpg) @@ -315,8 +315,11 @@ describe('Test article fetching', () => { const dir = path.join(dataDir, '2019', '05', '05'); const file = path.join(dir, testIndex); const file2 = path.join(dir, 'draft.md'); - utils.createEmptyDirs([dir]); - utils.createEmptyFiles([file, file2]); + utils.createEmptyDirs([ dir ]); + utils.createEmptyFiles([ + file, + file2, + ]); const date = new Date(2019, 5, 5); date.setUTCHours(0); fw.fetchArticles((err, dict) => { diff --git a/test/renderer.test.js b/test/renderer.test.js index 9cd8711..7b138cd 100644 --- a/test/renderer.test.js +++ b/test/renderer.test.js @@ -6,23 +6,23 @@ const dataDir = 'test_data'; const file = path.join(dataDir, 'test.md'); const config = { - 'test': true, - 'modules': { + test: true, + modules: { 'prism': true, 'mathjax': true, 'plantuml': true, 'fa-diagrams': true, }, - 'showdown': { - 'simplifiedAutoLink': true, - 'smartIndentationFix': true, + showdown: { + simplifiedAutoLink: true, + smartIndentationFix: true, }, - 'mathjax': { - 'output_format': 'html', - 'speak_text': false, + mathjax: { + output_format: 'html', + speak_text: false, }, - 'plantuml': { - 'output_format': 'svg', + plantuml: { + output_format: 'svg', }, }; @@ -47,9 +47,7 @@ describe('get parts', () => { test('normal', () => { const data = 'Hello\nthere\ngeneral\nkenobi'; const parts = renderer.getParts(data); - expect(parts.map(p => p.text)).toEqual([ - 'Hello\nthere\ngeneral\nkenobi', - ]); + expect(parts.map(p => p.text)).toEqual([ 'Hello\nthere\ngeneral\nkenobi' ]); }); test('lot of stuff', () => { const data = 'Hello\nthere\n```code```\ngeneral\n\n``````kenobi'; @@ -285,4 +283,4 @@ describe('Test render', () => { done(); }); }); -}); \ No newline at end of file +}); diff --git a/test/script_loader.test.js b/test/script_loader.test.js index e626d04..966c43b 100644 --- a/test/script_loader.test.js +++ b/test/script_loader.test.js @@ -47,4 +47,4 @@ test('load 2 script', () => { require('../src/script_loader.js')(file2); expect(global['b']).toBeDefined(); expect(global['b']()).toBe(9); -}); \ No newline at end of file +}); diff --git a/test/test_utils.js b/test/test_utils.js index e62096a..5538f16 100644 --- a/test/test_utils.js +++ b/test/test_utils.js @@ -14,7 +14,7 @@ const deleteFolderSync = (dir) => { } }; do { - items = fs.readdirSync(dir, {withFileTypes: true}); + items = fs.readdirSync(dir, { withFileTypes: true }); try { items.forEach(deleteItem); } catch (e) { @@ -26,6 +26,6 @@ const deleteFolderSync = (dir) => { module.exports = { deleteFolderSync: deleteFolderSync, - createEmptyDirs: (list) => list.forEach((path) => fs.mkdirSync(path, {recursive: true})), + createEmptyDirs: (list) => list.forEach((path) => fs.mkdirSync(path, { recursive: true })), createEmptyFiles: (list) => list.forEach((file) => fs.writeFileSync(file, '')), -}; \ No newline at end of file +}; From 6439f8eb92d41d6a2980906ece6847b00eeca2bf Mon Sep 17 00:00:00 2001 From: Klemek Date: Tue, 30 Mar 2021 19:11:31 +0200 Subject: [PATCH 4/5] hit-counter --- README.md | 9 ++ package-lock.json | 85 ++++++++++++ package.json | 1 + src/app.js | 91 ++++++++----- src/config.default.json | 10 +- src/hit_counter.js | 45 ++++++- test/app.test.js | 50 +++++++ test/hit_counter.test.js | 281 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 529 insertions(+), 43 deletions(-) create mode 100644 test/hit_counter.test.js diff --git a/README.md b/README.md index 5d42b1b..fa3b851 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,8 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link activate PlantUML diagram rendering * `fa-diagrams` (default: true) activate fa-diagrams rendering + * `hit_counter` (default: true) + activate /stats endpoints and visitor counting (need an active redis connection) * `home` * `title` (default: GitBlog.md) the title of your blog, **strongly advised to be changed** @@ -332,4 +334,11 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link specify the output format between svg, html or MathMl (mml) * `speak_text`: (default: true) activate the alternate text in equations +* `hit_counter` + * `unique_visitor_timeout`: (default: 7200000 / 2h) + specify the time (in ms) before a visitor can be accounted again +* `redis` + Options to connect to redis (see [redis options](https://github.com/NodeRedis/node-redis#options-object-properties) for more info) + * `host`: (default: localhost) + * `port`: (default: 6379) diff --git a/package-lock.json b/package-lock.json index 1dad291..9414459 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "ncp": "^2.0.0", "node-prismjs": "^0.1.0", "prismjs": "^1.23.0", + "redis": "^3.0.2", "rss": "^1.2.2", "showdown": "^1.9.1" }, @@ -1897,6 +1898,14 @@ "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", "optional": true }, + "node_modules/denque": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", + "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -6966,6 +6975,48 @@ "node": ">=4" } }, + "node_modules/redis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz", + "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==", + "dependencies": { + "denque": "^1.4.1", + "redis-commands": "^1.5.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-redis" + } + }, + "node_modules/redis-commands": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", + "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -10365,6 +10416,11 @@ "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", "optional": true }, + "denque": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", + "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -14305,6 +14361,35 @@ "util.promisify": "^1.0.0" } }, + "redis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz", + "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==", + "requires": { + "denque": "^1.4.1", + "redis-commands": "^1.5.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + } + }, + "redis-commands": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", + "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" + }, + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "requires": { + "redis-errors": "^1.0.0" + } + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", diff --git a/package.json b/package.json index abcada1..12f4a62 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "ncp": "^2.0.0", "node-prismjs": "^0.1.0", "prismjs": "^1.23.0", + "redis": "^3.0.2", "rss": "^1.2.2", "showdown": "^1.9.1" }, diff --git a/src/app.js b/src/app.js index 341128d..5dcea61 100644 --- a/src/app.js +++ b/src/app.js @@ -51,7 +51,16 @@ module.exports = (config) => { let showError; const fw = require('./file_walker')(config); const renderer = require('./renderer')(config); - const hc = require('./hit_counter')(config); + const hc = require('./hit_counter')(config, + () => { + console.log(cons.ok, 'redis connected'); + }, + (err) => { + if (err.code !== 'ECONNREFUSED') { + console.log(cons.warn, 'redis error: ' + err); + } + }, + ); // set view engine from configuration app.set('view engine', config['view_engine']); @@ -159,22 +168,28 @@ module.exports = (config) => { if (err) { showError(req, res, 404); } else { - hc.count(req, '/'); - render(req, res, homePath, - { - articles: Object.values(articles) - .filter(d => !d.draft) - .sort((a, b) => ('' + b.path).localeCompare(a.path)), - }); + hc.count(req, '/', () => { + render(req, res, homePath, + { + articles: Object.values(articles) + .filter(d => !d.draft) + .sort((a, b) => ('' + b.path).localeCompare(a.path)), + }); + }); } }); }); app.get('/stats', (req, res) => { - const data = hc.read('/'); - res.json({ - hits: data.hits, - visitors: data.visitors, - }); + if (config['modules']['hit_counter']) { + hc.read('/', (data) => { + res.json({ + hits: data.hits, + visitors: data.visitors, + }); + }); + } else { + showError(req, res, 404); + } }); //RSS endpoint @@ -251,29 +266,35 @@ module.exports = (config) => { if (!article) { showError(req, res, 404); } else if (req.path.endsWith('stats')) { - const data = hc.read(articlePath); - res.json({ - hits: data.hits, - visitors: data.visitors, - }); - } else { - hc.count(req, articlePath); - renderer.render(article.realPath, (err, html) => { - if (err) { - console.log(cons.error, `failed to render article ${req.path} : ${err}`); - showError(req, res, 500); - } else { - article.content = html; - const templatePath = path.join(config['data_dir'], config['article']['template']); - fs.access(templatePath, fs.constants.R_OK, (err) => { - if (err) { - console.log(cons.error, `no template found at ${templatePath}`); - showError(req, res, 500); - } else { - render(req, res, templatePath, { article: article }); - } + if (config['modules']['hit_counter']) { + hc.read(articlePath, (data) => { + res.json({ + hits: data.hits, + visitors: data.visitors, }); - } + }); + } else { + showError(req, res, 404); + } + } else { + hc.count(req, articlePath, () => { + renderer.render(article.realPath, (err, html) => { + if (err) { + console.log(cons.error, `failed to render article ${req.path} : ${err}`); + showError(req, res, 500); + } else { + article.content = html; + const templatePath = path.join(config['data_dir'], config['article']['template']); + fs.access(templatePath, fs.constants.R_OK, (err) => { + if (err) { + console.log(cons.error, `no template found at ${templatePath}`); + showError(req, res, 500); + } else { + render(req, res, templatePath, { article: article }); + } + }); + } + }); }); } } else { diff --git a/src/config.default.json b/src/config.default.json index f0d0678..49565f9 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -12,7 +12,8 @@ "prism": true, "mathjax": true, "plantuml": true, - "fa-diagrams": true + "fa-diagrams": true, + "hit_counter": true }, "home": { "title": "GitBlog.md", @@ -58,5 +59,12 @@ }, "plantuml": { "output_format": "svg" + }, + "hit_counter": { + "unique_visitor_timeout": 7200000 + }, + "redis": { + "host": "localhost", + "port": 6379 } } diff --git a/src/hit_counter.js b/src/hit_counter.js index df922f7..e36b22c 100644 --- a/src/hit_counter.js +++ b/src/hit_counter.js @@ -1,13 +1,44 @@ -module.exports = (config) => { - const count = (req, path) => { +const redis = require('redis'); +module.exports = (config, onConnect, onError) => { + const client = config['modules']['hit_counter'] ? redis.createClient(config['redis']) : { connected: false, on: () => { /* ignore */ } }; + + client.on('connect', onConnect); + client.on('error', onError); + + const visitors = {}; + + const count = (req, path, cb) => { + if (!client.connected) { + cb(); + } else { + const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; + const key = path + ':' + ip; + const now = Date.now(); + const isNewVisitor = (now - (visitors[key] || 0)) > config['hit_counter']['unique_visitor_timeout']; + visitors[key] = now; + client + .multi() + .hincrby(path, 'h', 1) + .hincrby(path, 'v', isNewVisitor ? 1 : 0) + .exec(cb); + } }; - const read = (path) => { - return { - hits: 0, - visitors: 0, - }; + const read = (path, cb) => { + if (!client.connected) { + cb({ + hits: 0, + visitors: 0, + }); + } else { + client.hgetall(path, (_, value) => { + cb({ + hits: value ? value.h || 0 : 0, + visitors: value ? value.v || 0 : 0, + }); + }); + } }; return { diff --git a/test/app.test.js b/test/app.test.js index 96a24ec..b8e0103 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -17,6 +17,7 @@ config['rss']['endpoint'] = '/rsstest'; config['rss']['length'] = 2; config['home']['error'] = testError; config['article']['template'] = testTemplate; +config['modules']['hit_counter'] = false; const app = require('../src/app')(config); @@ -28,6 +29,7 @@ beforeEach((done, fail) => { config['error_log'] = ''; config['modules']['rss'] = true; config['modules']['webhook'] = true; + config['modules']['hit_counter'] = false; utils.deleteFolderSync(dataDir); fs.mkdirSync(dataDir); @@ -195,6 +197,22 @@ describe('Test root path', () => { }); }, fail); }); + test('404 index no stats', (done) => { + request(app).get('/stats') + .then((response) => { + expect(response.statusCode).toBe(404); + done(); + }); + }); + test('200 index stats', (done) => { + config['modules']['hit_counter'] = true; + request(app).get('/stats') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ hits: 0, visitors: 0 }); + done(); + }); + }); }); describe('Test RSS feed', () => { @@ -433,6 +451,38 @@ describe('Test articles rendering', () => { }); }, fail); }); + + test('404 article no stats', (done) => { + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); + utils.createEmptyFiles([ + path.join(dataDir, '2019', '05', '05', 'index.md'), + path.join(dataDir, testTemplate), + ]); + app.reload(() => { + request(app).get('/2019/05/05/hello/stats') + .then((response) => { + expect(response.statusCode).toBe(404); + done(); + }); + }); + }); + + test('200 index stats', (done) => { + config['modules']['hit_counter'] = true; + utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]); + utils.createEmptyFiles([ + path.join(dataDir, '2019', '05', '05', 'index.md'), + path.join(dataDir, testTemplate), + ]); + app.reload(() => { + request(app).get('/2019/05/05/anything/stats') + .then((response) => { + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ hits: 0, visitors: 0 }); + done(); + }); + }); + }); }); diff --git a/test/hit_counter.test.js b/test/hit_counter.test.js new file mode 100644 index 0000000..a4afaad --- /dev/null +++ b/test/hit_counter.test.js @@ -0,0 +1,281 @@ +const mockClient = { + options: {}, + connected: true, + on: () => { /* ignore */ }, +}; + +jest.mock('redis', () => { + return { + createClient: (options) => { + mockClient.options = options; + return mockClient; + }, + }; +}); + +const config = { + test: true, + modules: { + hit_counter: true, + }, + redis: { + host: 'test-host', + port: 'test-port', + }, + hit_counter: { + unique_visitor_timeout: -1, + }, +}; + +const hc = require('../src/hit_counter')(config, () => { /* ignore */ }, () => { /* ignore */ }); + +afterAll(() => { + jest.resetModules(); +}); + +test('options passed to redis', () => { + expect(mockClient.options).toEqual(config['redis']); +}); + +describe('read()', () => { + beforeEach(() => { + mockClient.hgetall = (_, cb) => { + cb(); + }; + }); + + test('read path', (done) => { + mockClient.hgetall = (path, cb) => { + expect(path).toBe('/test/path/'); + cb(undefined, { h: 12, v: 34 }); + }; + hc.read('/test/path/', (data) => { + expect(data).toBeDefined(); + expect(data.hits).toBe(12); + expect(data.visitors).toBe(34); + done(); + }); + }); + + test('read path with error', (done) => { + mockClient.hgetall = (path, cb) => { + expect(path).toBe('/test/path/'); + cb('error', undefined); + }; + hc.read('/test/path/', (data) => { + expect(data).toBeDefined(); + expect(data.hits).toBe(0); + expect(data.visitors).toBe(0); + done(); + }); + }); + + test('read path with error 2', (done) => { + mockClient.hgetall = (path, cb) => { + expect(path).toBe('/test/path/'); + cb(undefined, {}); + }; + hc.read('/test/path/', (data) => { + expect(data).toBeDefined(); + expect(data.hits).toBe(0); + expect(data.visitors).toBe(0); + done(); + }); + }); +}); + +describe('count()', () => { + beforeEach(() => { + mockClient.multi = () => mockClient; + mockClient.hincrby = () => mockClient; + mockClient.exec = (cb) => { + cb(); + }; + }); + + test('simple visit', (done) => { + let multiCalled = false; + let execCalled = false; + let hincrbyCalls = []; + mockClient.multi = () => { + multiCalled = true; + return mockClient; + }; + mockClient.hincrby = (hash, key, value) => { + hincrbyCalls.push([ + hash, + key, + value, + ]); + return mockClient; + }; + mockClient.exec = (cb) => { + execCalled = true; + cb(); + }; + hc.count({ + headers: {}, + connection: { remoteAddress: 'test1' }, + }, '/test/path/1', () => { + expect(multiCalled).toBeTruthy(); + expect(hincrbyCalls).toEqual([ + [ + '/test/path/1', + 'h', + 1, + ], + [ + '/test/path/1', + 'v', + 1, + ], + ]); + expect(execCalled).toBeTruthy(); + done(); + }); + }); +}); + +describe('count()', () => { + beforeEach(() => { + mockClient.multi = () => mockClient; + mockClient.hincrby = () => mockClient; + mockClient.exec = (cb) => { + cb(); + }; + config['hit_counter']['unique_visitor_timeout'] = -1; + }); + + test('simple visit', (done) => { + let multiCalled = false; + let execCalled = false; + let hincrbyCalls = []; + mockClient.multi = () => { + multiCalled = true; + return mockClient; + }; + mockClient.hincrby = (hash, key, value) => { + hincrbyCalls.push([ + hash, + key, + value, + ]); + return mockClient; + }; + mockClient.exec = (cb) => { + execCalled = true; + cb(); + }; + hc.count({ + headers: {}, + connection: { remoteAddress: 'test1' }, + }, '/test/path/1', () => { + expect(multiCalled).toBeTruthy(); + expect(hincrbyCalls).toEqual([ + [ + '/test/path/1', + 'h', + 1, + ], + [ + '/test/path/1', + 'v', + 1, + ], + ]); + expect(execCalled).toBeTruthy(); + done(); + }); + }); + + test('re-visit after long time', (done) => { + let hincrbyCalls = []; + mockClient.hincrby = (hash, key, value) => { + hincrbyCalls.push([ + hash, + key, + value, + ]); + return mockClient; + }; + hc.count({ + headers: {}, + connection: { remoteAddress: 'test2' }, + }, '/test/path/2', () => { + hc.count({ + headers: {}, + connection: { remoteAddress: 'test2' }, + }, '/test/path/2', () => { + expect(hincrbyCalls).toEqual([ + [ + '/test/path/2', + 'h', + 1, + ], + [ + '/test/path/2', + 'v', + 1, + ], + [ + '/test/path/2', + 'h', + 1, + ], + [ + '/test/path/2', + 'v', + 1, + ], + ]); + done(); + }); + }); + }); + + test('re-visit after short time', (done) => { + config['hit_counter']['unique_visitor_timeout'] = 10000; + let hincrbyCalls = []; + mockClient.hincrby = (hash, key, value) => { + hincrbyCalls.push([ + hash, + key, + value, + ]); + return mockClient; + }; + hc.count({ + headers: {}, + connection: { remoteAddress: 'test3' }, + }, '/test/path/3', () => { + hc.count({ + headers: {}, + connection: { remoteAddress: 'test3' }, + }, '/test/path/3', () => { + expect(hincrbyCalls).toEqual([ + [ + '/test/path/3', + 'h', + 1, + ], + [ + '/test/path/3', + 'v', + 1, + ], + [ + '/test/path/3', + 'h', + 1, + ], + [ + '/test/path/3', + 'v', + 0, + ], + ]); + done(); + }); + }); + }); +}); From dd088a04a3e07aa669d8fc4b604ba2e0f89fdf70 Mon Sep 17 00:00:00 2001 From: Klemek Date: Tue, 30 Mar 2021 19:14:00 +0200 Subject: [PATCH 5/5] removed invalid tests --- test/hit_counter.test.js | 52 ---------------------------------------- 1 file changed, 52 deletions(-) diff --git a/test/hit_counter.test.js b/test/hit_counter.test.js index a4afaad..af5c7c6 100644 --- a/test/hit_counter.test.js +++ b/test/hit_counter.test.js @@ -84,58 +84,6 @@ describe('read()', () => { }); }); -describe('count()', () => { - beforeEach(() => { - mockClient.multi = () => mockClient; - mockClient.hincrby = () => mockClient; - mockClient.exec = (cb) => { - cb(); - }; - }); - - test('simple visit', (done) => { - let multiCalled = false; - let execCalled = false; - let hincrbyCalls = []; - mockClient.multi = () => { - multiCalled = true; - return mockClient; - }; - mockClient.hincrby = (hash, key, value) => { - hincrbyCalls.push([ - hash, - key, - value, - ]); - return mockClient; - }; - mockClient.exec = (cb) => { - execCalled = true; - cb(); - }; - hc.count({ - headers: {}, - connection: { remoteAddress: 'test1' }, - }, '/test/path/1', () => { - expect(multiCalled).toBeTruthy(); - expect(hincrbyCalls).toEqual([ - [ - '/test/path/1', - 'h', - 1, - ], - [ - '/test/path/1', - 'v', - 1, - ], - ]); - expect(execCalled).toBeTruthy(); - done(); - }); - }); -}); - describe('count()', () => { beforeEach(() => { mockClient.multi = () => mockClient;