From 0a709b71fd5e2d74ebca964aa1e7f0f6e0fc47a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Bregu=C3=AAz?= Date: Sun, 11 Oct 2026 09:23:44 -0300 Subject: [PATCH] chore: remove the upstream Staticman code The Workers service in src/ replaces it (specs 001-003). Removed with maintainer approval: lib/, controllers/, test/ (Jest), index.js, server.js, config.js, siteConfig.js, config.sample.json, config.test.json, app.json, Dockerfile, docker-compose*.yml, .dockerignore, .travis.yml, staticman_key.pub, staticman.sample.yml and the empty .env (54 files). contribute.sample.yml documents the legacy entry route's config; lint no longer needs exclusions. The upstream code stays in git history and is credited in NOTICE.md; its logo is kept for docs/UPSTREAM-README.md. Part of #2 Co-Authored-By: Claude Opus 5.5 (1M context) --- .dockerignore | 2 - .env | 0 .travis.yml | 8 - Dockerfile | 15 - README.md | 6 +- app.json | 8 - config.js | 144 -- config.sample.json | 6 - config.test.json | 14 - contribute.sample.yml | 17 + controllers/auth.js | 65 - controllers/connect.js | 57 - controllers/encrypt.js | 14 - controllers/handlePR.js | 61 - controllers/home.js | 7 - controllers/process.js | 144 -- docker-compose.development.yml | 10 - docker-compose.yml | 15 - eslint.config.js | 3 +- index.js | 10 - lib/ErrorHandler.js | 109 -- lib/GitHub.js | 204 --- lib/GitLab.js | 120 -- lib/GitService.js | 104 -- lib/GitServiceFactory.js | 13 - lib/Logger.js | 34 - lib/Notification.js | 46 - lib/OAuth.js | 49 - lib/RSA.js | 24 - lib/Staticman.js | 621 --------- lib/SubscriptionsManager.js | 81 -- lib/Transforms.js | 15 - lib/TypeUtils.js | 11 - lib/models/Review.js | 28 - lib/models/User.js | 37 - server.js | 186 --- siteConfig.js | 216 --- staticman.sample.yml | 77 -- staticman_key.pub | 1 - test/acceptance/api.test.js | 275 ---- test/helpers/CatchAllApiMock.js | 21 - test/helpers/Config.js | 22 - test/helpers/index.js | 125 -- test/helpers/sampleData.js | 196 --- test/unit/controllers/auth.test.js | 314 ----- test/unit/controllers/connect.test.js | 108 -- test/unit/controllers/encrypt.test.js | 36 - test/unit/controllers/handlePR.test.js | 210 --- test/unit/controllers/process.test.js | 506 ------- test/unit/lib/GitHub.test.js | 495 ------- test/unit/lib/GitLab.test.js | 567 -------- test/unit/lib/Notification.test.js | 60 - test/unit/lib/OAuth.test.js | 51 - test/unit/lib/Staticman.test.js | 1666 ------------------------ test/unit/lib/Transforms.test.js | 21 - test/utils/coverage.js | 41 - test/utils/coverage.svg | 1 - 57 files changed, 21 insertions(+), 7276 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .env delete mode 100644 .travis.yml delete mode 100644 Dockerfile delete mode 100644 app.json delete mode 100644 config.js delete mode 100644 config.sample.json delete mode 100644 config.test.json create mode 100644 contribute.sample.yml delete mode 100644 controllers/auth.js delete mode 100644 controllers/connect.js delete mode 100644 controllers/encrypt.js delete mode 100644 controllers/handlePR.js delete mode 100644 controllers/home.js delete mode 100644 controllers/process.js delete mode 100644 docker-compose.development.yml delete mode 100644 docker-compose.yml delete mode 100644 index.js delete mode 100644 lib/ErrorHandler.js delete mode 100644 lib/GitHub.js delete mode 100644 lib/GitLab.js delete mode 100644 lib/GitService.js delete mode 100644 lib/GitServiceFactory.js delete mode 100644 lib/Logger.js delete mode 100644 lib/Notification.js delete mode 100644 lib/OAuth.js delete mode 100644 lib/RSA.js delete mode 100644 lib/Staticman.js delete mode 100644 lib/SubscriptionsManager.js delete mode 100644 lib/Transforms.js delete mode 100644 lib/TypeUtils.js delete mode 100644 lib/models/Review.js delete mode 100644 lib/models/User.js delete mode 100644 server.js delete mode 100644 siteConfig.js delete mode 100644 staticman.sample.yml delete mode 100644 staticman_key.pub delete mode 100644 test/acceptance/api.test.js delete mode 100644 test/helpers/CatchAllApiMock.js delete mode 100644 test/helpers/Config.js delete mode 100644 test/helpers/index.js delete mode 100644 test/helpers/sampleData.js delete mode 100644 test/unit/controllers/auth.test.js delete mode 100644 test/unit/controllers/connect.test.js delete mode 100644 test/unit/controllers/encrypt.test.js delete mode 100644 test/unit/controllers/handlePR.test.js delete mode 100644 test/unit/controllers/process.test.js delete mode 100644 test/unit/lib/GitHub.test.js delete mode 100644 test/unit/lib/GitLab.test.js delete mode 100644 test/unit/lib/Notification.test.js delete mode 100644 test/unit/lib/OAuth.test.js delete mode 100644 test/unit/lib/Staticman.test.js delete mode 100644 test/unit/lib/Transforms.test.js delete mode 100644 test/utils/coverage.js delete mode 100644 test/utils/coverage.svg diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index b83c451..0000000 --- a/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -docker.env \ No newline at end of file diff --git a/.env b/.env deleted file mode 100644 index e69de29..0000000 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 70df652..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: node_js -cache: - directories: - - node_modules -notifications: - email: false -node_js: -- '8.11.3' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 29e5bdb..0000000 --- a/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:8.11.3 - -# Create app directory -RUN mkdir -p /app -WORKDIR /app - -# Install app dependencies -COPY package.json /app/ -RUN npm install - -# Bundle app source -COPY . /app - -EXPOSE 3000 -CMD [ "npm", "start" ] \ No newline at end of file diff --git a/README.md b/README.md index b226147..094d42f 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ --- -> **Status:** the Workers service is being built spec by spec in `src/` — see the [roadmap](docs/specs/ROADMAP.md). -> The upstream Staticman code (`lib/`, `controllers/`, …) stays in the tree for reference until the port reaches parity; -> it is not part of the Worker. +> **Status:** the Workers service in `src/` implements specs 001–003 — see the [roadmap](docs/specs/ROADMAP.md). The +> upstream Staticman code was removed after the port; it remains in the git history and is credited in +> [NOTICE.md](NOTICE.md). ## How it works diff --git a/app.json b/app.json deleted file mode 100644 index 9848e28..0000000 --- a/app.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Staticman", - "description": "A Node.js application that enables dynamic content as part of a fully static website.", - "repository": "https://github.com/eduardoboucas/staticman/", - "logo": "https://raw.githubusercontent.com/InternetPublicLibrary/contribute/main/.github/assets/logo.png", - "keywords": ["node", "express", "static", "staticman", "github-pages"] - } - \ No newline at end of file diff --git a/config.js b/config.js deleted file mode 100644 index 13e0f9b..0000000 --- a/config.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict' - -const convict = require('convict') -const path = require('path') - -const schema = { - akismet: { - site: { - doc: 'URL of an Akismet account used for spam checking.', - docExample: 'http://yourdomain.com', - format: String, - default: null, - env: 'AKISMET_SITE' - }, - apiKey: { - doc: 'API key to be used with Akismet.', - format: String, - default: null, - env: 'AKISMET_API_KEY' - } - }, - analytics: { - uaTrackingId: { - doc: 'Universal Analytics account ID.', - docExample: 'uaTrackingId: "UA-XXXX-XX"', - format: String, - default: null, - env: 'UA_TRACKING_ID' - } - }, - email: { - apiKey: { - doc: 'Mailgun API key to be used for email notifications. Will be overridden by a `notifications.apiKey` parameter in the site config, if one is set.', - format: String, - default: null, - env: 'EMAIL_API_KEY' - }, - domain: { - doc: 'Domain to be used with Mailgun for email notifications. Will be overridden by a `notifications.domain` parameter in the site config, if one is set.', - format: String, - default: 'ipl.better-web.org', - env: 'EMAIL_DOMAIN' - }, - fromAddress: { - doc: 'Email address to send notifications from. Will be overridden by a `notifications.fromAddress` parameter in the site config, if one is set.', - format: String, - default: 'noreply@ipl.better-web.org', - env: 'EMAIL_FROM' - } - }, - env: { - doc: 'The applicaton environment.', - format: ['production', 'development', 'test'], - default: 'development', - env: 'NODE_ENV' - }, - githubAccessTokenUri: { - doc: 'URI for the GitHub authentication provider.', - format: String, - default: 'https://github.com/login/oauth/access_token', - env: 'GITHUB_ACCESS_TOKEN_URI' - }, - githubAppID: { - doc: 'ID of the GitHub App.', - format: String, - default: null, - env: 'GITHUB_APP_ID' - }, - githubBaseUrl: { - doc: 'Base URL for the GitHub API.', - format: String, - default: 'https://api.github.com', - env: 'GITHUB_BASE_URL' - }, - githubPrivateKey: { - doc: 'Private key for the GitHub App.', - format: String, - default: null, - env: 'GITHUB_PRIVATE_KEY' - }, - githubToken: { - doc: 'Access token to the GitHub account (legacy)', - format: String, - default: null, - env: 'GITHUB_TOKEN' - }, - gitlabAccessTokenUri: { - doc: 'URI for the GitLab authentication provider.', - format: String, - default: 'https://gitlab.com/oauth/token', - env: 'GITLAB_ACCESS_TOKEN_URI' - }, - gitlabBaseUrl: { - doc: 'Base URL for the GitLab API.', - format: String, - default: 'https://gitlab.com', - env: 'GITLAB_BASE_URL' - }, - gitlabToken: { - doc: 'Access token to the GitLab account being used to push files with.', - format: String, - default: null, - env: 'GITLAB_TOKEN' - }, - port: { - doc: 'The port to bind the application to.', - format: 'port', - default: 0, - env: 'PORT' - }, - rsaPrivateKey: { - doc: 'RSA private key to encrypt sensitive configuration parameters with.', - docExample: 'rsaPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\\nkey\\n-----END RSA PRIVATE KEY-----"', - format: String, - default: null, - env: 'RSA_PRIVATE_KEY' - }, - logging: { - slackWebhook: { - doc: 'Slack webhook URL to pipe log output to', - format: String, - default: null, - env: 'SLACK_WEBHOOK' - } - } -} - -let config - -try { - config = convict(schema) - - const fileName = 'config.' + config.get('env') + '.json' - - config.loadFile(path.join(__dirname, fileName)) - config.validate() - - console.log('(*) Local config file loaded') -} catch (e) { - -} - -module.exports = config -module.exports.schema = schema diff --git a/config.sample.json b/config.sample.json deleted file mode 100644 index 3558162..0000000 --- a/config.sample.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "gitlabToken": "YOUR_GITLAB_TOKEN", - "githubToken": "YOUR_GITHUB_TOKEN", - "rsaPrivateKey": "-----BEGIN RSA PRIVATE KEY-----YOUR_KEY-----END RSA PRIVATE KEY-----", - "port": 80 -} \ No newline at end of file diff --git a/config.test.json b/config.test.json deleted file mode 100644 index 0a842eb..0000000 --- a/config.test.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "gitlabToken": "r4e3w2q1", - "githubToken": "1q2w3e4r", - "rsaPrivateKey": "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA2nX81/5w6nZLolEh0uKks3//FlqizKxFWi8GaEJYbud8FYwLH6l+LDfZkjQiDXvm6mxngDGCjlG9b7hgfa/sfWSuqtJUQ2D1Nenn11gwAUaI7OQNSassE+nVFL2BGedl6DOgpZFkKrDTQT7jyvvc3r/2IqWahAYyFWXImD50qwNWIGyZ2Sry2/WXeCydiUWV8ZG3GOlGZLgtlK8igdmLje/6Ja+1oRBFwdbWrf/nKsMB0fCL3R5MkWTnumR8sGx2Xud8Q3mF7cOYWQiy2AqrFQOzJ+QwH6hO6CGVk8eBh8xRkrjaZEsvolZ33N+0aS9NBxHuOyGq0Te9HMLFqwVGzwIDAQABAoIBAQDPD4gAnb0erdMQXT/m50TekdIQuQWXYy00xl+XUFMLg0L8FUmxz++0L5d72Qfxqd97kBYlzkeFZ3pbOvHSD33ieByJ8mNFnc+tMy+4z3BotRcxGRJzIdfcZAS/7MJB8C6KAO0iIQVE5WbGb7pu+XwmcOH1gutKeajc2SVhD8l7ECRARPuV3JxlhbVt+ylM05h7+5HwwnEmodNQHPeGvLEWp7eODNdcLLE5EFVjmN0dwGu9D+xCFna9bL7p921GOTGf349l3021ONaGvWd77Frhl9tHL1k40hqq4LiXDk3RkiP2oDb65+ohW128Q1QPgjMQuXbbFTCa3++pbz7OMbUhAoGBAPxt6A5B3s5GnnaCTE0YJ13aTeUyqrLimBxWEp7L/WXug8SDBW5tVAmBNHlZC4GuR089E7g26BVr0i3k/dOknKa0GLyGGJNphxrrcD05p+XB6Gd6/N1Yr6amM+8XqU1+UKLL2CBrLOSi6r4J4fPS8sb9362+FROwCJZfxd5Wznd/AoGBAN2NEz+anDBgulxLoguGuvc0FaqSsVIP/cdb5Ve5t29/uheVjLgDXSkV5P/RFfk1ygH1gNjmUrsoxfsGWQa3KMTk292maYRj7GPpOalSlp8BYyiQ9omPdOgusVzY4CEjMiDkKal0YrBswXKV8E2lTokEEkbdrQpToVdBmRCzT1ixAoGBAMNMBQ2CyO6ulEr75CyBU3O5QirhWE+uICFMNnvFNvd14VxYQgt9alcwL3jy+4QJYgJcLrWHRWfNU39Oe1MTOF+BVuIEnV7vdifNn7i+srd/nl7xOFHIG53DWMrSc5oQ8DIDo+LxCHqb0SHWY4pQ2qQ6JqQ1O/lPaFVvI5cxcevhAoGALyNtDQNgaAqsnCabe8hWz3INihVcFRHB9UDgMyIYnWiXt5ziK+TLVYqLBsL94eBH8tLBb2TSBXBPb3GST5N44SuwfCCEIt7/1OIymowv7/TjnCX7zpjvTtdgdVjPlz1d1RG2q49P0CnTnwW3801QwGZVXS6dOq1AjsguQRdlsoECgYBrFqKa3S48UbGOKkocgMb/1HzVs2sxNWMu+/jEiBvI4RrCiun9MALtrVtohgHRml4yZJaOojVk9F0z/MAak9eTgSmHpwxYZMqR1PvM4c/ekfun4hQoK04Thoexi/Z0RfsI6nBdBk8MAra03ldDvnYZY4Fat21YuNBspbrbTW8UoA==-----END RSA PRIVATE KEY-----", - "githubPrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2nX81/5w6nZLolEh0uKks3//FlqizKxFWi8GaEJYbud8FYwLH6l+LDfZkjQiDXvm6mxngDGCjlG9b7hgfa/sfWSuqtJUQ2D1Nenn11gwAUaI7OQNSassE+nVFL2BGedl6DOgpZFkKrDTQT7jyvvc3r/2IqWahAYyFWXImD50qwNWIGyZ2Sry2/WXeCydiUWV8ZG3GOlGZLgtlK8igdmLje/6Ja+1oRBFwdbWrf/nKsMB0fCL3R5MkWTnumR8sGx2Xud8Q3mF7cOYWQiy2AqrFQOzJ+QwH6hO6CGVk8eBh8xRkrjaZEsvolZ33N+0aS9NBxHuOyGq0Te9HMLFqwVGzwIDAQABAoIBAQDPD4gAnb0erdMQXT/m50TekdIQuQWXYy00xl+XUFMLg0L8FUmxz++0L5d72Qfxqd97kBYlzkeFZ3pbOvHSD33ieByJ8mNFnc+tMy+4z3BotRcxGRJzIdfcZAS/7MJB8C6KAO0iIQVE5WbGb7pu+XwmcOH1gutKeajc2SVhD8l7ECRARPuV3JxlhbVt+ylM05h7+5HwwnEmodNQHPeGvLEWp7eODNdcLLE5EFVjmN0dwGu9D+xCFna9bL7p921GOTGf349l3021ONaGvWd77Frhl9tHL1k40hqq4LiXDk3RkiP2oDb65+ohW128Q1QPgjMQuXbbFTCa3++pbz7OMbUhAoGBAPxt6A5B3s5GnnaCTE0YJ13aTeUyqrLimBxWEp7L/WXug8SDBW5tVAmBNHlZC4GuR089E7g26BVr0i3k/dOknKa0GLyGGJNphxrrcD05p+XB6Gd6/N1Yr6amM+8XqU1+UKLL2CBrLOSi6r4J4fPS8sb9362+FROwCJZfxd5Wznd/AoGBAN2NEz+anDBgulxLoguGuvc0FaqSsVIP/cdb5Ve5t29/uheVjLgDXSkV5P/RFfk1ygH1gNjmUrsoxfsGWQa3KMTk292maYRj7GPpOalSlp8BYyiQ9omPdOgusVzY4CEjMiDkKal0YrBswXKV8E2lTokEEkbdrQpToVdBmRCzT1ixAoGBAMNMBQ2CyO6ulEr75CyBU3O5QirhWE+uICFMNnvFNvd14VxYQgt9alcwL3jy+4QJYgJcLrWHRWfNU39Oe1MTOF+BVuIEnV7vdifNn7i+srd/nl7xOFHIG53DWMrSc5oQ8DIDo+LxCHqb0SHWY4pQ2qQ6JqQ1O/lPaFVvI5cxcevhAoGALyNtDQNgaAqsnCabe8hWz3INihVcFRHB9UDgMyIYnWiXt5ziK+TLVYqLBsL94eBH8tLBb2TSBXBPb3GST5N44SuwfCCEIt7/1OIymowv7/TjnCX7zpjvTtdgdVjPlz1d1RG2q49P0CnTnwW3801QwGZVXS6dOq1AjsguQRdlsoECgYBrFqKa3S48UbGOKkocgMb/1HzVs2sxNWMu+/jEiBvI4RrCiun9MALtrVtohgHRml4yZJaOojVk9F0z/MAak9eTgSmHpwxYZMqR1PvM4c/ekfun4hQoK04Thoexi/Z0RfsI6nBdBk8MAra03ldDvnYZY4Fat21YuNBspbrbTW8UoA==\n-----END RSA PRIVATE KEY-----", - "email": { - "apiKey": "key-1q2w3e4r" - }, - "port": 8094, - "akismet": { - "site": "https://test.ipl.better-web.org", - "apiKey": "1q2w3e4r5t6y7u8i9o0p" - } -} diff --git a/contribute.sample.yml b/contribute.sample.yml new file mode 100644 index 0000000..c734c6b --- /dev/null +++ b/contribute.sample.yml @@ -0,0 +1,17 @@ +# contribute.yml — rules for the legacy entry route (POST /v3/entry/github/…), read from the default branch of the +# target repository. The signed-in /v1 API (edits, suggestions) needs no configuration file. +comments: + allowedFields: [name, message] + requiredFields: [name, message] + format: yaml # yaml | json | frontmatter + path: "_data/comments/{options.slug}" + filename: "entry{@timestamp}" + commitMessage: "Add comment by {fields.name}" + pullRequestBody: "New comment for review." + generatedFields: + date: + type: date + options: + format: iso8601 # iso8601 | timestamp | timestamp-seconds + transforms: + name: [downcase] # upcase | downcase | frontmatterContent diff --git a/controllers/auth.js b/controllers/auth.js deleted file mode 100644 index 60b23f4..0000000 --- a/controllers/auth.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict' - -const gitFactory = require('../lib/GitServiceFactory') -const oauth = require('../lib/OAuth') -const RSA = require('../lib/RSA') -const Staticman = require('../lib/Staticman') - -module.exports = async (req, res) => { - const staticman = await new Staticman(req.params) - staticman.setConfigPath() - - let requestAccessToken - - switch (req.params.service) { - case 'gitlab': - requestAccessToken = siteConfig => - oauth.requestGitLabAccessToken( - req.query.code, - siteConfig.get('gitlabAuth.clientId'), - siteConfig.get('gitlabAuth.clientSecret'), - siteConfig.get('gitlabAuth.redirectUri') - ) - break - default: - requestAccessToken = siteConfig => - oauth.requestGitHubAccessToken( - req.query.code, - siteConfig.get('githubAuth.clientId'), - siteConfig.get('githubAuth.clientSecret'), - siteConfig.get('githubAuth.redirectUri') - ) - } - - return staticman.getSiteConfig() - .then(requestAccessToken) - .then(async (accessToken) => { - const git = await gitFactory.create(req.params.service, { - oauthToken: accessToken, - version: req.params.version - }) - - // TODO: Simplify this when v2 support is dropped. - const getUser = req.params.version === '2' && req.params.service === 'github' - ? git.api.users.getAuthenticated({}).then(({data}) => data) - : git.getCurrentUser() - - return getUser - .then((user) => { - res.send({ - accessToken: RSA.encrypt(accessToken), - user - }) - }) - }) - .catch((err) => { - console.log('ERR:', err) - - const statusCode = err.statusCode || 401 - - res.status(statusCode).send({ - statusCode, - message: err.message - }) - }) -} diff --git a/controllers/connect.js b/controllers/connect.js deleted file mode 100644 index a6b6a5d..0000000 --- a/controllers/connect.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict' - -const path = require('path') -const config = require(path.join(__dirname, '/../config')) -const GitHub = require(path.join(__dirname, '/../lib/GitHub')) - -module.exports = async (req, res) => { - const ua = config.get('analytics.uaTrackingId') - ? require('universal-analytics')(config.get('analytics.uaTrackingId')) - : null - - const github = await new GitHub({ - username: req.params.username, - repository: req.params.repository, - branch: req.params.branch, - token: config.get('githubToken'), - version: req.params.version - }) - - const isAppAuth = config.get('githubAppID') && config.get('githubPrivateKey') - - if (isAppAuth) { - return res.send('OK!') - } - - return github.api.repos.listInvitationsForAuthenticatedUser({}).then(({ data }) => { - let invitationId = null - - const invitation = Array.isArray(data) && data.some(invitation => { - if (invitation.repository.full_name === (req.params.username + '/' + req.params.repository)) { - invitationId = invitation.id - - return true - } - }) - - if (!invitation) { - return res.status(404).send('Invitation not found') - } - - return github.api.repos.acceptInvitation({ - invitation_id: invitationId - }).then(response => { - res.send('OK!') - - if (ua) { - ua.event('Repositories', 'Connect').send() - } - }).catch(err => { // eslint-disable-line handle-callback-err - res.status(500).send('Error') - - if (ua) { - ua.event('Repositories', 'Connect error').send() - } - }) - }) -} diff --git a/controllers/encrypt.js b/controllers/encrypt.js deleted file mode 100644 index ab8d26b..0000000 --- a/controllers/encrypt.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict' - -const path = require('path') -const RSA = require(path.join(__dirname, '/../lib/RSA')) - -module.exports = (req, res) => { - const encryptedText = RSA.encrypt(req.params.text) - - if (!encryptedText) { - res.status(500).send('Could not encrypt text') - } - - res.send(encryptedText) -} diff --git a/controllers/handlePR.js b/controllers/handlePR.js deleted file mode 100644 index 87aeacd..0000000 --- a/controllers/handlePR.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict' - -const config = require('../config') -const GitHub = require('../lib/GitHub') -const Staticman = require('../lib/Staticman') - -module.exports = async (repo, data) => { - const ua = config.get('analytics.uaTrackingId') - ? require('universal-analytics')(config.get('analytics.uaTrackingId')) - : null - - if (!data.number) { - return - } - - const github = await new GitHub({ - username: data.repository.owner.login, - repository: data.repository.name, - version: '1' - }) - - try { - let review = await github.getReview(data.number) - if (review.sourceBranch.indexOf('staticman_')) { - return null - } - - if (review.state !== 'merged' && review.state !== 'closed') { - return null - } - - if (review.state === 'merged') { - const bodyMatch = review.body.match(/(?:.*?)(?:.*?)/i) - - if (bodyMatch && (bodyMatch.length === 2)) { - try { - const parsedBody = JSON.parse(bodyMatch[1]) - const staticman = await new Staticman(parsedBody.parameters) - - staticman.setConfigPath(parsedBody.configPath) - staticman.processMerge(parsedBody.fields, parsedBody.options) - } catch (err) { - return Promise.reject(err) - } - } - } - - if (ua) { - ua.event('Hooks', 'Delete branch').send() - } - return github.deleteBranch(review.sourceBranch) - } catch (e) { - console.log(e.stack || e) - - if (ua) { - ua.event('Hooks', 'Delete branch error').send() - } - - return Promise.reject(e) - } -} diff --git a/controllers/home.js b/controllers/home.js deleted file mode 100644 index 4299769..0000000 --- a/controllers/home.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -const pkg = require('./../package.json') - -module.exports = (req, res) => { - res.send(`Hello from Staticman version ${pkg.version}!`) -} diff --git a/controllers/process.js b/controllers/process.js deleted file mode 100644 index dc7a77c..0000000 --- a/controllers/process.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict' - -const path = require('path') -const config = require(path.join(__dirname, '/../config')) -const errorHandler = require('../lib/ErrorHandler') -const reCaptcha = require('express-recaptcha') -const Staticman = require('../lib/Staticman') -const universalAnalytics = require('universal-analytics') - -function checkRecaptcha (staticman, req) { - return new Promise((resolve, reject) => { - staticman.getSiteConfig().then(siteConfig => { - if (!siteConfig.get('reCaptcha.enabled')) { - return resolve(false) - } - - const reCaptchaOptions = req.body.options && req.body.options.reCaptcha - - if (!reCaptchaOptions || !reCaptchaOptions.siteKey || !reCaptchaOptions.secret) { - return reject(errorHandler('RECAPTCHA_MISSING_CREDENTIALS')) - } - - let decryptedSecret - - try { - decryptedSecret = staticman.decrypt(reCaptchaOptions.secret) - } catch (err) { - return reject(errorHandler('RECAPTCHA_CONFIG_MISMATCH')) - } - - if ( - reCaptchaOptions.siteKey !== siteConfig.get('reCaptcha.siteKey') || - decryptedSecret !== siteConfig.get('reCaptcha.secret') - ) { - return reject(errorHandler('RECAPTCHA_CONFIG_MISMATCH')) - } - - reCaptcha.init(reCaptchaOptions.siteKey, decryptedSecret) - reCaptcha.verify(req, err => { - if (err) { - return reject(errorHandler(err)) - } - - return resolve(true) - }) - }).catch(err => reject(err)) - }) -} - -function createConfigObject (apiVersion, property) { - let remoteConfig = {} - - if (apiVersion === '1') { - remoteConfig.file = '_config.yml' - remoteConfig.path = 'staticman' - } else { - remoteConfig.file = 'staticman.yml' - remoteConfig.path = property || '' - } - - return remoteConfig -} - -function process (staticman, req, res) { - const ua = config.get('analytics.uaTrackingId') - ? universalAnalytics(config.get('analytics.uaTrackingId')) - : null - const fields = req.query.fields || req.body.fields - const options = req.query.options || req.body.options || {} - - return staticman.processEntry(fields, options).then(data => { - sendResponse(res, { - redirect: data.redirect, - fields: data.fields - }) - - if (ua) { - ua.event('Entries', 'New entry').send() - } - }) -} - -function sendResponse (res, data) { - const error = data && data.err - const statusCode = error ? 500 : 200 - - if (!error && data.redirect) { - return res.redirect(data.redirect) - } - - if (error && data.redirectError) { - return res.redirect(data.redirectError) - } - - let payload = { - success: !error - } - - if (error && error._smErrorCode) { - const errorCode = errorHandler.getInstance().getErrorCode(error._smErrorCode) - const errorMessage = errorHandler.getInstance().getMessage(error._smErrorCode) - - if (errorMessage) { - payload.message = errorMessage - } - - if (error.data) { - payload.data = error.data - } - - if (error) { - payload.rawError = error - } - - payload.errorCode = errorCode - } else if (error) { - payload.rawError = data.err.toString() - } else { - payload.fields = data.fields - } - - res.status(statusCode).send(payload) -} - -module.exports = async (req, res, next) => { - const staticman = await new Staticman(req.params) - - staticman.setConfigPath() - staticman.setIp(req.headers['x-forwarded-for'] || req.connection.remoteAddress) - staticman.setUserAgent(req.headers['user-agent']) - - return checkRecaptcha(staticman, req) - .then(usedRecaptcha => process(staticman, req, res)) - .catch(err => sendResponse(res, { - err, - redirect: req.body.options && req.body.options.redirect, - redirectError: req.body.options && req.body.options.redirectError - })) -} - -module.exports.checkRecaptcha = checkRecaptcha -module.exports.createConfigObject = createConfigObject -module.exports.process = process -module.exports.sendResponse = sendResponse diff --git a/docker-compose.development.yml b/docker-compose.development.yml deleted file mode 100644 index 141e560..0000000 --- a/docker-compose.development.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: '2' -services: - staticman: - extends: - file: docker-compose.yml - service: staticman - environment: - NODE_ENV: development - volumes: - - ./:/app \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 821b83f..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: '2' -services: - staticman: - build: . - env_file: .env - ports: - - '8080:3000' - restart: unless-stopped - environment: - NODE_ENV: production - PORT: 3000 - GITHUB_TOKEN: #insert your GitHub token here - RSA_PRIVATE_KEY: | -# RSA private key to encrypt sensitive configuration parameters with. -# the correct format starts with "-----BEGIN RSA PRIVATE KEY-----" \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 7324d93..b2c184a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,8 +4,7 @@ import vitest from '@vitest/eslint-plugin'; import tseslint from 'typescript-eslint'; export default tseslint.config( - // Upstream Staticman code is kept for reference until it is removed; it is not part of the Worker. - { ignores: ['node_modules/', 'dist/', 'lib/', 'controllers/', 'test/', '*.cjs', 'index.js', 'server.js', 'config.js', 'siteConfig.js', 'src/worker-configuration.d.ts'] }, + { ignores: ['node_modules/', 'dist/', 'src/worker-configuration.d.ts'] }, js.configs.recommended, ...tseslint.configs.strict, { files: ['scripts/**/*.mjs'], languageOptions: { globals: globals.node } }, diff --git a/index.js b/index.js deleted file mode 100644 index 2089a7b..0000000 --- a/index.js +++ /dev/null @@ -1,10 +0,0 @@ -try { - const StaticmanAPI = require('./server') - const api = new StaticmanAPI() - - api.start(port => { - console.log('Staticman API running on port', port) - }) -} catch (e) { - console.error(e) -} diff --git a/lib/ErrorHandler.js b/lib/ErrorHandler.js deleted file mode 100644 index cc5734d..0000000 --- a/lib/ErrorHandler.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict' - -const {StatusCodeError, RequestError} = require('request-promise/errors') - -class ApiError { - constructor (message, statusCode = 500, smErrorCode = '') { - this.message = message - this.statusCode = statusCode - this._smErrorCode = smErrorCode - } - - toJSON () { - return { - message: this.message, - statusCode: this.statusCode - } - } -} - -class ErrorHandler { - constructor () { - this.ERROR_MESSAGES = { - 'missing-input-secret': 'reCAPTCHA: The secret parameter is missing', - 'invalid-input-secret': 'reCAPTCHA: The secret parameter is invalid or malformed', - 'missing-input-response': 'reCAPTCHA: The response parameter is missing', - 'invalid-input-response': 'reCAPTCHA: The response parameter is invalid or malformed', - 'RECAPTCHA_MISSING_CREDENTIALS': 'Missing reCAPTCHA API credentials', - 'RECAPTCHA_FAILED_DECRYPT': 'Could not decrypt reCAPTCHA secret', - 'RECAPTCHA_CONFIG_MISMATCH': 'reCAPTCHA options do not match Staticman config', - 'PARSING_ERROR': 'Error whilst parsing config file', - 'GITHUB_AUTH_TOKEN_MISSING': 'The site requires a valid GitHub authentication token to be supplied in the `options[github-token]` field', - 'MISSING_CONFIG_BLOCK': 'Error whilst parsing Staticman config file' - } - - this.ERROR_CODE_ALIASES = { - 'missing-input-secret': 'RECAPTCHA_MISSING_INPUT_SECRET', - 'invalid-input-secret': 'RECAPTCHA_INVALID_INPUT_SECRET', - 'missing-input-response': 'RECAPTCHA_MISSING_INPUT_RESPONSE', - 'invalid-input-response': 'RECAPTCHA_INVALID_INPUT_RESPONSE' - } - } - - getErrorCode (error) { - return this.ERROR_CODE_ALIASES[error] || error - } - - getMessage (error) { - return this.ERROR_MESSAGES[error] - } - - log (err, instance) { - let parameters = {} - let prefix = '' - - if (instance) { - parameters = instance.getParameters() - - prefix += `${parameters.username}/${parameters.repository}` - } - - console.log(`${prefix}`, err) - } - - _save (errorCode, data = {}) { - const {err} = data - - if (err) { - err._smErrorCode = err._smErrorCode || errorCode - - // Re-wrap API request errors as these could expose - // request/response details that the user should not - // be allowed to see e.g. access tokens. - // `request-promise` is the primary offender here, - // but we similarly do not want others to leak too. - if ( - err instanceof StatusCodeError || - err instanceof RequestError - ) { - const statusCode = err.statusCode || err.code - - return new ApiError(err.message, statusCode, err._smErrorCode) - } - - return err - } - - let payload = { - _smErrorCode: errorCode - } - - if (data.data) { - payload.data = data.data - } - - return payload - } -} - -const errorHandler = new ErrorHandler() - -module.exports = function () { - return errorHandler._save.apply(errorHandler, arguments) -} - -module.exports.getInstance = function () { - return errorHandler -} - -module.exports.ApiError = ApiError diff --git a/lib/GitHub.js b/lib/GitHub.js deleted file mode 100644 index 6755824..0000000 --- a/lib/GitHub.js +++ /dev/null @@ -1,204 +0,0 @@ -'use strict' - -const config = require('../config') -const errorHandler = require('./ErrorHandler') -const GithubApi = require('@octokit/rest') -const { App } = require('@octokit/app') -const { request } = require('@octokit/request') -const GitService = require('./GitService') -const Review = require('./models/Review') -const User = require('./models/User') - -const normalizeResponse = ({data}) => data - -class GitHub extends GitService { - constructor (options = {}) { - super(options.username, options.repository, options.branch) - - return (async () => { - const isAppAuth = config.get('githubAppID') && - config.get('githubPrivateKey') - const isLegacyAuth = config.get('githubToken') && - ['1', '2'].includes(options.version) - - let authToken - - if (options.oauthToken) { - authToken = options.oauthToken - } else if (isLegacyAuth) { - authToken = config.get('githubToken') - } else if (isAppAuth) { - authToken = await this._authenticate(options.username, options.repository) - } else { - throw new Error('Require an `oauthToken` or `token` option') - } - - this.api = GithubApi({ - auth: `token ${authToken}`, - userAgent: 'Staticman', - baseUrl: config.get('githubBaseUrl'), - request: { - timeout: 5000 - } - }) - - return this - })() - } - - async _authenticate (username, repository) { - const app = new App( - { - id: config.get('githubAppID'), - privateKey: config.get('githubPrivateKey'), - baseUrl: config.get('githubBaseUrl') - } - ) - - const jwt = app.getSignedJsonWebToken() - - const {data} = await request('GET /repos/:owner/:repo/installation', { - owner: username, - repo: repository, - headers: { - authorization: `Bearer ${jwt}`, - accept: 'application/vnd.github.machine-man-preview+json' - } - }) - - const installationId = data.id - - let token = await app.getInstallationAccessToken({installationId}) - - return token - } - - _pullFile (filePath, branch) { - return this.api.repos.getContents({ - owner: this.username, - repo: this.repository, - path: filePath, - ref: branch - }) - .then(normalizeResponse) - .catch(err => Promise.reject(errorHandler('GITHUB_READING_FILE', {err}))) - } - - _commitFile (filePath, content, commitMessage, branch) { - return this.api.repos.createOrUpdateFile({ - owner: this.username, - repo: this.repository, - path: filePath, - message: commitMessage, - content, - branch - }) - .then(normalizeResponse) - } - - writeFile (filePath, data, targetBranch, commitTitle) { - return super.writeFile(filePath, data, targetBranch, commitTitle) - .catch(err => { - try { - const message = err && err.message - - if (message) { - const parsedError = JSON.parse(message) - - if ( - parsedError && - parsedError.message && - parsedError.message.includes('"sha" wasn\'t supplied') - ) { - return Promise.reject(errorHandler('GITHUB_FILE_ALREADY_EXISTS', {err})) - } - } - } catch (err) { - console.log(err) - } - - return Promise.reject(errorHandler('GITHUB_WRITING_FILE')) - }) - } - - getBranchHeadCommit (branch) { - return this.api.repos.getBranch({ - owner: this.username, - repo: this.repository, - branch - }) - .then(res => res.data.commit.sha) - } - - createBranch (branch, sha) { - return this.api.git.createRef({ - owner: this.username, - repo: this.repository, - ref: `refs/heads/${branch}`, - sha - }) - .then(normalizeResponse) - } - - deleteBranch (branch) { - return this.api.git.deleteRef({ - owner: this.username, - repo: this.repository, - ref: `heads/${branch}` - }) - } - - createReview (reviewTitle, branch, reviewBody) { - return this.api.pullRequests.create({ - owner: this.username, - repo: this.repository, - title: reviewTitle, - head: branch, - base: this.branch, - body: reviewBody - }) - .then(normalizeResponse) - } - - getReview (reviewId) { - return this.api.pulls.get({ - owner: this.username, - repo: this.repository, - pull_number: reviewId - }) - .then(normalizeResponse) - .then(({base, body, head, merged, state, title}) => - new Review( - title, - body, - (merged && state === 'closed') ? 'merged' : state, - head.ref, - base.ref - ) - ) - } - - async readFile (filePath, getFullResponse) { - try { - return await super.readFile(filePath, getFullResponse) - } catch (err) { - throw errorHandler('GITHUB_READING_FILE', {err}) - } - } - - writeFileAndSendReview (filePath, data, branch, commitTitle, reviewBody) { - return super.writeFileAndSendReview(filePath, data, branch, commitTitle, reviewBody) - .catch(err => Promise.reject(errorHandler('GITHUB_CREATING_PR', {err}))) - } - - getCurrentUser () { - return this.api.users.getAuthenticated({}) - .then(normalizeResponse) - .then(({login, email, avatar_url, name, bio, company, blog}) => - new User('github', login, email, name, avatar_url, bio, blog, company) - ) - .catch(err => Promise.reject(errorHandler('GITHUB_GET_USER', {err}))) - } -} - -module.exports = GitHub diff --git a/lib/GitLab.js b/lib/GitLab.js deleted file mode 100644 index 26d55d5..0000000 --- a/lib/GitLab.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict' - -const config = require('../config') -const errorHandler = require('./ErrorHandler') -const GitLabApi = require('gitlab/dist/es5').default -const GitService = require('./GitService') -const Review = require('./models/Review') -const User = require('./models/User') - -class GitLab extends GitService { - constructor (options = {}) { - super(options.username, options.repository, options.branch) - - const token = config.get('gitlabToken') - - if (options.oauthToken) { - this.api = new GitLabApi({ - url: config.get('gitlabBaseUrl'), - oauthToken: options.oauthToken - }) - } else if (token) { - this.api = new GitLabApi({ - url: config.get('gitlabBaseUrl'), - token - }) - } else { - throw new Error('Require an `oauthToken` or `token` option') - } - } - - get repositoryId () { - if (this.username && this.repository) { - return `${this.username}/${this.repository}` - } - - return '' - } - - _pullFile (path, branch) { - return this.api.RepositoryFiles.show(this.repositoryId, path, branch) - .catch(err => Promise.reject(errorHandler('GITLAB_READING_FILE', {err}))) - } - - _commitFile (filePath, content, commitMessage, branch) { - return this.api.RepositoryFiles.create(this.repositoryId, filePath, branch, { - content, - commit_message: commitMessage, - encoding: 'base64' - }) - } - - getBranchHeadCommit (branch) { - return this.api.Branches.show(this.repositoryId, branch) - .then(res => res.commit.id) - } - - createBranch (branch, sha) { - return this.api.Branches.create(this.repositoryId, branch, sha) - } - - deleteBranch (branch) { - return this.api.Branches.remove(this.repositoryId, branch) - } - - createReview (reviewTitle, branch, reviewBody) { - return this.api.MergeRequests.create(this.repositoryId, branch, this.branch, reviewTitle, { - description: reviewBody, - remove_source_branch: true - }) - } - - getReview (reviewId) { - return this.api.MergeRequests.show(this.repositoryId, reviewId) - .then(({ - description: body, - source_branch: sourceBranch, - target_branch: targetBranch, - state, - title - }) => new Review( - title, - body, - state, - sourceBranch, - targetBranch - ) - ) - } - - readFile (filePath, getFullResponse) { - return super.readFile(filePath, getFullResponse) - .catch(err => Promise.reject(errorHandler('GITLAB_READING_FILE', {err}))) - } - - writeFile (filePath, data, targetBranch, commitTitle) { - return super.writeFile(filePath, data, targetBranch, commitTitle) - .catch(err => { - if (err.error && err.error.message === 'A file with this name already exists') { - return Promise.reject(errorHandler('GITLAB_FILE_ALREADY_EXISTS', {err})) - } - - return Promise.reject(errorHandler('GITLAB_WRITING_FILE', {err})) - }) - } - - writeFileAndSendReview (filePath, data, branch, commitTitle, reviewBody) { - return super.writeFileAndSendReview(filePath, data, branch, commitTitle, reviewBody) - .catch(err => Promise.reject(errorHandler('GITLAB_CREATING_PR', {err}))) - } - - getCurrentUser () { - return this.api.Users.current() - .then(({username, email, name, avatar_url, bio, website_url, organisation}) => - new User('gitlab', username, email, name, avatar_url, bio, website_url, organisation) - ) - .catch(err => Promise.reject(errorHandler('GITLAB_GET_USER', {err}))) - } -} - -module.exports = GitLab diff --git a/lib/GitService.js b/lib/GitService.js deleted file mode 100644 index 1cc7966..0000000 --- a/lib/GitService.js +++ /dev/null @@ -1,104 +0,0 @@ -'use strict' - -const errorHandler = require('./ErrorHandler') -const yaml = require('js-yaml') - -class GitService { - constructor (username, repository, branch) { - this.username = username - this.repository = repository - this.branch = branch - } - - _pullFile (filePath, branch) { - throw new Error('Abstract method `_pullFile` should be implemented') - } - - _commitFile (filePath, contents, commitTitle, branch) { - throw new Error('Abstract method `_commitFile` should be implemented') - } - - getBranchHeadCommit (branch) { - throw new Error('Abstract method `getBranchHeadCommit` should be implemented') - } - - createBranch (branch, sha) { - throw new Error('Abstract method `createBranch` should be implemented') - } - - deleteBranch (branch) { - throw new Error('Abstract method `deleteBranch` should be implemented') - } - - createReview (commitTitle, branch, reviewBody) { - throw new Error('Abstract method `createReview` should be implemented') - } - - getReview (reviewId) { - throw new Error('Abstract method `getReview` should be implemented') - } - - getCurrentUser () { - throw new Error('Abstract method `getCurrentUser` should be implemented') - } - - async readFile (path, getFullResponse) { - const extension = path.split('.').pop() - - let res = await this._pullFile(path, this.branch) - - let content - try { - content = Buffer.from(res.content, 'base64').toString() - } catch (err) { - throw errorHandler('GITHUB_READING_FILE', {err}) - } - - try { - switch (extension) { - case 'yml': - case 'yaml': - content = yaml.safeLoad(content, 'utf8') - - break - - case 'json': - content = JSON.parse(content) - - break - } - - if (getFullResponse) { - return { - content: content, - file: { - content: res.content - } - } - } - - return content - } catch (err) { - let errorData = {err} - - if (err.message) { - errorData.data = err.message - } - - throw errorHandler('PARSING_ERROR', errorData) - } - } - - writeFile (filePath, data, branch = this.branch, commitTitle = 'Add Staticman file') { - return this._commitFile(filePath, Buffer.from(data).toString('base64'), commitTitle, branch) - } - - writeFileAndSendReview (filePath, data, branch, commitTitle = 'Add Staticman file', reviewBody = '') { - return this.getBranchHeadCommit(this.branch) - .then(sha => this.createBranch(branch, sha)) - .then(() => this.writeFile(filePath, data, branch, commitTitle)) - .then(() => this.createReview(commitTitle, branch, reviewBody)) - } -} - -module.exports = GitService diff --git a/lib/GitServiceFactory.js b/lib/GitServiceFactory.js deleted file mode 100644 index 58d54d5..0000000 --- a/lib/GitServiceFactory.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict' - -const GitLab = require('./GitLab') -const GitHub = require('./GitHub') - -module.exports.create = async (service, options) => { - switch (service) { - case 'gitlab': - return new GitLab(options) - default: - return new GitHub(options) - } -} diff --git a/lib/Logger.js b/lib/Logger.js deleted file mode 100644 index 7aa6812..0000000 --- a/lib/Logger.js +++ /dev/null @@ -1,34 +0,0 @@ -const logger = require('@dadi/logger') -const BunyanSlack = require('bunyan-slack') -const path = require('path') -const config = require(path.join(__dirname, '/../config')) - -const Logger = function () { - let options = { - enabled: true, - level: 'info', - stream: process.stdout - } - - if (typeof config.get('logging.slackWebhook') === 'string') { - this.formatFn = t => '```\n' + t + '\n```' - - options.stream = new BunyanSlack({ - webhook_url: config.get('logging.slackWebhook') - }) - } - - logger.init(options) -} - -Logger.prototype.info = function (data) { - const formattedData = typeof this.formatFn === 'function' - ? this.formatFn(data) - : data - - logger.info(formattedData) -} - -const instance = new Logger() - -module.exports = instance diff --git a/lib/Notification.js b/lib/Notification.js deleted file mode 100644 index 3d20c18..0000000 --- a/lib/Notification.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict' - -const path = require('path') -const config = require(path.join(__dirname, '/../config')) - -const Notification = function (mailAgent) { - this.mailAgent = mailAgent -} - -Notification.prototype._buildMessage = function (fields, options, data) { - return ` - - - Dear human,
-
- Someone replied to a comment you subscribed to${data.siteName ? ` on ${data.siteName}` : ''}.
-
- ${options.origin ? `Click here to see it.` : ''} If you do not wish to receive any further notifications for this thread, click here.
-
- #ftw,
- -- IPL contribute - - - ` -} - -Notification.prototype.send = function (to, fields, options, data) { - const subject = data.siteName ? `New reply on "${data.siteName}"` : 'New reply' - - return new Promise((resolve, reject) => { - this.mailAgent.messages().send({ - from: `Staticman <${config.get('email.fromAddress')}>`, - to, - subject, - html: this._buildMessage(fields, options, data) - }, (err, res) => { - if (err) { - return reject(err) - } - - return resolve(res) - }) - }) -} - -module.exports = Notification diff --git a/lib/OAuth.js b/lib/OAuth.js deleted file mode 100644 index 96f6f6f..0000000 --- a/lib/OAuth.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict' - -const config = require('../config') -const request = require('request-promise') -const errorHandler = require('./ErrorHandler') - -const requestGitHubAccessToken = (code, clientId, clientSecret, redirectUri) => { - return request({ - headers: { - 'Accept': 'application/json' - }, - json: true, - method: 'POST', - uri: config.get('githubAccessTokenUri'), - qs: { - code, - client_id: clientId, - client_secret: clientSecret, - redirect_uri: redirectUri - } - }) - .then(res => res.access_token) - .catch(err => Promise.reject(errorHandler('GITHUB_AUTH_FAILED', {err}))) // eslint-disable-line handle-callback-err -} - -const requestGitLabAccessToken = (code, clientId, clientSecret, redirectUri) => { - return request({ - headers: { - 'Accept': 'application/json' - }, - json: true, - method: 'POST', - uri: config.get('gitlabAccessTokenUri'), - qs: { - code, - client_id: clientId, - client_secret: clientSecret, - grant_type: 'authorization_code', - redirect_uri: redirectUri - } - }) - .then(res => res.access_token) - .catch(err => Promise.reject(errorHandler('GITLAB_AUTH_FAILED', {err}))) // eslint-disable-line handle-callback-err -} - -module.exports = { - requestGitHubAccessToken, - requestGitLabAccessToken -} diff --git a/lib/RSA.js b/lib/RSA.js deleted file mode 100644 index d072436..0000000 --- a/lib/RSA.js +++ /dev/null @@ -1,24 +0,0 @@ -const path = require('path') -const config = require(path.join(__dirname, '/../config')) -const NodeRSA = require('node-rsa') -const key = new NodeRSA() - -key.importKey(config.get('rsaPrivateKey'), 'private') - -module.exports.encrypt = text => { - try { - const encryptedText = key.encrypt(text, 'base64') - - return encryptedText - } catch (err) { - return null - } -} - -module.exports.decrypt = text => { - try { - return key.decrypt(text, 'utf8') - } catch (err) { - return null - } -} diff --git a/lib/Staticman.js b/lib/Staticman.js deleted file mode 100644 index 85dafa1..0000000 --- a/lib/Staticman.js +++ /dev/null @@ -1,621 +0,0 @@ -const akismetApi = require('akismet') -const config = require('../config') -const errorHandler = require('./ErrorHandler') -const gitFactory = require('./GitServiceFactory') -const markdownTable = require('markdown-table') -const moment = require('moment') -const Mailgun = require('mailgun-js') -const NodeRSA = require('node-rsa') -const objectPath = require('object-path') -const RSA = require('./RSA') -const SiteConfig = require('../siteConfig') -const slugify = require('slug') -const SubscriptionsManager = require('./SubscriptionsManager') -const Transforms = require('./Transforms') -const uuidv1 = require('uuid/v1') -const yaml = require('js-yaml') - -class Staticman { - constructor (parameters) { - return (async () => { - this.parameters = parameters - - const { - branch, - repository, - service, - username, - version - } = parameters - - // Initialise the Git service API - this.git = await gitFactory.create(service, { - branch, - repository, - username, - version - }) - - // Generate unique id - this.uid = uuidv1() - - this.rsa = new NodeRSA() - this.rsa.importKey(config.get('rsaPrivateKey'), 'private') - - return this - })() - } - - _applyInternalFields (data) { - let internalFields = { - _id: this.uid - } - - // Inject parent, if present - if (this.options.parent) { - internalFields._parent = this.options.parent - } - - return Object.assign(internalFields, data) - } - - _applyGeneratedFields (data) { - const generatedFields = this.siteConfig.get('generatedFields') - - if (!generatedFields) return data - - Object.keys(generatedFields).forEach(field => { - const generatedField = generatedFields[field] - - if ((typeof generatedField === 'object') && (!(generatedField instanceof Array))) { - const options = generatedField.options || {} - - switch (generatedField.type) { - case 'date': - data[field] = this._createDate(options) - - break - - // TODO: Remove 'github' when v2 API is no longer supported - case 'github': - case 'user': - if (this.gitUser && typeof options.property === 'string') { - data[field] = objectPath.get(this.gitUser, options.property) - } - - break - - case 'slugify': - if ( - typeof options.field === 'string' && - typeof data[options.field] === 'string' - ) { - data[field] = slugify(data[options.field]).toLowerCase() - } - - break - } - } else { - data[field] = generatedField - } - }) - - return data - } - - _applyTransforms (fields) { - const transforms = this.siteConfig.get('transforms') - - if (!transforms) return Promise.resolve(fields) - - // This doesn't serve any purpose for now, but we might want to have - // asynchronous transforms in the future. - let queue = [] - - Object.keys(transforms).forEach(field => { - if (!fields[field]) return - - let transformNames = [].concat(transforms[field]) - - transformNames.forEach(transformName => { - let transformFn = Transforms[transformName] - - if (transformFn) { - fields[field] = transformFn(fields[field]) - } - }) - }) - - return Promise.all(queue).then((results) => { - return fields - }) - } - - _checkForSpam (fields) { - if (!this.siteConfig.get('akismet.enabled')) return Promise.resolve(fields) - - return new Promise((resolve, reject) => { - const akismet = akismetApi.client({ - apiKey: config.get('akismet.apiKey'), - blog: config.get('akismet.site') - }) - - akismet.checkSpam({ - user_ip: this.ip, - user_agent: this.userAgent, - comment_type: this.siteConfig.get('akismet.type'), - comment_author: fields[this.siteConfig.get('akismet.author')], - comment_author_email: fields[this.siteConfig.get('akismet.authorEmail')], - comment_author_url: fields[this.siteConfig.get('akismet.authorUrl')], - comment_content: fields[this.siteConfig.get('akismet.content')] - }, (err, isSpam) => { - if (err) return reject(err) - - if (isSpam) return reject(errorHandler('IS_SPAM')) - - return resolve(fields) - }) - }) - } - - async _checkAuth () { - // TODO: Remove when v2 API is no longer supported - if (this.parameters.version === '2') { - return this._checkAuthV2() - } - - if (!this.siteConfig.get('auth.required')) { - return Promise.resolve(false) - } - - if (!this.options['auth-token']) { - return Promise.reject(errorHandler('AUTH_TOKEN_MISSING')) - } - - const oauthToken = RSA.decrypt(this.options['auth-token']) - - if (!oauthToken) { - return Promise.reject(errorHandler('AUTH_TOKEN_INVALID')) - } - - const git = await gitFactory.create(this.options['auth-type'], { - oauthToken, - version: this.parameters.version - }) - - return git.getCurrentUser().then(user => { - this.gitUser = user - - return true - }) - } - - async _checkAuthV2 () { - if (!this.siteConfig.get('githubAuth.required')) { - return Promise.resolve(false) - } - - if (!this.options['github-token']) { - return Promise.reject(errorHandler('GITHUB_AUTH_TOKEN_MISSING')) - } - - const oauthToken = RSA.decrypt(this.options['github-token']) - - if (!oauthToken) { - return Promise.reject(errorHandler('GITHUB_AUTH_TOKEN_INVALID')) - } - - const git = await gitFactory.create('github', { - oauthToken, - version: this.parameters.version - }) - - return git.api.users.getAuthenticated({}).then(({data}) => { - this.gitUser = data - - return true - }) - } - - _createDate (options) { - options = options || {} - - const date = new Date() - - switch (options.format) { - case 'timestamp': - return date.getTime() - - case 'timestamp-seconds': - return Math.floor(date.getTime() / 1000) - - case 'iso8601': - default: - return date.toISOString() - } - } - - _createFile (fields) { - return new Promise((resolve, reject) => { - switch (this.siteConfig.get('format').toLowerCase()) { - case 'json': - return resolve(JSON.stringify(fields)) - - case 'yaml': - case 'yml': - try { - const output = yaml.safeDump(fields) - - return resolve(output) - } catch (err) { - return reject(err) - } - - case 'frontmatter': - const transforms = this.siteConfig.get('transforms') - - const contentField = transforms && Object.keys(transforms).find(field => { - return transforms[field] === 'frontmatterContent' - }) - - if (!contentField) { - return reject(errorHandler('NO_FRONTMATTER_CONTENT_TRANSFORM')) - } - - const content = fields[contentField] - const attributeFields = Object.assign({}, fields) - - delete attributeFields[contentField] - - try { - const output = `---\n${yaml.safeDump(attributeFields)}---\n${content}\n` - - return resolve(output) - } catch (err) { - return reject(err) - } - - default: - return reject(errorHandler('INVALID_FORMAT')) - } - }) - } - - _generateReviewBody (fields) { - let table = [ - ['Field', 'Content'] - ] - - Object.keys(fields).forEach(field => { - table.push([field, fields[field]]) - }) - - let message = this.siteConfig.get('pullRequestBody') + markdownTable(table) - - if (this.siteConfig.get('notifications.enabled')) { - const notificationsPayload = { - configPath: this.configPath, - fields, - options: this.options, - parameters: this.parameters - } - - message += `\n\n` - } - - return message - } - - _getNewFilePath (data) { - const configFilename = this.siteConfig.get('filename') - const filename = (configFilename && configFilename.length) - ? this._resolvePlaceholders(configFilename, { - fields: data, - options: this.options - }) - : this.uid - - let path = this._resolvePlaceholders(this.siteConfig.get('path'), { - fields: data, - options: this.options - }) - - // Remove trailing slash, if existing - if (path.slice(-1) === '/') { - path = path.slice(0, -1) - } - - const extension = this.siteConfig.get('extension').length - ? this.siteConfig.get('extension') - : this._getExtensionForFormat(this.siteConfig.get('format')) - - return `${path}/${filename}.${extension}` - } - - _getExtensionForFormat (format) { - switch (format.toLowerCase()) { - case 'json': - return 'json' - - case 'yaml': - case 'yml': - return 'yml' - - case 'frontmatter': - return 'md' - } - } - - _initialiseSubscriptions () { - if (!this.siteConfig.get('notifications.enabled')) return null - - // Initialise Mailgun - const mailgun = Mailgun({ - apiKey: this.siteConfig.get('notifications.apiKey') || config.get('email.apiKey'), - domain: this.siteConfig.get('notifications.domain') || config.get('email.domain') - }) - - // Initialise SubscriptionsManager - const subscriptions = new SubscriptionsManager(this.parameters, this.git, mailgun) - - return subscriptions - } - - _resolvePlaceholders (subject, baseObject) { - const matches = subject.match(/{(.*?)}/g) - - if (!matches) return subject - - matches.forEach((match) => { - const escapedMatch = match.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') - const property = match.slice(1, -1) - - let newText - - switch (property) { - case '@timestamp': - newText = new Date().getTime() - - break - - case '@id': - newText = this.uid - - break - - default: - const timeIdentifier = '@date:' - - if (property.indexOf(timeIdentifier) === 0) { - const timePattern = property.slice(timeIdentifier.length) - - newText = moment().format(timePattern) - } else { - newText = objectPath.get(baseObject, property) || '' - } - } - - subject = subject.replace(new RegExp(escapedMatch, 'g'), newText) - }) - - return subject - } - - _validateConfig (config) { - if (!config) { - return errorHandler('MISSING_CONFIG_BLOCK') - } - - const requiredFields = [ - 'allowedFields', - 'branch', - 'format', - 'path' - ] - - let missingFields = [] - - // Checking for missing required fields - requiredFields.forEach(requiredField => { - if (objectPath.get(config, requiredField) === undefined) { - missingFields.push(requiredField) - } - }) - - if (missingFields.length) { - return errorHandler('MISSING_CONFIG_FIELDS', { - data: missingFields - }) - } - - this.siteConfig = SiteConfig(config, this.rsa) - - return null - } - - _validateFields (fields) { - let missingRequiredFields = [] - let invalidFields = [] - - Object.keys(fields).forEach(field => { - // Check for any invalid fields - if ((this.siteConfig.get('allowedFields').indexOf(field) === -1) && (fields[field] !== '')) { - invalidFields.push(field) - } - - // Trim fields - if (typeof fields[field] === 'string') { - fields[field] = fields[field].trim() - } - }) - - // Check for missing required fields - this.siteConfig.get('requiredFields').forEach(field => { - if ((fields[field] === undefined) || (fields[field] === '')) { - missingRequiredFields.push(field) - } - }) - - if (missingRequiredFields.length) { - return errorHandler('MISSING_REQUIRED_FIELDS', { - data: missingRequiredFields - }) - } - - if (invalidFields.length) { - return errorHandler('INVALID_FIELDS', { - data: invalidFields - }) - } - - return null - } - - decrypt (encrypted) { - return this.rsa.decrypt(encrypted, 'utf8') - } - - getParameters () { - return this.parameters - } - - getSiteConfig (force) { - if (this.siteConfig && !force) return Promise.resolve(this.siteConfig) - - if (!this.configPath) return Promise.reject(errorHandler('NO_CONFIG_PATH')) - - return this.git.readFile(this.configPath.file).then(data => { - const config = objectPath.get(data, this.configPath.path) - const validationErrors = this._validateConfig(config) - - if (validationErrors) { - return Promise.reject(validationErrors) - } - - if (config.branch !== this.parameters.branch) { - return Promise.reject(errorHandler('BRANCH_MISMATCH')) - } - - return this.siteConfig - }) - } - - processEntry (fields, options) { - this.fields = Object.assign({}, fields) - this.options = Object.assign({}, options) - - return this.getSiteConfig().then(config => { - return this._checkAuth() - }).then(() => { - return this._checkForSpam(fields) - }).then(fields => { - // Validate fields - const fieldErrors = this._validateFields(fields) - - if (fieldErrors) return Promise.reject(fieldErrors) - - // Add generated fields - fields = this._applyGeneratedFields(fields) - - // Apply transforms - return this._applyTransforms(fields) - }).then(transformedFields => { - return this._applyInternalFields(transformedFields) - }).then(extendedFields => { - // Create file - return this._createFile(extendedFields) - }).then(data => { - const filePath = this._getNewFilePath(fields) - const subscriptions = this._initialiseSubscriptions() - const commitMessage = this._resolvePlaceholders(this.siteConfig.get('commitMessage'), { - fields, - options - }) - - // Subscribe user, if applicable - if (subscriptions && options.parent && options.subscribe && this.fields[options.subscribe]) { - subscriptions.set(options.parent, this.fields[options.subscribe]).catch(err => { - console.log(err.stack || err) - }) - } - - if (this.siteConfig.get('moderation')) { - const newBranch = 'staticman_' + this.uid - - return this.git.writeFileAndSendReview( - filePath, - data, - newBranch, - commitMessage, - this._generateReviewBody(fields) - ) - } else if (subscriptions && options.parent) { - subscriptions.send(options.parent, fields, options, this.siteConfig) - } - - return this.git.writeFile( - filePath, - data, - this.parameters.branch, - commitMessage - ) - }).then(result => { - return { - fields: fields, - redirect: options.redirect ? options.redirect : false - } - }).catch(err => { - return Promise.reject(errorHandler('ERROR_PROCESSING_ENTRY', { - err, - instance: this - })) - }) - } - - processMerge (fields, options) { - this.fields = Object.assign({}, fields) - this.options = Object.assign({}, options) - - return this.getSiteConfig().then(config => { - const subscriptions = this._initialiseSubscriptions() - - return subscriptions.send(options.parent, fields, options, this.siteConfig) - }).catch(err => { - return Promise.reject(errorHandler('ERROR_PROCESSING_MERGE', { - err, - instance: this - })) - }) - } - - setConfigPath (configPath) { - // Default config path - if (!configPath) { - if (this.parameters.version === '1') { - this.configPath = { - file: '_config.yml', - path: 'staticman' - } - } else { - this.configPath = { - file: 'staticman.yml', - path: this.parameters.property || '' - } - } - - return - } - - this.configPath = configPath - } - - setIp (ip) { - this.ip = ip - } - - setUserAgent (userAgent) { - this.userAgent = userAgent - } -} - -module.exports = Staticman diff --git a/lib/SubscriptionsManager.js b/lib/SubscriptionsManager.js deleted file mode 100644 index fdfc66a..0000000 --- a/lib/SubscriptionsManager.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict' - -const md5 = require('md5') -const Notification = require('./Notification') - -const SubscriptionsManager = function (parameters, dataStore, mailAgent) { - this.parameters = parameters - this.dataStore = dataStore - this.mailAgent = mailAgent -} - -SubscriptionsManager.prototype._getListAddress = function (entryId) { - const compoundId = md5(`${this.parameters.username}-${this.parameters.repository}-${entryId}`) - - return `${compoundId}@${this.mailAgent.domain}` -} - -SubscriptionsManager.prototype._get = function (entryId) { - const listAddress = this._getListAddress(entryId) - - return new Promise((resolve, reject) => { - this.mailAgent.lists(listAddress).info((err, value) => { - if (err && (err.statusCode !== 404)) { - return reject(err) - } - - if (err || !value || !value.list) { - return resolve(null) - } - - return resolve(listAddress) - }) - }) -} - -SubscriptionsManager.prototype.send = function (entryId, fields, options, siteConfig) { - return this._get(entryId).then(list => { - if (list) { - const notifications = new Notification(this.mailAgent) - - return notifications.send(list, fields, options, { - siteName: siteConfig.get('name') - }) - } - }) -} - -SubscriptionsManager.prototype.set = function (entryId, email) { - const listAddress = this._getListAddress(entryId) - - return new Promise((resolve, reject) => { - let queue = [] - - return this._get(entryId).then(list => { - if (!list) { - queue.push(new Promise((resolve, reject) => { - this.mailAgent.lists().create({ - address: listAddress - }, (err, result) => { - if (err) return reject(err) - - return resolve(result) - }) - })) - } - - return Promise.all(queue).then(() => { - this.mailAgent.lists(listAddress).members().create({ - address: email - }, (err, result) => { - // A 400 is fine-ish, means the address already exists - if (err && (err.statusCode !== 400)) return reject(err) - - return resolve(result) - }) - }) - }) - }) -} - -module.exports = SubscriptionsManager diff --git a/lib/Transforms.js b/lib/Transforms.js deleted file mode 100644 index 034601e..0000000 --- a/lib/Transforms.js +++ /dev/null @@ -1,15 +0,0 @@ -const md5 = require('md5') - -const upcase = (value) => { - return String(value).toUpperCase() -} - -const downcase = (value) => { - return String(value).toLowerCase() -} - -module.exports = { - md5, - upcase, - downcase -} diff --git a/lib/TypeUtils.js b/lib/TypeUtils.js deleted file mode 100644 index 77e4db3..0000000 --- a/lib/TypeUtils.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -const assertString = (value, message = `${value} is not a string`) => { - if (typeof value !== 'string') { - throw new TypeError(message) - } -} - -module.exports = { - assertString -} diff --git a/lib/models/Review.js b/lib/models/Review.js deleted file mode 100644 index 1d1e4f5..0000000 --- a/lib/models/Review.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict' - -const {assertString} = require('../TypeUtils') - -class Review { - /** - * @param {string} title - * @param {string} body - * @param {string} state - * @param {string} sourceBranch - * @param {string} targetBranch - */ - constructor (title, body, state, sourceBranch, targetBranch) { - assertString(title) - assertString(body) - assertString(state) - assertString(sourceBranch) - assertString(targetBranch) - - this.title = title - this.body = body - this.state = state - this.sourceBranch = sourceBranch - this.targetBranch = targetBranch - } -} - -module.exports = Review diff --git a/lib/models/User.js b/lib/models/User.js deleted file mode 100644 index 2affe46..0000000 --- a/lib/models/User.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict' - -const {assertString} = require('../TypeUtils') - -class User { - /** - * @param {string} type - * @param {string} username - * @param {string} email - * @param {string} name - * @param {string=""} avatarUrl - * @param {string=""} bio - * @param {string=""} siteUrl - * @param {string=""} organisation - */ - constructor (type, username, email, name, avatarUrl = '', bio = '', siteUrl = '', organisation = '') { - assertString(type) - assertString(username) - assertString(email) - assertString(name) - assertString(avatarUrl) - assertString(bio) - assertString(siteUrl) - assertString(organisation) - - this.type = type - this.username = username - this.email = email - this.name = name - this.avatarUrl = avatarUrl - this.bio = bio - this.siteUrl = siteUrl - this.organisation = organisation - } -} - -module.exports = User diff --git a/server.js b/server.js deleted file mode 100644 index 003af11..0000000 --- a/server.js +++ /dev/null @@ -1,186 +0,0 @@ -const bodyParser = require('body-parser') -const config = require('./config') -const express = require('express') -const ExpressBrute = require('express-brute') -const GithubWebHook = require('express-github-webhook') -const objectPath = require('object-path') - -class StaticmanAPI { - constructor () { - this.controllers = { - connect: require('./controllers/connect'), - encrypt: require('./controllers/encrypt'), - auth: require('./controllers/auth'), - handlePR: require('./controllers/handlePR'), - home: require('./controllers/home'), - process: require('./controllers/process') - } - - this.server = express() - this.server.use(bodyParser.json()) - this.server.use(bodyParser.urlencoded({ - extended: true - // type: '*' - })) - - this.initialiseWebhookHandler() - this.initialiseCORS() - this.initialiseBruteforceProtection() - this.initialiseRoutes() - } - - initialiseBruteforceProtection () { - const store = new ExpressBrute.MemoryStore() - - this.bruteforce = new ExpressBrute(store) - } - - initialiseCORS () { - this.server.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*') - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') - - next() - }) - } - - initialiseRoutes () { - // Route: connect - this.server.get( - '/v:version/connect/:username/:repository', - this.bruteforce.prevent, - this.requireApiVersion([1, 2]), - this.controllers.connect - ) - - // Route: process - this.server.post( - '/v:version/entry/:username/:repository/:branch', - this.bruteforce.prevent, - this.requireApiVersion([1, 2]), - this.requireParams(['fields']), - this.controllers.process - ) - - this.server.post( - '/v:version/entry/:username/:repository/:branch/:property', - this.bruteforce.prevent, - this.requireApiVersion([2]), - this.requireParams(['fields']), - this.controllers.process - ) - - this.server.post( - '/v:version/entry/:service/:username/:repository/:branch/:property', - this.bruteforce.prevent, - this.requireApiVersion([3]), - this.requireService(['github', 'gitlab']), - this.requireParams(['fields']), - this.controllers.process - ) - - // Route: encrypt - this.server.get( - '/v:version/encrypt/:text', - this.bruteforce.prevent, - this.requireApiVersion([2, 3]), - this.controllers.encrypt - ) - - // Route: oauth - this.server.get( - '/v:version/auth/:service/:username/:repository/:branch/:property', - this.bruteforce.prevent, - this.requireApiVersion([2, 3]), - this.requireService(['github', 'gitlab']), - this.controllers.auth - ) - - // Route: root - this.server.get( - '/', - this.controllers.home - ) - } - - initialiseWebhookHandler () { - const webhookHandler = GithubWebHook({ - path: '/v1/webhook' - }) - - webhookHandler.on('pull_request', this.controllers.handlePR) - - this.server.use(webhookHandler) - } - - requireApiVersion (versions) { - return (req, res, next) => { - const versionMatch = versions.some(version => { - return version.toString() === req.params.version - }) - - if (!versionMatch) { - return res.status(400).send({ - success: false, - errorCode: 'INVALID_VERSION' - }) - } - - return next() - } - } - - requireService (services) { - return (req, res, next) => { - const serviceMatch = services.some(service => service === req.params.service) - - if (!serviceMatch) { - return res.status(400).send({ - success: false, - errorCode: 'INVALID_SERVICE' - }) - } - - return next() - } - } - - requireParams (params) { - return function (req, res, next) { - let missingParams = [] - - params.forEach(param => { - if ( - objectPath.get(req.query, param) === undefined && - objectPath.get(req.body, param) === undefined - ) { - missingParams.push(param) - } - }) - - if (missingParams.length) { - return res.status(500).send({ - success: false, - errorCode: 'MISSING_PARAMS', - data: missingParams - }) - } - - return next() - } - } - - start (callback) { - this.instance = this.server.listen(config.get('port'), () => { - if (typeof callback === 'function') { - callback(config.get('port')) - } - }) - } - - close () { - this.instance.close() - } -} - -module.exports = StaticmanAPI diff --git a/siteConfig.js b/siteConfig.js deleted file mode 100644 index e8a6d33..0000000 --- a/siteConfig.js +++ /dev/null @@ -1,216 +0,0 @@ -'use strict' - -const convict = require('convict') - -const schema = { - allowedFields: { - doc: 'An array with the names of the allowed fields. If any of the fields sent is not part of this list, the entry will be discarded and an error will be thrown.', - docExample: 'allowedFields: ["name", "email", "message"]', - format: Array, - default: [] - }, - allowedOrigins: { - doc: 'When allowedOrigins is defined, only requests sent from one of the domains listed will be accepted.', - docExample: 'allowedOrigins: ["localhost", "eduardoboucas.com"]', - default: [], - format: Array - }, - akismet: { - enabled: { - doc: 'Whether to use Akismet to check entries for spam. This requires an Akismet account to be configured in the Staticman API instance being used.', - format: Boolean, - default: false - }, - author: { - doc: 'Name of the field to be used as the entry\'s author in Akistmet', - format: String, - default: '' - }, - authorEmail: { - doc: 'Name of the field to be used as the entry\'s author\'s email in Akistmet', - format: String, - default: '' - }, - authorUrl: { - doc: 'Name of the field to be used as the entry\'s author\'s URL in Akistmet', - format: String, - default: '' - }, - content: { - doc: 'Name of the field to be used as the entry\'s body in Akistmet', - format: String, - default: '' - }, - type: { - doc: 'Type of entry to be sent to Akismet', - format: String, - default: 'comment' - } - }, - auth: { - required: { - doc: 'Whether authentication is required for an entry to be accepted.', - format: Boolean, - default: false - } - }, - branch: { - doc: 'Name of the branch being used within the GitHub repository.', - format: String, - default: 'master' - }, - commitMessage: { - doc: 'Text to be used as the commit message when pushing entries to the GitHub repository.', - format: String, - default: 'Add Staticman data' - }, - extension: { - doc: 'The extension to be used in the generated data files (defaults to the extension associated with the `format` field)', - format: String, - default: '' - }, - filename: { - doc: 'Name for the data files being uploaded to the repository. You can use placeholders (denoted by curly braces), which will be dynamically replaced with the content of a field (e.g. `{fields.name}`), the content of an option (e.g. `{options.slug}`) or other dynamic placeholders such as the entry\'s unique id (`{@id}`).', - format: String, - default: '' - }, - format: { - doc: 'Format of the data files being uploaded to the repository.', - format: ['yaml', 'yml', 'json', 'frontmatter'], - default: 'yml' - }, - generatedFields: { - doc: 'List of fields to be appended to entries automatically. It consists of an object where keys correspond to the names of the fields being created and values being of mixed type. If values are objects, Staticman will look for a `type` and `options` keys inside and perform different operations based on their type; otherwise, the value will be used directly as the content of the generated field.', - docExample: 'generatedFields:\n someField: "some string" # Simple field (string)\n date: # Extended field (date)\n type: date\n options:\n format: "timestamp-seconds"', - format: Object, - default: {} - }, - githubAuth: { - clientId: { - doc: 'The client ID to the GitHub Application used for GitHub OAuth.', - format: 'EncryptedString', - default: null - }, - clientSecret: { - doc: 'The client secret to the GitHub Application used for GitHub OAuth.', - format: 'EncryptedString', - default: null - }, - redirectUri: { - doc: 'The URL to redirect to after authenticating with GitHub.', - format: String, - default: '' - }, - required: { - doc: 'Whether GitHub Auth is required for an entry to be accepted. This is only included for backwards compatibility with the v2 API. For the v3 API, please use the `auth.required` option instead.', - format: Boolean, - default: false - } - }, - gitlabAuth: { - clientId: { - doc: 'The client ID to the GitLab Application used for GitLab OAuth.', - format: 'EncryptedString', - default: null - }, - clientSecret: { - doc: 'The client secret to the GitLab Application used for GitLab OAuth.', - format: 'EncryptedString', - default: null - }, - redirectUri: { - doc: 'The URL to redirect to after authenticating with GitLab.', - format: String, - default: '' - } - }, - moderation: { - doc: 'When set to `true`, a pull request with the data files will be created to allow site administrators to approve or reject an entry. Otherwise, entries will be pushed to `branch` immediately.', - format: Boolean, - default: true - }, - name: { - doc: 'Human-friendly name of the property/website. This is used in notification emails.', - docExample: 'name: "My awesome blog"', - format: String, - default: '' - }, - notifications: { - enabled: { - doc: 'Whether email notifications are enabled. This allows users to subscribe to future comments on a thread. A [Mailgun](http://mailgun.com) account is required.', - format: Boolean, - default: false - }, - apiKey: { - doc: 'Mailgun API key', - format: 'EncryptedString', - default: null - }, - domain: { - doc: 'Mailgun domain', - format: 'EncryptedString', - default: null - } - }, - path: { - doc: 'Path to the directory where entry files are stored. You can use placeholders (denoted by curly braces), which will be dynamically replaced with the content of a field (e.g. `{fields.name}`), the content of an option (e.g. `{options.slug}`) or other dynamic placeholders such as the entry\'s unique id (`{@id}`).', - format: String, - default: '_data/results/{@timestamp}' - }, - pullRequestBody: { - doc: 'Text to be used as the pull request body when pushing moderated entries.', - format: String, - default: 'Dear human,\n\nHere\'s a new entry for your approval. :tada:\n\nMerge the pull request to accept it, or close it to send it away.\n\n:heart: Your friend [IPL contribute](https://github.com/InternetPublicLibrary/contribute) :muscle:\n\n---\n' - }, - requiredFields: { - doc: 'An array with the names of the fields that must be supplies as part of an entry. If any of these is not present, the entry will be discarded and an error will be thrown.', - format: Array, - default: [] - }, - transforms: { - doc: 'List of transformations to be applied to any of the fields supplied. It consists of an object where keys correspond to the names of the fields being transformed. The value determines the type of transformation being applied.', - docExample: 'transforms:\n email: "md5" # The email field will be MD5-hashed', - format: Object, - default: {} - }, - reCaptcha: { - enabled: { - doc: 'Set to `true` to force reCAPTCHA validation, set to `false` to accept comments without reCAPTCHA.', - format: Boolean, - default: false - }, - siteKey: { - doc: 'Site Key for your reCAPTCHA site registration', - format: String, - default: '' - }, - secret: { - doc: 'Encrypted Secret for your reCAPTCHA site registration', - format: 'EncryptedString', - default: '' - } - } -} - -module.exports = (data, rsa) => { - convict.addFormat({ - name: 'EncryptedString', - validate: val => true, - coerce: val => { - return rsa.decrypt(val, 'utf8') - } - }) - - const config = convict(schema) - - try { - config.load(data) - config.validate() - - return config - } catch (e) { - throw e - } -} - -module.exports.schema = schema diff --git a/staticman.sample.yml b/staticman.sample.yml deleted file mode 100644 index ec5e58b..0000000 --- a/staticman.sample.yml +++ /dev/null @@ -1,77 +0,0 @@ -# Name of the property. You can have multiple properties with completely -# different config blocks for different sections of your site. -# For example, you can have one property to handle comment submission and -# another one to handle posts. -comments: - # (*) REQUIRED - # - # Names of the fields the form is allowed to submit. If a field that is - # not here is part of the request, an error will be thrown. - allowedFields: ["name", "email", "url", "message"] - - # (*) REQUIRED - # - # Name of the branch being used. Must match the one sent in the URL of the - # request. - branch: "master" - - # Text to use as the commit message or pull request title. Accepts placeholders. - commitMessage: "Add Staticman data" - - # (*) REQUIRED - # - # Destination path (filename) for the data files. Accepts placeholders. - filename: "entry{@timestamp}" - - # The format of the generated data files. Accepted values are "json", "yaml" - # or "frontmatter" - format: "yaml" - - # List of fields to be populated automatically by Staticman and included in - # the data file. Keys are the name of the field. The value can be an object - # with a `type` property, which configures the generated field, or any value - # to be used directly (e.g. a string, number or array) - generatedFields: - date: - type: date - options: - format: "timestamp-seconds" - - # Whether entries need to be appproved before they are published to the main - # branch. If set to `true`, a pull request will be created for your approval. - # Otherwise, entries will be published to the main branch automatically. - moderation: true - - # Name of the site. Used in notification emails. - name: "eduardoboucas.com" - - # Notification settings. When enabled, users can choose to receive notifications - # via email when someone adds a reply or a new comment. This requires an account - # with Mailgun, which you can get for free at http://mailgun.com. - #notifications: - # Enable notifications - #enabled: true - - # (!) ENCRYPTED - # - # Mailgun API key - #apiKey: "1q2w3e4r" - - # (!) ENCRYPTED - # - # Mailgun domain (encrypted) - #domain: "4r3e2w1q" - - # (*) REQUIRED - # - # Destination path (directory) for the data files. Accepts placeholders. - path: "_data/comments/{options.slug}" - - # Names of required fields. If any of these isn't in the request or is empty, - # an error will be thrown. - requiredFields: ["name", "email", "message"] - - # List of transformations to apply to any of the fields supplied. Keys are - # the name of the field and values are possible transformation types. - transforms: - email: md5 diff --git a/staticman_key.pub b/staticman_key.pub deleted file mode 100644 index 726e6c9..0000000 --- a/staticman_key.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDSE6JaDYgjKUdXi7GsZIwossaXuwjlZn0BjOdmnNfS4mWMji4n5WXwtWqs91M8AmffLgwj0HpV7Dit7C1jlC9nyx0dPkJr2BCQHzCezO3hA/Hs+GYrtGo8CS9qp6vl2cQIfJGIFUTrNS+kAUErbtS4qlYxa2co+e90AK2Ijt8v1Q== hello@upstream-staticman diff --git a/test/acceptance/api.test.js b/test/acceptance/api.test.js deleted file mode 100644 index 36758af..0000000 --- a/test/acceptance/api.test.js +++ /dev/null @@ -1,275 +0,0 @@ -const config = require('../../config') -const githubToken = config.get('githubToken') -const helpers = require('../helpers') -const nock = require('nock') -const querystring = require('querystring') -const request = helpers.wrappedRequest -const sampleData = require('../helpers/sampleData') -const StaticmanAPI = require('../../server') - -const btoa = contents => Buffer.from(contents).toString('base64') - -let server - -beforeAll(done => { - server = new StaticmanAPI() - - server.start(() => {}) - - done() -}) - -afterAll(done => { - server.close() - - done() -}) - -describe('Connect endpoint', () => { - test('accepts the invitation if one is found and replies with "OK!"', async () => { - const invitationId = 123 - - const reqListInvititations = nock('https://api.github.com', { - reqheaders: { - authorization: `token ${githubToken}` - } - }) - .get('/user/repository_invitations') - .reply(200, [ - { - id: invitationId, - repository: { - full_name: `johndoe/foobar` - } - } - ]) - - const reqAcceptInvitation = nock('https://api.github.com', { - reqheaders: { - authorization: `token ${githubToken}` - } - }) - .patch(`/user/repository_invitations/${invitationId}`) - .reply(204) - - let response = await request('/v2/connect/johndoe/foobar') - expect(reqListInvititations.isDone()).toBe(true) - expect(reqAcceptInvitation.isDone()).toBe(true) - expect(response).toBe('OK!') - }) - - test('returns a 404 and an error message if a matching invitation is not found', async () => { - const invitationId = 123 - const reqListInvititations = nock('https://api.github.com', { - reqheaders: { - authorization: `token ${githubToken}` - } - }) - .get('/user/repository_invitations') - .reply(200, [ - { - id: invitationId, - repository: { - full_name: `johndoe/anotherrepo` - } - } - ]) - - const reqAcceptInvitation = nock('https://api.github.com', { - reqheaders: { - authorization: `token ${githubToken}` - } - }) - .patch(`/user/repository_invitations/${invitationId}`) - .reply(204) - - expect.assertions(4) - - try { - await request('/v2/connect/johndoe/foobar') - } catch (err) { - expect(reqListInvititations.isDone()).toBe(true) - expect(reqAcceptInvitation.isDone()).toBe(false) - expect(err.response.body).toBe('Invitation not found') - expect(err.statusCode).toBe(404) - } - }) -}) - -describe('Entry endpoint', () => { - test('outputs a RECAPTCHA_CONFIG_MISMATCH error if reCaptcha options do not match (wrong site key)', async () => { - const data = Object.assign({}, helpers.getParameters(), { - path: 'staticman.yml' - }) - const reCaptchaSecret = helpers.encrypt('Some little secret') - const mockConfig = sampleData.config1 - .replace('@reCaptchaSecret@', reCaptchaSecret) - - nock('https://api.github.com', { - reqheaders: { - Authorization: `token ${githubToken}` - } - }) - .get(`/repos/${data.username}/${data.repository}/contents/${data.path}?ref=${data.branch}`) - .reply(200, { - type: 'file', - encoding: 'base64', - size: 5362, - name: 'staticman.yml', - path: 'staticman.yml', - content: btoa(mockConfig), - sha: '3d21ec53a331a6f037a91c368710b99387d012c1', - url: 'https://api.github.com/repos/octokit/octokit.rb/contents/staticman.yml', - git_url: 'https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1', - html_url: 'https://github.com/octokit/octokit.rb/blob/master/staticman.yml', - download_url: 'https://raw.githubusercontent.com/octokit/octokit.rb/master/staticman.yml', - _links: { - git: 'https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1', - self: 'https://api.github.com/repos/octokit/octokit.rb/contents/staticman.yml', - html: 'https://github.com/octokit/octokit.rb/blob/master/staticman.yml' - } - }) - - const form = { - 'fields[name]': 'Eduardo Boucas', - 'options[reCaptcha][siteKey]': 'wrongSiteKey', - 'options[reCaptcha][secret]': reCaptchaSecret - } - const formData = querystring.stringify(form) - - expect.assertions(3) - - try { - await request({ - body: formData, - method: 'POST', - uri: `/v2/entry/${data.username}/${data.repository}/${data.branch}/${data.property}`, - headers: { - 'content-type': 'application/x-www-form-urlencoded' - } - }) - } catch(response) { - const error = JSON.parse(response.error) - - expect(error.success).toBe(false) - expect(error.errorCode).toBe('RECAPTCHA_CONFIG_MISMATCH') - expect(error.message).toBe('reCAPTCHA options do not match Staticman config') - } - }) - - test('outputs a RECAPTCHA_CONFIG_MISMATCH error if reCaptcha options do not match (wrong secret)', async () => { - const data = Object.assign({}, helpers.getParameters(), { - path: 'staticman.yml' - }) - const reCaptchaSecret = 'Some little secret' - const mockConfig = sampleData.config1 - .replace('@reCaptchaSecret@', helpers.encrypt(reCaptchaSecret)) - - nock('https://api.github.com', { - reqHeaders: { - Authorization: `token ${githubToken}` - } - }) - .get(`/repos/${data.username}/${data.repository}/contents/${data.path}?ref=${data.branch}`) - .reply(200, { - type: 'file', - encoding: 'base64', - size: 5362, - name: 'staticman.yml', - path: 'staticman.yml', - content: btoa(mockConfig), - sha: '3d21ec53a331a6f037a91c368710b99387d012c1', - url: 'https://api.github.com/repos/octokit/octokit.rb/contents/staticman.yml', - git_url: 'https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1', - html_url: 'https://github.com/octokit/octokit.rb/blob/master/staticman.yml', - download_url: 'https://raw.githubusercontent.com/octokit/octokit.rb/master/staticman.yml', - _links: { - git: 'https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1', - self: 'https://api.github.com/repos/octokit/octokit.rb/contents/staticman.yml', - html: 'https://github.com/octokit/octokit.rb/blob/master/staticman.yml' - } - }) - - const form = { - 'fields[name]': 'Eduardo Boucas', - 'options[reCaptcha][siteKey]': '123456789', - 'options[reCaptcha][secret]': 'foo' - } - const formData = querystring.stringify(form) - - expect.assertions(3) - - try { - await request({ - body: formData, - method: 'POST', - uri: '/v2/entry/johndoe/foobar/master/comments', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - } - }) - } catch (response) { - const error = JSON.parse(response.error) - - expect(error.success).toBe(false) - expect(error.errorCode).toBe('RECAPTCHA_CONFIG_MISMATCH') - expect(error.message).toBe('reCAPTCHA options do not match Staticman config') - } - }) - - test('outputs a PARSING_ERROR error if the site config is malformed', async () => { - const data = Object.assign({}, helpers.getParameters(), { - path: 'staticman.yml' - }) - - const mockGetConfig = nock('https://api.github.com', { - reqheaders: { - Authorization: `token ${githubToken}` - } - }) - .get(`/repos/${data.username}/${data.repository}/contents/${data.path}?ref=${data.branch}`) - .reply(200, { - type: 'file', - encoding: 'base64', - size: 5362, - name: 'staticman.yml', - path: 'staticman.yml', - content: btoa(sampleData.config3), - sha: '3d21ec53a331a6f037a91c368710b99387d012c1', - url: 'https://api.github.com/repos/octokit/octokit.rb/contents/staticman.yml', - git_url: 'https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1', - html_url: 'https://github.com/octokit/octokit.rb/blob/master/staticman.yml', - download_url: 'https://raw.githubusercontent.com/octokit/octokit.rb/master/staticman.yml', - _links: { - git: 'https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1', - self: 'https://api.github.com/repos/octokit/octokit.rb/contents/staticman.yml', - html: 'https://github.com/octokit/octokit.rb/blob/master/staticman.yml' - } - }) - - const form = { - 'fields[name]': 'Eduardo Boucas' - } - const formData = querystring.stringify(form) - - expect.assertions(4) - - try { - await request({ - body: formData, - method: 'POST', - uri: '/v2/entry/johndoe/foobar/master/comments', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - } - }) - } catch (response) { - const error = JSON.parse(response.error) - - expect(error.success).toBe(false) - expect(error.errorCode).toBe('PARSING_ERROR') - expect(error.message).toBe('Error whilst parsing config file') - expect(error.rawError).toBeDefined() - } - }) -}) diff --git a/test/helpers/CatchAllApiMock.js b/test/helpers/CatchAllApiMock.js deleted file mode 100644 index a2652d9..0000000 --- a/test/helpers/CatchAllApiMock.js +++ /dev/null @@ -1,21 +0,0 @@ -const nock = require('nock') - -const CatchAllApiMock = function (callback) { - this.NUM_MOCKS = 7 - - this.mock = nock(/api\.github\.com/) - .persist() - .filteringPath(() => '/').delete('/').reply(200, callback) - .filteringPath(() => '/').get('/').reply(200, callback) - .filteringPath(() => '/').head('/').reply(200, callback) - .filteringPath(() => '/').merge('/').reply(200, callback) - .filteringPath(() => '/').patch('/').reply(200, callback) - .filteringPath(() => '/').post('/').reply(200, callback) - .filteringPath(() => '/').put('/').reply(200, callback) -} - -CatchAllApiMock.prototype.hasIntercepted = function () { - return this.mock.pendingMocks().length < this.NUM_MOCKS -} - -module.exports = CatchAllApiMock diff --git a/test/helpers/Config.js b/test/helpers/Config.js deleted file mode 100644 index 65b6868..0000000 --- a/test/helpers/Config.js +++ /dev/null @@ -1,22 +0,0 @@ -const objectPath = require('object-path') -const yaml = require('js-yaml') - -const Config = function (rawContent) { - this.data = yaml.safeLoad(rawContent, 'utf8') -} - -Config.prototype.get = function (key) { - if (key) { - return objectPath.get(this.data, key) - } - - return this.data -} - -Config.prototype.set = function (key, value) { - this.data = objectPath.set(this.data, key, value) - - return this.data -} - -module.exports = Config diff --git a/test/helpers/index.js b/test/helpers/index.js deleted file mode 100644 index 2916962..0000000 --- a/test/helpers/index.js +++ /dev/null @@ -1,125 +0,0 @@ -const CatchAllApiMock = require('./CatchAllApiMock') -const cloneDeep = require('lodash/cloneDeep') -const config = require('./../../config') -const objectPath = require('object-path') -const markdownTable = require('markdown-table') -const NodeRSA = require('node-rsa') -const request = require('request-promise') -const sampleData = require('./sampleData') -const SiteConfig = require('./../../siteConfig') -const yaml = require('js-yaml') - -// Disable console.log() for tests -if (process.env.TEST_DEV !== 'true') { - console.debug = console.log - console.log = jest.fn() - console.warn = jest.fn() -} - -const rsa = new NodeRSA() -rsa.importKey(config.get('rsaPrivateKey'), 'private') - -const fields = { - name: 'Eduardo Bouças', - email: 'mail@eduardoboucas.com', - url: 'https://eduardoboucas.com', - message: 'This is a sample comment' -} - -const parameters = { - branch: 'master', - property: 'comments', - repository: 'foobar', - username: 'johndoe', - version: '2' -} - -const parsedConfig = yaml.safeLoad(sampleData.config1, 'utf8') -const siteConfig = SiteConfig(parsedConfig.comments, rsa) - -module.exports.baseUrl = '' - -module.exports.decrypt = text => { - return rsa.decrypt(text, 'utf8') -} - -module.exports.encrypt = text => { - return rsa.encrypt(text, 'base64') -} - -module.exports.getCatchAllApiMock = callback => { - return new CatchAllApiMock(callback) -} - -module.exports.getConfig = () => { - const config = cloneDeep(siteConfig) - config.getRaw = key => objectPath.get(parsedConfig, `comments.${key}`) - - return config -} - -module.exports.getConfigObject = () => { - return { - file: 'path/to/staticman.yml', - path: 'comments' - } -} - -module.exports.getFields = () => { - return Object.assign({}, fields) -} - -module.exports.getFieldsTable = () => { - let rows = [ - ['Field', 'Content'] - ] - - Object.keys(fields).forEach(field => { - rows.push([field, fields[field]]) - }) - - return markdownTable(rows) -} - -module.exports.getMockRequest = () => { - return { - headers: { - 'x-forwarded-for': '123.456.78.9' - }, - params: Object.assign({}, parameters) - } -} - -module.exports.getMockResponse = () => { - const redirectFn = jest.fn() - const sendFn = jest.fn() - const statusFn = jest.fn(code => ({ - send: sendFn - })) - - return { - redirect: redirectFn, - send: sendFn, - status: statusFn - } -} - -module.exports.getParameters = () => Object.assign({}, parameters) - -module.exports.getParsedConfig = () => { - return yaml.safeLoad(sampleData.config1, 'utf8') -} - -module.exports.getUserAgent = () => { - return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36' -} - -module.exports.wrappedRequest = options => { - const newOptions = typeof options === 'string' - ? `http://localhost:${config.get('port')}${options}` - : Object.assign({}, options, { - uri: `http://localhost:${config.get('port')}${options.uri}` - }) - - return request(newOptions) -} diff --git a/test/helpers/sampleData.js b/test/helpers/sampleData.js deleted file mode 100644 index aa1bc00..0000000 --- a/test/helpers/sampleData.js +++ /dev/null @@ -1,196 +0,0 @@ -module.exports.config1 = `# Name of the property. You can have multiple properties with completely -# different config blocks for different sections of your site. -# For example, you can have one property to handle comment submission and -# another one to handle posts. -comments: - # (*) REQUIRED - # - # Names of the fields the form is allowed to submit. If a field that is - # not here is part of the request, an error will be thrown. - allowedFields: ["name", "email", "url", "message"] - - # When allowedOrigins is defined, only requests sent from one of the domains - # listed will be accepted. - allowedOrigins: ["localhost", "eduardoboucas.com"] - - auth: - required: false - - # (*) REQUIRED - # - # Name of the branch being used. Must match the one sent in the URL of the - # request. - branch: "master" - - # List of fields to be populated automatically by Staticman and included in - # the data file. Keys are the name of the field. The value can be an object - # with a \`type\` property, which configures the generated field, or any value - # to be used directly (e.g. a string, number or array) - generatedFields: - date: - type: date - options: - format: "timestamp-seconds" - - # The format of the generated data files. Accepted values are "json", "yaml" - # or "frontmatter" - format: "yaml" - - # Whether entries need to be appproved before they are published to the main - # branch. If set to \`true\`, a pull request will be created for your approval. - # Otherwise, entries will be published to the main branch automatically. - moderation: false - - # Name of the site. Used in notification emails. - name: "eduardoboucas.com" - - notifications: - enabled: true - - # (*) REQUIRED - # - # Destination path (directory) for the data files. Accepts placeholders. - path: "_data/comments/{options.slug}" - - # (*) REQUIRED - # - # Destination path (filename) for the data files. Accepts placeholders. - filename: "entry{@timestamp}" - - # Names of required fields. If any of these isn't in the request or is empty, - # an error will be thrown. - requiredFields: ["name", "email", "message"] - - # List of transformations to apply to any of the fields supplied. Keys are - # the name of the field and values are possible transformation types. - transforms: - email: md5 - - reCaptcha: - enabled: true - siteKey: "123456789" - secret: "DTK5WxH6117ez/piZpmkvyAtieYWlu60+SApt9hFRMfNs4WBC0mvRtsW9Jmhz4fJYDcIX18wKHV6KYh3PXYN3d/pozCskwwkuJq0qHJQHrTycjgrGS5mti4QgrMYP0rq2p5hMTgPL/UK0lwkxuRRcxnvxqRlkZHMv6o/CUkZOVnkJ8lGqWa8uJAEIv/9rd6Bm12+F1ezLJZ+LogebHEDpyJWz9kwum9bFBQqZbun+43rxzJBQmAGQEWZ4hshY2aLSAyBpr/pjSDUwRGtwoBh8Ee1qKNuMzY0XVUOn+dcHrkpQotmKL4TMFQN4slo/lVKmfXW5N6t9vdP/lGmIiVXdw==" - - githubAuth: - clientId: "L4M3LIshioHbe3j+vMxEbGlCGDhyIcQF2jhmVOUp8DqC+RqNgvZSQp7qYYmjPPoyjFCVOsu5aHwcD1FkMlEaxLTqYOYUeq49Wb6uxePTBycmW14JI6fiM/PYTm6nqKH5fB/7wnohVgK+/1IVAF6DA7UAs0Ju+srlnqEbn30f84sySOeR+V6t9aF7OiF9DsGedsTfVrfj8opptwQe7nycsxQaTxvmwgQgP9FrDYH+PGy/3ThpQsPj+/Mnvbnn7PMJEJlZFtGZsMWWcE2anJlJ7fbHKNPNNg6l2qosh6/kMTrloCU6wA67ouai0OFiNR+gyQaqUiL3NMgN4k39nZuwOg==" - clientSecret: "0anSY5FBW+YF8BuFRRYxKX0yjDGU8/HhdirQMh+xFOEIvBQ8n/PdYi3qv2p4ngpFo5gb3PY2W6oWvHYLGgbFFse3YvzP5cbKRG0BN90hanlpVwmtAsapC7UepvOOUmNCRKHJ/pYICYcleUX/xGRiTugl9rcVw1MLg7kxcCuEMWcsBc9qCA9YHRcN3ucP+rT9x/2hLMiUmv3glYYZNgyQ3x1iqpOuTAIMeIkxMQo83vxGR57fpAx4+Yn1+hALhSl3sGaesUZhY9Py/OEZDFVWiN9RvrM4ND0IcfqyaQ4DuRPu2g8Es5fbmgSQoqkNPOcHT2+40pDbz3FPgz6QJou+pQ==" - redirectUri: "https://my-test-site.com" - - gitlabAuth: - clientId: "Rr9d1XmVVMe8ogldH6rBtdGhf49c29ldwcBVsiMn6DLRAiYWmHY08eKC6xLnP6mXwMe/qCHJ6JMKURDODL8Yjm+nQf09zynkIRCr4J7tRHh4bPAYXPG+W1+TK7l8QD4gC+WXamxJiggwGCaDtNylI1QQhbKtevv7n/T+Iq98rBj8SLxxpi3qR0oZeN/zsoQsDYgzZ+HgvA3hY+5H897ijx1oBjoTsfI1Sfx8Qqix/QLZoXorOUJyEo+83WWvTEgo8X3OyFbXGZ758Kw6A7fcHxu8oVAjDvtFJFiwrDb4iBz9rffx7llZXjkcjzYzfwFcjG7mzZnfYgn9WcCwO7zlsA==" - clientSecret: "1zzjCrOZQ9dVs1p/WLgT8Lvwez3EKd1tp3D+7P5uGlEdqP1RN7kQvcaqOmOpm5SIY6g+yKJQGZq9G/IqUoKdsZDhA2VGYGXVzETU6eB48AL0OXlFumhjzJoGAXpnqDWzfevglkVuAkivBv6o9S1r/FL1GydwlRwWcYU6NNJjjkB04A00B4s0J7FRR3VFRxpJqDznHgXgT32E2+F3s6enh9/aErqi9uqn+iVtw7gvbd9PN1ejlo95R3BVNKUxNi2Dn4BbsH3MjQG4DyuzX8BiS9Nb+Xt+CwLygTT/i4C5Aj+KkMjAEiYOyttFbk3jkvYVXJ1XtW+taloBVPYCHgDzmg==" - redirectUri: "https://my-test-site-2.com"` - -module.exports.config2 = `{ - "comments": { - "allowedFields": [ - "name", - "email", - "url", - "message" - ], - "allowedOrigins": [ - "localhost", - "eduardoboucas.com" - ], - "branch": "master", - "generatedFields": { - "date": { - "type": "date", - "options": { - "format": "timestamp-seconds" - } - } - }, - "format": "yaml", - "moderation": false, - "name": "eduardoboucas.com", - "notifications": { - "enabled": true - }, - "path": "_data/comments/{options.slug}", - "filename": "entry{@timestamp}", - "requiredFields": [ - "name", - "email", - "message" - ], - "transforms": { - "email": "md5" - }, - "reCaptcha": { - "enabled": true, - "siteKey": "123456789", - "secret": "ZguqL+tEc+XPFmWZdaFxWqqB1xtwe79o5SLWrjuAIA/45N5hPQk3HcKKfLBl0ZyqVff+JEY76xLBVFn+jn4Wc8egnKtA7HJfjMpbR4WdSFVm/Hcca3L3id9JNYmGPFRJmzOlG2qjSr2Z8y3Y1i02EjQrzUcfqCuCfeEbZxmCNp0=" - } - } -}` - -module.exports.config3 = `comments: - allowedFields: ["name", "email", "url", "message"] - allowedOrigins: ["localhost", "eduardoboucas.com"] - branch: "master" - generatedFields: - date: - type date - options: - format: "timestamp-seconds" - format: "yaml" - moderation: false - name: "eduardoboucas.com" - notifications: - enabled: true - path: "_data/comments/{options.slug}" - filename: "entry{@timestamp}" - requiredFields: ["name", "email", "message"] - transforms: - email: md5 - reCaptcha: - enabled: true - siteKey: "123456789" - secret: "@reCaptchaSecret@"` - -module.exports.configInvalidYML = `invalid: -- x -y - foo -bar -` - -module.exports.prBody1 = `Dear human, - -Here's a new entry for your approval. :tada: - -Merge the pull request to accept it, or close it to send it away. - -:heart: Your friend [IPL contribute](https://github.com/InternetPublicLibrary/contribute) :muscle: - ---- -| Field | Content | -| ------- | -------------------------------- | -| name | John | -| email | 017dab421e1e1cf6257bcadc0d289c62 | -| url | http://johndoe.com | -| address | | -| message | This is a test entry | -| date | 1485597255 | - -` - -module.exports.prBody2 = `Dear human, - -Here's a new entry for your approval. :tada: - -Merge the pull request to accept it, or close it to send it away. - -:heart: Your friend [IPL contribute](https://github.com/InternetPublicLibrary/contribute) :muscle: - ---- -| Field | Content | -| ------- | -------------------------------- | -| name | John | -| email | 017dab421e1e1cf6257bcadc0d289c62 | -| url | http://johndoe.com | -| address | | -| message | This is a test entry | -| date | 1485597255 |` diff --git a/test/unit/controllers/auth.test.js b/test/unit/controllers/auth.test.js deleted file mode 100644 index 5c28155..0000000 --- a/test/unit/controllers/auth.test.js +++ /dev/null @@ -1,314 +0,0 @@ -const auth = require('../../../controllers/auth') -const helpers = require('./../../helpers') -const nock = require('nock') -const Staticman = require('./../../../lib/Staticman') -const User = require('../../../lib/models/User') - -Staticman.prototype.getSiteConfig = function () { - return Promise.resolve(helpers.getConfig()) -} - -let req -let res - -beforeEach(() => { - req = helpers.getMockRequest() - res = helpers.getMockResponse() -}) - -describe('Auth controller', () => { - describe('GitHub', () => { - test('authenticates to GitHub with the given code and returns the authenticated user', async () => { - const mockAccessToken = 'qwertyuiop' - const mockCode = '1q2w3e4r' - const mockUser = { - login: 'johndoe', - name: 'John Doe', - email: 'johndoe@test.com' - } - - const siteConfig = helpers.getConfig() - - nock(/github\.com/) - .post('/login/oauth/access_token') - .query({ - client_id: siteConfig.get('githubAuth.clientId'), - client_secret: siteConfig.get('githubAuth.clientSecret'), - code: mockCode, - redirect_uri: siteConfig.get('githubAuth.redirectUri') - }) - .reply(200, { - access_token: mockAccessToken - }) - - nock((/github\.com/), { - reqheaders: { - authorization: `token ${mockAccessToken}` - } - }) - .get('/user') - .reply(200, mockUser) - - const reqWithQuery = Object.assign({}, req, { - query: { - code: mockCode - } - }) - - await auth(reqWithQuery, res) - expect(res.send).toHaveBeenCalledTimes(1) - expect(helpers.decrypt(res.send.mock.calls[0][0].accessToken)).toBe(mockAccessToken) - expect(res.send.mock.calls[0][0].user).toEqual(new User('github', mockUser.login, mockUser.email, mockUser.name)) - }) - - test('authenticates to GitHub with the given code and returns the original GitHub user when using v2 API', async () => { - const mockAccessToken = 'qwertyuiop' - const mockCode = '1q2w3e4r' - const mockUser = { - login: 'johndoe' - } - - const siteConfig = helpers.getConfig() - - nock(/github\.com/) - .post('/login/oauth/access_token') - .query({ - client_id: siteConfig.get('githubAuth.clientId'), - client_secret: siteConfig.get('githubAuth.clientSecret'), - code: mockCode, - redirect_uri: siteConfig.get('githubAuth.redirectUri') - }) - .reply(200, { - access_token: mockAccessToken - }) - - nock((/github\.com/), { - reqheaders: { - authorization: `token ${mockAccessToken}` - } - }) - .get('/user') - .reply(200, mockUser) - - const reqWithQuery = Object.assign({}, req, { - params: { - service: 'github', - version: '2' - }, - query: { - code: mockCode - } - }) - - await auth(reqWithQuery, res) - expect(res.send).toHaveBeenCalledTimes(1) - expect(helpers.decrypt(res.send.mock.calls[0][0].accessToken)).toBe(mockAccessToken) - expect(res.send.mock.calls[0][0].user).toEqual(mockUser) - }) - - test('returns a 401 response when unable to get an access token from GitHub', async () => { - const mockCode = '1q2w3e4r' - const siteConfig = helpers.getConfig() - - nock(/github\.com/) - .post('/login/oauth/access_token') - .query({ - client_id: siteConfig.get('githubAuth.clientId'), - client_secret: siteConfig.get('githubAuth.clientSecret'), - code: mockCode, - redirect_uri: siteConfig.get('githubAuth.redirectUri') - }) - .reply(401, { - error: 'invalid_code' - }) - - const reqWithQuery = Object.assign({}, req, { - params: { - service: 'github', - version: '2' - }, - query: { - code: mockCode - } - }) - - await auth(reqWithQuery, res) - expect(res.status.mock.calls[0][0]).toBe(401) - expect(res.send.mock.calls[0][0].statusCode).toBe(401) - expect(res.send.mock.calls[0][0].message).toContain('invalid_code') - }) - - test('returns a 401 response when an incorrect access token is used for the GitHub API', async () => { - const mockAccessToken = 'qwertyuiop' - const mockCode = '1q2w3e4r' - - const siteConfig = helpers.getConfig() - - nock(/github\.com/) - .post('/login/oauth/access_token') - .query({ - client_id: siteConfig.get('githubAuth.clientId'), - client_secret: siteConfig.get('githubAuth.clientSecret'), - code: mockCode, - redirect_uri: siteConfig.get('githubAuth.redirectUri') - }) - .reply(200, { - access_token: mockAccessToken - }) - - nock((/github\.com/), { - reqheaders: { - authorization: `token ${mockAccessToken}` - } - }) - .get('/user') - .reply(401, { - message: 'Unauthorized' - }) - - const reqWithQuery = Object.assign({}, req, { - params: { - service: 'github', - version: '2' - }, - query: { - code: mockCode - } - }) - - await auth(reqWithQuery, res) - expect(res.status.mock.calls[0][0]).toBe(401) - expect(res.send.mock.calls[0][0].statusCode).toBe(401) - expect(res.send.mock.calls[0][0].message).toContain('Unauthorized') - }) - }) - - describe('GitLab', () => { - test('authenticates to GitLab with the given code and returns the authenticated user', () => { - const mockAccessToken = 'qwertyuiop' - const mockCode = '1q2w3e4r' - const mockUser = { - username: 'johndoe', - name: 'John Doe', - email: 'johndoe@test.com' - } - - const siteConfig = helpers.getConfig() - - nock(/gitlab\.com/) - .post('/oauth/token') - .query({ - client_id: siteConfig.get('gitlabAuth.clientId'), - client_secret: siteConfig.get('gitlabAuth.clientSecret'), - code: mockCode, - grant_type: 'authorization_code', - redirect_uri: siteConfig.get('gitlabAuth.redirectUri') - }) - .reply(200, { - access_token: mockAccessToken - }) - - nock(/gitlab\.com/, { - reqheaders: { - authorization: `Bearer ${mockAccessToken}` - } - }) - .get('/api/v4/user') - .reply(200, mockUser) - - const reqWithQuery = Object.assign({}, req, { - params: { - service: 'gitlab' - }, - query: { - code: mockCode - } - }) - - return auth(reqWithQuery, res).then(result => { - expect(res.send).toHaveBeenCalledTimes(1) - expect(helpers.decrypt(res.send.mock.calls[0][0].accessToken)).toBe(mockAccessToken) - expect(res.send.mock.calls[0][0].user) - .toEqual(new User('gitlab', mockUser.username, mockUser.email, mockUser.name)) - }) - }) - - test('returns a 401 response when unable to get an access token from GitLab', () => { - const mockCode = '1q2w3e4r' - const siteConfig = helpers.getConfig() - - nock(/gitlab\.com/) - .post('/oauth/token') - .query({ - client_id: siteConfig.get('gitlabAuth.clientId'), - client_secret: siteConfig.get('gitlabAuth.clientSecret'), - code: mockCode, - grant_type: 'authorization_code', - redirect_uri: siteConfig.get('gitlabAuth.redirectUri') - }) - .reply(401, { - error: 'invalid_code' - }) - - const reqWithQuery = Object.assign({}, req, { - params: { - service: 'gitlab' - }, - query: { - code: mockCode - } - }) - - return auth(reqWithQuery, res).then(result => { - expect(res.status.mock.calls[0][0]).toBe(401) - expect(res.send.mock.calls[0][0].statusCode).toBe(401) - expect(res.send.mock.calls[0][0].message).toContain('invalid_code') - }) - }) - - test('returns a 401 response when an incorrect access token is used for the GitLab API', () => { - const mockAccessToken = 'qwertyuiop' - const mockCode = '1q2w3e4r' - - const siteConfig = helpers.getConfig() - - nock(/gitlab\.com/) - .post('/oauth/token') - .query({ - client_id: siteConfig.get('gitlabAuth.clientId'), - client_secret: siteConfig.get('gitlabAuth.clientSecret'), - code: mockCode, - grant_type: 'authorization_code', - redirect_uri: siteConfig.get('gitlabAuth.redirectUri') - }) - .reply(200, { - access_token: mockAccessToken - }) - - nock(/gitlab\.com/, { - reqheaders: { - authorization: `Bearer ${mockAccessToken}` - } - }) - .get('/api/v4/user') - .reply(401, { - message: '401 Unauthorized' - }) - - const reqWithQuery = Object.assign({}, req, { - params: { - service: 'gitlab' - }, - query: { - code: mockCode - } - }) - - return auth(reqWithQuery, res).then(result => { - expect(res.status.mock.calls[0][0]).toBe(401) - expect(res.send.mock.calls[0][0].statusCode).toBe(401) - expect(res.send.mock.calls[0][0].message).toContain('401 Unauthorized') - }) - }) - }) -}) diff --git a/test/unit/controllers/connect.test.js b/test/unit/controllers/connect.test.js deleted file mode 100644 index 0008858..0000000 --- a/test/unit/controllers/connect.test.js +++ /dev/null @@ -1,108 +0,0 @@ -const helpers = require('./../../helpers') - -let req, res - -beforeEach(() => { - req = helpers.getMockRequest() - res = helpers.getMockResponse() - - jest.resetModules() - jest.unmock('@octokit/rest') -}) - -describe('Connect controller', () => { - test('accepts the invitation if one is found and replies with "OK!"', () => { - const invitationId = 123 - const mockAcceptRepoInvite = jest.fn(() => Promise.resolve()) - const mockGetRepoInvites = jest.fn(() => Promise.resolve({ - data: [ - { - id: invitationId, - repository: { - full_name: `${req.params.username}/${req.params.repository}` - } - } - ] - })) - - jest.mock('@octokit/rest', () => - _ => ({ - authenticate: jest.fn(), - repos: { - acceptInvitation: mockAcceptRepoInvite, - listInvitationsForAuthenticatedUser: mockGetRepoInvites - } - }) - ) - - const connect = require('./../../../controllers/connect') - - return connect(req, res).then(response => { - expect(mockGetRepoInvites).toHaveBeenCalledTimes(1) - expect(mockAcceptRepoInvite).toHaveBeenCalledTimes(1) - expect(res.send.mock.calls[0][0]).toBe('OK!') - }) - }) - - test('returns a 404 and an error message if a matching invitation is not found', () => { - const invitationId = 123 - const mockAcceptRepoInvite = jest.fn(() => Promise.resolve()) - const mockGetRepoInvites = jest.fn(() => Promise.resolve({ - data: [ - { - id: invitationId, - repository: { - full_name: `${req.params.username}/anotherrepo` - } - } - ] - })) - - jest.mock('@octokit/rest', () => - _ => ({ - authenticate: jest.fn(), - repos: { - acceptInvitation: mockAcceptRepoInvite, - listInvitationsForAuthenticatedUser: mockGetRepoInvites - } - }) - ) - - const connect = require('./../../../controllers/connect') - - return connect(req, res).then(response => { - expect(mockGetRepoInvites).toHaveBeenCalledTimes(1) - expect(mockAcceptRepoInvite).not.toHaveBeenCalled() - expect(res.send.mock.calls[0][0]).toBe('Invitation not found') - expect(res.status.mock.calls[0][0]).toBe(404) - }) - }) - - test('returns a 404 and an error message if the response from GitHub is invalid', () => { - const mockAcceptRepoInvite = jest.fn(() => Promise.resolve()) - const mockGetRepoInvites = jest.fn(() => Promise.resolve({ - data: { - invalidProperty: 'invalidValue' - } - })) - - jest.mock('@octokit/rest', () => - _ => ({ - authenticate: jest.fn(), - repos: { - acceptInvitation: mockAcceptRepoInvite, - listInvitationsForAuthenticatedUser: mockGetRepoInvites - } - }) - ) - - const connect = require('./../../../controllers/connect') - - return connect(req, res).then(response => { - expect(mockGetRepoInvites).toHaveBeenCalledTimes(1) - expect(mockAcceptRepoInvite).not.toHaveBeenCalled() - expect(res.send.mock.calls[0][0]).toBe('Invitation not found') - expect(res.status.mock.calls[0][0]).toBe(404) - }) - }) -}) diff --git a/test/unit/controllers/encrypt.test.js b/test/unit/controllers/encrypt.test.js deleted file mode 100644 index 004bce5..0000000 --- a/test/unit/controllers/encrypt.test.js +++ /dev/null @@ -1,36 +0,0 @@ -const config = require('./../../../config') -const helpers = require('./../../helpers') - -let encrypt -let mockEncryptFn -let mockImportKeyFn -let req -let res - -beforeEach(() => { - req = helpers.getMockRequest() - res = helpers.getMockResponse() - - mockEncryptFn = jest.fn(() => 'Encrypted text') - mockImportKeyFn = jest.fn() - - jest.mock('node-rsa', () => { - return jest.fn(() => ({ - encrypt: mockEncryptFn, - importKey: mockImportKeyFn - })) - }) - - encrypt = require('./../../../controllers/encrypt') -}) - -describe('Encrypt controller', () => { - test('returns an encrypted version of the given text', () => { - req.params.text = 'This is the text to encrypt' - - encrypt(req, res) - - expect(mockEncryptFn.mock.calls[0][0]).toBe(req.params.text) - expect(res.send.mock.calls[0][0]).toBe('Encrypted text') - }) -}) diff --git a/test/unit/controllers/handlePR.test.js b/test/unit/controllers/handlePR.test.js deleted file mode 100644 index 786525b..0000000 --- a/test/unit/controllers/handlePR.test.js +++ /dev/null @@ -1,210 +0,0 @@ -const helpers = require('./../../helpers') -const sampleData = require('./../../helpers/sampleData') -const Review = require('../../../lib/models/Review') - -let mockSetConfigPathFn -let mockProcessMergeFn -let req - -// Mock Staticman module -jest.mock('../../../lib/Staticman', () => { - return jest.fn().mockImplementation(() => { - return { - setConfigPath: mockSetConfigPathFn, - processMerge: mockProcessMergeFn - } - }) -}) - -beforeEach(() => { - mockSetConfigPathFn = jest.fn() - mockProcessMergeFn = jest.fn() - req = helpers.getMockRequest() - res = helpers.getMockResponse() - - jest.resetAllMocks() - jest.resetModules() -}) - -describe('HandlePR controller', () => { - test('ignores pull requests from branches not prefixed with `staticman_`', async () => { - const pr = { - number: 123, - title: 'Some random PR', - body: 'Unrelated review body', - head: { - ref: 'some-other-branch' - }, - base: { - ref: 'master' - }, - merged: false, - repository: { - name: req.params.repository, - owner: { - login: req.params.username - } - }, - state: 'open' - } - - const mockReview = new Review(pr.title, pr.body, 'false', pr.head.ref, pr.base.ref) - const mockGetReview = jest.fn().mockResolvedValue(mockReview) - - jest.mock('../../../lib/GitHub', () => { - return jest.fn().mockImplementation(() => { - return { - getReview: mockGetReview - } - }) - }) - - const handlePR = require('./../../../controllers/handlePR') - - let response = await handlePR(req.params.repository, pr) - expect(mockGetReview).toHaveBeenCalledTimes(1) - expect(response).toBe(null) - }) - - describe('processes notifications if the pull request has been merged', () => { - test('do nothing if PR body doesn\'t match template', async () => { - const pr = { - number: 123, - title: 'Add Staticman data', - body: sampleData.prBody2, - head: { - ref: 'staticman_1234567' - }, - base: { - ref: 'master' - }, - merged: true, - repository: { - name: req.params.repository, - owner: { - login: req.params.username - } - }, - state: 'open' - } - - const mockReview = new Review(pr.title, pr.body, 'false', pr.head.ref, pr.base.ref) - const mockGetReview = jest.fn().mockResolvedValue(mockReview) - const mockDeleteBranch = jest.fn() - - jest.mock('../../../lib/GitHub', () => { - return jest.fn().mockImplementation(() => { - return { - getReview: mockGetReview, - deleteBranch: mockDeleteBranch - } - }) - }) - - const handlePR = require('./../../../controllers/handlePR') - - await handlePR(req.params.repository, pr) - expect(mockGetReview).toHaveBeenCalledTimes(1) - expect(mockDeleteBranch).not.toHaveBeenCalled() - }) - - test('abort and return an error if `processMerge` fails', async () => { - const pr = { - number: 123, - title: 'Add Staticman data', - body: sampleData.prBody1, - head: { - ref: 'staticman_1234567' - }, - base: { - ref: 'master' - }, - merged: true, - repository: { - name: req.params.repository, - owner: { - login: req.params.username - } - }, - state: 'closed' - } - - const mockReview = new Review(pr.title, pr.body, 'merged', pr.head.ref, pr.base.ref) - const mockGetReview = jest.fn().mockResolvedValue(mockReview) - const mockDeleteBranch = jest.fn() - - jest.mock('../../../lib/GitHub', () => { - return jest.fn().mockImplementation(() => { - return { - getReview: mockGetReview, - deleteBranch: mockDeleteBranch - } - }) - }) - - const errorMessage = 'some error' - - mockProcessMergeFn = jest.fn(() => { - throw errorMessage - }) - - const handlePR = require('./../../../controllers/handlePR') - - expect.assertions(4) - try { - await handlePR(req.params.repository, pr) - } catch (e) { - expect(e).toBe(errorMessage) - expect(mockGetReview).toHaveBeenCalledTimes(1) - // expect(mockSetConfigPathFn.mock.calls.length).toBe(1) - // expect(mockProcessMergeFn.mock.calls.length).toBe(1) - expect(mockSetConfigPathFn).toHaveBeenCalledTimes(1) - expect(mockProcessMergeFn).toHaveBeenCalledTimes(1) - } - }) - - test('delete the branch if the pull request is closed', async () => { - const pr = { - number: 123, - title: 'Add Staticman data', - body: sampleData.prBody1, - head: { - ref: 'staticman_1234567' - }, - base: { - ref: 'master' - }, - merged: true, - repository: { - name: req.params.repository, - owner: { - login: req.params.username - } - }, - state: 'closed' - } - - const mockReview = new Review(pr.title, pr.body, 'merged', pr.head.ref, pr.base.ref) - const mockDeleteBranch = jest.fn() - const mockGetReview = jest.fn().mockResolvedValue(mockReview) - - jest.mock('../../../lib/GitHub', () => { - return jest.fn().mockImplementation(() => { - return { - deleteBranch: mockDeleteBranch, - getReview: mockGetReview - } - }) - }) - - const handlePR = require('./../../../controllers/handlePR') - - await handlePR(req.params.repository, pr) - expect(mockGetReview).toHaveBeenCalledTimes(1) - expect(mockGetReview.mock.calls[0][0]).toEqual(123) - expect(mockDeleteBranch).toHaveBeenCalledTimes(1) - expect(mockSetConfigPathFn.mock.calls.length).toBe(1) - expect(mockProcessMergeFn.mock.calls.length).toBe(1) - }) - }) -}) diff --git a/test/unit/controllers/process.test.js b/test/unit/controllers/process.test.js deleted file mode 100644 index ce05500..0000000 --- a/test/unit/controllers/process.test.js +++ /dev/null @@ -1,506 +0,0 @@ -const config = require('./../../../config') -const errorHandler = require('./../../../lib/ErrorHandler').getInstance() -const githubToken = config.get('githubToken') -const mockHelpers = require('./../../helpers') -const sampleData = require('./../../helpers/sampleData') - -let mockSiteConfig -let req - -beforeEach(() => { - jest.resetModules() - - mockSiteConfig = mockHelpers.getConfig() - - req = mockHelpers.getMockRequest() -}) - -describe('Process controller', () => { - describe('checkRecaptcha', () => { - test('does nothing if reCaptcha is not enabled in config', () => { - mockSiteConfig.set('reCaptcha.enabled', false) - - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - getSiteConfig: () => Promise.resolve(mockSiteConfig) - })) - }) - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - return checkRecaptcha(staticman, req).then(response => { - expect(response).toBe(false) - }) - }) - - test('throws an error if reCaptcha block is not in the request body', () => { - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - getSiteConfig: () => Promise.resolve(mockSiteConfig) - })) - }) - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - mockSiteConfig.set('reCaptcha.enabled', true) - - req.body = { - options: {} - } - - return checkRecaptcha(staticman, req).catch(err => { - expect(err._smErrorCode).toBe('RECAPTCHA_MISSING_CREDENTIALS') - }) - }) - - test('throws an error if reCaptcha site key is not in the request body', () => { - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - getSiteConfig: () => Promise.resolve(mockSiteConfig) - })) - }) - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - options: { - reCaptcha: { - secret: '1q2w3e4r' - } - } - } - - return checkRecaptcha(staticman, req).catch(err => { - expect(err._smErrorCode).toBe('RECAPTCHA_MISSING_CREDENTIALS') - }) - }) - - test('throws an error if reCaptcha secret is not in the request body', () => { - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - getSiteConfig: () => Promise.resolve(mockSiteConfig) - })) - }) - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - options: { - reCaptcha: { - siteKey: '123456789' - } - } - } - - return checkRecaptcha(staticman, req).catch(err => { - expect(err._smErrorCode).toBe('RECAPTCHA_MISSING_CREDENTIALS') - }) - }) - - test('throws an error if the reCatpcha secret fails to decrypt', () => { - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - decrypt: () => { - throw 'someError' - }, - getSiteConfig: () => Promise.resolve(mockSiteConfig) - })) - }) - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - options: { - reCaptcha: { - siteKey: '123456789', - secret: '1q2w3e4r' - } - } - } - - return checkRecaptcha(staticman, req).catch(err => { - expect(err._smErrorCode).toBe('RECAPTCHA_CONFIG_MISMATCH') - }) - }) - - test('throws an error if the reCatpcha siteKey provided does not match the one in config', () => { - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - getSiteConfig: () => Promise.resolve(mockSiteConfig) - })) - }) - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - options: { - reCaptcha: { - siteKey: '987654321', - secret: mockSiteConfig.getRaw('reCaptcha.secret') - } - } - } - - return checkRecaptcha(staticman, req).catch(err => { - expect(err._smErrorCode).toBe('RECAPTCHA_CONFIG_MISMATCH') - }) - }) - - test('throws an error if the reCatpcha secret provided does not match the one in config', () => { - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - getSiteConfig: () => Promise.resolve(mockSiteConfig) - })) - }) - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - options: { - reCaptcha: { - siteKey: mockSiteConfig.get('reCaptcha.siteKey'), - secret: mockHelpers.encrypt('some other secret') - } - } - } - - return checkRecaptcha(staticman, req).catch(err => { - expect(err._smErrorCode).toBe('RECAPTCHA_CONFIG_MISMATCH') - }) - }) - - test('initialises and triggers a verification from the reCaptcha module', () => { - const mockInitFn = jest.fn() - const mockVerifyFn = jest.fn((req, reCaptchaCallback) => { - reCaptchaCallback(false) - }) - - jest.mock('express-recaptcha', () => { - return { - init: mockInitFn, - verify: mockVerifyFn - } - }) - - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - decrypt: mockHelpers.decrypt, - getSiteConfig: () => Promise.resolve(mockSiteConfig) - })) - }) - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - options: { - reCaptcha: { - siteKey: mockSiteConfig.get('reCaptcha.siteKey'), - secret: mockSiteConfig.getRaw('reCaptcha.secret') - } - } - } - - return checkRecaptcha(staticman, req).then(response => { - expect(response).toBe(true) - expect(mockInitFn.mock.calls.length).toBe(1) - expect(mockInitFn.mock.calls[0][0]).toBe(mockSiteConfig.get('reCaptcha.siteKey')) - expect(mockInitFn.mock.calls[0][1]).toBe(mockSiteConfig.get('reCaptcha.secret')) - expect(mockVerifyFn.mock.calls[0][0]).toBe(req) - }) - }) - - test('displays an error if the reCaptcha verification fails', () => { - const reCaptchaError = new Error('someError') - const mockInitFn = jest.fn() - const mockVerifyFn = jest.fn((req, reCaptchaCallback) => { - reCaptchaCallback(reCaptchaError) - }) - - jest.mock('express-recaptcha', () => { - return { - init: mockInitFn, - verify: mockVerifyFn - } - }) - - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - decrypt: mockHelpers.decrypt, - getSiteConfig: () => Promise.resolve(mockSiteConfig) - })) - }) - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - options: { - reCaptcha: { - siteKey: mockSiteConfig.get('reCaptcha.siteKey'), - secret: mockSiteConfig.getRaw('reCaptcha.secret') - } - } - } - - return checkRecaptcha(staticman, req).catch(err => { - expect(err).toEqual({ - _smErrorCode: reCaptchaError - }) - }) - }) - }) - - describe('createConfigObject', () => { - const createConfigObject = require('./../../../controllers/process').createConfigObject - - test('creates a config object for version 1 of API', () => { - const configv1 = { - file: '_config.yml', - path: 'staticman' - } - - const config1 = createConfigObject('1') - const config2 = createConfigObject('1', 'someProperty') - - expect(config1).toEqual(configv1) - expect(config2).toEqual(configv1) - }) - - test('creates a config object for version 2+ of API', () => { - const configv2File = 'staticman.yml' - - const config3 = createConfigObject('2') - const config4 = createConfigObject('2', 'someProperty') - const config5 = createConfigObject() - - expect(config3).toEqual({ - file: configv2File, - path: '' - }) - expect(config4).toEqual({ - file: configv2File, - path: 'someProperty' - }) - expect(config5).toEqual({ - file: configv2File, - path: '' - }) - }) - }) - - describe('process', () => { - const processFn = require('./../../../controllers/process').process - - test('send a redirect to the URL provided, if the `redirect` option is provided, if `processEntry` succeeds', () => { - const redirectUrl = 'https://eduardoboucas.com' - const mockProcessEntry = jest.fn((fields, options) => Promise.resolve({ - fields: ['name', 'email'], - redirect: redirectUrl - })) - - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - processEntry: mockProcessEntry - })) - }) - - const res = mockHelpers.getMockResponse() - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - fields: { - name: 'Eduardo Boucas', - email: 'mail@eduardoboucas.com' - }, - options: { - reCaptcha: { - siteKey: mockSiteConfig.get('reCaptcha.siteKey'), - secret: mockSiteConfig.getRaw('reCaptcha.secret') - }, - redirect: redirectUrl - } - } - req.query = {} - - return processFn(staticman, req, res).then(response => { - expect(res.redirect.mock.calls.length).toBe(1) - expect(res.redirect.mock.calls[0][0]).toBe(redirectUrl) - }) - }) - - test('deliver an object with the processed fields if `processEntry` succeeds', () => { - const fields = { - name: 'Eduardo Boucas', - email: 'mail@eduardoboucas.com' - } - const mockProcessEntry = jest.fn((fields, options) => Promise.resolve({ - fields: fields - })) - - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - processEntry: mockProcessEntry - })) - }) - - const res = mockHelpers.getMockResponse() - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - fields: fields, - options: { - reCaptcha: { - siteKey: mockSiteConfig.get('reCaptcha.siteKey'), - secret: mockSiteConfig.getRaw('reCaptcha.secret') - } - } - } - req.query = {} - - return processFn(staticman, req, res).then(response => { - expect(res.send.mock.calls.length).toBe(1) - expect(res.send.mock.calls[0][0]).toEqual({ - fields: fields, - success: true - }) - }) - }) - - test('reject if `processEntry` fails', () => { - const processEntryError = new Error('someError') - const mockProcessEntry = jest.fn((fields, options) => { - return Promise.reject(processEntryError) - }) - - jest.mock('./../../../lib/Staticman', () => { - return jest.fn(parameters => ({ - processEntry: mockProcessEntry - })) - }) - - const res = mockHelpers.getMockResponse() - - const checkRecaptcha = require('./../../../controllers/process').checkRecaptcha - const Staticman = require('./../../../lib/Staticman') - const staticman = new Staticman(req.params) - - req.body = { - fields: { - name: 'Eduardo Boucas', - email: 'mail@eduardoboucas.com' - }, - options: { - reCaptcha: { - siteKey: mockSiteConfig.get('reCaptcha.siteKey'), - secret: mockSiteConfig.getRaw('reCaptcha.secret') - } - } - } - req.query = {} - - return processFn(staticman, req, res).catch(err => { - expect(err).toEqual(processEntryError) - }) - }) - }) - - describe('sendResponse', () => { - const sendResponse = require('./../../../controllers/process').sendResponse - - test('redirects if there is a `redirect` option and no errors', () => { - const data = { - redirect: 'https://eduardoboucas.com' - } - - const res = mockHelpers.getMockResponse() - - sendResponse(res, data) - - expect(res.redirect.mock.calls.length).toBe(1) - expect(res.redirect.mock.calls[0][0]).toBe(data.redirect) - }) - - test('redirects if there is a `redirectError` option there is an error', () => { - const data = { - err: 'someError', - redirect: 'https://eduardoboucas.com', - redirectError: 'https://eduardoboucas.com/error' - } - - const res = mockHelpers.getMockResponse() - - sendResponse(res, data) - - expect(res.redirect.mock.calls.length).toBe(1) - expect(res.redirect.mock.calls[0][0]).toBe(data.redirectError) - }) - - test('sends a 200 with a fields object if there are no errors', () => { - const data = { - fields: { - name: 'Eduardo Bouças', - email: 'mail@eduardoboucas.com' - } - } - - const res = mockHelpers.getMockResponse() - - sendResponse(res, data) - - expect(res.send.mock.calls.length).toBe(1) - expect(res.send.mock.calls[0][0]).toEqual({ - success: true, - fields: data.fields - }) - expect(res.status.mock.calls.length).toBe(1) - expect(res.status.mock.calls[0][0]).toBe(200) - }) - - test('sends a 500 with an error object if there is an error', () => { - const data = { - err: { - _smErrorCode: 'missing-input-secret' - }, - redirect: 'https://eduardoboucas.com' - } - - const res = mockHelpers.getMockResponse() - - sendResponse(res, data) - - expect(res.send.mock.calls.length).toBe(1) - expect(res.send.mock.calls[0][0].success).toBe(false) - expect(res.send.mock.calls[0][0].message).toBe( - errorHandler.getMessage(data.err._smErrorCode) - ) - expect(res.send.mock.calls[0][0].errorCode).toBe( - errorHandler.getErrorCode(data.err._smErrorCode) - ) - expect(res.status.mock.calls.length).toBe(1) - expect(res.status.mock.calls[0][0]).toBe(500) - }) - }) -}) diff --git a/test/unit/lib/GitHub.test.js b/test/unit/lib/GitHub.test.js deleted file mode 100644 index 24bc509..0000000 --- a/test/unit/lib/GitHub.test.js +++ /dev/null @@ -1,495 +0,0 @@ -const mockHelpers = require('./../../helpers') -const sampleData = require('./../../helpers/sampleData') -const User = require('../../../lib/models/User') -const yaml = require('js-yaml') -const GitHub = require('./../../../lib/GitHub') -const nock = require('nock') -const config = require('../../../config') - -let req - -const btoa = contents => Buffer.from(contents).toString('base64') - -beforeEach(() => { - jest.resetModules() - jest.restoreAllMocks() - - req = mockHelpers.getMockRequest() -}) - -describe('GitHub interface', () => { - test('initialises the GitHub API wrapper', async () => { - const githubInstance = await new GitHub(req.params) - expect(githubInstance.api).toBeDefined() - }) - - test('authenticates with the GitHub API using a personal access token', async () => { - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/user/repository_invitations') - .reply(200) - - const githubInstance = await new GitHub(req.params) - await githubInstance.api.repos.listInvitationsForAuthenticatedUser(); - expect(scope.isDone()).toBe(true) - }) - - test('authenticates with the GitHub API using an OAuth token', async () => { - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('test-oauth-token') - } - }) - .get('/user/repository_invitations') - .reply(200) - - const githubInstance = await new GitHub({ - ...req.params, - oauthToken: 'test-oauth-token' - }) - await githubInstance.api.repos.listInvitationsForAuthenticatedUser(); - expect(scope.isDone()).toBe(true) - }) - - test('throws error if no personal access token or OAuth token is provided', async () => { - jest.spyOn(config, 'get').mockImplementation(() => null) - expect.assertions(1) - try { - await new GitHub({}) - } catch (e) { - expect(e.message).toBe('Require an `oauthToken` or `token` option') - } - }) - - describe('readFile', () => { - test('reads a file and returns its contents', async () => { - const filePath = 'path/to/file.yml' - const parsedConfig = yaml.safeLoad(sampleData.config1, 'utf8') - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/repos/johndoe/foobar/contents/path/to/file.yml?ref=master') - .reply(200, { - content: btoa(sampleData.config1) - }) - - const githubInstance = await new GitHub(req.params) - - const contents = await githubInstance.readFile(filePath) - expect(contents).toEqual(parsedConfig) - expect(scope.isDone()).toBe(true) - }) - - test('returns an error if GitHub API call errors', async () => { - const filePath = 'path/to/file.yml' - const parsedConfig = yaml.safeLoad(sampleData.config1, 'utf8') - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/repos/johndoe/foobar/contents/path/to/file.yml?ref=master') - .replyWithError('Error encountered oh no') - - const githubInstance = await new GitHub(req.params) - - expect.assertions(2) - - try { - await githubInstance.readFile(filePath) - } catch (err) { - expect(err._smErrorCode).toEqual('GITHUB_READING_FILE') - } - - expect(scope.isDone()).toBe(true) - }) - - test('returns an error if the config file cannot be read', async () => { - const filePath = 'path/to/file.yml' - const githubInstance = await new GitHub(req.params) - - expect.assertions(2) - - try { - await githubInstance.readFile(filePath) - } catch (err) { - expect(err._smErrorCode).toEqual('GITHUB_READING_FILE') - expect(err.message).toBeDefined() - } - }) - - test('returns an error if the config file cannot be parsed', async () => { - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/repos/johndoe/foobar/contents/path/to/file.yml?ref=master') - .reply(200, { - content: btoa(sampleData.configInvalidYML) - }) - - const filePath = 'path/to/file.yml' - const githubInstance = await new GitHub(req.params) - - expect.assertions(3) - - try { - await githubInstance.readFile(filePath) - } catch (err) { - expect(err._smErrorCode).toEqual('PARSING_ERROR') - expect(err.message).toBeDefined() - } - - expect(scope.isDone()).toBe(true) - }) - - test('reads a YAML file and returns its parsed contents', async () => { - const filePath = 'path/to/file.yml' - const parsedConfig = yaml.safeLoad(sampleData.config1, 'utf8') - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/repos/johndoe/foobar/contents/path/to/file.yml?ref=master') - .reply(200, { - content: btoa(sampleData.config1) - }) - - const githubInstance = await new GitHub(req.params) - - const contents = await githubInstance.readFile(filePath) - expect(contents).toEqual(parsedConfig) - expect(scope.isDone()).toBe(true) - }) - - test('reads a YAML file and returns its parsed and raw contents if `getFullResponse` is `true`', async () => { - const parsedConfig = yaml.safeLoad(sampleData.config1, 'utf8') - const filePath = 'path/to/file.yml' - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/repos/johndoe/foobar/contents/path/to/file.yml?ref=master') - .reply(200, { - content: btoa(sampleData.config1) - }) - - const githubInstance = await new GitHub(req.params) - - const response = await githubInstance.readFile(filePath, true) - - expect(response.content).toEqual(parsedConfig) - expect(scope.isDone()).toBe(true) - }) - - test('reads a JSON file and returns its parsed contents', async () => { - const filePath = 'path/to/file.json' - const parsedConfig = yaml.safeLoad(sampleData.config2, 'utf8') - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/repos/johndoe/foobar/contents/path/to/file.json?ref=master') - .reply(200, { - content: btoa(sampleData.config2) - }) - - const githubInstance = await new GitHub(req.params) - - const contents = await githubInstance.readFile(filePath) - - expect(contents).toEqual(parsedConfig) - expect(scope.isDone()).toBe(true) - }) - - test('reads a JSON file and returns its parsed and raw contents if `getFullResponse` is `true`', async () => { - const fileContents = { - content: btoa(sampleData.config2) - } - const filePath = 'path/to/file.json' - const parsedConfig = yaml.safeLoad(sampleData.config2, 'utf8') - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/repos/johndoe/foobar/contents/path/to/file.json?ref=master') - .reply(200, { - content: btoa(sampleData.config2) - }) - - const githubInstance = await new GitHub(req.params) - - const response = await githubInstance.readFile(filePath, true) - expect(response.content).toEqual(parsedConfig) - expect(response.file).toEqual(fileContents) - expect(scope.isDone()).toBe(true) - }) - }) - - describe('writeFile', () => { - test('creates a file on the given branch using the commit title provided', async () => { - const options = { - branch: 'master', - commitTitle: 'Adds a new file', - content: 'This is a new file', - path: 'path/to/file.txt' - } - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .put('/repos/johndoe/foobar/contents/path/to/file.txt') - .reply(200, { - number: 123 - }) - - const githubInstance = await new GitHub(req.params) - - await githubInstance.writeFile( - options.path, - options.content, - options.branch, - options.commitTitle - ) - - expect(scope.isDone()).toBe(true) - }) - - test('creates a file using the branch present in the request, if one is not provided to the method, and the default commit title', async () => { - const options = { - content: 'This is a new file', - commitTitle: 'Add Staticman file', - path: 'path/to/file.txt' - } - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .put('/repos/johndoe/foobar/contents/path/to/file.txt') - .reply(200, { - number: 123 - }) - - const githubInstance = await new GitHub(req.params) - - await githubInstance.writeFile( - options.path, - options.content - ) - - expect(scope.isDone()).toBe(true) - }) - - test('returns an error object if the save operation fails', async () => { - const options = { - branch: 'master', - commitTitle: 'Adds a new file', - content: 'This is a new file', - path: 'path/to/file.txt' - } - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .put('/repos/johndoe/foobar/contents/path/to/file.txt') - .replyWithError('An error') - - const githubInstance = await new GitHub(req.params) - - expect.assertions(2) - - try { - await githubInstance.writeFile( - options.path, - options.content, - options.branch, - options.commitTitle - ) - } catch (err) { - expect(err).toEqual({ - _smErrorCode: 'GITHUB_WRITING_FILE' - }) - } - - expect(scope.isDone()).toBe(true) - }) - }) - - describe('writeFileAndSendReview', () => { - test('writes a file to a new branch and sends a PR to the base branch provided, using the given title and body for the commit/PR', async () => { - const options = { - commitBody: 'This is a very cool file indeed...', - commitTitle: 'Adds a new file', - content: 'This is a new file', - name: 'file.txt', - newBranch: 'staticman_123456789', - path: 'path/to/file.txt', - sha: '7fd1a60b01f91b314f59955a4e4d4e80d8edf11d' - } - - const branchScope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/repos/johndoe/foobar/branches/master') - .reply(200, { - commit: { - sha: options.sha - } - }) - - const refsScope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .post('/repos/johndoe/foobar/git/refs') - .reply(200, { - ref: `refs/heads/${options.newBranch}` - }) - - const fileScope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .put('/repos/johndoe/foobar/contents/path/to/file.txt') - .reply(200, { - number: 123 - }) - - const pullScope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .post('/repos/johndoe/foobar/pulls') - .reply(200, { - id: 1 - }) - - expect.assertions(5) - - const githubInstance = await new GitHub(req.params) - - const data = await githubInstance.writeFileAndSendReview( - options.path, - options.content, - options.newBranch, - options.commitTitle, - options.commitBody - ) - - expect(data).toEqual({"id": 1}) - - expect(branchScope.isDone()).toBe(true) - expect(refsScope.isDone()).toBe(true) - expect(fileScope.isDone()).toBe(true) - expect(pullScope.isDone()).toBe(true) - }) - - // TODO: Figure out why this works with no mocks - test('returns an error if any of the API calls fail', async () => { - const options = { - commitBody: '', - commitTitle: 'Add Staticman file', - content: 'This is a new file', - name: 'file.txt', - newBranch: 'staticman_123456789', - path: 'path/to/file.txt', - sha: '7fd1a60b01f91b314f59955a4e4d4e80d8edf11d' - } - - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/repos/johndoe/foobar/branches/master') - .replyWithError('An error, oh no.') - - const githubInstance = await new GitHub(req.params) - - expect.assertions(2) - - try { - await githubInstance.writeFileAndSendReview( - options.path, - options.content, - options.newBranch, - options.commitTitle, - options.commitBody - ) - } catch (err) { - expect(err._smErrorCode).toEqual('GITHUB_CREATING_PR') - } - expect(scope.isDone()).toBe(true) - }) - }) - - describe('getCurrentUser', () => { - test('returns the current authenticated user', async () => { - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/user') - .reply(200, { - login: 'johndoe', - email: 'johndoe@test.com', - name: 'John Doe' - }) - - const githubInstance = await new GitHub(req.params) - - const user = await githubInstance.getCurrentUser() - expect(user).toEqual(new User('github', 'johndoe', 'johndoe@test.com', 'John Doe')) - expect(scope.isDone()).toBe(true) - }) - - test('throws an error if unable to retrieve the current unauthenticated user', async () => { - const scope = nock((/api\.github\.com/), { - reqheaders: { - authorization: 'token '.concat('1q2w3e4r') - } - }) - .get('/user') - .replyWithError('Oops, an error') - - const githubInstance = await new GitHub(req.params) - - expect.assertions(2) - - try { - await githubInstance.getCurrentUser() - } catch (err) { - expect(err._smErrorCode).toEqual('GITHUB_GET_USER') - } - expect(scope.isDone()).toBe(true) - }) - }) -}) diff --git a/test/unit/lib/GitLab.test.js b/test/unit/lib/GitLab.test.js deleted file mode 100644 index f60f7c9..0000000 --- a/test/unit/lib/GitLab.test.js +++ /dev/null @@ -1,567 +0,0 @@ -const mockHelpers = require('./../../helpers') -const sampleData = require('./../../helpers/sampleData') -const User = require('../../../lib/models/User') -const yaml = require('js-yaml') -const GitLab = require('../../../lib/GitLab') -const config = require('../../../config') - -let req - -const btoa = contents => Buffer.from(contents).toString('base64') - -beforeEach(() => { - jest.resetModules() - jest.restoreAllMocks() - - req = mockHelpers.getMockRequest() - req.params.token = 'test-token' -}) - -describe('GitLab interface', () => { - test('initialises the GitLab API wrapper', () => { - const gitlab = new GitLab(req.params) - - expect(gitlab.api).toBeDefined() - }) - - test('authenticates with the GitLab API using a personal access token', () => { - const mockConstructor = jest.fn() - jest.mock('gitlab/dist/es5', () => { - return { - default: class { - constructor (params) { - mockConstructor(params) - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) // eslint-disable-line no-unused-vars - - expect(mockConstructor.mock.calls[0][0]).toEqual({ - url: 'https://gitlab.com', - token: 'r4e3w2q1' - }) - }) - - test('authenticates with the GitLab API using an OAuth token', () => { - const mockConstructor = jest.fn() - jest.mock('gitlab/dist/es5', () => { - return { - default: class { - constructor (params) { - mockConstructor(params) - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - - const oauthToken = 'test-oauth-token' - const gitlab = new GitLab(Object.assign({}, req.params, {oauthToken})) // eslint-disable-line no-unused-vars - - expect(mockConstructor.mock.calls[0][0]).toEqual({ - url: 'https://gitlab.com', - oauthToken: oauthToken - }) - }) - - test('throws error if no personal access token or OAuth token is provided', () => { - jest.spyOn(config, 'get').mockImplementation(() => null) - - expect(() => new GitLab({})).toThrowError('Require an `oauthToken` or `token` option') - }) - - describe('readFile', () => { - test('reads a file and returns its contents', () => { - const fileContents = 'This is a text file!' - const filePath = 'path/to/file.txt' - const mockRepoShowFile = jest.fn(() => Promise.resolve({ - content: btoa(fileContents) - })) - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - show: mockRepoShowFile - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.readFile(filePath).then(contents => { - expect(mockRepoShowFile.mock.calls[0][0]).toBe(`${req.params.username}/${req.params.repository}`) - expect(mockRepoShowFile.mock.calls[0][1]).toBe(filePath) - expect(mockRepoShowFile.mock.calls[0][2]).toBe(req.params.branch) - }) - }) - - test('returns an error if GitLab API call errors', () => { - const filePath = 'path/to/file.yml' - const mockShowRepoFile = jest.fn(() => Promise.reject()) // eslint-disable-line prefer-promise-reject-errors - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - show: mockShowRepoFile - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.readFile(filePath).catch(err => { - expect(mockShowRepoFile.mock.calls[0][0]).toBe(`${req.params.username}/${req.params.repository}`) - expect(mockShowRepoFile.mock.calls[0][1]).toBe(filePath) - expect(mockShowRepoFile.mock.calls[0][2]).toBe(req.params.branch) - - expect(err).toEqual({ - _smErrorCode: 'GITLAB_READING_FILE' - }) - }) - }) - - test('returns an error if parsing fails for the given file', () => { - const fileContents = ` - foo: "bar" - baz - ` - const filePath = 'path/to/file.yml' - const mockShowRepoFile = jest.fn(() => Promise.resolve({ - content: btoa(fileContents) - })) - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - show: mockShowRepoFile - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.readFile(filePath).catch(err => { - expect(mockShowRepoFile.mock.calls[0][0]).toBe(`${req.params.username}/${req.params.repository}`) - expect(mockShowRepoFile.mock.calls[0][1]).toBe(filePath) - expect(mockShowRepoFile.mock.calls[0][2]).toBe(req.params.branch) - expect(err._smErrorCode).toBe('PARSING_ERROR') - expect(err.message).toBeDefined() - }) - }) - - test('reads a YAML file and returns its parsed contents', () => { - const filePath = 'path/to/file.yml' - const parsedConfig = yaml.safeLoad(sampleData.config1, 'utf8') - const mockShowRepoFile = jest.fn(() => Promise.resolve({ - content: btoa(sampleData.config1) - })) - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - show: mockShowRepoFile - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.readFile(filePath).then(contents => { - expect(mockShowRepoFile.mock.calls[0][0]).toBe(`${req.params.username}/${req.params.repository}`) - expect(mockShowRepoFile.mock.calls[0][1]).toBe(filePath) - expect(mockShowRepoFile.mock.calls[0][2]).toBe(req.params.branch) - expect(contents).toEqual(parsedConfig) - }) - }) - - test('reads a YAML file and returns its parsed and raw contents if `getFullResponse` is `true`', () => { - const parsedConfig = yaml.safeLoad(sampleData.config1, 'utf8') - const fileContents = { - content: btoa(sampleData.config1) - } - const filePath = 'path/to/file.yml' - const mockShowRepoFile = jest.fn(() => Promise.resolve(fileContents)) - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - show: mockShowRepoFile - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.readFile(filePath, true).then(response => { - expect(response.content).toEqual(parsedConfig) - expect(response.file).toEqual(fileContents) - }) - }) - - test('reads a JSON file and returns its parsed contents', () => { - const filePath = 'path/to/file.json' - const parsedConfig = yaml.safeLoad(sampleData.config2, 'utf8') - const mockShowRepoFile = jest.fn(() => Promise.resolve({ - content: btoa(sampleData.config2) - })) - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - show: mockShowRepoFile - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.readFile(filePath).then(contents => { - expect(contents).toEqual(parsedConfig) - }) - }) - - test('reads a JSON file and returns its parsed and raw contents if `getFullResponse` is `true`', () => { - const fileContents = { - content: btoa(sampleData.config2) - } - const filePath = 'path/to/file.json' - const parsedConfig = yaml.safeLoad(sampleData.config2, 'utf8') - const mockShowRepoFile = jest.fn(() => Promise.resolve(fileContents)) - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - show: mockShowRepoFile - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.readFile(filePath, true).then(response => { - expect(response.content).toEqual(parsedConfig) - expect(response.file).toEqual(fileContents) - }) - }) - }) - - describe('writeFile', () => { - test('creates a file on the given branch using the commit title provided', () => { - const options = { - branch: 'master', - commitTitle: 'Adds a new file', - content: 'This is a new file', - path: 'path/to/file.txt' - } - const mockCreateRepoFile = jest.fn(() => Promise.resolve(null)) - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - create: mockCreateRepoFile - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.writeFile( - options.path, - options.content, - options.branch, - options.commitTitle - ).then(response => { - expect(mockCreateRepoFile).toHaveBeenCalledTimes(1) - expect(mockCreateRepoFile.mock.calls[0][0]).toBe(`${req.params.username}/${req.params.repository}`) - expect(mockCreateRepoFile.mock.calls[0][1]).toBe(options.path) - expect(mockCreateRepoFile.mock.calls[0][2]).toBe(options.branch) - expect(mockCreateRepoFile.mock.calls[0][3]).toEqual({ - content: btoa(options.content), - commit_message: options.commitTitle, - encoding: 'base64' - }) - }) - }) - - test( - 'creates a file using the branch present in the request, if one is not provided to the method, and the default commit title', - () => { - const mockCreateRepoFile = jest.fn(() => Promise.resolve(null)) - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - create: mockCreateRepoFile - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - const options = { - content: 'This is a new file', - commitTitle: 'Add Staticman file', - path: 'path/to/file.txt' - } - - return gitlab.writeFile( - options.path, - options.content - ).then(response => { - expect(mockCreateRepoFile.mock.calls[0][0]).toBe(`${req.params.username}/${req.params.repository}`) - expect(mockCreateRepoFile.mock.calls[0][1]).toBe(options.path) - expect(mockCreateRepoFile.mock.calls[0][2]).toBe(req.params.branch) - expect(mockCreateRepoFile.mock.calls[0][3]).toEqual({ - content: btoa(options.content), - commit_message: options.commitTitle, - encoding: 'base64' - }) - }) - } - ) - - test('returns an error object if the save operation fails', () => { - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - RepositoryFiles: { - create: () => Promise.reject(new Error()) - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - const options = { - branch: 'master', - commitTitle: 'Adds a new file', - content: 'This is a new file', - path: 'path/to/file.txt' - } - - return gitlab.writeFile( - options.path, - options.content, - options.branch, - options.commitTitle - ).catch(err => { - expect(err._smErrorCode).toBe('GITLAB_WRITING_FILE') - }) - }) - }) - - describe('writeFileAndSendReview', () => { - test( - 'writes a file to a new branch and sends a PR to the base branch provided, using the given title and body for the commit/PR', - () => { - const options = { - commitBody: 'This is a very cool file indeed...', - commitTitle: 'Adds a new file', - content: 'This is a new file', - name: 'file.txt', - newBranch: 'staticman_123456789', - path: 'path/to/file.txt', - sha: '7fd1a60b01f91b314f59955a4e4d4e80d8edf11d' - } - const mockCreateMergeRequest = jest.fn(() => Promise.resolve({ - number: 123 - })) - const mockCreateBranch = jest.fn(() => Promise.resolve({ - ref: `refs/heads/${options.newBranch}` - })) - const mockShowBranch = jest.fn(() => Promise.resolve({ - commit: { - id: options.sha - } - })) - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - Branches: { - create: mockCreateBranch, - show: mockShowBranch - }, - MergeRequests: { - create: mockCreateMergeRequest - }, - RepositoryFiles: { - create: () => Promise.resolve(null) - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.writeFileAndSendReview( - options.path, - options.content, - options.newBranch, - options.commitTitle, - options.commitBody - ).then(response => { - expect(mockCreateMergeRequest.mock.calls[0][0]).toBe(`${req.params.username}/${req.params.repository}`) - expect(mockCreateMergeRequest.mock.calls[0][1]).toBe(options.newBranch) - expect(mockCreateMergeRequest.mock.calls[0][2]).toBe(req.params.branch) - expect(mockCreateMergeRequest.mock.calls[0][3]).toBe(options.commitTitle) - expect(mockCreateMergeRequest.mock.calls[0][4]).toEqual({ - description: options.commitBody, - remove_source_branch: true - }) - - expect(mockCreateBranch.mock.calls[0][0]).toBe(`${req.params.username}/${req.params.repository}`) - expect(mockCreateBranch.mock.calls[0][1]).toBe(options.newBranch) - expect(mockCreateBranch.mock.calls[0][2]).toBe(options.sha) - - expect(mockShowBranch.mock.calls[0][0]).toBe(`${req.params.username}/${req.params.repository}`) - expect(mockShowBranch.mock.calls[0][1]).toBe(req.params.branch) - }) - } - ) - - test('returns an error if any of the API calls fail', () => { - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - Branches: { - create: () => Promise.resolve(), - show: () => Promise.reject(new Error()) - }, - RepositoryFiles: { - create: () => Promise.reject(new Error()) - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - const options = { - commitBody: '', - commitTitle: 'Add Staticman file', - content: 'This is a new file', - name: 'file.txt', - newBranch: 'staticman_123456789', - path: 'path/to/file.txt', - sha: '7fd1a60b01f91b314f59955a4e4d4e80d8edf11d' - } - - return gitlab.writeFileAndSendReview( - options.path, - options.content, - options.newBranch, - options.commitTitle, - options.commitBody - ).catch(err => { - expect(err._smErrorCode).toBe('GITLAB_CREATING_PR') - }) - }) - }) - - describe('getCurrentUser', () => { - test('returns the current authenticated user', () => { - const mockUser = { - username: 'johndoe', - email: 'johndoe@test.com', - name: 'John Doe' - } - - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - Users: { - current: () => Promise.resolve(mockUser) - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.getCurrentUser().then((user) => { - expect(user).toEqual(new User('gitlab', 'johndoe', 'johndoe@test.com', 'John Doe')) - }) - }) - - test('throws an error if unable to retrieve the current unauthenticated user', () => { - jest.mock('gitlab/dist/es5', () => { - return { - default: function () { - return { - Users: { - current: () => Promise.reject(new Error()) - } - } - } - } - }) - - const GitLab = require('./../../../lib/GitLab') - const gitlab = new GitLab(req.params) - - return gitlab.getCurrentUser().catch((err) => { - expect(err._smErrorCode).toBe('GITLAB_GET_USER') - }) - }) - }) -}) diff --git a/test/unit/lib/Notification.test.js b/test/unit/lib/Notification.test.js deleted file mode 100644 index b148465..0000000 --- a/test/unit/lib/Notification.test.js +++ /dev/null @@ -1,60 +0,0 @@ -const config = require('./../../../config') -const mockHelpers = require('./../../helpers') -const Notification = require('./../../../lib/Notification') - -const mockSendFn = jest.fn() -const mockMessagesFn = jest.fn(() => ({ - send: mockSendFn -})) -const mockMailAgent = { - messages: mockMessagesFn -} - -beforeEach(() => { - mockSendFn.mockClear() - mockMessagesFn.mockClear() -}) - -describe('Notification interface', () => { - const mockData = { - data: { - siteName: 'Eduardo\'s blog' - }, - fields: { - name: 'Eduardo Bouças', - email: 'mail@eduardoboucas.com' - }, - options: { - origin: 'https://eduardoboucas.com' - } - } - - test('builds an email message from the template, replacing the placeholders with the data provided', () => { - const notification = new Notification(mockMailAgent) - const message = notification._buildMessage(mockData.fields, mockData.options, mockData.data) - - expect(message.includes(mockData.data.siteName)).toBe(true) - expect(message.includes(mockData.options.origin)).toBe(true) - }) - - test('sends an email through the mail agent', () => { - const notification = new Notification(mockMailAgent) - const message = notification._buildMessage(mockData.fields, mockData.options, mockData.data) - const recipient = 'john.doe@foobar.com' - - notification.send( - recipient, - mockData.fields, - mockData.options, - mockData.data - ) - - expect(mockSendFn.mock.calls.length).toBe(1) - expect(mockSendFn.mock.calls[0][0]).toEqual({ - from: `Staticman <${config.get('email.fromAddress')}>`, - to: recipient, - subject: `New reply on "${mockData.data.siteName}"`, - html: message - }) - }) -}) diff --git a/test/unit/lib/OAuth.test.js b/test/unit/lib/OAuth.test.js deleted file mode 100644 index bcd08b4..0000000 --- a/test/unit/lib/OAuth.test.js +++ /dev/null @@ -1,51 +0,0 @@ -const nock = require('nock') -const oauth = require('../../../lib/OAuth') - -describe('OAuth access tokens', () => { - test('requests OAuth access token from GitHub', () => { - const accessToken = 'asdfghjkl' - const clientId = '123456789' - const clientSecret = '1q2w3e4r5t6y7u8i9o' - const code = 'abcdefghijklmnopqrst' - const redirectUri = 'http://my-test-site.com' - - nock(/github\.com/) - .post('/login/oauth/access_token') - .query({ - client_id: clientId, - client_secret: clientSecret, - code, - redirect_uri: redirectUri - }) - .reply(200, { - access_token: accessToken - }) - - oauth.requestGitHubAccessToken(code, clientId, clientSecret, redirectUri) - .then(token => expect(token).toEqual(accessToken)) - }) - - test('requests OAuth access token from GitLab', () => { - const accessToken = 'asdfghjkl' - const clientId = '123456789' - const clientSecret = '1q2w3e4r5t6y7u8i9o' - const code = 'abcdefghijklmnopqrst' - const redirectUri = 'http://my-test-site.com' - - nock(/gitlab\.com/) - .post('/oauth/token') - .query({ - client_id: clientId, - client_secret: clientSecret, - code, - grant_type: 'authorization_code', - redirect_uri: redirectUri - }) - .reply(200, { - access_token: accessToken - }) - - oauth.requestGitLabAccessToken(code, clientId, clientSecret, redirectUri) - .then(token => expect(token).toEqual(accessToken)) - }) -}) diff --git a/test/unit/lib/Staticman.test.js b/test/unit/lib/Staticman.test.js deleted file mode 100644 index a5915a4..0000000 --- a/test/unit/lib/Staticman.test.js +++ /dev/null @@ -1,1666 +0,0 @@ -const config = require('./../../../config') -const errorHandler = require('./../../../lib/ErrorHandler') -const frontMatter = require('front-matter') -const moment = require('moment') -const mockHelpers = require('./../../helpers') -const slugify = require('slug') -const yaml = require('js-yaml') -const User = require('../../../lib/models/User') - -let mockConfig -let mockParameters - -beforeEach(() => { - mockConfig = mockHelpers.getConfig() - mockParameters = mockHelpers.getParameters() - - jest.resetModules() - jest.unmock('./../../../lib/SubscriptionsManager') - jest.unmock('node-rsa') -}) - -describe('Staticman interface', () => { - describe('initialisation', () => { - test('creates an instance of the GitHub module', async () => { - const GitHub = require('../../../lib/GitHub') - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - expect(staticman.git).toBeInstanceOf(GitHub) - expect(staticman.git.username).toBe(mockParameters.username) - expect(staticman.git.repository).toBe(mockParameters.repository) - expect(staticman.git.branch).toBe(mockParameters.branch) - }) - - test('creates an instance of the GitLab module', async () => { - const GitLab = require('../../../lib/GitLab') - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(Object.assign({}, mockParameters, {service: 'gitlab'})) - - expect(staticman.git).toBeInstanceOf(GitLab) - expect(staticman.git.username).toBe(mockParameters.username) - expect(staticman.git.repository).toBe(mockParameters.repository) - expect(staticman.git.branch).toBe(mockParameters.branch) - }) - - test('generates a new unique ID', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman1 = await new Staticman(mockParameters) - const staticman2 = await new Staticman(mockParameters) - - expect(staticman1.uid.length).toBeGreaterThan(0) - expect(staticman2.uid.length).toBeGreaterThan(0) - expect(staticman1.uid).not.toBe(staticman2.uid) - }) - - test('creates an instance of the NodeRSA module and import the private key', async () => { - const mockImportKeyFn = jest.fn() - - jest.mock('node-rsa', () => { - return jest.fn(() => ({ - importKey: mockImportKeyFn - })) - }) - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - expect(staticman.rsa).toBeDefined() - expect(mockImportKeyFn).toHaveBeenCalled() - expect(mockImportKeyFn.mock.calls[0][0]).toBe(config.get('rsaPrivateKey')) - }) - - test('saves an internal reference to the parameters provided', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - expect(staticman.parameters).toEqual(mockParameters) - }) - - test('exposes the parameters via the `getParameters()` method', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - expect(staticman.getParameters()).toEqual(staticman.parameters) - }) - - test('sets the config path via the `setConfigPath()` method', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const configObject = mockHelpers.getConfigObject() - - staticman.setConfigPath(configObject) - - expect(staticman.configPath).toEqual(configObject) - }) - - test('sets the request IP via the `setIp()` method', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const ip = '123.456.78.9' - - staticman.setIp(ip) - - expect(staticman.ip).toEqual(ip) - }) - - test('sets the request User Agent via the `setUserAgent()` method', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const userAgent = mockHelpers.getUserAgent() - - staticman.setUserAgent(userAgent) - - expect(staticman.userAgent).toEqual(userAgent) - }) - }) - - describe('internal fields', () => { - test('adds an _id field to the data object', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.options = {} - - const data = mockHelpers.getFields() - const extendedData = staticman._applyInternalFields(data) - - expect(extendedData).toEqual(Object.assign({}, data, { - _id: staticman.uid - })) - }) - - test('adds an _parent field if the parent option is defined', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman1 = await new Staticman(mockParameters) - const staticman2 = await new Staticman(mockParameters) - - staticman1.options = { - parent: '123456789' - } - - staticman2.options = {} - - const data = mockHelpers.getFields() - const extendedData1 = staticman1._applyInternalFields(data) - const extendedData2 = staticman2._applyInternalFields(data) - - expect(extendedData1).toEqual(Object.assign({}, data, { - _id: staticman1.uid, - _parent: staticman1.options.parent - })) - - expect(extendedData2).toEqual(Object.assign({}, data, { - _id: staticman2.uid - })) - }) - }) - - describe('generated fields', () => { - test('returns the data object unchanged if the `generatedFields` property is not in the site config', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('generatedFields', undefined) - staticman.siteConfig = mockConfig - - const extendedData = staticman._applyGeneratedFields(mockHelpers.getFields()) - - expect(extendedData).toEqual(mockHelpers.getFields()) - }) - - test('adds the generated fields to the data object', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman._createDate = jest.fn(() => 'generatedDate') - - mockConfig.set('generatedFields', { - date: { - options: { - format: 'timestamp-seconds' - }, - type: 'date' - }, - slug: { - options: { - field: 'name' - }, - type: 'slugify' - } - }) - staticman.siteConfig = mockConfig - - const data = mockHelpers.getFields() - const extendedData = staticman._applyGeneratedFields(data) - - expect(staticman._createDate).toHaveBeenCalledTimes(1) - expect(extendedData).toEqual(Object.assign({}, data, { - date: 'generatedDate', - slug: slugify(data.name).toLowerCase() - })) - }) - - test('adds the `user` generated fields to the data object', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('generatedFields', { - username: { - options: { - property: 'username' - }, - type: 'user' - }, - name: { - options: { - property: 'name' - }, - type: 'user' - } - }) - staticman.siteConfig = mockConfig - - staticman.gitUser = new User('github', 'johndoe', 'johndoe@test.com', 'John Doe') - - const data = mockHelpers.getFields() - const extendedData = staticman._applyGeneratedFields(data) - - expect(extendedData).toEqual(Object.assign({}, data, { - name: 'John Doe', - username: 'johndoe' - })) - }) - - test('adds the `github` generated fields to the data object in the v2 API', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('generatedFields', { - username: { - options: { - property: 'login' - }, - type: 'github' - }, - name: { - options: { - property: 'name' - }, - type: 'github' - } - }) - staticman.siteConfig = mockConfig - - staticman.gitUser = { - login: 'johndoe', - name: 'John Doe' - } - - const data = mockHelpers.getFields() - const extendedData = staticman._applyGeneratedFields(data) - - expect(extendedData).toEqual(Object.assign({}, data, { - name: 'John Doe', - username: 'johndoe' - })) - }) - }) - - describe('field transforms', () => { - test('returns the data object unchanged if the `transforms` property is not in the site config', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('transforms', undefined) - staticman.siteConfig = mockConfig - - const data = mockHelpers.getFields() - - return staticman._applyTransforms(data).then(extendedData => { - expect(extendedData).toEqual(data) - }) - }) - - test('transforms the fields defined in the `transforms` property', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('transforms', { - name: 'md5', - email: 'upcase' - }) - staticman.siteConfig = mockConfig - - const data = mockHelpers.getFields() - const extendedData = Object.assign({}, data, { - name: 'f710ffc7114e4dfe5239ce411c160a70', - email: 'MAIL@EDUARDOBOUCAS.COM' - }) - - return staticman._applyTransforms(data).then(transformedData => { - expect(transformedData).toEqual(extendedData) - }) - }) - - test('handles multiple transforms per field', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('transforms', { - email: ['md5', 'upcase'] - }) - staticman.siteConfig = mockConfig - - const data = mockHelpers.getFields() - const extendedData = Object.assign({}, data, { - email: '4F8072E22FAE3CD98B876DF304886BED' - }) - - return staticman._applyTransforms(data).then(transformedData => { - expect(transformedData).toEqual(extendedData) - }) - }) - }) - - describe('spam detection', () => { - test('returns the data object unchanged if Akismet is not enabled in config', async () => { - const fields = mockHelpers.getFields() - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('akismet.enabled', false) - staticman.siteConfig = mockConfig - - const catchAllMockRequest = mockHelpers.getCatchAllApiMock() - - return staticman._checkForSpam(fields).then(response => { - expect(response).toEqual(fields) - expect(catchAllMockRequest.hasIntercepted()).toBe(false) - }) - }) - - test('makes a request to the Akismet API sending the correct data', async () => { - const fields = mockHelpers.getFields() - const mockCheckSpamFn = jest.fn((options, callback) => { - callback(null, false) - }) - const mockClientFn = jest.fn(options => ({ - checkSpam: mockCheckSpamFn - })) - - jest.mock('akismet', () => ({ - client: mockClientFn - })) - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('akismet.enabled', true) - mockConfig.set('akismet.author', 'name') - mockConfig.set('akismet.authorEmail', 'email') - mockConfig.set('akismet.authorUrl', 'url') - mockConfig.set('akismet.content', 'message') - staticman.siteConfig = mockConfig - - return staticman._checkForSpam(fields).then(response => { - expect(response).toEqual(fields) - - expect(mockClientFn).toHaveBeenCalledTimes(1) - expect(mockClientFn.mock.calls[0][0]).toEqual({ - apiKey: config.get('akismet.apiKey'), - blog: config.get('akismet.site') - }) - - expect(mockCheckSpamFn).toHaveBeenCalledTimes(1) - expect(mockCheckSpamFn.mock.calls[0][0]).toEqual({ - comment_type: 'comment', - comment_author: fields.name, - comment_author_email: fields.email, - comment_author_url: fields.url, - comment_content: fields.message - }) - expect(mockCheckSpamFn.mock.calls[0][1]).toBeInstanceOf(Function) - }) - }) - - test('throws an error if the Akismet API call fails', async () => { - const akismetError = new Error('Akismet error') - const fields = mockHelpers.getFields() - const mockCheckSpamFn = jest.fn((options, callback) => { - callback(akismetError) - }) - const mockClientFn = jest.fn(options => ({ - checkSpam: mockCheckSpamFn - })) - - jest.mock('akismet', () => ({ - client: mockClientFn - })) - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('akismet.enabled', true) - mockConfig.set('akismet.author', 'name') - mockConfig.set('akismet.authorEmail', 'email') - mockConfig.set('akismet.authorUrl', 'url') - mockConfig.set('akismet.content', 'message') - staticman.siteConfig = mockConfig - - return staticman._checkForSpam(fields).catch(err => { - expect(err).toEqual(akismetError) - }) - }) - - test('throws an error if the content is flagged as spam', async () => { - const fields = mockHelpers.getFields() - const mockCheckSpamFn = jest.fn((options, callback) => { - callback(null, true) - }) - const mockClientFn = jest.fn(options => ({ - checkSpam: mockCheckSpamFn - })) - - jest.mock('akismet', () => ({ - client: mockClientFn - })) - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('akismet.enabled', true) - mockConfig.set('akismet.author', 'name') - mockConfig.set('akismet.authorEmail', 'email') - mockConfig.set('akismet.authorUrl', 'url') - mockConfig.set('akismet.content', 'message') - staticman.siteConfig = mockConfig - - return staticman._checkForSpam(fields).catch(err => { - expect(err).toEqual({ - _smErrorCode: 'IS_SPAM' - }) - }) - }) - }) - - describe('authentication ', () => { - beforeEach(() => { - mockConfig.set('auth.required', true) - }) - - test('returns false if `auth.required` config is false', async () => { - mockConfig.set('auth.required', false) - - const fields = mockHelpers.getFields() - const options = {} - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - - return staticman._checkAuth().then(result => expect(result).toBeFalsy()) - }) - - test('throws an error if `auth-token` field is missing', async () => { - const fields = mockHelpers.getFields() - const options = {} - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - - return staticman._checkAuth().catch(err => { - expect(err).toEqual({ - _smErrorCode: 'AUTH_TOKEN_MISSING' - }) - }) - }) - - test('throws an error if unable to decrypt the `auth-token` option', async () => { - const fields = mockHelpers.getFields() - const options = { - 'auth-token': 'invalid token', - 'auth-type': 'github' - } - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - - return staticman._checkAuth().catch(err => { - expect(err).toEqual({ - _smErrorCode: 'AUTH_TOKEN_INVALID' - }) - }) - }) - - test('authenticates with GitHub by default using the OAuth access token', async () => { - const mockConstructor = jest.fn() - const mockUser = new User('github', 'johndoe', 'johndoe@test.com', 'John Doe') - - jest.mock('../../../lib/GitHub', () => { - return mockConstructor.mockImplementation(() => { - return { - getCurrentUser: () => Promise.resolve(mockUser) - } - }) - }) - - const fields = mockHelpers.getFields() - const options = { - 'auth-token': mockHelpers.encrypt('test-token'), - } - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - staticman.parameters.version = '3' - - await staticman._checkAuth() - expect(mockConstructor.mock.calls[1][0]).toEqual({ - oauthToken: 'test-token', - version: '3' - }) - }) - - test('authenticates with GitLab (using `auth-type` option) using OAuth access token', async () => { - const mockConstructor = jest.fn() - const mockUser = new User('gitlab', 'johndoe', 'johndoe@test.com', 'John Doe') - - jest.mock('../../../lib/GitLab', () => { - return mockConstructor.mockImplementation(() => { - return { - getCurrentUser: () => Promise.resolve(mockUser) - } - }) - }) - - const fields = mockHelpers.getFields() - const options = { - 'auth-token': mockHelpers.encrypt('test-token'), - 'auth-type': 'gitlab' - } - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - staticman.parameters.version = '3' - - await staticman._checkAuth() - expect(mockConstructor.mock.calls[0][0]).toEqual({ - oauthToken: 'test-token', - version: '3' - }) - }) - - test('sets the `gitUser` property to the authenticated User and returns true for GitHub authentication', async () => { - const mockGetCurrentUser = jest.fn(() => Promise.resolve(mockUser)) - - jest.mock('../../../lib/GitHub', () => { - return function () { - return { - getCurrentUser: mockGetCurrentUser - } - } - }) - - const fields = mockHelpers.getFields() - const options = { - 'auth-token': mockHelpers.encrypt('test-token') - } - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - staticman.parameters.version = '3' - - const mockUser = new User('github', 'johndoe', 'johndoe@test.com', 'John Doe') - - let result = await staticman._checkAuth() - expect(mockGetCurrentUser).toHaveBeenCalledTimes(1) - expect(staticman.gitUser).toEqual(mockUser) - expect(result).toBeTruthy() - }) - - test('sets the `gitUser` property to the authenticated User and returns true for GitLab authentication', async () => { - const mockGetCurrentUser = jest.fn(() => Promise.resolve(mockUser)) - - jest.mock('../../../lib/GitLab', () => { - return function () { - return { - getCurrentUser: mockGetCurrentUser - } - } - }) - - const fields = mockHelpers.getFields() - const options = { - 'auth-token': mockHelpers.encrypt('test-token'), - 'auth-type': 'gitlab' - } - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - staticman.parameters.version = '3' - - const mockUser = new User('github', 'johndoe', 'johndoe@test.com', 'John Doe') - - let result = await staticman._checkAuth() - expect(mockGetCurrentUser).toHaveBeenCalledTimes(1) - expect(staticman.gitUser).toEqual(mockUser) - expect(result).toBeTruthy() - }) - }) - - describe('authentication v2', async () => { - beforeEach(() => { - mockConfig.set('githubAuth.required', true) - }) - - test('returns false if `githubAuth.required` config is false', async () => { - mockConfig.set('githubAuth.required', false) - - const fields = mockHelpers.getFields() - const options = {} - - mockParameters.version = '2' - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - - return staticman._checkAuth().then(result => expect(result).toBeFalsy()) - }) - - test('throws an error if `github-token` field is missing in v2 API', async () => { - const fields = mockHelpers.getFields() - const options = {} - - mockParameters.version = '2' - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - - return staticman._checkAuth().catch(err => { - expect(err).toEqual({ - _smErrorCode: 'GITHUB_AUTH_TOKEN_MISSING' - }) - }) - }) - - test('throws an error if unable to decrypt the `github-token` option in the v2 API', async () => { - const fields = mockHelpers.getFields() - const options = { - 'github-token': 'invalid token' - } - - mockParameters.version = '2' - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - - return staticman._checkAuth().catch(err => { - expect(err).toEqual({ - _smErrorCode: 'GITHUB_AUTH_TOKEN_INVALID' - }) - }) - }) - - test('sets the `gitUser` property to the GitHub user in the v2 API', async () => { - const mockUser = { - login: 'johndoe', - name: 'John Doe' - } - const mockGetCurrentUser = jest.fn(() => Promise.resolve({ - data: mockUser - })) - - jest.mock('../../../lib/GitHub', () => { - return function () { - return { - api: { - users: { - getAuthenticated: mockGetCurrentUser - } - } - } - } - }) - - const fields = mockHelpers.getFields() - const options = { - 'github-token': mockHelpers.encrypt('test-token') - } - - mockParameters.version = '2' - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.fields = fields - staticman.options = options - staticman.siteConfig = mockConfig - - return staticman._checkAuth().then((result) => { - expect(mockGetCurrentUser).toHaveBeenCalledTimes(1) - expect(staticman.gitUser).toEqual(mockUser) - expect(result).toBeTruthy() - }) - }) - }) - - describe('date creator', () => { - const mockDate = new Date('1988-08-31T11:00:00') - - // eslint-disable-next-line no-global-assign - Date = class extends Date { - constructor () { - return mockDate - } - } - - test('creates a timestamp in milliseconds if the format is set to `timestamp`', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - const date = staticman._createDate({ - format: 'timestamp' - }) - - expect(date).toBe(mockDate.getTime()) - }) - - test('creates a timestamp in seconds if the format is set to `timestamp-seconds`', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - const date = staticman._createDate({ - format: 'timestamp-seconds' - }) - - expect(date).toBe(Math.floor(mockDate.getTime() / 1000)) - }) - - test( - 'creates a ISO-8601 representation of the date if the format is set to `iso8601`, absent, or set to none of the other supported formats', - async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - const date1 = staticman._createDate({ - format: 'iso8601' - }) - const date2 = staticman._createDate({ - format: 'somethingNotValid' - }) - const date3 = staticman._createDate() - - expect(date1).toBe(mockDate.toISOString()) - expect(date2).toBe(mockDate.toISOString()) - expect(date3).toBe(mockDate.toISOString()) - } - ) - }) - - describe('file formatting', () => { - test('formats the given fields as JSON if `format` is set to `json`', async () => { - const fields = mockHelpers.getFields() - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('format', 'json') - staticman.siteConfig = mockConfig - - return staticman._createFile(fields).then(file => { - expect(file).toBe(JSON.stringify(fields)) - }) - }) - - test('formats the given fields as YAML if `format` is set to `yaml` or `yml`', async () => { - const fields = mockHelpers.getFields() - const Staticman = require('./../../../lib/Staticman') - const staticman1 = await new Staticman(mockParameters) - const staticman2 = await new Staticman(mockParameters) - const config1 = mockHelpers.getConfig() - const config2 = mockHelpers.getConfig() - - config1.set('format', 'yaml') - config2.set('format', 'yaml') - - staticman1.siteConfig = config1 - staticman2.siteConfig = config2 - - return staticman1._createFile(fields).then(file1 => { - return staticman2._createFile(fields).then(file2 => { - expect(file1).toBe(yaml.safeDump(fields)) - expect(file2).toBe(yaml.safeDump(fields)) - }) - }) - }) - - test('formats the given fields as YAML/Frontmatter if `format` is set to `frontmatter`', async () => { - const fields = mockHelpers.getFields() - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('format', 'frontmatter') - mockConfig.set('transforms', { - message: 'frontmatterContent' - }) - staticman.siteConfig = mockConfig - - let attributeFields = Object.assign({}, fields) - delete attributeFields.message - - return staticman._createFile(fields).then(file => { - const parsedFile = frontMatter(file) - - expect(parsedFile.attributes).toEqual(attributeFields) - expect(parsedFile.body.trim()).toBe(fields.message.trim()) - }) - }) - - test( - 'throws an error if `format` is set to `frontmatter` but there is no `frontmatterContent` transform defined', - async () => { - const fields = mockHelpers.getFields() - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('format', 'frontmatter') - mockConfig.set('transforms', undefined) - staticman.siteConfig = mockConfig - - return staticman._createFile(fields).catch(err => { - expect(err).toEqual({ - _smErrorCode: 'NO_FRONTMATTER_CONTENT_TRANSFORM' - }) - }) - } - ) - - test('throws an error if `format` contains an invalid format', async () => { - const fields = mockHelpers.getFields() - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('format', 'someWeirdFormat') - staticman.siteConfig = mockConfig - - return staticman._createFile(fields).catch(err => { - expect(err).toEqual({ - _smErrorCode: 'INVALID_FORMAT' - }) - }) - }) - }) - - describe('generating a message for the pull request body', () => { - test('generates a PR body with the message set in config and a table listing fields and their values', async () => { - const fields = mockHelpers.getFields() - const fieldsTable = mockHelpers.getFieldsTable() - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('notifications.enabled', false) - staticman.siteConfig = mockConfig - - const pullRequestBody = staticman._generateReviewBody(fields) - - expect(pullRequestBody).toBe(mockConfig.get('pullRequestBody') + fieldsTable) - }) - - test('adds an HTML comment containing notification settings if `notifications.enabled` is set to `true`', async () => { - const req = mockHelpers.getMockRequest() - const configObject = { - file: 'staticman.yml', - path: req.params.property - } - const fields = mockHelpers.getFields() - const fieldsTable = mockHelpers.getFieldsTable() - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('notifications.enabled', true) - staticman.siteConfig = mockConfig - staticman.setConfigPath(configObject) - staticman.setIp(req.headers['x-forwarded-for'] || req.connection.remoteAddress) - staticman.setUserAgent(req.headers['user-agent']) - - const notificationsData = { - configPath: staticman.configPath, - fields: fields, - parameters: req.params - } - const notificationsComment = `\n\n` - const pullRequestBody = staticman._generateReviewBody(fields) - - expect(pullRequestBody).toBe( - mockConfig.get('pullRequestBody') + - fieldsTable + - notificationsComment - ) - }) - }) - - describe('computes the full path and extension for new files', () => { - test('uses UID as the default file name and extension if `filename` and `extension` are not set in config', async () => { - const fields = mockHelpers.getFields() - const directory = 'some/directory' - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('filename', '') - mockConfig.set('format', 'json') - mockConfig.set('path', directory) - staticman.siteConfig = mockConfig - - const filePath = staticman._getNewFilePath(fields) - - expect(filePath).toBe(`${directory}/${staticman.uid}.json`) - }) - - test('uses the config value of `filename`, if defined, as the file name', async () => { - const fields = mockHelpers.getFields() - const directory = 'some/directory' - const name = 'my-file' - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('filename', name) - mockConfig.set('format', 'json') - mockConfig.set('path', directory) - staticman.siteConfig = mockConfig - - const filePath = staticman._getNewFilePath(fields) - - expect(filePath).toBe(`${directory}/${name}.json`) - }) - - test('uses the config value of `extension`, if defined, as the file extension', async () => { - const fields = mockHelpers.getFields() - const directory = 'some/directory' - const name = 'my-file' - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('extension', 'html') - mockConfig.set('filename', name) - mockConfig.set('format', 'json') - mockConfig.set('path', directory) - staticman.siteConfig = mockConfig - - const filePath = staticman._getNewFilePath(fields) - - expect(filePath).toBe(`${directory}/${name}.html`) - }) - - test('removes a trailing slash from `path` if it exists', async () => { - const fields = mockHelpers.getFields() - const Staticman = require('./../../../lib/Staticman') - const staticman1 = await new Staticman(mockParameters) - const staticman2 = await new Staticman(mockParameters) - const config1 = mockHelpers.getConfig() - const config2 = mockHelpers.getConfig() - - config1.set('filename', 'my-file') - config1.set('format', 'json') - config1.set('path', 'some/directory') - config2.set('filename', 'my-file') - config2.set('format', 'json') - config2.set('path', 'some/directory/') - staticman1.siteConfig = config1 - staticman2.siteConfig = config2 - - const filePath1 = staticman1._getNewFilePath(fields) - const filePath2 = staticman2._getNewFilePath(fields) - - expect(filePath1).toBe(filePath2) - }) - - test('resolves placeholders in the filename and path', async () => { - const data = { - fields: { - group: 50 - }, - options: { - slug: 'some-slug' - } - } - const directory = 'groups/{fields.group}' - const name = 'file-{options.slug}' - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - mockConfig.set('filename', name) - mockConfig.set('format', 'json') - mockConfig.set('path', directory) - - staticman.options = data.options - staticman.siteConfig = mockConfig - - const filePath = staticman._getNewFilePath(data.fields) - const processedDirectory = directory.replace('{fields.group}', data.fields.group) - const processedName = name.replace('{options.slug}', data.options.slug) - - expect(filePath).toBe(`${processedDirectory}/${processedName}.json`) - }) - - test('gets the correct extension for each supported format', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - const extension1 = staticman._getExtensionForFormat('json') - const extension2 = staticman._getExtensionForFormat('yaml') - const extension3 = staticman._getExtensionForFormat('yml') - const extension4 = staticman._getExtensionForFormat('frontmatter') - - expect(extension1).toBe('json') - expect(extension2).toBe('yml') - expect(extension3).toBe('yml') - expect(extension4).toBe('md') - }) - }) - - describe('placeholders (`_resolvePlaceholders`)', () => { - test('returns the given string unchanged if it does not contain placeholders', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - const subject = 'This is a normal string, nothing to replace here.' - const data = mockHelpers.getParameters() - - expect(staticman._resolvePlaceholders(subject, data)).toBe(subject) - }) - - test('returns the given string with placeholders replaced with data from the data object provided', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - const subject = 'My name is {name} and I come from {location.city}, {location.country}' - const data = { - name: 'Eduardo', - location: { - city: 'London', - country: 'United Kingdom' - } - } - const subjectReplaced = subject - .replace('{name}', data.name) - .replace('{location.city}', data.location.city) - .replace('{location.country}', data.location.country) - - expect(staticman._resolvePlaceholders(subject, data)).toBe(subjectReplaced) - }) - - test('returns the given string with special placeholders replaced', async () => { - const mockDate = new Date('1988-08-31T11:00:00') - - // eslint-disable-next-line no-global-assign - Date = class extends Date { - constructor () { - return mockDate - } - } - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - const data = { - name: 'Eduardo' - } - const subject = 'Hi {name}, the time is {@timestamp} and my ID is {@id}' - const subjectReplaced = subject - .replace('{name}', data.name) - .replace('{@timestamp}', mockDate.getTime()) - .replace('{@id}', staticman.uid) - - expect(staticman._resolvePlaceholders(subject, data)).toBe(subjectReplaced) - }) - - test('returns the given string with `date:` placeholders replaced', async () => { - const mockDate = new Date('1988-08-31T11:00:00') - - // eslint-disable-next-line no-global-assign - Date = class extends Date { - constructor () { - return mockDate - } - } - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - const data = { - title: 'this-is-a-title' - } - const subject = '{@date:YYYY-MM-DD}-{title}' - const subjectReplaced = subject - .replace('{title}', data.title) - .replace('{@date:YYYY-MM-DD}', moment().format('YYYY-MM-DD')) - - expect(staticman._resolvePlaceholders(subject, data)).toBe(subjectReplaced) - }) - }) - - describe('`_validateConfig`', () => { - test('throws an error if no config is provided', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - expect(staticman._validateConfig(null)).toEqual({ - _smErrorCode: 'MISSING_CONFIG_BLOCK' - }) - }) - - test('throws an error if the config provided is missing any of the required fields', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const config = { - allowedFields: ['name', 'email'], - format: 'json' - } - - expect(staticman._validateConfig(config)).toEqual({ - _smErrorCode: 'MISSING_CONFIG_FIELDS', - data: ['branch', 'path'] - }) - }) - - test('creates a SiteConfig object and assigns it to the Staticman instance', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const config = { - allowedFields: ['name', 'email'], - branch: 'master', - format: 'json', - path: 'some/path' - } - - staticman._validateConfig(config) - - expect(staticman.siteConfig.get('allowedFields')).toEqual(config.allowedFields) - expect(staticman.siteConfig.get('branch')).toEqual(config.branch) - expect(staticman.siteConfig.get('format')).toEqual(config.format) - expect(staticman.siteConfig.get('path')).toEqual(config.path) - }) - }) - - describe('`_validateFields`', () => { - test('throws an error if the payload contains a field that is not allowed', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const payload = mockHelpers.getFields() - - mockConfig.set('allowedFields', ['streetName', 'country']) - - staticman.siteConfig = mockConfig - - const validationResult = staticman._validateFields(payload) - - expect(validationResult._smErrorCode).toBe('INVALID_FIELDS') - expect(validationResult.data).toEqual(Object.keys(payload)) - }) - - test('returns a copy of the fields provided with all strings trimmed', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const payload = mockHelpers.getFields() - const paddedPayload = Object.assign({}, payload) - - paddedPayload.name = ' ' + payload.name - paddedPayload.email = payload.email + ' ' - paddedPayload.url = ' ' + payload.url + ' ' - paddedPayload.message = '\n\n' + payload.message - - mockConfig.set('allowedFields', Object.keys(payload)) - - staticman.siteConfig = mockConfig - - staticman._validateFields(paddedPayload) - - expect(paddedPayload).toEqual(payload) - }) - - test('throws an error if the payload is missing a required field', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const payload = mockHelpers.getFields() - - payload.someField1 = ' ' - - const requiredFields = [ - 'name', - 'someField1', - 'someField2' - ] - - mockConfig.set('allowedFields', Object.keys(payload)) - mockConfig.set('requiredFields', requiredFields) - - staticman.siteConfig = mockConfig - - expect(staticman._validateFields(payload)).toEqual({ - _smErrorCode: 'MISSING_REQUIRED_FIELDS', - data: ['someField1', 'someField2'] - }) - }) - }) - - describe('`getSiteConfig()`', () => { - test('returns the existing site config if `force` is falsy', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.siteConfig = mockConfig - - return staticman.getSiteConfig().then(config => { - expect(config).toEqual(mockConfig) - }) - }) - - test('throws an error if the config path has not been set', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - return staticman.getSiteConfig().catch(err => { - expect(err).toEqual({ - _smErrorCode: 'NO_CONFIG_PATH' - }) - }) - }) - - test( - 'fetches the site config from the repository, even if there is one already defined, if `force` is truthy', - async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const configObject = mockHelpers.getConfigObject() - - staticman.setConfigPath(configObject) - staticman.siteConfig = mockConfig - staticman.git = { - readFile: jest.fn(() => { - const config = mockHelpers.getParsedConfig() - - return Promise.resolve(config) - }) - } - - return staticman.getSiteConfig(true).then(config => { - expect(staticman.git.readFile).toHaveBeenCalledTimes(1) - expect(staticman.git.readFile.mock.calls[0][0]).toBe(configObject.file) - }) - } - ) - - test('fetches the site config from the repository and throws an error if it fails validation', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const configObject = mockHelpers.getConfigObject() - const validationErrors = { - _smErrorCode: 'MISSING_CONFIG_FIELDS', - data: ['branch', 'path'] - } - - const invalidConfig = { - missingFields: true - } - - staticman.setConfigPath(configObject) - staticman.git = { - readFile: jest.fn(() => { - return Promise.resolve({ - [configObject.path]: invalidConfig - }) - }) - } - staticman._validateConfig = jest.fn(() => validationErrors) - - return staticman.getSiteConfig().catch(err => { - expect(err).toEqual(validationErrors) - expect(staticman._validateConfig.mock.calls[0][0]).toEqual(invalidConfig) - }) - }) - - test('fetches the site config from the repository and throws an error if there is a branch mismatch', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const configObject = mockHelpers.getConfigObject() - const mockRemoteConfig = Object.assign({}, mockConfig.getProperties()) - - mockRemoteConfig.branch = 'some-other-branch' - - staticman.setConfigPath(configObject) - staticman.git = { - readFile: jest.fn(() => { - return Promise.resolve({ - [configObject.path]: mockRemoteConfig - }) - }) - } - staticman._validateConfig = jest.fn(() => null) - - return staticman.getSiteConfig().catch(err => { - expect(err).toEqual({ - _smErrorCode: 'BRANCH_MISMATCH' - }) - }) - }) - - test('fetches the site config from the repository and returns the new site config object', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const configObject = mockHelpers.getConfigObject() - - staticman.setConfigPath(configObject) - staticman.git = { - readFile: jest.fn(() => { - const mockRemoteConfig = mockHelpers.getParsedConfig() - - return Promise.resolve(mockRemoteConfig) - }) - } - - return staticman.getSiteConfig().then(config => { - expect(config.getProperties()).toEqual(mockConfig.getProperties()) - }) - }) - }) - - describe('`processEntry()`', () => { - test('gets site config and checks for spam, throwing an error if found', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - - staticman.getSiteConfig = jest.fn(() => { - staticman.siteConfig = mockConfig - - return Promise.resolve(mockConfig) - }) - - staticman._checkForSpam = jest.fn(fields => { - return Promise.reject(errorHandler('IS_SPAM')) - }) - - return staticman.processEntry( - mockHelpers.getFields(), - {} - ).catch(err => { - expect(err).toEqual({ - _smErrorCode: 'IS_SPAM' - }) - }) - }) - - test('validates fields, throwing an error if validation fails', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const fields = mockHelpers.getFields() - - mockConfig.set('allowedFields', Object.keys(fields)) - - fields.someField1 = 'Some value' - - staticman._checkForSpam = () => Promise.resolve(fields) - staticman.siteConfig = mockConfig - - return staticman.processEntry( - mockHelpers.getFields(), - {} - ).catch(err => { - expect(err).toEqual({ - _smErrorCode: 'INVALID_FIELDS', - data: ['someField1'] - }) - }) - }) - - test( - 'creates a file after applying generated fields, transforms and internal fields, throwing an error if file creation fails', - async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const fields = mockHelpers.getFields() - - mockConfig.set('allowedFields', Object.keys(fields)) - - staticman.siteConfig = mockConfig - staticman._checkForSpam = () => Promise.resolve(fields) - - const spyApplyGeneratedFields = jest.spyOn(staticman, '_applyGeneratedFields') - const spyApplyTransforms = jest.spyOn(staticman, '_applyTransforms') - const spyApplyInternalFields = jest.spyOn(staticman, '_applyInternalFields') - - staticman._createFile = jest.fn(() => { - return Promise.reject(errorHandler('INVALID_FORMAT')) - }) - - return staticman.processEntry( - mockHelpers.getFields(), - {} - ).catch(err => { - expect(spyApplyGeneratedFields).toHaveBeenCalled() - expect(spyApplyTransforms).toHaveBeenCalled() - expect(spyApplyInternalFields).toHaveBeenCalled() - expect(err).toEqual({ - _smErrorCode: 'INVALID_FORMAT' - }) - }) - } - ) - - test('authenticates user before creating file', async () => { - const mockUser = new User('github', 'johndoe', 'johndoe@test.com', 'John Doe') - const mockGetCurrentUser = jest.fn(() => Promise.resolve(mockUser)) - - jest.mock('../../../lib/GitHub', () => { - return function () { - return { - getCurrentUser: mockGetCurrentUser - } - } - }) - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const fields = mockHelpers.getFields() - const options = { - 'auth-token': mockHelpers.encrypt('test-token') - } - - mockConfig.set('auth.required', true) - - staticman.siteConfig = mockConfig - staticman.parameters.version = '3' - staticman._checkForSpam = () => Promise.resolve(fields) - staticman.git.writeFile = jest.fn(() => Promise.resolve()) - - const spyCheckAuth = jest.spyOn(staticman, '_checkAuth') - - await staticman.processEntry(fields, options) - expect(spyCheckAuth).toHaveBeenCalledTimes(1) - expect(mockGetCurrentUser).toHaveBeenCalledTimes(1) - expect(staticman.gitUser).toEqual(mockUser) - }) - - test('authenticates user before creating file, throwing an error if unable to authenticate', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const fields = mockHelpers.getFields() - const options = { - 'auth-token': 'invalid token' - } - - mockConfig.set('auth.required', true) - - staticman.siteConfig = mockConfig - staticman._checkForSpam = () => Promise.resolve(fields) - staticman.git.writeFile = jest.fn(() => Promise.resolve()) - - return staticman.processEntry(fields, options).catch(err => { - err._smErrorCode = 'AUTH_TOKEN_INVALID' - }) - }) - - test('subscribes the user to notifications', async () => { - const mockSubscriptionSet = jest.fn(() => Promise.resolve(true)) - - jest.mock('./../../../lib/SubscriptionsManager', () => { - return jest.fn(() => ({ - send: jest.fn(), - set: mockSubscriptionSet - })) - }) - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const fields = mockHelpers.getFields() - const options = { - parent: '1a2b3c4d5e6f', - subscribe: 'email' - } - - mockConfig.set('allowedFields', Object.keys(fields)) - mockConfig.set('moderation', false) - mockConfig.set('notifications.enabled', true) - - staticman.siteConfig = mockConfig - staticman._checkForSpam = () => Promise.resolve(fields) - staticman.git.writeFile = jest.fn(() => { - return Promise.resolve() - }) - - return staticman.processEntry( - fields, - options - ).then(response => { - expect(mockSubscriptionSet.mock.calls[0][0]).toBe(options.parent) - expect(mockSubscriptionSet.mock.calls[0][1]).toBe(mockHelpers.getFields().email) - }) - }) - - test('creates a pull request with the generated file if moderation is enabled', async () => { - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const fields = mockHelpers.getFields() - - mockConfig.set('allowedFields', Object.keys(fields)) - mockConfig.set('moderation', true) - mockConfig.set('notifications.enabled', false) - - staticman.siteConfig = mockConfig - staticman._checkForSpam = () => Promise.resolve(fields) - staticman.git.writeFileAndSendReview = jest.fn(() => { - return Promise.resolve() - }) - - return staticman.processEntry( - fields, - {} - ).then(response => { - return staticman._createFile(staticman._applyInternalFields(fields)) - }).then(expectedFile => { - const expectedCommitMessage = staticman._resolvePlaceholders( - mockConfig.get('commitMessage'), { - fields, - options: {} - } - ) - - expect(staticman.git.writeFileAndSendReview.mock.calls[0][0]) - .toBe(staticman._getNewFilePath(fields)) - expect(staticman.git.writeFileAndSendReview.mock.calls[0][1]) - .toBe(expectedFile) - expect(staticman.git.writeFileAndSendReview.mock.calls[0][2]) - .toBe(`staticman_${staticman.uid}`) - expect(staticman.git.writeFileAndSendReview.mock.calls[0][3]) - .toBe(expectedCommitMessage) - expect(staticman.git.writeFileAndSendReview.mock.calls[0][4]) - .toBe(staticman._generateReviewBody(fields)) - }) - }) - - test('commits the generated file directly if moderation is disabled', async () => { - const mockSubscriptionSend = jest.fn() - - jest.mock('./../../../lib/SubscriptionsManager', () => { - return jest.fn(() => ({ - send: mockSubscriptionSend, - set: () => Promise.resolve(true) - })) - }) - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const fields = mockHelpers.getFields() - const options = { - parent: '1a2b3c4d5e6f', - subscribe: 'email' - } - - mockConfig.set('allowedFields', Object.keys(fields)) - mockConfig.set('moderation', false) - mockConfig.set('notifications.enabled', true) - - staticman.siteConfig = mockConfig - staticman._checkForSpam = () => Promise.resolve(fields) - staticman.git.writeFile = jest.fn(() => { - return Promise.resolve() - }) - - return staticman.processEntry( - fields, - options - ).then(response => { - return staticman._createFile(staticman._applyInternalFields(fields)) - }).then(expectedFile => { - const expectedCommitMessage = staticman._resolvePlaceholders( - mockConfig.get('commitMessage'), { - fields, - options: {} - } - ) - - expect(mockSubscriptionSend.mock.calls[0][0]).toBe(options.parent) - expect(mockSubscriptionSend.mock.calls[0][1]).toEqual(fields) - expect(staticman.git.writeFile.mock.calls[0][0]) - .toBe(staticman._getNewFilePath(fields)) - expect(staticman.git.writeFile.mock.calls[0][1]) - .toBe(expectedFile) - expect(staticman.git.writeFile.mock.calls[0][2]) - .toBe(mockParameters.branch) - expect(staticman.git.writeFile.mock.calls[0][3]) - .toBe(expectedCommitMessage) - }) - }) - - describe('`processMerge()`', () => { - test('subscribes the user to notifications', async () => { - const mockSubscriptionSend = jest.fn() - - jest.mock('./../../../lib/SubscriptionsManager', () => { - return jest.fn(() => ({ - send: mockSubscriptionSend - })) - }) - - const Staticman = require('./../../../lib/Staticman') - const staticman = await new Staticman(mockParameters) - const fields = mockHelpers.getFields() - const options = { - parent: '1a2b3c4d5e6f', - subscribe: 'email' - } - - mockConfig.set('notifications.enabled', true) - - staticman.siteConfig = mockConfig - - return staticman.processMerge( - fields, - options - ).then(response => { - expect(mockSubscriptionSend.mock.calls[0][0]).toBe(options.parent) - expect(mockSubscriptionSend.mock.calls[0][1]).toEqual(fields) - expect(mockSubscriptionSend.mock.calls[0][2]).toEqual(options) - expect(mockSubscriptionSend.mock.calls[0][3]).toEqual(mockConfig) - }) - }) - }) - }) -}) diff --git a/test/unit/lib/Transforms.test.js b/test/unit/lib/Transforms.test.js deleted file mode 100644 index fdb5fd8..0000000 --- a/test/unit/lib/Transforms.test.js +++ /dev/null @@ -1,21 +0,0 @@ -const Transforms = require('./../../../lib/Transforms') - -describe('Transforms', () => { - describe('md5', () => { - test('returns an MD5 of the value', () => { - expect(Transforms.md5('test-value')).toEqual('83b3c112b82dcca8376da029e8101bcc'); - }) - }) - - describe('upcase', () => { - test('returns an upcased value', () => { - expect(Transforms.upcase('foobar')).toEqual('FOOBAR') - }) - }) - - describe('downcase', () => { - test('returns an downcased value', () => { - expect(Transforms.downcase('FOOBAR')).toEqual('foobar') - }) - }) -}) diff --git a/test/utils/coverage.js b/test/utils/coverage.js deleted file mode 100644 index 3f29f8c..0000000 --- a/test/utils/coverage.js +++ /dev/null @@ -1,41 +0,0 @@ -#! /usr/bin/env node - -const coberturaBadger = require('istanbul-cobertura-badger') -const fs = require('fs') -const path = require('path') - -const opts = { - badgeFileName: 'coverage', - destinationDir: __dirname, - istanbulReportFile: path.resolve(__dirname, '../../coverage', 'cobertura-coverage.xml'), - thresholds: { - excellent: 90, // overall percent >= excellent, green badge - good: 60 // overall percent < excellent and >= good, yellow badge - // overall percent < good, red badge - } -} - -// Load the badge for the report$ -coberturaBadger(opts, function parsingResults(err, badgeStatus) { - if (err) { - console.log('An error occurred: ' + err.message) - } - - const readme = path.resolve(__dirname, '../../README.md') - const badgeUrl = badgeStatus.url // e.g. http://img.shields.io/badge/coverage-60%-yellow.svg - - // open the README.md and add this url - fs.readFile(readme, {encoding: 'utf-8'}, (err, body) => { - if (err) console.log(err.toString()) - - body = body.replace(/(!\[coverage\]\()(.+?)(\))/g, (whole, a, b, c) => { - return a + badgeUrl + c - }) - - fs.writeFile(readme, body, {encoding: 'utf-8'}, (err) => { - if (err) console.log(err.toString()) - - console.log('Coverage badge successfully added to ' + readme) - }) - }) -}) \ No newline at end of file diff --git a/test/utils/coverage.svg b/test/utils/coverage.svg deleted file mode 100644 index 02d44a7..0000000 --- a/test/utils/coverage.svg +++ /dev/null @@ -1 +0,0 @@ - coveragecoverage82%82% \ No newline at end of file