Compare commits

...

61 Commits

Author SHA1 Message Date
dependabot[bot] f8a7fb303d Bump ajv from 6.10.2 to 6.12.6
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.10.2 to 6.12.6.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.10.2...v6.12.6)

---
updated-dependencies:
- dependency-name: ajv
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-12 14:11:06 +00:00
Klemek ab23e4aa3c Create docker.yml 2021-06-09 15:34:57 +02:00
Klemek ab573f91ee Merge pull request #58 from Klemek/f-stats-all
/stats?all=true
2021-04-06 10:10:00 +02:00
Klemek 8a9b9cdcfe /stats?all=true 2021-04-06 09:58:51 +02:00
Klemek e56867a269 Update bot_detector.js 2021-04-04 23:33:59 +02:00
Klemek 8e795c6371 Merge pull request #57 from Klemek/f-ignore-bots
ignore bots in hit counter
2021-04-04 23:31:38 +02:00
Klemek c3e53c7df8 more unit tests 2021-04-04 23:24:33 +02:00
Klemek 404b02830d unit testing 2021-04-04 22:48:21 +02:00
Klemek 2fe9a8fecd updated package.json 2021-04-04 21:57:54 +02:00
Klemek 078f3d7416 updated readme 2021-04-04 21:57:16 +02:00
Klemek d69e10202c bot detector handling requests and disabling hit counter 2021-04-04 21:57:09 +02:00
Klemek 140e472e29 bot detector base code 2021-04-04 21:56:39 +02:00
Klemek 823d97f4bb parseInt redis values 2021-03-30 20:19:37 +02:00
Klemek f7167a85a8 1.3.1: current visitors 2021-03-30 20:16:15 +02:00
Klemek 4a3b8267ec update Dockerfile 2021-03-30 19:56:08 +02:00
Klemek d0bebcba87 fix Dockerfile 2021-03-30 19:53:26 +02:00
Klemek 56d7993116 bump to node 15 2021-03-30 19:47:18 +02:00
Klemek 99a19edb93 updated README.md 2021-03-30 19:19:55 +02:00
Klemek da900d2d02 update version to 1.3.0 2021-03-30 19:19:29 +02:00
Klemek 51c1afb4d6 Merge pull request #53 from Klemek/f-hit-counter
Hit counter #47
2021-03-30 19:17:30 +02:00
Klemek dd088a04a3 removed invalid tests 2021-03-30 19:14:00 +02:00
Klemek 6439f8eb92 hit-counter 2021-03-30 19:11:31 +02:00
Klemek 88cb7ce30f eslint update 2021-03-30 16:29:20 +02:00
Klemek b0ba52e140 work in progress hit_counter 2021-03-30 15:30:54 +02:00
Klemek fa5cf31983 comma dangle 2021-03-30 15:20:22 +02:00
Klemek 18b02cf267 eslint jest 2021-03-30 15:07:08 +02:00
Klemek d194fbf032 eslint in CI 2021-03-30 14:56:04 +02:00
Klemek e9d67985a6 eslint integration 2021-03-30 14:53:26 +02:00
Klemek f6d6f04d59 Ci setup (#52)
* Create node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml
2021-03-30 13:37:31 +02:00
Klemek 5e817cc296 fix tests 2021-03-30 12:48:04 +02:00
Klemek 0f5e40c9ff fix dependencies 2021-03-30 12:32:18 +02:00
Klemek ec79c88bdf Merge pull request #51 from Klemek/snyk-fix-1fcf8450e3fce8a6dcdfb766b19e91f2
[Snyk] Security upgrade ejs from 2.7.1 to 3.1.6
2021-03-30 10:56:07 +02:00
snyk-bot 604d291c37 fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-EJS-1049328
2021-03-30 08:55:25 +00:00
Klemek 0896256e20 Merge branch 'master' of github.com:klemek/gitblog.md 2021-03-30 10:50:32 +02:00
Klemek 250b8a8625 Removed travis CI 2021-03-30 10:50:26 +02:00
Klemek 4f0d37f959 Merge pull request #49 from Klemek/dependabot/npm_and_yarn/y18n-4.0.1
Bump y18n from 4.0.0 to 4.0.1
2021-03-30 10:46:34 +02:00
dependabot[bot] 1bdf10a046 Bump y18n from 4.0.0 to 4.0.1
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-30 08:46:04 +00:00
Klemek d25d1a3aa9 Merge pull request #48 from Klemek/dependabot/npm_and_yarn/handlebars-4.7.7
Bump handlebars from 4.2.0 to 4.7.7
2021-03-30 10:45:52 +02:00
Klemek 507d0a792e Merge branch 'master' into dependabot/npm_and_yarn/handlebars-4.7.7 2021-03-30 10:45:45 +02:00
dependabot[bot] da613867d9 Bump handlebars from 4.2.0 to 4.7.7
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.2.0 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.2.0...v4.7.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-30 08:45:09 +00:00
Klemek 50074a17c8 Merge pull request #45 from Klemek/dependabot/npm_and_yarn/prismjs-1.23.0
Bump prismjs from 1.17.1 to 1.23.0
2021-03-30 10:44:47 +02:00
Klemek 53c197146a Merge pull request #41 from Klemek/dependabot/npm_and_yarn/handlebars-4.7.6
Bump handlebars from 4.2.0 to 4.7.6
2021-03-30 10:44:35 +02:00
Klemek 09bef2aadd Merge branch 'master' into dependabot/npm_and_yarn/handlebars-4.7.6 2021-03-30 10:44:16 +02:00
Klemek 77e723e6f6 Merge pull request #40 from Klemek/dependabot/npm_and_yarn/showdown-1.9.1
Bump showdown from 1.9.0 to 1.9.1
2021-03-30 10:44:06 +02:00
Klemek 1d50072cc5 Merge pull request #37 from Klemek/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2021-03-30 10:43:54 +02:00
Klemek 157bb405c3 Merge pull request #34 from Klemek/dependabot/npm_and_yarn/acorn-5.7.4
Bump acorn from 5.7.3 to 5.7.4
2021-03-30 10:43:41 +02:00
Klemek 4e10d0326b removed travis CI 2021-03-30 10:43:27 +02:00
dependabot[bot] c03393604c Bump prismjs from 1.17.1 to 1.23.0
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.17.1 to 1.23.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.17.1...v1.23.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-01 20:51:32 +00:00
klemek d15f3e04fb Dockerfile support 2021-01-20 17:00:28 +01:00
dependabot[bot] a21393294b Bump handlebars from 4.2.0 to 4.7.6
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.2.0 to 4.7.6.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.2.0...v4.7.6)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-07 08:56:42 +00:00
dependabot[bot] 191e24d218 Bump showdown from 1.9.0 to 1.9.1
Bumps [showdown](https://github.com/showdownjs/showdown) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/showdownjs/showdown/releases)
- [Changelog](https://github.com/showdownjs/showdown/blob/1.9.1/CHANGELOG.md)
- [Commits](https://github.com/showdownjs/showdown/compare/1.9.0...1.9.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-04 06:02:55 +00:00
dependabot[bot] 2c31015a14 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-18 08:05:56 +00:00
dependabot[bot] 4b681d5c3e Bump acorn from 5.7.3 to 5.7.4
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-16 12:59:05 +00:00
Klemek b19cf416f9 Merge pull request #29 from Klemek/add-license-1
Create LICENSE
2019-11-12 15:11:43 +01:00
Klemek 5c1a41c319 Create LICENSE 2019-11-12 15:09:46 +01:00
klemek 6e4b50eaa5 link to top was not working in most browsers 2019-10-25 08:26:58 +02:00
klemek 72e880eaf2 test 2019-10-22 21:48:42 +02:00
klemek 919b13db9c test 2019-10-19 16:25:52 +02:00
klemek e598a43acc test 2019-10-19 16:23:11 +02:00
Klemek 6fe5260bd2 Update README.md 2019-10-01 09:04:52 +02:00
Klemek 29ca0d291c Update README.md 2019-10-01 09:04:19 +02:00
31 changed files with 12498 additions and 2350 deletions
View File
+106
View File
@@ -0,0 +1,106 @@
module.exports = {
plugins: [ 'jest' ],
env: {
'commonjs': true,
'es2021': true,
'node': true,
'jest/globals': true,
},
extends: [
'eslint:recommended',
'plugin:jest/recommended',
],
parserOptions: {
ecmaVersion: 12,
},
rules: {
'indent': [
'error',
4,
],
'linebreak-style': [
'error',
'unix',
],
'quotes': [
'error',
'single',
],
'semi': [
'error',
'always',
],
'curly': [
'error',
'all',
],
'brace-style': [
'error',
'1tbs',
],
'jest/no-done-callback': 'off',
'jest/expect-expect': 'off',
'comma-dangle': [
'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',
},
};
+34
View File
@@ -0,0 +1,34 @@
name: Docker
on: ["push", "pull_request"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build
uses: docker/build-push-action@v2
with:
context: ./
file: ./Dockerfile
builder: ${{ steps.buildx.outputs.name }}
push: false
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+31
View File
@@ -0,0 +1,31 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Tests and coverage
on: ["push", "pull_request"]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 15.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present
- name: ESLint
run: npm run test-lint
- name: Jest
run: npm run test-cov
- name: Coveralls
run: npm run coveralls
env:
COVERALLS_REPO_TOKEN: "${{ secrets.COVERALLS_REPO_TOKEN }}"
COVERALLS_GIT_BRANCH: "${{ github.ref }}"
+1
View File
@@ -2,6 +2,7 @@
/node_modules
/config.json
/config.example.json
/robots_list.json
/data
/data/*
/test_data
-21
View File
@@ -1,21 +0,0 @@
{
"esversion": 6,
"maxerr": 999,
"indent": true,
"camelcase": true,
"eqeqeq": true,
"forin": true,
"immed": true,
"latedef": true,
"noarg": true,
"noempty": true,
"nonew": true,
"undef": true,
"unused": true,
"varstmt": true,
"sub": true,
"quotmark": "single",
"node": true,
"globals": {
}
}
-16
View File
@@ -1,16 +0,0 @@
dist: xenial
language: node_js
node_js:
- "12"
cache:
npm: true
directories:
- node_modules
install:
- npm install
before_script:
- npm install -g jshint
script:
- jest --coverage --silent
- jshint ./src
- cat ./coverage/lcov.info | coveralls
Executable
+22
View File
@@ -0,0 +1,22 @@
FROM node:15
# Create app directory
WORKDIR /usr/src/app
VOLUME [ "/usr/src/app/data" ]
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
COPY src/postinstall.js ./src/postinstall.js
COPY src/config.default.json ./src/config.default.json
RUN npm install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
CMD [ "sh", "-c", "node src/server.js" ]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+16 -1
View File
@@ -1,4 +1,4 @@
[![Build Status](https://img.shields.io/travis/Klemek/GitBlog.md.svg?branch=master)](https://travis-ci.org/Klemek/GitBlog.md)
[![Scc Count Badge](https://sloc.xyz/github/klemek/gitblog.md/?category=code)](https://github.com/boyter/scc/#badges-beta)
[![Coverage Status](https://img.shields.io/coveralls/github/Klemek/GitBlog.md.svg?branch=master)](https://coveralls.io/github/Klemek/GitBlog.md?branch=master)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/Klemek/GitBlog.md.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/GitBlog.md/context:javascript)
[![Total alerts](https://img.shields.io/lgtm/alerts/g/Klemek/GitBlog.md.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/GitBlog.md/alerts/)
@@ -281,6 +281,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**
@@ -331,3 +333,16 @@ 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
* `robots`
* `list_url`: (default: https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/COUNTER_Robots_list.json)
url to fetch for web crawlers patterns
* `list_file`: (default: robots_list.json)
file to store web crawlers patterns
* `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)
+9741 -781
View File
File diff suppressed because it is too large Load Diff
+14 -7
View File
@@ -1,32 +1,39 @@
{
"name": "gitblog.md",
"version": "1.2.8",
"version": "1.3.3",
"description": "A static blog using Markdown pulled from your git repository.",
"main": "src/server.js",
"dependencies": {
"@iarna/toml": "^2.2.3",
"body-parser": "^1.19.0",
"ejs": "^2.6.2",
"ejs": "^3.1.6",
"express": "^4.17.1",
"express-rate-limit": "^5.0.0",
"fa-diagrams": "^1.0.3",
"mathjax-node": "^2.1.1",
"ncp": "^2.0.0",
"node-prismjs": "^0.1.2",
"prismjs": "^1.16.0",
"node-prismjs": "^0.1.0",
"prismjs": "^1.23.0",
"redis": "^3.0.2",
"rss": "^1.2.2",
"showdown": "^1.9.0"
"showdown": "^1.9.1"
},
"devDependencies": {
"coveralls": "^3.0.4",
"eslint": "^7.23.0",
"eslint-plugin-jest": "^24.3.2",
"jest": "^24.8.0",
"superagent": "^5.1.0",
"supertest": "^4.0.2"
},
"scripts": {
"start": "node src/server.js",
"test": "jest --silent",
"install": "node src/postinstall.js"
"test": "jest --silent -i",
"test-cov": "jest --silent -i --coverage",
"coveralls": "coveralls < coverage/lcov.info",
"test-lint": "eslint .",
"install": "node src/postinstall.js",
"lint": "eslint --fix ."
},
"repository": {
"type": "git",
+1 -1
View File
@@ -5,7 +5,7 @@
<title><%= info.title %> - <%= article.title %></title>
</head>
<body>
<main class="article">
<main class="article" id="top">
<div class="header">
<a class="link-home" href="/">↑</a>
<h1><%= article.title %></h1>
+339 -256
View File
@@ -21,276 +21,359 @@ app.use(bodyParser.json());
* @type {{warn: string, ok: string, error: string}}
*/
const cons = {
ok: '\x1b[32m✔\x1b[0m %s',
warn: '\x1b[33m⚠\x1b[0m %s',
error: '\x1b[31m✘\x1b[0m %s',
ok: '\x1b[32m✔\x1b[0m %s',
warn: '\x1b[33m⚠\x1b[0m %s',
error: '\x1b[31m✘\x1b[0m %s',
};
module.exports = (config) => {
/**
* 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
*/
let render;
/**
* 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);
// set view engine from configuration
app.set('view engine', config['view_engine']);
// reroute the views folder to the root folder
app.set('views', path.join(__dirname, '..'));
const articles = {};
let lastRSS = '';
let host = config['host'];
reload = (success, error) => {
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)`);
else
console.log(cons.warn, `no articles loaded, check your configuration`);
lastRSS = '';
success();
});
};
if (config['test'])
app.reload = reload;
render = (req, res, vPath, data, code = 200) => {
data.info = {
title: config['home']['title'],
description: config['home']['description'],
host: host,
version: pjson.version,
request: req,
config: config
};
res.render(vPath, data, (err, html) => {
if (err && vPath !== path.join(config['data_dir'], config['home']['error'])) {
console.log(cons.error, `failed to render page ${vPath} : ${err}`);
showError(req, res, 500);
} else if (err) {
res.sendStatus(500);
console.log(cons.error, `failed to render error page : ${err}`);
} else
res.status(code).send(html);
});
};
showError = (req, res, code) => {
const errorPath = path.join(config['data_dir'], config['home']['error']);
fs.access(errorPath, fs.constants.R_OK, (err) => {
if (err)
res.sendStatus(code);
else
render(req, res, errorPath, {error: code}, code);
});
};
app.use((req, res, next) => {
if (!host) {
host = 'http://' + req.headers.host;
console.log(cons.ok, 'Currently hosted on ' + host);
}
next();
});
//rate limit for safer server
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: config['rate_limit']
});
app.use(limiter);
//log request at result end
app.use((req, res, next) => {
if (config['access_log']) {
const end = res.end;
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'}, () => {
res.end = end;
res.end(chunk, encoding);
});
};
}
next();
});
// home endpoint : send the correct index page or error if not existing
app.get('/', (req, res) => {
const homePath = path.join(config['data_dir'], config['home']['index']);
fs.access(homePath, fs.constants.R_OK, (err) => {
if (err)
showError(req, res, 404);
else
render(req, res, homePath,
{
articles: Object.values(articles)
.filter(d => !d.draft).sort((a, b) => ('' + b.path).localeCompare(a.path))
});
});
});
//RSS endpoint
app.get(config['rss']['endpoint'], (req, res) => {
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
});
Object.values(articles)
.slice(0, config['rss']['length'])
.forEach((article) => {
feed.item({
title: article.title,
url: host + article.url,
date: article.date
});
});
lastRSS = feed.xml();
}
res.type(req.headers['user-agent'].match(/Mozilla/) ? 'text/xml' : 'rss').send(lastRSS);
} else {
showError(req, res, 404);
}
});
//webhook endpoint
app.post(config['webhook']['endpoint'], (req, res) => {
if (config['modules']['webhook']) {
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);
/**
* 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
*/
let render;
/**
* 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);
const hc = require('./hit_counter')(config,
() => {
console.log(cons.ok, 'redis connected');
},
(err) => {
if (err.code !== 'ECONNREFUSED') {
console.log(cons.warn, 'redis error: ' + err);
}
},
);
const botDetector = require('./bot_detector')(config);
botDetector.load((status, err) => {
switch (status) {
case botDetector.status.FETCH_OK:
console.log(cons.ok, 'fetched robots list');
break;
case botDetector.status.FETCH_ERROR:
console.error(cons.error, 'error fetching robots list : ' + err);
break;
case botDetector.status.READ_OK:
console.log(cons.ok, `read robots list: ${botDetector.count}`);
break;
case botDetector.status.READ_ERROR:
console.error(cons.error, 'error reading robots list : ' + err);
break;
}
}
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);
});
});
} else {
res.sendStatus(400);
}
});
});
//rewrite urls to hide articles titles : /2019/05/05/sometitle/img.png => /2019/05/05/img.png
app.use((req, res, next) => {
if (/^\/\d{4}\/\d{2}\/\d{2}\//.test(req.url))
req.url = req.url.slice(0, 11) + req.url.slice(req.url.lastIndexOf('/'));
next();
});
// set view engine from configuration
app.set('view engine', config['view_engine']);
// reroute the views folder to the root folder
app.set('views', path.join(__dirname, '..'));
// catch all article urls and render them
app.get('*', (req, res, next) => {
if (/^\/\d{4}\/\d{2}\/\d{2}\/$/.test(req.path)) {
const articlePath = req.path.substr(1, 10);
const article = articles[articlePath];
if (!article)
showError(req, res, 404);
else {
renderer.render(article.realPath, (err, html) => {
if (err) {
console.log(cons.error, `failed to render article ${req.path} : ${err}`);
return showError(req, res, 500);
}
article.content = html;
const templatePath = path.join(config['data_dir'], config['article']['template']);
fs.access(templatePath, fs.constants.R_OK, (err) => {
const articles = {};
let lastRSS = '';
let host = config['host'];
reload = (success, error) => {
fw.fetchArticles((err, dict) => {
if (err) {
console.log(cons.error, `no template found at ${templatePath}`);
showError(req, res, 500);
} else
render(req, res, templatePath, {article: article});
});
console.error(cons.error, 'error loading articles : ' + err);
error();
} else {
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();
}
});
}
} else {
next();
};
if (config['test']) {
app.reload = reload;
}
});
// catch all hidden file type and return 404
config['home']['hidden'].forEach(pathMatcher => {
app.get(pathMatcher, (req, res) => {
showError(req, res, 404);
render = (req, res, vPath, data, code = 200) => {
data.info = {
title: config['home']['title'],
description: config['home']['description'],
host: host,
version: pjson.version,
request: req,
config: config,
};
res.render(vPath, data, (err, html) => {
if (err && vPath !== path.join(config['data_dir'], config['home']['error'])) {
console.log(cons.error, `failed to render page ${vPath} : ${err}`);
showError(req, res, 500);
} else if (err) {
res.sendStatus(500);
console.log(cons.error, `failed to render error page : ${err}`);
} else {
res.status(code).send(html);
}
});
};
showError = (req, res, code) => {
const errorPath = path.join(config['data_dir'], config['home']['error']);
fs.access(errorPath, fs.constants.R_OK, (err) => {
if (err) {
res.sendStatus(code);
} else {
render(req, res, errorPath, { error: code }, code);
}
});
};
app.use((req, res, next) => {
if (!host) {
host = 'http://' + req.headers.host;
console.log(cons.ok, 'Currently hosted on ' + host);
}
next();
});
});
// serve all static files via get
app.get('*', express.static(path.join(__dirname, '..', config['data_dir'])));
// catch express.static errors (mostly not found) by displaying 404
app.get('*', (req, res) => {
showError(req, res, 404);
});
// catch all other methods and return 400
app.all('*', (req, res) => {
res.status(400).send('bad request');
});
//log all server errors
app.use((err, req, res, next) => {
console.log(cons.error, `error when handling ${req.method} ${req.path} request : ${err}`);
if (!config['error_log'])
next(err);
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'}, () => {
next(err);
});
});
// must be use in a server.js to start the server
app.start = () => {
reload(() => {
app.listen(config['node_port'], () => {
console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`);
});
//rate limit for safer server
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: config['rate_limit'],
});
};
app.use(limiter);
return app;
//detect robots
app.use(botDetector.handle);
//log request at result end
app.use((req, res, next) => {
if (config['access_log']) {
const end = res.end;
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' }, () => {
res.end = end;
res.end(chunk, encoding);
});
};
}
next();
});
// home endpoint : send the correct index page or error if not existing
app.get('/', (req, res) => {
const homePath = path.join(config['data_dir'], config['home']['index']);
fs.access(homePath, fs.constants.R_OK, (err) => {
if (err) {
showError(req, res, 404);
} else {
hc.count(req, '/', req.isRobot, () => {
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) => {
if (config['modules']['hit_counter']) {
if (req.query['all']) {
const keys = Object.keys(articles).filter(key => !articles[key].draft);
keys.unshift('/');
const read = (i, outputData) => {
if (i >= keys.length) {
res.json(outputData);
} else {
hc.read(keys[i], (data) => {
outputData.push(data);
read(i + 1, outputData);
});
}
};
read(0, []);
} else {
hc.read('/', (data) => {
res.json(data);
});
}
} else {
showError(req, res, 404);
}
});
//RSS endpoint
app.get(config['rss']['endpoint'], (req, res) => {
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,
});
Object.values(articles)
.slice(0, config['rss']['length'])
.forEach((article) => {
feed.item({
title: article.title,
url: host + article.url,
date: article.date,
});
});
lastRSS = feed.xml();
}
res.type(req.headers['user-agent'].match(/Mozilla/) ? 'text/xml' : 'rss').send(lastRSS);
} else {
showError(req, res, 404);
}
});
//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) {
res.sendStatus(403);
valid = false;
}
}
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);
}
});
//rewrite urls to hide articles titles : /2019/05/05/sometitle/img.png => /2019/05/05/img.png
app.use((req, res, next) => {
if (/^\/\d{4}\/\d{2}\/\d{2}\//.test(req.url)) {
req.url = req.url.slice(0, 11) + req.url.slice(req.url.lastIndexOf('/'));
}
next();
});
// catch all article urls and render them
app.get('*', (req, res, next) => {
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')) {
if (config['modules']['hit_counter']) {
hc.read(articlePath, (data) => {
res.json(data);
});
} else {
showError(req, res, 404);
}
} else {
hc.count(req, articlePath, req.isRobot, () => {
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 {
next();
}
});
// catch all hidden file type and return 404
config['home']['hidden'].forEach(pathMatcher => {
app.get(pathMatcher, (req, res) => {
showError(req, res, 404);
});
});
// serve all static files via get
app.get('*', express.static(path.join(__dirname, '..', config['data_dir'])));
// catch express.static errors (mostly not found) by displaying 404
app.get('*', (req, res) => {
showError(req, res, 404);
});
// catch all other methods and return 400
app.all('*', (req, res) => {
res.status(400).send('bad request');
});
//log all server errors
app.use((err, req, res, next) => {
console.log(cons.error, `error when handling ${req.method} ${req.path} request : ${err}`);
if (!config['error_log']) {
next(err);
}
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' }, () => {
next(err);
});
});
// must be use in a server.js to start the server
app.start = () => {
reload(() => {
app.listen(config['node_port'], () => {
console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`);
});
});
};
return app;
};
+66
View File
@@ -0,0 +1,66 @@
const https = require('https');
const fs = require('fs');
module.exports = (config) => {
const _this = {
status: {
FETCH_OK: 1,
FETCH_ERROR: 2,
READ_OK: 3,
READ_ERROR: 4,
},
count: [],
regex: null,
};
const fetchList = (cb) => {
https.get(config['robots']['list_url'], (res) => {
if (res.statusCode !== 200) {
cb(res.statusCode);
} else {
const file = fs.createWriteStream(config['robots']['list_file']);
res.pipe(file);
file.on('finish', () => {
file.close(cb);
});
}
}).on('error', (err) => {
cb(err.message);
});
};
const readFile = (cb) => {
fs.readFile(config['robots']['list_file'], { encoding: 'utf-8' }, (err, data) => {
if (err) {
cb(err, undefined);
} else {
try {
cb(undefined, JSON.parse(data));
} catch (err) {
cb(err, undefined);
}
}
});
};
_this.load = (cb) => {
fetchList((err) => {
cb(err ? _this.status.FETCH_ERROR : _this.status.FETCH_OK, err);
readFile((err, data) => {
if (!err) {
_this.count = data.length;
_this.regex = new RegExp('(' + data.filter(v => v['pattern']).map(v => v['pattern'])
.join('|') + ')');
}
cb(err ? _this.status.READ_ERROR : _this.status.READ_OK, err);
});
});
};
_this.handle = (req, res, next) => {
req.isRobot = !!((req.headers['user-agent'] || '').match(_this.regex));
next();
};
return _this;
};
+13 -1
View File
@@ -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,16 @@
},
"plantuml": {
"output_format": "svg"
},
"hit_counter": {
"unique_visitor_timeout": 7200000
},
"robots": {
"list_url": "https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/COUNTER_Robots_list.json",
"list_file": "robots_list.json"
},
"redis": {
"host": "localhost",
"port": 6379
}
}
+22 -22
View File
@@ -8,28 +8,28 @@ const fs = require('fs');
* @returns {*}
*/
const merge = (ref, src) => {
if (typeof ref !== typeof src) {
return ref;
} else if (ref.length && !src.length) {
return ref;
} else if (ref.length && src.length) {
return src;
} else if (typeof ref === 'object') {
const out = {};
Object.keys(ref).forEach((key) => out[key] = merge(ref[key], src[key]));
return out;
} else {
return src;
}
if (typeof ref !== typeof src) {
return ref;
} else if (ref.length && !src.length) {
return ref;
} else if (ref.length && src.length) {
return src;
} else if (typeof ref === 'object') {
const out = {};
Object.keys(ref).forEach((key) => out[key] = merge(ref[key], src[key]));
return out;
} else {
return src;
}
};
module.exports = () => {
try {
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;
}
};
try {
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;
}
};
+99 -84
View File
@@ -9,29 +9,35 @@ const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
* @param cb
*/
const getFileTree = (dir, cb) => {
let list = [];
let remaining = 0;
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));
}
let list = [];
let remaining = 0;
fs.readdir(dir, { withFileTypes: true }, (err, items) => {
if (err) {
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);
});
};
/**
@@ -41,71 +47,80 @@ const getFileTree = (dir, cb) => {
* @param cb
*/
const readIndexFile = (path, thumbnailTag, cb) => {
fs.readFile(path, {encoding: 'UTF-8'}, (err, data) => {
if (err)
return cb(err);
fs.readFile(path, { encoding: 'UTF-8' }, (err, data) => {
if (err) {
cb(err);
} else {
let info = {};
let info = {};
const regRes1 = data.match(/(^|[^#])#([^#\r\n]*)\r?\n?$/m);
info.title = regRes1 ? regRes1[2].trim() : undefined;
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;
const thumbnailRegEx = new RegExp(`!\\[${thumbnailTag}]\\(([^)]*)\\)`, 'i');
const regRes2 = data.match(thumbnailRegEx);
info.thumbnail = regRes2 ? regRes2[1].trim() : undefined;
cb(null, info);
});
cb(null, info);
}
});
};
module.exports = (config) => {
return {
fileTree: config['test'] ? getFileTree : undefined,
readIndexFile: config['test'] ? readIndexFile : undefined,
/**
* 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);
});
});
});
}
};
};
return {
fileTree: config['test'] ? getFileTree : undefined,
readIndexFile: config['test'] ? readIndexFile : undefined,
/**
* find and read all articles inside the data directory
* @param cb
*/
fetchArticles: (cb) => {
getFileTree(config['data_dir'], (err, fileList) => {
if (err) {
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);
}
}
});
});
}
});
},
};
};
+66
View File
@@ -0,0 +1,66 @@
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, disable, cb) => {
if (!client.connected || disable) {
cb();
} else {
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
visitors[path] = (visitors[path] || {});
const now = Date.now();
const isNewVisitor = (now - (visitors[path][ip] || 0)) > config['hit_counter']['unique_visitor_timeout'];
visitors[path][ip] = now;
client
.multi()
.hincrby(path, 'h', 1)
.hincrby(path, 'v', isNewVisitor ? 1 : 0)
.exec(cb);
}
};
const cleanVisitors = (path) => {
visitors[path] = (visitors[path] || {});
const now = Date.now();
let count = 0;
for (let ip in visitors[path]) {
if ((now - visitors[path][ip]) > config['hit_counter']['unique_visitor_timeout']) {
delete visitors[path][ip];
} else {
count++;
}
}
return count;
};
const read = (path, cb) => {
if (!client.connected) {
cb({
path: path,
hits: 0,
total_visitors: 0,
current_visitors: cleanVisitors(path),
});
} else {
client.hgetall(path, (_, value) => {
cb({
path: path,
hits: value ? parseInt(value.h) || 0 : 0,
total_visitors: value ? parseInt(value.v) || 0 : 0,
current_visitors: cleanVisitors(path),
});
});
}
};
return {
count: count,
read: read,
};
};
+17 -15
View File
@@ -3,28 +3,30 @@ const path = require('path');
const ncp = require('ncp').ncp;
const copy = (src, dest) => {
ncp(src, dest, function (err) {
if (err)
console.error(err);
else
console.log(`copied ${src} to ${dest}`);
});
ncp(src, dest, function (err) {
if (err) {
console.error(err);
} else {
console.log(`copied ${src} to ${dest}`);
}
});
};
copy(path.join('src', 'config.default.json'), 'config.example.json');
if (!fs.existsSync('data')) {
fs.mkdirSync('data');
fs.mkdirSync('data');
copy(path.join('sample_data', 'home'), 'data');
copy(path.join('sample_data', 'home'), 'data');
const pad0 = (n) => ('0' + n).substr(-2);
const pad0 = (n) => ('0' + n).substr(-2);
const datetime = new Date();
const dir = path.join('data', datetime.getFullYear().toString(), pad0(datetime.getMonth() + 1), pad0(datetime.getDate()));
const datetime = new Date();
const dir = path.join('data', datetime.getFullYear().toString(), pad0(datetime.getMonth() + 1), pad0(datetime.getDate()));
if (!fs.existsSync(dir))
fs.mkdirSync(dir, {recursive: true});
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
copy(path.join('sample_data', 'article'), dir);
}
copy(path.join('sample_data', 'article'), dir);
}
+228 -198
View File
@@ -3,216 +3,246 @@ const path = require('path');
const showdown = require('showdown');
module.exports = (config) => {
const converter = new showdown.Converter(config['showdown']);
const converter = new showdown.Converter(config['showdown']);
/**
* get parts outside of codes/scripts
* @param {string} data
* @returns {{index:number, end:number, text:string}[]} parts
*/
const getParts = (data) => {
let parts = [];
let match;
let i = 0;
while ((match = /```/m.exec(data.slice(i)))) {
parts.push({
index: i,
text: data.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < data.length)
parts.push({
index: i,
text: data.slice(i, data.length),
});
parts = parts.filter((p, i) => i % 2 === 0); //filter out code parts
// detect scripts outside of code
parts.forEach((p, pi) => {
let i = 0;
const subParts = [];
while ((match = /(<script>((?:(?!<\/script>)[\s\S])*)<\/script>)/gm.exec(p.text.slice(i)))) {
subParts.push({
index: p.index + i,
text: p.text.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < p.text.length)
subParts.push({
index: p.index + i,
text: p.text.slice(i, p.text.length),
});
parts.splice(pi, 1, ...subParts);
});
parts.forEach(part => part.end = part.index + part.text.length);
return parts;
};
const renderShowDown = (data, cb) => {
const html = converter.makeHtml(data);
cb(html);
};
let Prism;
if (config['modules']['prism'])
Prism = require('node-prismjs');
const renderPrism = (data, cb) => {
if (!config['modules']['prism'])
return 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) + `<pre><code class="${lang} language-${lang}">` + block + '</code></pre>' + data.slice(match.index + match[0].length);
}
cb(data);
};
if (config['modules']['plantuml']) {
require('./script_loader')(path.join(__dirname, 'lib', 'plantuml_synchro.js'));
}
const renderPlantUML = (data, cb) => {
if (!config['modules']['plantuml'])
return 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)); // jshint ignore:line
const compressed = global['zip_deflate'](s);
const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`;// jshint ignore:line
part.text = part.text.slice(0, match.index) + `<img alt="generated PlantUML diagram" src="${url}">` + part.text.slice(match.index + match[0].length);
}
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data);
};
let mjAPI;
if (config['modules']['mathjax']) {
mjAPI = require('mathjax-node');
mjAPI.config({
MathJax: {
tex2jax: {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
/**
* get parts outside of codes/scripts
* @param {string} data
* @returns {{index:number, end:number, text:string}[]} parts
*/
const getParts = (data) => {
let parts = [];
let match;
let i = 0;
while ((match = /```/m.exec(data.slice(i)))) {
parts.push({
index: i,
text: data.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < data.length) {
parts.push({
index: i,
text: data.slice(i, data.length),
});
}
}
});
}
const renderMathJax = (data, cb) => {
if (!config['modules']['mathjax'])
return cb(data);
parts = parts.filter((p, i) => i % 2 === 0); //filter out code parts
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);
// detect scripts outside of code
parts.forEach((p, pi) => {
let i = 0;
const subParts = [];
while ((match = /(<script>((?:(?!<\/script>)[\s\S])*)<\/script>)/gm.exec(p.text.slice(i)))) {
subParts.push({
index: p.index + i,
text: p.text.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < p.text.length) {
subParts.push({
index: p.index + i,
text: p.text.slice(i, p.text.length),
});
}
parts.splice(pi, 1, ...subParts);
});
});
parts.forEach(part => part.end = part.index + part.text.length);
return parts;
};
const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
const inlineEqRegex = /\$([^$\n]*)\$/;
const renderShowDown = (data, cb) => {
const html = converter.makeHtml(data);
cb(html);
};
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 Prism;
if (config['modules']['prism']) {
Prism = require('node-prismjs');
}
cb(data);
};
let faDiagrams;
let toml;
if (config['modules']['fa-diagrams']) {
faDiagrams = require('fa-diagrams');
toml = require('@iarna/toml');
}
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 = `<b style="color:red">${err.toString()}</b>`;
const renderPrism = (data, cb) => {
if (!config['modules']['prism']) {
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) + `<pre><code class="${lang} language-${lang}">` + block + '</code></pre>' + data.slice(match.index + match[0].length);
}
cb(data);
}
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);
};
};
return {
getParts: config['test'] ? getParts : undefined,
renderShowDown: config['test'] ? renderShowDown : undefined,
renderPrism: config['test'] ? renderPrism : undefined,
renderPlantUML: config['test'] ? renderPlantUML : undefined,
renderMathJax: config['test'] ? renderMathJax : undefined,
renderFaDiagrams: config['test'] ? renderFaDiagrams : undefined,
render: (file, cb) => {
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);
});
});
});
});
});
});
if (config['modules']['plantuml']) {
require('./script_loader')(path.join(__dirname, 'lib', 'plantuml_synchro.js'));
}
};
const renderPlantUML = (data, cb) => {
/* global encode64 */
if (!config['modules']['plantuml']) {
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) + `<img alt="generated PlantUML diagram" src="${url}">` + part.text.slice(match.index + match[0].length);
}
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data);
}
};
let mjAPI;
if (config['modules']['mathjax']) {
mjAPI = require('mathjax-node');
mjAPI.config({
MathJax: {
tex2jax: {
inlineMath: [
[
'$',
'$',
],
],
displayMath: [
[
'$$',
'$$',
],
],
},
},
});
}
const renderMathJax = (data, cb) => {
if (!config['modules']['mathjax']) {
cb(data);
} else {
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 eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
const inlineEqRegex = /\$([^$\n]*)\$/;
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);
}
}
};
let faDiagrams;
let toml;
if (config['modules']['fa-diagrams']) {
faDiagrams = require('fa-diagrams');
toml = require('@iarna/toml');
}
const renderFaDiagrams = (data, cb) => {
if (!config['modules']['fa-diagrams']) {
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 = `<b style="color:red">${err.toString()}</b>`;
}
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);
}
};
return {
getParts: config['test'] ? getParts : undefined,
renderShowDown: config['test'] ? renderShowDown : undefined,
renderPrism: config['test'] ? renderPrism : undefined,
renderPlantUML: config['test'] ? renderPlantUML : undefined,
renderMathJax: config['test'] ? renderMathJax : undefined,
renderFaDiagrams: config['test'] ? renderFaDiagrams : undefined,
render: (file, cb) => {
fs.readFile(file, { encoding: 'UTF-8' }, (err, data) => {
if (err) {
cb(err);
} else {
renderPlantUML(data, (data) => {
renderFaDiagrams(data, (data) => {
renderMathJax(data, (data) => {
renderPrism(data, (data) => {
renderShowDown(data, (html) => {
cb(null, html);
});
});
});
});
});
}
});
},
};
};
+1 -1
View File
@@ -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' }));
};
+1 -1
View File
@@ -1,4 +1,4 @@
const config = require('./config')();
const app = require('./app')(config);
app.start();
app.start();
+536 -381
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const request = require('supertest');
const fs = require('fs');
const path = require('path');
@@ -18,457 +17,613 @@ 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);
beforeEach((done, fail) => {
config['home']['index'] = testIndex;
config['data_dir'] = dataDir;
config['article']['index'] = 'index.md';
config['access_log'] = '';
config['error_log'] = '';
config['modules']['rss'] = true;
config['modules']['webhook'] = true;
config['home']['index'] = testIndex;
config['data_dir'] = dataDir;
config['article']['index'] = 'index.md';
config['access_log'] = '';
config['error_log'] = '';
config['modules']['rss'] = true;
config['modules']['webhook'] = true;
config['modules']['hit_counter'] = false;
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
app.reload(done, fail);
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
app.reload(done, fail);
});
afterAll(() => {
if (fs.existsSync(dataDir)) {
utils.deleteFolderSync(dataDir);
}
if (fs.existsSync(dataDir)) {
utils.deleteFolderSync(dataDir);
}
});
describe('Test reload', () => {
test('reload fail', (done, fail) => {
config['data_dir'] = '';
app.reload(fail, done);
});
test('reload fail', (done, fail) => {
config['data_dir'] = '';
app.reload(fail, done);
});
});
describe('Test request logging', () => {
test('test no log', (done) => {
request(app).get('/rsstest').then(() => {
expect(fs.existsSync(path.join(dataDir, 'access.log'))).toBe(false);
done();
test('no log', (done) => {
request(app).get('/rsstest')
.then(() => {
expect(fs.existsSync(path.join(dataDir, 'access.log'))).toBe(false);
done();
});
});
});
test('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('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('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('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('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' +
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();
});
});
done();
});
});
});
});
});
});
describe('Test error logging', () => {
test('test no log', (done) => {
config['home']['index'] = null;
request(app).get('/').then(() => {
expect(fs.existsSync(path.join(dataDir, 'error.log'))).toBe(false);
done();
test('no log', (done) => {
config['home']['index'] = null;
request(app).get('/')
.then(() => {
expect(fs.existsSync(path.join(dataDir, 'error.log'))).toBe(false);
done();
});
});
});
test('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. Received type object';
expect(start).toBe(expected);
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();
});
});
});
});
});
describe('Test root path', () => {
test('404 no index no error', (done) => {
request(app).get('/').then((response) => {
expect(response.statusCode).toBe(404);
done();
test('404 no index no error', (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();
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();
});
});
});
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();
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();
});
});
});
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();
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();
});
});
});
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();
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();
});
});
});
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();
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();
});
});
test('200 2 articles 1 drafted', (done, fail) => {
utils.createEmptyDirs([
path.join(dataDir, '2019', '05', '05'),
path.join(dataDir, '2018', '05', '05'),
path.join(dataDir, '2017', '05', '05'),
]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'draft.md'),
path.join(dataDir, '2018', '05', '05', 'index.md'),
path.join(dataDir, '2018', '05', '05', 'draft.md'),
path.join(dataDir, '2017', '05', '05', 'index.md'),
]);
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();
});
}, fail);
});
});
test('200 2 articles 1 drafted', (done, fail) => {
utils.createEmptyDirs([
path.join(dataDir, '2019', '05', '05'),
path.join(dataDir, '2018', '05', '05'),
path.join(dataDir, '2017', '05', '05')
]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'draft.md'),
path.join(dataDir, '2018', '05', '05', 'index.md'),
path.join(dataDir, '2018', '05', '05', 'draft.md'),
path.join(dataDir, '2017', '05', '05', 'index.md'),
]);
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();
});
}, fail);
});
});
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();
test('404 rss deactivated', (done) => {
config['modules']['rss'] = false;
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('<item>').length).toBe(1);
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('<item>').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 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('<item>').length).toBe(1);
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('<item>').length).toBe(1);
done();
});
});
});
test('200 2 rss items', (done, fail) => {
utils.createEmptyDirs([
path.join(dataDir, '2019', '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'),
]);
app.reload(() => {
request(app).get('/rsstest')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text.length).toBeGreaterThan(0);
expect(response.text.split('<item>').length).toBe(3);
done();
});
}, fail);
});
test('200 max rss items', (done, fail) => {
utils.createEmptyDirs([
path.join(dataDir, '2019', '05', '05'),
path.join(dataDir, '2018', '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'),
]);
app.reload(() => {
request(app).get('/rsstest')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text.length).toBeGreaterThan(0);
expect(response.text.split('<item>').length).toBe(3);
done();
});
}, fail);
});
});
test('200 2 rss items', (done, fail) => {
utils.createEmptyDirs([
path.join(dataDir, '2019', '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')
]);
app.reload(() => {
request(app).get('/rsstest').then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text.length).toBeGreaterThan(0);
expect(response.text.split('<item>').length).toBe(3);
done();
});
}, fail);
});
test('200 max rss items', (done, fail) => {
utils.createEmptyDirs([
path.join(dataDir, '2019', '05', '05'),
path.join(dataDir, '2018', '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')
]);
app.reload(() => {
request(app).get('/rsstest').then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text.length).toBeGreaterThan(0);
expect(response.text.split('<item>').length).toBe(3);
done();
});
}, fail);
});
});
describe('Test webhook', () => {
test('400 webhook deactivated', (done) => {
config['modules']['webhook'] = false;
request(app).post('/webhooktest').then((response) => {
expect(response.statusCode).toBe(400);
done();
test('400 webhook deactivated', (done) => {
config['modules']['webhook'] = false;
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.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) => {
expect(response.statusCode).toBe(200);
done();
});
test('200 no secret', (done) => {
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) => {
expect(response.statusCode).toBe(200);
done();
});
});
});
});
test('500 command failed', (done) => {
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();
test('500 command failed', (done) => {
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();
});
});
});
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();
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();
});
});
});
test('200 valid secret', (done) => {
config['webhook']['signature_header'] = 'testheader';
config['webhook']['secret'] = 'testvalue';
config['webhook']['pull_command'] = 'git --help';
request(app).post('/webhooktest')
.send({})
.set('testheader', 'sha1=d924d5bd4b36faf9d572844ac9c12a09ce3e7134')
.then((response) => {
expect(response.statusCode).toBe(200);
done();
test('200 valid secret', (done) => {
config['webhook']['signature_header'] = 'testheader';
config['webhook']['secret'] = 'testvalue';
config['webhook']['pull_command'] = 'git --help';
request(app).post('/webhooktest')
.send({})
.set('testheader', 'sha1=d924d5bd4b36faf9d572844ac9c12a09ce3e7134')
.then((response) => {
expect(response.statusCode).toBe(200);
done();
});
});
});
});
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();
test('404 article not found', (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'),]);
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello');
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- articl.content %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
request(app).get('/2019/05/05/hello/').then((response) => {
expect(response.statusCode).toBe(500);
done();
});
}, fail);
});
test('500 fail to render', (done, fail) => {
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 %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
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'),]);
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();
});
}, fail);
});
test('500 no template', (done, fail) => {
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();
});
}, fail);
});
test('200 rendered article', (done, fail) => {
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 %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
request(app).get('/2019/05/05/hello/').then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
done();
});
}, fail);
});
test('200 rendered article', (done, fail) => {
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 %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
request(app).get('/2019/05/05/hello/')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
done();
});
}, fail);
});
test('200 rendered draft', (done, fail) => {
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 %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
request(app).get('/2019/05/05/hello/').then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
done();
});
}, fail);
});
test('200 rendered draft', (done, fail) => {
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 %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
request(app).get('/2019/05/05/hello/')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
done();
});
}, fail);
});
test('200 other url', (done, fail) => {
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();
});
}, fail);
});
test('200 other url', (done, fail) => {
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();
});
}, fail);
});
test('200 other url 2', (done, fail) => {
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();
});
}, fail);
});
test('200 other url 2', (done, fail) => {
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();
});
}, fail);
});
});
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();
test('404 invalid file no error page', (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();
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();
});
});
});
test('404 hidden file', (done) => {
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();
test('404 hidden file', (done) => {
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();
});
});
});
test('404 hidden folder', (done) => {
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();
test('404 hidden folder', (done) => {
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();
});
});
});
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();
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();
});
});
});
test('200 valid resource of article', (done) => {
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();
test('200 valid resource of article', (done) => {
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();
});
});
});
});
describe('Test other requests', () => {
test('400 POST', (done) => {
request(app).post('/').then((response) => {
expect(response.statusCode).toBe(400);
done();
test('400 POST', (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();
test('400 PUT', (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();
test('400 DELETE', (done) => {
request(app).delete('/')
.then((response) => {
expect(response.statusCode).toBe(400);
done();
});
});
});
describe('Test stats', () => {
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({
path: '/',
hits: 0,
total_visitors: 0,
current_visitors: 0,
});
done();
});
});
test('200 index stats all no article', (done) => {
config['modules']['hit_counter'] = true;
app.reload(() => {
request(app).get('/stats?all=true')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual([
{
path: '/',
hits: 0,
total_visitors: 0,
current_visitors: 0,
},
]);
done();
});
});
});
test('200 index stats all 2 article 1 drafted', (done) => {
config['modules']['hit_counter'] = true;
utils.createEmptyDirs([
path.join(dataDir, '2019', '05', '05'),
path.join(dataDir, '2019', '04', '05'),
]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'),
path.join(dataDir, '2019', '04', '05', 'draft.md'),
]);
app.reload(() => {
request(app).get('/stats?all=true')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual([
{
path: '/',
hits: 0,
total_visitors: 0,
current_visitors: 0,
},
{
path: '2019/05/05',
hits: 0,
total_visitors: 0,
current_visitors: 0,
},
]);
done();
});
});
});
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 article 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({
path: '2019/05/05',
hits: 0,
total_visitors: 0,
current_visitors: 0,
});
done();
});
});
});
});
});
+116
View File
@@ -0,0 +1,116 @@
const fs = require('fs');
const utils = require('./test_utils');
const dataDir = 'test_data';
const config = {
robots: {
list_url: '',
list_file: `${dataDir}/robots_list.json`,
},
};
beforeAll(() => {
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
});
afterAll(() => {
if (fs.existsSync(dataDir)) {
utils.deleteFolderSync(dataDir);
}
});
const botDetector = require('../src/bot_detector')(config);
describe('load()', () => {
test('success', (done) => {
config.robots = {
list_url: 'https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/COUNTER_Robots_list.json',
list_file: `${dataDir}/robots_list_success.json`,
};
let count = 0;
botDetector.load((status, err) => {
expect(err).not.toBeDefined();
expect(status).toBe(count === 0 ? botDetector.status.FETCH_OK : botDetector.status.READ_OK);
if (count > 0) {
done();
}
count++;
});
});
test('fetch and file failure', (done) => {
let count = 0;
config.robots = {
list_url: 'https://blog.klemek.fr/invalid.json',
list_file: `${dataDir}/robots_list_fail_1.json`,
};
botDetector.load((status) => {
expect(status).toBe(count === 0 ? botDetector.status.FETCH_ERROR : botDetector.status.READ_ERROR);
if (count > 0) {
done();
}
count++;
});
});
test('fetch failure and file ok', (done) => {
let count = 0;
config.robots = {
list_url: 'https://blog.klemek.fr/invalid.json',
list_file: `${dataDir}/robots_list_fail_2.json`,
};
fs.writeFile(config.robots.list_file, '[]\n', { encoding: 'utf-8' }, () => {
botDetector.load((status) => {
expect(status).toBe(count === 0 ? botDetector.status.FETCH_ERROR : botDetector.status.READ_OK);
if (count > 0) {
done();
}
count++;
});
});
});
});
describe('handle()', () => {
beforeAll((done) => {
config.robots = {
list_url: 'https://blog.klemek.fr/invalid.json',
list_file: `${dataDir}/robots_list_fake.json`,
};
fs.writeFile(config.robots.list_file, '[{"pattern":"bot"}]\n', { encoding: 'utf-8' }, () => {
botDetector.load((status) => {
if (status !== botDetector.status.FETCH_ERROR) {
done();
}
});
});
});
test('not bot', (done) => {
const req = {
headers: {
'user-agent': 'my user agent',
},
};
botDetector.handle(req, null, () => {
expect(req.isRobot).toBeFalsy();
done();
});
});
test('bot', (done) => {
const req = {
headers: {
'user-agent': 'bot',
},
};
botDetector.handle(req, null, () => {
expect(req.isRobot).toBeTruthy();
done();
});
});
});
+56 -50
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const fs = require('fs');
const path = require('path');
@@ -6,76 +5,83 @@ const configFile = 'config.json';
const tmpConfigFile = 'config.temp.json';
beforeAll(() => {
if (fs.existsSync(configFile)) {
fs.renameSync(configFile, tmpConfigFile);
}
expect(fs.existsSync(configFile)).toBeFalsy();
if (fs.existsSync(configFile)) {
fs.renameSync(configFile, tmpConfigFile);
}
});
afterAll(() => {
if (fs.existsSync(tmpConfigFile)) {
fs.renameSync(tmpConfigFile, configFile);
} else if (fs.existsSync(configFile)) {
fs.unlinkSync(configFile); //remove config file if remaining
}
if (fs.existsSync(tmpConfigFile)) {
fs.renameSync(tmpConfigFile, configFile);
} else if (fs.existsSync(configFile)) {
fs.unlinkSync(configFile); //remove config file if remaining
}
});
test('no config', () => {
if (fs.existsSync(configFile))
fs.unlinkSync(configFile);
expect(fs.existsSync(configFile)).toBeFalsy();
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(3000);
expect(config['data_dir']).toBe('data');
if (fs.existsSync(configFile)) {
fs.unlinkSync(configFile);
}
expect(fs.existsSync(configFile)).toBeFalsy();
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(3000);
expect(config['data_dir']).toBe('data');
});
test('example config', () => {
if (fs.existsSync(configFile))
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 config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(3333);
expect(config['data_dir']).toBe('data');
if (fs.existsSync(configFile)) {
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 config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(3333);
expect(config['data_dir']).toBe('data');
});
test('invalid config ignored', () => {
fs.writeFileSync(configFile, 'invalid JSON');
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(3000);
expect(config['data_dir']).toBe('data');
fs.writeFileSync(configFile, 'invalid JSON');
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(3000);
expect(config['data_dir']).toBe('data');
});
test('good config merged', () => {
fs.writeFileSync(configFile, '{"node_port":5000}');
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(5000);
expect(config['data_dir']).toBe('data');
fs.writeFileSync(configFile, '{"node_port":5000}');
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(5000);
expect(config['data_dir']).toBe('data');
});
test('wrong config fixed', () => {
fs.writeFileSync(configFile, '{"node_port":"hello","data_dir":"data2"}');
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(3000);
expect(config['data_dir']).toBe('data2');
fs.writeFileSync(configFile, '{"node_port":"hello","data_dir":"data2"}');
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['node_port']).toBe(3000);
expect(config['data_dir']).toBe('data2');
});
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']);
fs.writeFileSync(configFile, '{"home":{"hidden":["item1","item2"]}}');
const config = require('../src/config')();
expect(config).toBeDefined();
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*']);
});
fs.writeFileSync(configFile, '{"home":{"hidden":{}}}');
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['home']['hidden']).toEqual([
'*.ejs',
'/.git*',
]);
});
+268 -266
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const fs = require('fs');
const path = require('path');
const utils = require('./test_utils');
@@ -9,336 +8,339 @@ 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',
},
};
const fw = require('../src/file_walker')(config);
beforeEach(() => {
config['data_dir'] = dataDir;
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
config['data_dir'] = dataDir;
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
});
afterAll(() => {
if (fs.existsSync(dataDir)) {
utils.deleteFolderSync(dataDir);
}
if (fs.existsSync(dataDir)) {
utils.deleteFolderSync(dataDir);
}
});
describe('Test function fileTree', () => {
test('empty root', (done) => {
fw.fileTree(dataDir, (err, list) => {
expect(err).toBeNull();
expect(list).toBeDefined();
expect(list.length).toBe(0);
done();
test('empty root', (done) => {
fw.fileTree(dataDir, (err, list) => {
expect(err).toBeNull();
expect(list).toBeDefined();
expect(list.length).toBe(0);
done();
});
});
});
test('empty folders', (done) => {
utils.createEmptyDirs([
path.join(dataDir, 'test', 'test'),
path.join(dataDir, 'test', 'test2'),
path.join(dataDir, 'test2')
]);
fw.fileTree(dataDir, (err, list) => {
expect(err).toBeNull();
expect(list).toBeDefined();
expect(list.length).toBe(0);
done();
test('empty folders', (done) => {
utils.createEmptyDirs([
path.join(dataDir, 'test', 'test'),
path.join(dataDir, 'test', 'test2'),
path.join(dataDir, 'test2'),
]);
fw.fileTree(dataDir, (err, list) => {
expect(err).toBeNull();
expect(list).toBeDefined();
expect(list.length).toBe(0);
done();
});
});
});
test('simple files', (done) => {
const fileList = [
path.join(dataDir, 'f1.txt'),
path.join(dataDir, 'f2.txt')
];
utils.createEmptyFiles(fileList);
fw.fileTree(dataDir, (err, list) => {
expect(err).toBeNull();
expect(list).toBeDefined();
expect(list.length).toBe(fileList.length);
expect(list).toEqual(expect.arrayContaining(fileList));
done();
test('simple files', (done) => {
const fileList = [
path.join(dataDir, 'f1.txt'),
path.join(dataDir, 'f2.txt'),
];
utils.createEmptyFiles(fileList);
fw.fileTree(dataDir, (err, list) => {
expect(err).toBeNull();
expect(list).toBeDefined();
expect(list.length).toBe(fileList.length);
expect(list).toEqual(expect.arrayContaining(fileList));
done();
});
});
});
test('nested files', (done) => {
utils.createEmptyDirs([
path.join(dataDir, 'test', 'test'),
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')
];
utils.createEmptyFiles(fileList);
fw.fileTree(dataDir, (err, list) => {
expect(err).toBeNull();
expect(list).toBeDefined();
expect(list.length).toBe(fileList.length);
expect(list).toEqual(expect.arrayContaining(fileList));
done();
test('nested files', (done) => {
utils.createEmptyDirs([
path.join(dataDir, 'test', 'test'),
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'),
];
utils.createEmptyFiles(fileList);
fw.fileTree(dataDir, (err, list) => {
expect(err).toBeNull();
expect(list).toBeDefined();
expect(list.length).toBe(fileList.length);
expect(list).toEqual(expect.arrayContaining(fileList));
done();
});
});
});
test('invalid root', (done) => {
fw.fileTree('invalid root', (err, list) => {
expect(err).not.toBeNull();
expect(list).not.toBeDefined();
done();
test('invalid root', (done) => {
fw.fileTree('invalid root', (err, list) => {
expect(err).not.toBeNull();
expect(list).not.toBeDefined();
done();
});
});
});
});
describe('Test index article reading', () => {
const file = path.join(dataDir, testIndex);
const file = path.join(dataDir, testIndex);
test('invalid file', (done) => {
fw.readIndexFile('invalid file', 'thumbnail', (err, info) => {
expect(err).not.toBeNull();
expect(info).not.toBeDefined();
done();
});
test('invalid file', (done) => {
fw.readIndexFile('invalid file', 'thumbnail', (err, info) => {
expect(err).not.toBeNull();
expect(info).not.toBeDefined();
done();
});
});
test('correct file', (done) => {
fs.writeFileSync(file, `
test('correct file', (done) => {
fs.writeFileSync(file, `
# This is an awesome title !?¤
![custom_thumbnail](./thumbnail.jpg)
this is some text
`);
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'This is an awesome title !?¤',
thumbnail: './thumbnail.jpg'
});
done();
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'This is an awesome title !?¤',
thumbnail: './thumbnail.jpg',
});
done();
});
});
});
test('no title', (done) => {
fs.writeFileSync(file, `
test('no title', (done) => {
fs.writeFileSync(file, `
## This is an awesome title !?¤
![custom_thumbnail](./thumbnail.jpg)
### this is some text
`);
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: undefined,
thumbnail: './thumbnail.jpg'
});
done();
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: undefined,
thumbnail: './thumbnail.jpg',
});
done();
});
});
});
test('title at beginning', (done) => {
fs.writeFileSync(file, '#title');
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'title',
thumbnail: undefined
});
done();
test('title at beginning', (done) => {
fs.writeFileSync(file, '#title');
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'title',
thumbnail: undefined,
});
done();
});
});
});
test('no thumbnail', (done) => {
fs.writeFileSync(file, `
test('no thumbnail', (done) => {
fs.writeFileSync(file, `
# This is an awesome title !?¤
![custom_thumbnail](./thumbnail.jpg)
this is some text
`);
fw.readIndexFile(file, 'thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'This is an awesome title !?¤',
thumbnail: undefined
});
done();
fw.readIndexFile(file, 'thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'This is an awesome title !?¤',
thumbnail: undefined,
});
done();
});
});
});
test('multiple thumbnails', (done) => {
fs.writeFileSync(file, `
test('multiple thumbnails', (done) => {
fs.writeFileSync(file, `
# This is an awesome title !?¤
![custom_thumbnail](./thumbnail.jpg)
this is some text
![custom_thumbnail](./thumbnail2.jpg)
`);
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'This is an awesome title !?¤',
thumbnail: './thumbnail.jpg'
});
done();
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'This is an awesome title !?¤',
thumbnail: './thumbnail.jpg',
});
done();
});
});
});
});
describe('Test article fetching', () => {
test('invalid data dir', (done) => {
config['data_dir'] = 'invalid root';
fw.fetchArticles((err, list) => {
expect(err).not.toBeNull();
expect(list).not.toBeDefined();
done();
test('invalid data dir', (done) => {
config['data_dir'] = 'invalid root';
fw.fetchArticles((err, list) => {
expect(err).not.toBeNull();
expect(list).not.toBeDefined();
done();
});
});
});
test('empty data dir', (done) => {
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(0);
done();
test('empty data dir', (done) => {
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(0);
done();
});
});
});
test('misplaced index file', (done) => {
utils.createEmptyDirs([
path.join(dataDir, 'test', 'test'),
path.join(dataDir, '2019', '05', '05')
]);
utils.createEmptyFiles([
path.join(dataDir, testIndex),
path.join(dataDir, 'test', 'test', testIndex),
path.join(dataDir, '2019', '05', testIndex)
]);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(0);
done();
test('misplaced index file', (done) => {
utils.createEmptyDirs([
path.join(dataDir, 'test', 'test'),
path.join(dataDir, '2019', '05', '05'),
]);
utils.createEmptyFiles([
path.join(dataDir, testIndex),
path.join(dataDir, 'test', 'test', testIndex),
path.join(dataDir, '2019', '05', testIndex),
]);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(0);
done();
});
});
});
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]);
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
draft: false,
day: 5,
date: date,
title: 'Untitled',
thumbnail: 'default.png',
escapedTitle: 'untitled',
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
});
done();
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 ]);
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
draft: false,
day: 5,
date: date,
title: 'Untitled',
thumbnail: 'default.png',
escapedTitle: 'untitled',
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
});
done();
});
});
});
test('correct index file', (done) => {
const dir = path.join(dataDir, '2019', '05', '05');
const file = path.join(dir, testIndex);
utils.createEmptyDirs([dir]);
fs.writeFileSync(file, `
test('correct index file', (done) => {
const dir = path.join(dataDir, '2019', '05', '05');
const file = path.join(dir, testIndex);
utils.createEmptyDirs([ dir ]);
fs.writeFileSync(file, `
# Title with : info !
![thumbnail](./thumbnail.jpg)
this is some text
`);
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
day: 5,
draft: false,
date: date,
title: 'Title with : info !',
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
escapedTitle: 'title_with___info',
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
});
done();
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
day: 5,
draft: false,
date: date,
title: 'Title with : info !',
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
escapedTitle: 'title_with___info',
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
});
done();
});
});
});
test('correct draft file', (done) => {
const dir = path.join(dataDir, '2019', '05', '05');
const file = path.join(dir, 'draft.md');
utils.createEmptyDirs([dir]);
fs.writeFileSync(file, `
test('correct draft file', (done) => {
const dir = path.join(dataDir, '2019', '05', '05');
const file = path.join(dir, 'draft.md');
utils.createEmptyDirs([ dir ]);
fs.writeFileSync(file, `
# Title with : info !
![thumbnail](./thumbnail.jpg)
this is some text
`);
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
day: 5,
draft: true,
date: date,
title: 'Title with : info !',
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
escapedTitle: 'title_with___info',
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
});
done();
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
day: 5,
draft: true,
date: date,
title: 'Title with : info !',
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
escapedTitle: 'title_with___info',
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
});
done();
});
});
});
test('index file override draft', (done) => {
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]);
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
draft: false,
day: 5,
date: date,
title: 'Untitled',
thumbnail: 'default.png',
escapedTitle: 'untitled',
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
});
done();
test('index file override draft', (done) => {
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,
]);
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
draft: false,
day: 5,
date: date,
title: 'Untitled',
thumbnail: 'default.png',
escapedTitle: 'untitled',
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
});
done();
});
});
});
});
+257
View File
@@ -0,0 +1,257 @@
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']);
});
beforeEach(() => {
mockClient.hgetall = (_, cb) => {
cb();
};
mockClient.multi = () => mockClient;
mockClient.hincrby = () => mockClient;
mockClient.exec = (cb) => {
cb();
};
config['hit_counter']['unique_visitor_timeout'] = -1;
});
describe('read()', () => {
test('normal', (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.total_visitors).toBe(34);
expect(data.current_visitors).toBe(0);
done();
});
});
test('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.total_visitors).toBe(0);
expect(data.current_visitors).toBe(0);
done();
});
});
test('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.total_visitors).toBe(0);
expect(data.current_visitors).toBe(0);
done();
});
});
test('1 visitor', (done) => {
config['hit_counter']['unique_visitor_timeout'] = 1000;
hc.count({
headers: {},
connection: { remoteAddress: 'test1' },
}, '/test/path/5', false, () => {
hc.read('/test/path/5', (data) => {
expect(data).toBeDefined();
expect(data.current_visitors).toBe(1);
done();
});
});
});
test('cleaned old visitor', (done) => {
hc.count({
headers: {},
connection: { remoteAddress: 'test1' },
}, '/test/path/5', false, () => {
hc.read('/test/path/5', (data) => {
expect(data).toBeDefined();
expect(data.current_visitors).toBe(0);
done();
});
});
});
});
describe('count()', () => {
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', false, () => {
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', false, () => {
hc.count({
headers: {},
connection: { remoteAddress: 'test2' },
}, '/test/path/2', false, () => {
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', false, () => {
hc.count({
headers: {},
connection: { remoteAddress: 'test3' },
}, '/test/path/3', false, () => {
expect(hincrbyCalls).toEqual([
[
'/test/path/3',
'h',
1,
],
[
'/test/path/3',
'v',
1,
],
[
'/test/path/3',
'h',
1,
],
[
'/test/path/3',
'v',
0,
],
]);
done();
});
});
});
});
+202 -205
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const fs = require('fs');
const path = require('path');
const utils = require('./test_utils');
@@ -7,283 +6,281 @@ const dataDir = 'test_data';
const file = path.join(dataDir, 'test.md');
const config = {
'test': true,
'modules': {
'prism': true,
'mathjax': true,
'plantuml': true,
'fa-diagrams': true,
},
'showdown': {
'simplifiedAutoLink': true,
'smartIndentationFix': true
},
'mathjax': {
'output_format': 'html',
'speak_text': false
},
'plantuml': {
'output_format': 'svg'
}
test: true,
modules: {
'prism': true,
'mathjax': true,
'plantuml': true,
'fa-diagrams': true,
},
showdown: {
simplifiedAutoLink: true,
smartIndentationFix: true,
},
mathjax: {
output_format: 'html',
speak_text: false,
},
plantuml: {
output_format: 'svg',
},
};
const renderer = require('../src/renderer')(config);
beforeEach(() => {
config['modules']['prism'] = true;
config['modules']['mathjax'] = true;
config['modules']['plantuml'] = true;
config['modules']['fa-diagrams'] = true;
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
config['modules']['prism'] = true;
config['modules']['mathjax'] = true;
config['modules']['plantuml'] = true;
config['modules']['fa-diagrams'] = true;
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
});
afterAll(() => {
if (fs.existsSync(dataDir)) {
utils.deleteFolderSync(dataDir);
}
if (fs.existsSync(dataDir)) {
utils.deleteFolderSync(dataDir);
}
});
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'
]);
});
test('lot of stuff', () => {
const data = 'Hello\nthere\n```code```\ngeneral<script>script</script>\n<script>script2</script>\n```<script>script3</script>```kenobi';
const parts = renderer.getParts(data);
expect(parts).toEqual([
{
index: 0,
end: 12,
text: 'Hello\nthere\n'
},
{
index: 22,
end: 30,
text: '\ngeneral'
},
{
index: 53,
end: 54,
text: '\n'
},
{
index: 78,
end: 79,
text: '\n'
},
{
index: 109,
end: 115,
text: 'kenobi'
},
]);
});
test('normal', () => {
const data = 'Hello\nthere\ngeneral\nkenobi';
const parts = renderer.getParts(data);
expect(parts.map(p => p.text)).toEqual([ 'Hello\nthere\ngeneral\nkenobi' ]);
});
test('lot of stuff', () => {
const data = 'Hello\nthere\n```code```\ngeneral<script>script</script>\n<script>script2</script>\n```<script>script3</script>```kenobi';
const parts = renderer.getParts(data);
expect(parts).toEqual([
{
index: 0,
end: 12,
text: 'Hello\nthere\n',
},
{
index: 22,
end: 30,
text: '\ngeneral',
},
{
index: 53,
end: 54,
text: '\n',
},
{
index: 78,
end: 79,
text: '\n',
},
{
index: 109,
end: 115,
text: 'kenobi',
},
]);
});
});
describe('Test Showdown', () => {
test('normal', (done) => {
renderer.renderShowDown('# Hello', (html) => {
expect(html).toBe('<h1 id="hello">Hello</h1>');
done();
test('normal', (done) => {
renderer.renderShowDown('# Hello', (html) => {
expect(html).toBe('<h1 id="hello">Hello</h1>');
done();
});
});
});
test('custom rules', (done) => {
renderer.renderShowDown('www.google.com', (html) => {
expect(html).toBe('<p><a href="http://www.google.com">www.google.com</a></p>');
done();
test('custom rules', (done) => {
renderer.renderShowDown('www.google.com', (html) => {
expect(html).toBe('<p><a href="http://www.google.com">www.google.com</a></p>');
done();
});
});
});
test('code format', (done) => {
renderer.renderShowDown('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```', (html) => {
expect(html).toBe('<pre><code class="python language-python">print("hello")\n</code></pre>\n<pre><code class="python language-python">print("hello")\n</code></pre>');
done();
test('code format', (done) => {
renderer.renderShowDown('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```', (html) => {
expect(html).toBe('<pre><code class="python language-python">print("hello")\n</code></pre>\n<pre><code class="python language-python">print("hello")\n</code></pre>');
done();
});
});
});
});
describe('Test Prism', () => {
test('no prism', (done) => {
config['modules']['prism'] = false;
renderer.renderPrism('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```', (data) => {
expect(data).toBe('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```');
done();
test('no prism', (done) => {
config['modules']['prism'] = false;
renderer.renderPrism('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```', (data) => {
expect(data).toBe('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```');
done();
});
});
});
test('prism correct', (done) => {
renderer.renderPrism('```python\nprint("hello")\n```', (data) => {
expect(data).not.toBe('<pre><code class="python language-python">print("hello")\n</code></pre>');
expect(data.indexOf('<pre><code class="python language-python">')).toBe(0);
done();
test('prism correct', (done) => {
renderer.renderPrism('```python\nprint("hello")\n```', (data) => {
expect(data).not.toBe('<pre><code class="python language-python">print("hello")\n</code></pre>');
expect(data.indexOf('<pre><code class="python language-python">')).toBe(0);
done();
});
});
});
test('prism invalid lang', (done) => {
renderer.renderPrism('```pythdon\nprint("hello")\n```', (data) => {
expect(data).not.toBe('<pre><code class="pythdon language-pythdon">print("hello")\n</code></pre>');
expect(data.indexOf('<pre><code class="pythdon language-pythdon">')).toBe(0);
done();
test('prism invalid lang', (done) => {
renderer.renderPrism('```pythdon\nprint("hello")\n```', (data) => {
expect(data).not.toBe('<pre><code class="pythdon language-pythdon">print("hello")\n</code></pre>');
expect(data.indexOf('<pre><code class="pythdon language-pythdon">')).toBe(0);
done();
});
});
});
test('prism mutliple code blocks', (done) => {
renderer.renderPrism('```python\n\n```\n```python\n\n```', (data) => {
expect(data).toBe('<pre><code class="python language-python"></code></pre>\n<pre><code class="python language-python"></code></pre>');
done();
test('prism mutliple code blocks', (done) => {
renderer.renderPrism('```python\n\n```\n```python\n\n```', (data) => {
expect(data).toBe('<pre><code class="python language-python"></code></pre>\n<pre><code class="python language-python"></code></pre>');
done();
});
});
});
});
describe('Test PlantUML', () => {
test('no plantuml', (done) => {
config['modules']['plantuml'] = false;
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml', (data) => {
expect(data).toBe('@startuml\nBob -> Alice : hello\n@enduml');
done();
test('no plantuml', (done) => {
config['modules']['plantuml'] = false;
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml', (data) => {
expect(data).toBe('@startuml\nBob -> Alice : hello\n@enduml');
done();
});
});
});
test('plantuml correct', (done) => {
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml', (data) => {
expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">');
done();
test('plantuml correct', (done) => {
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml', (data) => {
expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">');
done();
});
});
});
test('plantuml ignored in code', (done) => {
renderer.renderPlantUML('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml', (data) => {
expect(data).toBe('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml');
done();
test('plantuml ignored in code', (done) => {
renderer.renderPlantUML('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml', (data) => {
expect(data).toBe('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml');
done();
});
});
});
test('plantuml multiple uml', (done) => {
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml\n@startuml\nBob -> Alice : hello\n@enduml', (data) => {
expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">\n<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">');
done();
test('plantuml multiple uml', (done) => {
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml\n@startuml\nBob -> Alice : hello\n@enduml', (data) => {
expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">\n<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">');
done();
});
});
});
});
describe('Test MathJax', () => {
test('no mathjax', (done) => {
config['modules']['mathjax'] = false;
renderer.renderMathJax('$$\nhello\n$$\ntest$test$', (data) => {
expect(data).toBe('$$\nhello\n$$\ntest$test$');
done();
test('no mathjax', (done) => {
config['modules']['mathjax'] = false;
renderer.renderMathJax('$$\nhello\n$$\ntest$test$', (data) => {
expect(data).toBe('$$\nhello\n$$\ntest$test$');
done();
});
});
});
test('full eq', (done) => {
renderer.renderMathJax('$$\n\nA\n\n$$', (data) => {
expect(data).toBe('<span class=\"mjx-chtml MJXc-display\" style=\"text-align: center;\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.519em; padding-bottom: 0.298em;\">' +
test('full eq', (done) => {
renderer.renderMathJax('$$\n\nA\n\n$$', (data) => {
expect(data).toBe('<span class="mjx-chtml MJXc-display" style="text-align: center;">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.519em; padding-bottom: 0.298em;">' +
'A' +
'</span></span></span></span></span>');
done();
done();
});
});
});
test('inline eq', (done) => {
renderer.renderMathJax('start $a$ end', (data) => {
expect(data).toBe('start ' +
'<span class=\"mjx-chtml\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.225em; padding-bottom: 0.298em;\">' +
test('inline eq', (done) => {
renderer.renderMathJax('start $a$ end', (data) => {
expect(data).toBe('start ' +
'<span class="mjx-chtml">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.225em; padding-bottom: 0.298em;">' +
'a' +
'</span></span></span></span></span>' +
' end');
done();
done();
});
});
});
test('fake inline eq', (done) => {
renderer.renderMathJax('i have $6\nyou have $5', (data) => {
expect(data).toBe('i have $6\nyou have $5');
done();
test('fake inline eq', (done) => {
renderer.renderMathJax('i have $6\nyou have $5', (data) => {
expect(data).toBe('i have $6\nyou have $5');
done();
});
});
});
test('no eq in code / script', (done) => {
renderer.renderMathJax('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```', (data) => {
expect(data).toBe('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```');
done();
test('no eq in code / script', (done) => {
renderer.renderMathJax('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```', (data) => {
expect(data).toBe('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```');
done();
});
});
});
test('multiple eq', (done) => {
renderer.renderMathJax('$$\n\nA\n\n$$\nstart $a$ end\n$$\n\nA\n\n$$', (data) => {
expect(data).toBe('' +
'<span class=\"mjx-chtml MJXc-display\" style=\"text-align: center;\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.519em; padding-bottom: 0.298em;\">' +
test('multiple eq', (done) => {
renderer.renderMathJax('$$\n\nA\n\n$$\nstart $a$ end\n$$\n\nA\n\n$$', (data) => {
expect(data).toBe('' +
'<span class="mjx-chtml MJXc-display" style="text-align: center;">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.519em; padding-bottom: 0.298em;">' +
'A' +
'</span></span></span></span></span>\n' +
'start ' +
'<span class=\"mjx-chtml\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.225em; padding-bottom: 0.298em;\">' +
'<span class="mjx-chtml">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.225em; padding-bottom: 0.298em;">' +
'a' +
'</span></span></span></span></span>' +
' end\n' +
'<span class=\"mjx-chtml MJXc-display\" style=\"text-align: center;\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.519em; padding-bottom: 0.298em;\">' +
'<span class="mjx-chtml MJXc-display" style="text-align: center;">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.519em; padding-bottom: 0.298em;">' +
'A' +
'</span></span></span></span></span>');
done();
done();
});
});
});
});
describe('Test fa-diagrams', () => {
test('no fa-diagrams', (done) => {
config['modules']['fa-diagrams'] = false;
renderer.renderFaDiagrams('@startfad\noptions.rendering.color=\'red\'\n@endfad', (data) => {
expect(data).toBe('@startfad\noptions.rendering.color=\'red\'\n@endfad');
done();
test('no fa-diagrams', (done) => {
config['modules']['fa-diagrams'] = false;
renderer.renderFaDiagrams('@startfad\noptions.rendering.color=\'red\'\n@endfad', (data) => {
expect(data).toBe('@startfad\noptions.rendering.color=\'red\'\n@endfad');
done();
});
});
});
test('no fa-diagrams in code', (done) => {
renderer.renderFaDiagrams('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```', (data) => {
expect(data).toBe('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```');
done();
test('no fa-diagrams in code', (done) => {
renderer.renderFaDiagrams('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```', (data) => {
expect(data).toBe('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```');
done();
});
});
});
test('valid fa-diagrams', (done) => {
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=\'red\'\n@endfad\nafter', (data) => {
expect(data).toBe('before\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0" width="0" height="0" font-family="Arial" font-size="15" fill="red" stroke-width="0"></svg>\nafter');
done();
test('valid fa-diagrams', (done) => {
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=\'red\'\n@endfad\nafter', (data) => {
expect(data).toBe('before\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0" width="0" height="0" font-family="Arial" font-size="15" fill="red" stroke-width="0"></svg>\nafter');
done();
});
});
});
test('invalid toml', (done) => {
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=red\n@endfad\nafter', (data) => {
expect(data).toBe('before\n<b style="color:red">TomlError: Unexpected character, expecting string, number, datetime, boolean, inline array or inline table at row 1, col 26, pos 25:\n' +
test('invalid toml', (done) => {
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=red\n@endfad\nafter', (data) => {
expect(data).toBe('before\n<b style="color:red">TomlError: Unexpected character, expecting string, number, datetime, boolean, inline array or inline table at row 1, col 26, pos 25:\n' +
'1> options.rendering.color=red\n' +
' ^\n' +
'\n</b>\nafter');
done();
done();
});
});
});
});
describe('Test render', () => {
test('invalid file', (done) => {
renderer.render('invalid file', (err, html) => {
expect(err).not.toBeNull();
expect(html).not.toBeDefined();
done();
test('invalid file', (done) => {
renderer.render('invalid file', (err, html) => {
expect(err).not.toBeNull();
expect(html).not.toBeDefined();
done();
});
});
});
test('normal file', (done) => {
fs.writeFileSync(file, `# Hello`);
renderer.render(file, (err, html) => {
expect(err).toBeNull();
expect(html).toBe('<h1 id="hello">Hello</h1>');
done();
test('normal file', (done) => {
fs.writeFileSync(file, '# Hello');
renderer.render(file, (err, html) => {
expect(err).toBeNull();
expect(html).toBe('<h1 id="hello">Hello</h1>');
done();
});
});
});
});
});
+21 -22
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const fs = require('fs');
const path = require('path');
const utils = require('./test_utils');
@@ -6,46 +5,46 @@ const utils = require('./test_utils');
const dataDir = 'test_data';
beforeEach(() => {
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
});
afterAll(() => {
if (fs.existsSync(dataDir)) {
utils.deleteFolderSync(dataDir);
}
if (fs.existsSync(dataDir)) {
utils.deleteFolderSync(dataDir);
}
});
test('load 1 script', () => {
const file = path.join(dataDir, 'test.js');
fs.writeFileSync(file, `
const file = path.join(dataDir, 'test.js');
fs.writeFileSync(file, `
var a = 5;
function b(){
return a;
}`);
require('../src/script_loader')(file);
expect(global['b']).toBeDefined();
expect(global['b']()).toBe(5);
require('../src/script_loader')(file);
expect(global['b']).toBeDefined();
expect(global['b']()).toBe(5);
});
test('load 2 script', () => {
const file1 = path.join(dataDir, 'test.js');
fs.writeFileSync(file1, `
const file1 = path.join(dataDir, 'test.js');
fs.writeFileSync(file1, `
var a = 5;
function b(){
return a;
}`);
const file2 = path.join(dataDir, 'test2.js');
fs.writeFileSync(file2, `
const file2 = path.join(dataDir, 'test2.js');
fs.writeFileSync(file2, `
var a = 9;
function b(){
return a;
}`);
require('../src/script_loader')(file1);
expect(global['b']).toBeDefined();
expect(global['b']()).toBe(5);
require('../src/script_loader')(file1);
expect(global['b']).toBeDefined();
expect(global['b']()).toBe(5);
require('../src/script_loader.js')(file2);
expect(global['b']).toBeDefined();
expect(global['b']()).toBe(9);
});
require('../src/script_loader.js')(file2);
expect(global['b']).toBeDefined();
expect(global['b']()).toBe(9);
});
+23 -21
View File
@@ -2,28 +2,30 @@ const fs = require('fs');
const path = require('path');
const deleteFolderSync = (dir) => {
if (!fs.existsSync(dir))
return;
let items;
const deleteItem = (item) => {
if (item.isDirectory())
deleteFolderSync(path.join(dir, item.name));
else
fs.unlinkSync(path.join(dir, item.name));
};
do {
items = fs.readdirSync(dir, {withFileTypes: true});
try {
items.forEach(deleteItem);
} catch (e) {
console.error(e);
if (!fs.existsSync(dir)) {
return;
}
} while (items.length > 0);
fs.rmdirSync(dir);
let items;
const deleteItem = (item) => {
if (item.isDirectory()) {
deleteFolderSync(path.join(dir, item.name));
} else {
fs.unlinkSync(path.join(dir, item.name));
}
};
do {
items = fs.readdirSync(dir, { withFileTypes: true });
try {
items.forEach(deleteItem);
} catch (e) {
console.error(e);
}
} while (items.length > 0);
fs.rmdirSync(dir);
};
module.exports = {
deleteFolderSync: deleteFolderSync,
createEmptyDirs: (list) => list.forEach((path) => fs.mkdirSync(path, {recursive: true})),
createEmptyFiles: (list) => list.forEach((file) => fs.writeFileSync(file, '')),
};
deleteFolderSync: deleteFolderSync,
createEmptyDirs: (list) => list.forEach((path) => fs.mkdirSync(path, { recursive: true })),
createEmptyFiles: (list) => list.forEach((file) => fs.writeFileSync(file, '')),
};